Updated twitter login

This commit is contained in:
2026-05-18 09:51:39 +00:00
parent 1a633d54e2
commit c7a623c386

View File

@@ -1,103 +1,221 @@
import argparse import argparse
from playwright.sync_api import sync_playwright import json
from tweety import Twitter import os
import shutil
import time import time
from playwright.sync_api import sync_playwright
def get_twitter_auth_token(username, password): SESSION_FILE_PERMISSIONS = 0o600
def normalize_cookie_for_playwright(cookie: dict) -> dict:
"""
Ensure cookie fields are compatible with Playwright's storage_state format.
Playwright requires 'domain' to start with a dot for cross-subdomain cookies,
and 'sameSite' must be one of 'Strict', 'Lax', or 'None'.
"""
c = dict(cookie)
# Normalize domain: x.com → .x.com
domain = c.get("domain", "")
if domain and not domain.startswith("."):
c["domain"] = f".{domain}"
# Normalize sameSite to valid Playwright values
same_site = c.get("sameSite", "")
valid_same_site = {"Strict", "Lax", "None"}
if same_site not in valid_same_site:
c["sameSite"] = "None"
# Ensure required fields have defaults
c.setdefault("path", "/")
c.setdefault("httpOnly", False)
c.setdefault("secure", True)
c.setdefault("expires", -1)
return c
def get_twitter_cookies(username: str, password: str) -> dict:
"""
Automates Twitter login via Playwright and returns a Playwright-compatible
storage_state dict (cookies + origins/localStorage).
"""
with sync_playwright() as p: with sync_playwright() as p:
print("🚀 Launching headless browser...") print("🚀 Launching headless browser...")
# Add arguments to bypass basic bot detection browser = p.chromium.launch(
browser = p.chromium.launch(headless=True, args=["--disable-blink-features=AutomationControlled"]) headless=True,
args=["--disable-blink-features=AutomationControlled"]
)
context = browser.new_context( context = browser.new_context(
user_agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" user_agent=(
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/145.0.7632.6 Safari/537.36"
),
viewport={"width": 1920, "height": 1080},
) )
page = context.new_page() page = context.new_page()
try: try:
print("🌐 Navigating to X login...") print("🌐 Navigating to X login...")
page.goto("https://x.com/i/flow/login") page.goto("https://x.com/i/flow/login", wait_until="domcontentloaded")
# 1. Fill with username # --- Username ---
print(f"👤 Entering username: {username}...") print(f"👤 Entering username: {username[:10]}...")
# Using the exact autocomplete attribute from your HTML
page.wait_for_selector('input[autocomplete="username"]', timeout=25000) page.wait_for_selector('input[autocomplete="username"]', timeout=25000)
time.sleep(10) # Wait a bit for any potential animations or dynamic content to load
# CLICK ON THE INPUT BEFORE POPULATING THE USERNAME
page.click('input[autocomplete="username"]') page.click('input[autocomplete="username"]')
time.sleep(10)
page.fill('input[autocomplete="username"]', username) page.fill('input[autocomplete="username"]', username)
# Enter and wait for the next screen print("➡️ Pressing Next...")
print("➡️ Pressing Enter to proceed...") page.locator('button:has-text("Next")').first.click()
page.keyboard.press("Enter")
# Wait a moment for the screen transition animation # --- Security challenge (email/phone) ---
time.sleep(10) page.wait_for_selector(
'input[name="password"], '
'input[data-testid="ocfEnterTextTextInput"], '
'input[name="text"]',
timeout=15000,
)
time.sleep(1)
# 2. Fill with the password if (
page.locator('input[data-testid="ocfEnterTextTextInput"]').is_visible()
or page.locator('input[name="text"]').is_visible()
):
print("🛡️ Security challenge detected — this script needs --email arg.")
raise RuntimeError(
"Security challenge appeared but no email/phone was provided. "
"Re-run with --email your_email_or_phone"
)
# --- Password ---
print("🔑 Entering password...") print("🔑 Entering password...")
# Using the exact name attribute from your HTML page.wait_for_selector('input[name="password"]', timeout=15000)
page.wait_for_selector('input[name="password"]', timeout=25000)
time.sleep(10) # Wait a bit for any potential animations or dynamic content to load
# CLICK ON THE INPUT BEFORE POPULATING THE PASSWORD
page.click('input[name="password"]')
page.fill('input[name="password"]', password) page.fill('input[name="password"]', password)
# 3. Click on Login
print("🖱️ Clicking 'Log in'...") print("🖱️ Clicking 'Log in'...")
# Targeting the exact span text you provided
page.locator('span:has-text("Log in")').first.click() page.locator('span:has-text("Log in")').first.click()
print("⏳ Waiting for login to complete...") # --- Poll for auth_token + ct0 ---
# Wait for the redirect to the home page print("🍪 Waiting for auth_token + ct0 cookies...")
page.wait_for_url("**/home", timeout=25000)
# 4. Extract Cookies
cookies = context.cookies()
auth_token = None auth_token = None
for cookie in cookies: ct0 = None
if cookie['name'] == 'auth_token': for _ in range(40):
auth_token = cookie['value'] cookies_list = context.cookies()
auth_token = next((c["value"] for c in cookies_list if c["name"] == "auth_token"), None)
ct0 = next((c["value"] for c in cookies_list if c["name"] == "ct0"), None)
if auth_token and ct0:
print("✅ Both cookies found!")
break break
page.wait_for_timeout(1000)
else:
raise TimeoutError("auth_token/ct0 cookies never appeared after 40 seconds.")
# --- Wait for page to fully settle before evaluate() ---
print("⏳ Waiting for page to stabilize...")
page.wait_for_load_state("domcontentloaded", timeout=15000)
try:
page.wait_for_selector('[data-testid="primaryColumn"]', timeout=20000)
except Exception:
print("⚠️ primaryColumn not found — page may still be usable.")
# --- Grab localStorage (non-critical) ---
try:
local_storage = page.evaluate(
"() => Object.entries(localStorage).map(([name, value]) => ({ name, value }))"
)
except Exception as ls_err:
print(f"⚠️ Could not extract localStorage (non-critical): {ls_err}")
local_storage = []
# --- Re-fetch final cookie list and normalize for Playwright ---
raw_cookies = context.cookies()
normalized_cookies = [normalize_cookie_for_playwright(c) for c in raw_cookies]
session_data = {
"cookies": normalized_cookies,
"origins": [
{
"origin": "https://x.com",
"localStorage": local_storage,
}
],
}
print(f"✅ auth_token: {auth_token[:10]}...")
print(f"✅ ct0: {ct0[:10]}...")
browser.close() browser.close()
return auth_token return session_data
except Exception as e: except Exception as e:
# IF IT FAILS, TAKE A SCREENSHOT!
print(f"❌ Error encountered. Taking a screenshot...") print(f"❌ Error encountered. Taking a screenshot...")
try:
page.screenshot(path="error_screenshot.png") page.screenshot(path="error_screenshot.png")
print("📸 Saved: error_screenshot.png")
except Exception:
pass
browser.close() browser.close()
raise e raise e
def save_session(session_data: dict, path: str):
"""Save Playwright storage_state JSON to disk with restricted permissions."""
os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
temp_path = f"{path}.tmp"
with open(temp_path, "w", encoding="utf-8") as f:
json.dump(session_data, f, indent=2)
os.replace(temp_path, path)
try:
os.chmod(path, SESSION_FILE_PERMISSIONS)
except Exception as e:
print(f"⚠️ Could not set file permissions on {path}: {e}")
print(f"💾 Session saved to: {path}")
def clear_session(path: str):
"""Delete a stale session file."""
if os.path.exists(path):
os.remove(path)
print(f"🧹 Removed stale session file: {path}")
else:
print(f" No session file found at {path} — nothing to clear.")
def main(): def main():
# Set up argument parsing parser = argparse.ArgumentParser(
parser = argparse.ArgumentParser(description="Automate Twitter login and save session token.") description="Automate Twitter login and save Playwright session state."
parser.add_argument("username", help="Your Twitter username") )
parser.add_argument("password", help="Your Twitter password") parser.add_argument("username", help="Twitter username or handle")
parser.add_argument("password", help="Twitter password")
parser.add_argument(
"--email",
default="",
help="Twitter email or phone (required if X shows a security challenge)",
)
parser.add_argument(
"--output",
default="twitter_browser_state.json", # ← matches twitter2bsky.py exactly
help="Output path for the Playwright storage state JSON",
)
parser.add_argument(
"--clear-session",
action="store_true",
help="Delete any existing session file before starting",
)
args = parser.parse_args() args = parser.parse_args()
try: if args.clear_session:
# Pass the arguments from the terminal to the function clear_session(args.output)
token = get_twitter_auth_token(args.username, args.password)
if token: session_data = get_twitter_cookies(args.username, args.password)
print("🔑 Successfully extracted auth_token!") save_session(session_data, args.output)
# Initialize the tweety session file print(f"\n✅ Done! Session saved to '{args.output}'")
app = Twitter("my_account_session") print(f" twitter2bsky.py will automatically pick it up on next run.")
print("💾 Saving session to tweety-ns...")
app.load_auth_token(token)
print("✅ Success! Your session has been saved.")
else:
print("❌ Failed to find auth_token cookie.")
except Exception as e:
print(f"❌ An error occurred: {e}")
print("📸 Check the 'error_screenshot.png' file in your folder to see what X showed the bot!")
if __name__ == "__main__": if __name__ == "__main__":
main() main()