Files
box/bin/muse-auth.py
T

141 lines
5.3 KiB
Python
Raw Normal View History

#!/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}")
# Holdings logging: record what the Chromebox holds for each node
# Logs to ~/Projects/NetVM/logs/auth-holdings.log
# Format: timestamp | node | state | email_hint | account_name | notes
# This lets us know which Proton address was used without asking the user again.
import os
from datetime import datetime
def log_holdings(node, state, email_hint="", account_name="", notes=""):
"""Log the Chromebox holdings for a node. Call after auth state detection."""
log_dir = os.path.expanduser("~/Projects/NetVM/logs")
os.makedirs(log_dir, exist_ok=True)
log_file = os.path.join(log_dir, "auth-holdings.log")
ts = datetime.now().isoformat()
# Never log full email or secrets — use hint (e.g., domain or first char)
line = f"{ts} | {node} | {state} | {email_hint} | {account_name} | {notes}\n"
with open(log_file, "a") as f:
f.write(line)
def get_holdings(node):
"""Get last known holdings for a node."""
log_file = os.path.expanduser("~/Projects/NetVM/logs/auth-holdings.log")
if not os.path.exists(log_file):
return None
with open(log_file) as f:
lines = [l.strip() for l in f if l.strip() and f"| {node} |" in l]
return lines[-1] if lines else None