113 lines
4.1 KiB
Python
113 lines
4.1 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""
|
||
|
|
muse-auth.py: Muse.ai authentication state detection and flows.
|
||
|
|
|
||
|
|
Handles the tricky parts of automation:
|
||
|
|
- Sign IN (existing account) vs Sign UP (new account) have DIFFERENT verification flows
|
||
|
|
- Meta account selection has UI quirks (+1 for extra accounts)
|
||
|
|
- Login can get stuck in incomplete states that need detection and restart
|
||
|
|
|
||
|
|
STATES (detectable via CDP):
|
||
|
|
LANDING: Title == "muse.ai", not logged in
|
||
|
|
ACCOUNT_SELECTION: Body contains "Your email matches multiple accounts"
|
||
|
|
UI QUIRK: Meta shows "+1" for extra accounts. The "+1" option is NOT
|
||
|
|
the right one — find the actual account name (e.g., "Nico Parada",
|
||
|
|
not "auxfate"). The +1 is a collapsed list, not a selectable account.
|
||
|
|
OTP_PROMPT: Body contains "To log in, enter the code"
|
||
|
|
LOGGED_OUT: Body contains "Log in" button
|
||
|
|
NOT_IN_CHAT: API messages times out (no chat UI to read)
|
||
|
|
LOGGED_IN: Title contains "Chat —" or "Muse —", body has "Main chat"/"Side chats",
|
||
|
|
message input present, API returns actual content.
|
||
|
|
|
||
|
|
SIGN IN vs SIGN UP:
|
||
|
|
Sign IN (existing account):
|
||
|
|
1. Enter email/phone
|
||
|
|
2. OTP prompt (code sent to email/phone)
|
||
|
|
3. Meta account selection (if multiple accounts match)
|
||
|
|
4. Chat UI
|
||
|
|
|
||
|
|
Sign UP (new account):
|
||
|
|
1. Enter email/phone
|
||
|
|
2. OTP prompt (code sent to email/phone)
|
||
|
|
3. **DIFFERENT:** May require additional verification:
|
||
|
|
- CAPTCHA
|
||
|
|
- Phone verification (if email used)
|
||
|
|
- Email verification (if phone used)
|
||
|
|
- Terms acceptance
|
||
|
|
- Profile setup (name, etc.)
|
||
|
|
4. **DIFFERENT:** May NOT have Meta account selection (new account = no existing Meta link)
|
||
|
|
5. Chat UI (possibly with onboarding tour)
|
||
|
|
|
||
|
|
The sign-up flow is less predictable and may require human intervention
|
||
|
|
for CAPTCHAs and other anti-bot measures. Sign-in is more automatable.
|
||
|
|
|
||
|
|
RESTART LOGIC:
|
||
|
|
If state != LOGGED_IN, restart from scratch:
|
||
|
|
- Clear cookies/session
|
||
|
|
- Navigate to muse.ai
|
||
|
|
- Begin sign-in flow
|
||
|
|
- Don't try to resume a stuck flow (OTP codes expire, sessions timeout)
|
||
|
|
"""
|
||
|
|
|
||
|
|
import json
|
||
|
|
import urllib.request
|
||
|
|
import websocket
|
||
|
|
import time
|
||
|
|
|
||
|
|
def get_state(cdp_url):
|
||
|
|
"""
|
||
|
|
Detect the current authentication state via CDP.
|
||
|
|
Returns: (state, details)
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
with urllib.request.urlopen(cdp_url, timeout=5) as r:
|
||
|
|
ts = json.load(r)
|
||
|
|
pages = [t for t in ts if t.get("type") == "page"]
|
||
|
|
if not pages:
|
||
|
|
return ("NO_PAGE", "No browser page found")
|
||
|
|
p = pages[0]
|
||
|
|
ws = websocket.create_connection(p["webSocketDebuggerUrl"], timeout=10)
|
||
|
|
except Exception as e:
|
||
|
|
return ("CDP_ERROR", str(e))
|
||
|
|
|
||
|
|
def ev(expr):
|
||
|
|
try:
|
||
|
|
ws.send(json.dumps({
|
||
|
|
"id": 1, "method": "Runtime.evaluate",
|
||
|
|
"params": {"expression": expr, "returnByValue": True}
|
||
|
|
}))
|
||
|
|
r = json.loads(ws.recv())
|
||
|
|
return r["result"]["result"].get("value", "")
|
||
|
|
except:
|
||
|
|
return ""
|
||
|
|
|
||
|
|
title = ev("document.title") or ""
|
||
|
|
body = ev("document.body.innerText.slice(0,2000)") or ""
|
||
|
|
ws.close()
|
||
|
|
|
||
|
|
# Check states in order of specificity
|
||
|
|
if "Your email matches multiple accounts" in body:
|
||
|
|
return ("ACCOUNT_SELECTION", "Meta account selection - watch for +1 quirk")
|
||
|
|
if "To log in, enter the code" in body:
|
||
|
|
return ("OTP_PROMPT", "Waiting for OTP code")
|
||
|
|
if "Main chat" in body and "Side chats" in body:
|
||
|
|
return ("LOGGED_IN", "In chat UI")
|
||
|
|
if title.strip() == "muse.ai":
|
||
|
|
return ("LANDING", "On landing page, not logged in")
|
||
|
|
if "Log in" in body:
|
||
|
|
return ("LOGGED_OUT", "Logged out or not started")
|
||
|
|
|
||
|
|
return ("UNKNOWN", f"Title: {title[:50]}")
|
||
|
|
|
||
|
|
def is_logged_in(cdp_url):
|
||
|
|
"""Quick check: are we in the chat?"""
|
||
|
|
state, _ = get_state(cdp_url)
|
||
|
|
return state == "LOGGED_IN"
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
import sys
|
||
|
|
cdp = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:9440/json/list"
|
||
|
|
state, details = get_state(cdp)
|
||
|
|
print(f"State: {state}")
|
||
|
|
print(f"Details: {details}")
|