Add cookies.txt authentication support
All checks were successful
CI / test (3.10) (push) Successful in 1m51s
CI / test (3.11) (push) Successful in 29s
CI / test (3.12) (push) Successful in 24s

This commit is contained in:
2026-05-08 16:43:01 +02:00
parent 7c634e0d39
commit bc82333e07
5 changed files with 138 additions and 7 deletions

View File

@@ -2,7 +2,8 @@
Supports:
1. Environment variables: TWITTER_AUTH_TOKEN + TWITTER_CT0
2. Auto-extract from browser via browser-cookie3
2. Cookie file: TWITTER_COOKIE_FILE or config auth.cookieFile (Netscape cookies.txt)
3. Auto-extract from browser via browser-cookie3
Extracts ALL Twitter cookies for full browser-like fingerprint.
Prefers in-process extraction (required on macOS for Keychain access),
falls back to subprocess if in-process fails (e.g. SQLite lock).
@@ -16,6 +17,7 @@ import logging
import os
import subprocess
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
from .constants import BEARER_TOKEN, get_user_agent
@@ -97,6 +99,67 @@ def load_from_env() -> Optional[Dict[str, str]]:
return None
def _parse_cookie_file(cookie_file: Path) -> Optional[Dict[str, str]]:
"""Load Twitter cookies from a Netscape cookies.txt file."""
try:
raw = cookie_file.read_text(encoding="utf-8")
except OSError as exc:
logger.debug("Failed to read cookie file %s: %s", cookie_file, exc)
return None
all_cookies: Dict[str, str] = {}
auth_token = ""
ct0 = ""
for line in raw.splitlines():
stripped = line.strip()
if not stripped or stripped.startswith("#"):
continue
parts = line.split("\t")
if len(parts) != 7:
continue
domain = parts[0].removeprefix("#HttpOnly_")
name = parts[5]
value = parts[6]
if not _is_twitter_domain(domain) or not name:
continue
all_cookies[name] = value
if name == "auth_token":
auth_token = value
elif name == "ct0":
ct0 = value
if not auth_token or not ct0:
logger.debug(
"Cookie file %s did not contain usable Twitter auth cookies (auth_token=%s, ct0=%s)",
cookie_file,
bool(auth_token),
bool(ct0),
)
return None
cookies = {"auth_token": auth_token, "ct0": ct0}
if all_cookies:
cookies["cookie_string"] = "; ".join(f"{key}={value}" for key, value in all_cookies.items())
return cookies
def load_from_cookie_file(cookie_file: Optional[str]) -> Optional[Dict[str, str]]:
"""Load cookies from a configured cookies.txt file path."""
if not cookie_file:
return None
path = Path(cookie_file).expanduser()
if not path.exists():
logger.debug("Cookie file does not exist: %s", path)
return None
return _parse_cookie_file(path)
def verify_cookies(auth_token: str, ct0: str, cookie_string: Optional[str] = None) -> Dict[str, Any]:
"""Verify cookies by calling a Twitter API endpoint.
@@ -586,8 +649,8 @@ def extract_from_browser() -> Tuple[Optional[Dict[str, str]], List[str]]:
return cookies, all_diagnostics
def get_cookies() -> Dict[str, str]:
"""Get Twitter cookies. Priority: env vars -> browser extraction.
def get_cookies(config: Optional[Dict[str, Any]] = None) -> Dict[str, str]:
"""Get Twitter cookies. Priority: env vars -> cookie file -> browser extraction.
Raises RuntimeError if no cookies found.
"""
@@ -599,7 +662,17 @@ def get_cookies() -> Dict[str, str]:
if cookies:
logger.info("Loaded cookies from environment variables")
# 2. Try browser extraction (auto-detect)
# 2. Try cookie file from env/config
if not cookies:
auth_config = (config or {}).get("auth", {})
cookie_file = os.environ.get("TWITTER_COOKIE_FILE", "")
if not cookie_file and isinstance(auth_config, dict):
cookie_file = str(auth_config.get("cookieFile", "") or "")
cookies = load_from_cookie_file(cookie_file)
if cookies:
logger.info("Loaded cookies from cookie file %s", cookie_file)
# 3. Try browser extraction (auto-detect)
if not cookies:
logger.debug("Attempting browser cookie extraction")
cookies, diagnostics = extract_from_browser()
@@ -614,7 +687,8 @@ def get_cookies() -> Dict[str, str]:
lines.extend(" " + line for line in hint.splitlines())
lines.append("")
lines.append("Option 1: Set TWITTER_AUTH_TOKEN and TWITTER_CT0 environment variables")
lines.append("Option 2: Make sure you are logged into x.com in your browser (Arc/Chrome/Edge/Firefox/Brave)")
lines.append("Option 2: Set TWITTER_COOKIE_FILE or config auth.cookieFile to a Netscape cookies.txt export")
lines.append("Option 3: Make sure you are logged into x.com in your browser (Arc/Chrome/Edge/Firefox/Brave)")
lines.append("")
lines.append("Run 'twitter -v <command>' for debug diagnostics.")
raise AuthenticationError("\n".join(lines))