diff --git a/tools/create_session_browser.py b/tools/create_session_browser.py index eb3936b..65ffeab 100644 --- a/tools/create_session_browser.py +++ b/tools/create_session_browser.py @@ -23,118 +23,353 @@ Output: import asyncio import json import os +import shutil import sys +import tempfile -import nodriver as uc +import zendriver as zd +from zendriver import cdp import pyotp -async def login_and_get_cookies(username, password, totp_seed=None, headless=False): - """Authenticate with X.com and extract session cookies""" - # Note: headless mode may increase detection risk from bot-detection systems - browser = await uc.start(headless=headless) - tab = await browser.get("https://x.com/i/flow/login") +# Disable password manager to prevent the "Save password?" bubble from +# stealing focus during automated login. +_SEED_PREFS = { + "credentials_enable_service": False, + "profile": {"password_manager_enabled": False}, +} +_BROWSER_ARGS = [ + "--password-store=basic", + "--no-first-run", + "--no-default-browser-check", + "--disable-notifications", +] + +def _log(*a): + print(*a, file=sys.stderr, flush=True) + + +def _make_profile(): + """Create a temp Chrome profile with password manager disabled.""" + profile = tempfile.mkdtemp(prefix="xsess_") + default = os.path.join(profile, "Default") + os.makedirs(default) + with open(os.path.join(default, "Preferences"), "w") as f: + json.dump(_SEED_PREFS, f) + return profile + + +def _extract_user_id(cookies_dict): + """Extract numeric user ID from the twid cookie.""" + twid = cookies_dict.get("twid", "").strip('"') + for prefix in ("u%3D", "u="): + if prefix in twid: + return twid.split(prefix)[1].split("&")[0].strip('"') + return None + + +async def _check_login_error(tab): + """Check if the login flow is showing an error (wrong password, etc.).""" try: - # Enter username - print(f"[*] Entering username {username}...", file=sys.stderr) + return await tab.evaluate('''(() => { + // Check role="alert" elements (X's standard error display) + const alert = document.querySelector('[role="alert"]'); + if (alert) { + const t = alert.textContent.trim(); + if (t.length > 0 && t.length < 200) return t; + } + // Check for common error strings in visible text + for (const el of document.querySelectorAll('p, span, div')) { + const t = el.textContent.trim(); + if (t.length > 5 && t.length < 150 + && (t.includes('Wrong password') + || t.includes('incorrect') + || t.includes('Could not log you in') + || t.includes("can\\'t find") + || t.includes('cannot find') + || t.includes('suspended') + || t.includes('locked') + || t.includes('unusual login'))) { + return t; + } + } + return ''; + })()''') + except Exception: + return '' - retry = 0 - while retry < 5: - username_input = await tab.find( - 'input[autocomplete="username"]', timeout=10 - ) - pos = await username_input.get_position() - await tab.mouse_move(pos.x, pos.y, steps=50, flash=True) - await asyncio.sleep(0.1) +async def _click_continue(tab): + """Click the 'Continue' / 'Log in' button in the jf onboarding flow. - await username_input.click() - await asyncio.sleep(0.5) - await username_input.send_keys(username) - await asyncio.sleep(0.2) - await username_input.send_keys("\n") - await asyncio.sleep(2) + The button is a nested
containing

Continue

(or

Log in

), + not a standard