diff --git a/bin/muse-auth.py b/bin/muse-auth.py new file mode 100755 index 0000000..0db21f5 --- /dev/null +++ b/bin/muse-auth.py @@ -0,0 +1,112 @@ +#!/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}")