2026-10-03 15:57:37 +00:00
#!/usr/bin/env python3
"""
Automated muse.ai sign-in with in-browser OTP approval.
Usage:
signin.py --email <email> [--otp <code>]
If --otp is not provided, the script will:
1. Navigate through login flow up to OTP prompt
2026-10-04 18:07:48 +00:00
2. Print " APPROVAL_NEEDED: OTP required for [redacted] "
2026-10-03 15:57:37 +00:00
3. Exit with code 2
The operator then obtains the OTP (via chat with user) and re-runs:
signin.py --email <email> --otp <code>
This keeps credentials out of the automation — the OTP is provided
transiently via the operator, never stored.
2026-10-04 16:37:26 +00:00
Exit codes:
0: Success / already logged in
2: APPROVAL_NEEDED (OTP prompt reached, awaiting code)
3: NEEDS_HUMAN (multi-account selection needs manual choice)
4: NEEDS_SIGNUP (account not found / registration required)
1: General error
2026-10-03 15:57:37 +00:00
"""
2026-10-03 21:28:36 +00:00
import json , urllib . request , websocket , time , sys , argparse , importlib . util
2026-10-03 15:57:37 +00:00
2026-10-03 21:28:36 +00:00
CDP_PORT_DEFAULT = 9410
2026-10-03 15:57:37 +00:00
2026-10-03 21:28:36 +00:00
def _registry_port ( node ) :
try :
path = " /home/super/Projects/NetVM/bin/netvm-registry.py "
spec = importlib . util . spec_from_file_location ( " netvm_registry " , path )
mod = importlib . util . module_from_spec ( spec )
spec . loader . exec_module ( mod )
return mod . port_for ( node )
except Exception :
return None
2026-10-04 16:56:15 +00:00
def is_registered_in_accounts ( email ) :
path = " /home/super/Projects/NetVM/ACCOUNTS.md "
try :
with open ( path ) as f :
for line in f :
if line . startswith ( " | " ) and email . lower ( ) in line . lower ( ) :
return True
except Exception :
pass
return False
2026-10-03 21:28:36 +00:00
def get_page ( port ) :
cdp_url = " http://127.0.0.1: %s /json/list " % port
with urllib . request . urlopen ( cdp_url , timeout = 5 ) as r :
2026-10-03 15:57:37 +00:00
ts = json . load ( r )
pages = [ t for t in ts if t . get ( ' type ' ) == ' page ' ]
if not pages :
print ( " ERROR: No page found " , file = sys . stderr )
sys . exit ( 1 )
return pages [ 0 ]
def ev ( ws , expr , await_p = False ) :
ws . send ( json . dumps ( {
" id " : 1 , " method " : " Runtime.evaluate " ,
" params " : { " expression " : expr , " returnByValue " : True , " awaitPromise " : await_p }
} ) )
resp = json . loads ( ws . recv ( ) )
return resp . get ( ' result ' , { } ) . get ( ' result ' , { } ) . get ( ' value ' )
def main ( ) :
p = argparse . ArgumentParser ( )
p . add_argument ( ' --email ' , required = True )
p . add_argument ( ' --otp ' , default = None )
2026-10-03 21:28:36 +00:00
p . add_argument ( ' --node ' , default = None ,
help = ' node name: CDP port comes from the NODES.md registry ' )
p . add_argument ( ' --cdp-port ' , default = None ,
help = ' explicit CDP port (overrides --node lookup) ' )
2026-10-03 21:30:32 +00:00
p . add_argument ( ' --account-name ' , default = None ,
help = ' display-name hint for Meta multi-account selection '
' (never the " +1 " entry) ' )
2026-10-03 15:57:37 +00:00
args = p . parse_args ( )
2026-10-04 16:56:15 +00:00
if is_registered_in_accounts ( args . email ) :
2026-10-04 18:07:48 +00:00
print ( " Registry check: [redacted] is registered in ACCOUNTS.md " )
2026-10-04 16:56:15 +00:00
2026-10-03 21:28:36 +00:00
if args . cdp_port :
port = args . cdp_port
elif args . node :
port = _registry_port ( args . node ) or CDP_PORT_DEFAULT
else :
port = CDP_PORT_DEFAULT
page = get_page ( port )
2026-10-03 15:57:37 +00:00
ws = websocket . create_connection ( page [ ' webSocketDebuggerUrl ' ] , timeout = 15 )
2026-10-04 21:43:32 +00:00
# Step 1: Check if already logged in or blocked at verification gate
2026-10-03 15:57:37 +00:00
title = ev ( ws , " document.title " )
2026-10-04 21:43:32 +00:00
body = ev ( ws , " document.body.innerText.slice(0,500) " )
url = ev ( ws , " location.href " ) or " "
if " access/verification " in url or " confirm your age " in body . lower ( ) :
ig_link = ev ( ws , """ (async()=> {
try {
const r = await fetch( ' /api/hatch/age-confirmation/linking-web-auth?account_type=instagram ' , {
headers: { ' Accept ' : ' application/json ' }
});
const j = await r.json();
return j && j.url ? j.url : null;
} catch(e) { return null; }
})() """ , True )
if ig_link :
print ( f " LINK_INSTAGRAM_URL: { ig_link } " )
print ( f " NEEDS_HUMAN: Age verification required for { args . email } at { url } . Client intervention required to confirm age or link Instagram/Facebook. " , file = sys . stderr )
ws . close ( )
sys . exit ( 3 )
if ( " Connected " in body or " Chats " in body ) and " Enter your code " not in body and " access/verification " not in url :
2026-10-03 15:57:37 +00:00
print ( f " Already logged in (title: { title } ) " )
ws . close ( )
return 0
2026-10-04 21:43:32 +00:00
already_on_otp = ( " Enter your code " in body or " code we sent " in body ) and args . email in body
if not already_on_otp :
# Step 2: Click Log in (if on landing page)
if " Log in " in body :
print ( " Clicking Log in... " )
ev ( ws , """ (async()=> {
const b=[...document.querySelectorAll( ' button ' )].find(x=>x.innerText.includes( ' Log in ' ));
if(b) b.click(); return !!b;
})() """ , True )
time . sleep ( 3 )
# Step 3: Enter email
print ( " Entering email: [redacted] " )
result = ev ( ws , f """ (async()=> {{
const inp=[...document.querySelectorAll( ' input ' )].find(i=>
(i.placeholder&&i.placeholder.toLowerCase().includes( ' email ' ))||
(i.getAttribute( ' aria-label ' )&&i.getAttribute( ' aria-label ' ).toLowerCase().includes( ' email ' ))
);
if(!inp) return ' NOINPUT ' ;
inp.focus();
document.execCommand( ' insertText ' ,false, ' { args . email } ' );
await new Promise(r=>setTimeout(r,500));
return ' entered: ' +inp.value;
}} )() """ , True )
print ( " Email: [redacted] " )
if result == ' NOINPUT ' :
print ( " ERROR: Email input not found " , file = sys . stderr )
ws . close ( )
sys . exit ( 1 )
time . sleep ( 1 )
# Step 4: Click Continue
print ( " Clicking Continue... " )
2026-10-03 15:57:37 +00:00
ev ( ws , """ (async()=> {
2026-10-04 21:43:32 +00:00
const b=[...document.querySelectorAll( ' button ' )].find(x=>x.innerText.includes( ' Continue ' ));
2026-10-03 15:57:37 +00:00
if(b) b.click(); return !!b;
})() """ , True )
2026-10-04 21:43:32 +00:00
time . sleep ( 4 )
else :
print ( " Page is already on OTP prompt for this email, skipping email entry. " )
2026-10-03 15:57:37 +00:00
# Step 5: Check for OTP prompt
2026-10-04 16:56:15 +00:00
body = ev ( ws , " document.body.innerText.slice(0,500) " )
if " Enter your code " in body or " code we sent " in body or " To log in " in body or " To confirm your account " in body :
2026-10-04 18:07:48 +00:00
print ( " OTP prompt detected for [redacted] " )
2026-10-03 15:57:37 +00:00
if not args . otp :
2026-10-04 18:07:48 +00:00
print ( " APPROVAL_NEEDED: OTP required for [redacted] " )
print ( " Re-run with: signin.py --email [redacted] --otp <code> " )
2026-10-03 15:57:37 +00:00
ws . close ( )
sys . exit ( 2 ) # Approval needed
# Enter OTP
print ( " Entering OTP... " )
result = ev ( ws , f """ (async()=> {{
2026-10-04 16:56:15 +00:00
const inp=[...document.querySelectorAll( ' input ' )].find(i=>i.type=== ' text ' ||i.type=== ' number ' );
2026-10-03 15:57:37 +00:00
if(!inp) return ' NOINPUT ' ;
inp.focus();
document.execCommand( ' insertText ' ,false, ' { args . otp } ' );
await new Promise(r=>setTimeout(r,500));
return ' entered: ' +inp.value;
}} )() """ , True )
2026-10-04 18:07:48 +00:00
print ( " OTP: [redacted] " )
2026-10-03 15:57:37 +00:00
time . sleep ( 1 )
2026-10-04 16:56:15 +00:00
# Click Next/Verify/Confirm
2026-10-03 15:57:37 +00:00
print ( " Submitting OTP... " )
ev ( ws , """ (async()=> {
const b=[...document.querySelectorAll( ' button ' )].find(x=>
2026-10-04 16:56:15 +00:00
x.innerText.includes( ' Next ' )||x.innerText.includes( ' Verify ' )||x.innerText.includes( ' Confirm ' )
2026-10-03 15:57:37 +00:00
);
if(b) b.click(); return !!b;
})() """ , True )
time . sleep ( 5 )
# Verify login
2026-10-04 16:56:15 +00:00
body = ev ( ws , " document.body.innerText.slice(0,500) " )
title = ev ( ws , " document.title " ) or " "
2026-10-04 21:43:32 +00:00
url = ev ( ws , " location.href " ) or " "
# Check for post-login gates requiring human/client intervention
if " access/verification " in url or " confirm your age " in body . lower ( ) :
ig_link = ev ( ws , """ (async()=> {
try {
const r = await fetch( ' /api/hatch/age-confirmation/linking-web-auth?account_type=instagram ' , {
headers: { ' Accept ' : ' application/json ' }
});
const j = await r.json();
return j && j.url ? j.url : null;
} catch(e) { return null; }
})() """ , True )
if ig_link :
print ( f " LINK_INSTAGRAM_URL: { ig_link } " )
print ( f " NEEDS_HUMAN: Age verification required for { args . email } at { url } . Client intervention required to confirm age or link Instagram/Facebook. " , file = sys . stderr )
ws . close ( )
sys . exit ( 3 )
if " Connected " in body or " Chats " in body or ( ( " Muse " in title or " Chat " in title ) and " access/verification " not in url ) :
2026-10-04 18:07:48 +00:00
print ( f " SUCCESS: Logged in as [redacted] (title: { title } ) " )
2026-10-03 15:57:37 +00:00
ws . close ( )
return 0
2026-10-03 21:30:32 +00:00
# Multi-account selection: Meta shows one button per matching
# account plus a "+1" collapsed entry. The "+1" is NOT a real
# account — click the button whose text contains the hinted
# display name. Without a hint (or no match), a human must pick.
if " Your email matches multiple accounts " in ev (
ws , " document.body.innerText.slice(0,2000) " ) :
if not args . account_name :
print ( " NEEDS_HUMAN: multiple accounts match and no "
" --account-name hint was given " , file = sys . stderr )
ws . close ( )
sys . exit ( 3 )
picked = ev ( ws , """ (async()=> {
const want= %s .toLowerCase();
const btns=[...document.querySelectorAll( ' button ' )];
const t=btns.find(b=>b.innerText.toLowerCase().includes(want)
&& !b.innerText.includes( ' +1 ' ));
if(t) { t.click();return true;} return false;
})() """ % json . dumps ( args . account_name ) , True )
if not picked :
print ( f " NEEDS_HUMAN: no account button matched "
2026-10-04 18:07:48 +00:00
f " ' [redacted] ' " , file = sys . stderr )
2026-10-03 21:30:32 +00:00
ws . close ( )
sys . exit ( 3 )
2026-10-04 18:07:48 +00:00
print ( f " Selected account ' [redacted] ' , waiting... " )
2026-10-03 21:30:32 +00:00
time . sleep ( 5 )
2026-10-04 16:56:15 +00:00
body = ev ( ws , " document.body.innerText.slice(0,500) " )
title = ev ( ws , " document.title " ) or " "
if " Connected " in body or " Chats " in body or " Muse " in title or " Chat " in title :
2026-10-04 18:07:48 +00:00
print ( " SUCCESS: Logged in as [redacted] "
" (account: [redacted]) " )
2026-10-03 21:30:32 +00:00
ws . close ( )
return 0
print ( " WARNING: account selection did not land in chat " ,
file = sys . stderr )
ws . close ( )
sys . exit ( 1 )
2026-10-03 15:57:37 +00:00
else :
print ( f " WARNING: Login may have failed. Title: { title } " , file = sys . stderr )
2026-10-04 16:56:15 +00:00
print ( f " Body: { body [ : 150 ] } " , file = sys . stderr )
2026-10-03 15:57:37 +00:00
ws . close ( )
sys . exit ( 1 )
else :
2026-10-04 16:56:15 +00:00
err_msg = ev ( ws , """ (()=> {
const el = document.querySelector( ' [role= " alert " ], .error, [data-error] ' );
return el ? el.innerText.trim() : ' ' ;
2026-10-04 16:37:26 +00:00
})() """ )
2026-10-04 16:56:15 +00:00
if err_msg :
print ( f " ERROR: { err_msg } " , file = sys . stderr )
else :
print ( " No OTP prompt found - check page state " , file = sys . stderr )
print ( f " Body: { body [ : 200 ] } " , file = sys . stderr )
2026-10-03 15:57:37 +00:00
ws . close ( )
sys . exit ( 1 )
if __name__ == ' __main__ ' :
sys . exit ( main ( ) )