Updated twitter login
This commit is contained in:
236
twitter_login.py
236
twitter_login.py
@@ -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...")
|
||||||
page.screenshot(path="error_screenshot.png")
|
try:
|
||||||
|
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()
|
||||||
Reference in New Issue
Block a user