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