Use grok-register.zip getTurnstileToken/fill_profile and pure MV2 turnstilePatch. Browser options match ReinerBRO headed flags. Local gmail worker + IMAP + defaultDomains catch-all stay intact.
346 lines
13 KiB
Python
346 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""Replace local Turnstile/browser CF path with pure zip implementations.
|
|
|
|
Keeps local Gmail / hotmail / register_cli / cpa_xai intact.
|
|
Source of truth: grok-register.zip (ReinerBRO) for CF + browser options.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(r"D:/Projects/AI/grok-keygen")
|
|
# Git Bash /tmp maps to %TEMP% on Windows
|
|
_TEMP = Path(os.environ.get("TEMP") or os.environ.get("TMP") or r"C:/Users/Chaos/AppData/Local/Temp")
|
|
ZIP_REG = _TEMP / "zip_full/grok-register/grok_register_ttk.py"
|
|
ZIP_CPA = _TEMP / "zip_full/grok_reg-protocol_cpa/grok_register_ttk.py"
|
|
ZIP_PATCH = _TEMP / "zip_full/grok-register/turnstilePatch"
|
|
LOCAL = ROOT / "grok_register_ttk.py"
|
|
LOCAL_PATCH = ROOT / "turnstilePatch"
|
|
|
|
|
|
def extract_func(text: str, name: str) -> str:
|
|
lines = text.splitlines(keepends=True)
|
|
starts = {}
|
|
for i, line in enumerate(lines):
|
|
m = re.match(r"^def (\w+)\(", line)
|
|
if m:
|
|
starts[m.group(1)] = i
|
|
m = re.match(r"^([A-Z_][A-Z0-9_]*)\s*=", line)
|
|
if m:
|
|
starts[m.group(1)] = i
|
|
if name not in starts:
|
|
raise KeyError(f"missing {name}")
|
|
s = starts[name]
|
|
e = len(lines)
|
|
for j in range(s + 1, len(lines)):
|
|
if re.match(r"^(def |class |[A-Z_][A-Z0-9_]*\s*=)", lines[j]):
|
|
e = j
|
|
break
|
|
return "".join(lines[s:e]).rstrip() + "\n\n\n"
|
|
|
|
|
|
def replace_func(src: str, name: str, new_body: str) -> str:
|
|
"""Replace a top-level def/assignment block by name."""
|
|
lines = src.splitlines(keepends=True)
|
|
start = None
|
|
for i, line in enumerate(lines):
|
|
if re.match(rf"^def {re.escape(name)}\(", line) or re.match(
|
|
rf"^{re.escape(name)}\s*=", line
|
|
):
|
|
start = i
|
|
break
|
|
if start is None:
|
|
raise KeyError(f"local missing {name}")
|
|
end = len(lines)
|
|
for j in range(start + 1, len(lines)):
|
|
if re.match(r"^(def |class |[A-Z_][A-Z0-9_]*\s*=)", lines[j]):
|
|
end = j
|
|
break
|
|
# preserve trailing blank style: ensure exactly two newlines before next
|
|
body = new_body
|
|
if not body.endswith("\n"):
|
|
body += "\n"
|
|
# normalize to end with two blank lines if next block exists
|
|
while body.endswith("\n\n\n"):
|
|
body = body[:-1]
|
|
if not body.endswith("\n\n"):
|
|
body = body.rstrip("\n") + "\n\n"
|
|
return "".join(lines[:start]) + body + "".join(lines[end:])
|
|
|
|
|
|
def remove_func(src: str, name: str) -> str:
|
|
lines = src.splitlines(keepends=True)
|
|
start = None
|
|
for i, line in enumerate(lines):
|
|
if re.match(rf"^def {re.escape(name)}\(", line) or re.match(
|
|
rf"^{re.escape(name)}\s*=", line
|
|
):
|
|
start = i
|
|
break
|
|
if start is None:
|
|
return src
|
|
end = len(lines)
|
|
for j in range(start + 1, len(lines)):
|
|
if re.match(r"^(def |class |[A-Z_][A-Z0-9_]*\s*=)", lines[j]):
|
|
end = j
|
|
break
|
|
return "".join(lines[:start]) + "".join(lines[end:])
|
|
|
|
|
|
def adapt_reg_get_turnstile(zip_fn: str) -> str:
|
|
"""grok-register uses session.page; local uses _get_page()."""
|
|
text = zip_fn
|
|
text = text.replace(
|
|
"def getTurnstileToken(session, log_callback=None, cancel_callback=None):\n page = session.page\n",
|
|
"def getTurnstileToken(log_callback=None, cancel_callback=None, max_rounds: int = 20):\n page = _get_page()\n",
|
|
)
|
|
# support max_rounds
|
|
text = text.replace(
|
|
"for _ in range(0, 20):",
|
|
"rounds = max(1, int(max_rounds or 20))\n for _ in range(0, rounds):",
|
|
)
|
|
# local uses human_sleep/cf_sleep; zip uses sleep_with_cancel — keep sleep_with_cancel (exists)
|
|
return text
|
|
|
|
|
|
def adapt_reg_fill_profile(zip_fn: str) -> str:
|
|
"""Adapt fill_profile from grok-register to local globals."""
|
|
text = zip_fn
|
|
text = text.replace(
|
|
"def fill_profile_and_submit(session, timeout=120, log_callback=None, cancel_callback=None):\n page = session.page\n",
|
|
"def fill_profile_and_submit(timeout=120, log_callback=None, cancel_callback=None):\n page = _get_page()\n check_timeout(time.time())\n dump_state(page, \"profile-form\")\n take_screenshot(page, \"profile-form\")\n",
|
|
)
|
|
# fix getTurnstileToken(session, ...) -> getTurnstileToken(...)
|
|
text = text.replace(
|
|
"token = getTurnstileToken(session, log_callback=log_callback, cancel_callback=cancel_callback)",
|
|
"token = getTurnstileToken(log_callback=log_callback, cancel_callback=cancel_callback)",
|
|
)
|
|
# add zip warmup like protocol_cpa
|
|
if "预热 Turnstile" not in text:
|
|
text = text.replace(
|
|
"given_name, family_name, password = build_profile()\n deadline = time.time() + timeout\n",
|
|
"given_name, family_name, password = build_profile()\n"
|
|
" if log_callback:\n"
|
|
" log_callback(\"[*] 预热 Turnstile...\")\n"
|
|
" human_sleep(2, cancel_callback)\n"
|
|
" deadline = time.time() + timeout\n",
|
|
)
|
|
return text
|
|
|
|
|
|
def adapt_create_browser_options(zip_fn: str) -> str:
|
|
"""Use zip minimal options; keep local headless/proxy helpers lightly."""
|
|
# Prefer ReinerBRO pure options (no disable-images). Keep proxy + headless glue.
|
|
return '''def create_browser_options(headless=None):
|
|
"""Zip-style Chromium options (grok-register / ReinerBRO).
|
|
|
|
Critical for CF:
|
|
- always load turnstilePatch
|
|
- no --disable-images
|
|
- no browser_path unless config/env set
|
|
- no AutomationControlled / heavy stealth flags on headed
|
|
"""
|
|
options = ChromiumOptions()
|
|
options.auto_port()
|
|
options.set_timeouts(base=1)
|
|
# ReinerBRO headed flags
|
|
for flag in (
|
|
"--no-first-run",
|
|
"--no-default-browser-check",
|
|
"--disable-background-networking",
|
|
"--disable-component-update",
|
|
"--disable-background-timer-throttling",
|
|
"--disable-backgrounding-occluded-windows",
|
|
"--disable-renderer-backgrounding",
|
|
):
|
|
options.set_argument(flag)
|
|
|
|
use_headless = should_use_headless(headless, log_hint=True)
|
|
if use_headless:
|
|
apply_headless_to_options(options, True)
|
|
for flag in (
|
|
"--no-sandbox",
|
|
"--disable-dev-shm-usage",
|
|
"--disable-gpu",
|
|
"--mute-audio",
|
|
):
|
|
options.set_argument(flag)
|
|
print(" [browser] headless=true", flush=True)
|
|
else:
|
|
print(" [browser] headless=false (zip headed)", flush=True)
|
|
|
|
# system default unless override
|
|
browser_path = resolve_browser_path()
|
|
if browser_path:
|
|
try:
|
|
options.set_browser_path(browser_path)
|
|
print(f" [browser] path={browser_path}", flush=True)
|
|
except Exception as exc:
|
|
print(f" [browser] set path failed: {exc}", flush=True)
|
|
else:
|
|
print(" [browser] path=system-default (zip style)", flush=True)
|
|
|
|
# Force UA from config (Chrome/138 etc.)
|
|
try:
|
|
ua = get_user_agent()
|
|
if ua:
|
|
options.set_user_agent(ua)
|
|
print(f" [browser] ua={ua[:60]}...", flush=True)
|
|
except Exception:
|
|
pass
|
|
|
|
if os.path.exists(EXTENSION_PATH):
|
|
try:
|
|
options.add_extension(EXTENSION_PATH)
|
|
print(f" [ext] turnstilePatch loaded: {EXTENSION_PATH}", flush=True)
|
|
except Exception as ext_exc:
|
|
print(f" [ext] turnstilePatch load failed: {ext_exc}", flush=True)
|
|
else:
|
|
print(f" [ext] turnstilePatch missing: {EXTENSION_PATH}", flush=True)
|
|
|
|
proxy = (config.get("proxy") or "").strip() if isinstance(config, dict) else ""
|
|
if proxy:
|
|
try:
|
|
from urllib.parse import urlparse
|
|
|
|
u = urlparse(proxy if "://" in proxy else f"http://{proxy}")
|
|
host = u.hostname or ""
|
|
if host:
|
|
port = u.port or (443 if (u.scheme or "http") == "https" else 80)
|
|
scheme = u.scheme or "http"
|
|
options.set_argument(f"--proxy-server={scheme}://{host}:{port}")
|
|
print(f" [proxy] browser --proxy-server={scheme}://{host}:{port}", flush=True)
|
|
except Exception as e:
|
|
print(f" [proxy] set browser proxy failed: {e}", flush=True)
|
|
return options
|
|
|
|
|
|
'''
|
|
|
|
|
|
def main() -> None:
|
|
if not ZIP_REG.is_file():
|
|
raise SystemExit(f"missing {ZIP_REG}")
|
|
reg_text = ZIP_REG.read_text(encoding="utf-8", errors="replace")
|
|
local = LOCAL.read_text(encoding="utf-8", errors="replace")
|
|
|
|
# backup
|
|
bak = ROOT / "grok_register_ttk.py.bak_pre_zip_port"
|
|
shutil.copy2(LOCAL, bak)
|
|
print(f"backup -> {bak}")
|
|
|
|
# restore pure zip turnstilePatch
|
|
if ZIP_PATCH.is_dir():
|
|
LOCAL_PATCH.mkdir(exist_ok=True)
|
|
for name in ("content.js", "manifest.json"):
|
|
src = ZIP_PATCH / name
|
|
if src.is_file():
|
|
shutil.copy2(src, LOCAL_PATCH / name)
|
|
print(f"copied {name}")
|
|
# remove MV3 leftovers
|
|
for stale in ("script.js",):
|
|
p = LOCAL_PATCH / stale
|
|
if p.exists():
|
|
p.unlink()
|
|
print(f"removed {stale}")
|
|
|
|
get_ts = adapt_reg_get_turnstile(extract_func(reg_text, "getTurnstileToken"))
|
|
fill = adapt_reg_fill_profile(extract_func(reg_text, "fill_profile_and_submit"))
|
|
create_opts = adapt_create_browser_options("")
|
|
|
|
# Remove interactive-safe helpers that zip doesn't need (keep if referenced elsewhere carefully)
|
|
for helper in (
|
|
"_click_turnstile_widget",
|
|
"_read_turnstile_token_simple",
|
|
"_turnstile_ui_state",
|
|
"_read_turnstile_token",
|
|
"_turnstile_diag",
|
|
):
|
|
before = local
|
|
local = remove_func(local, helper)
|
|
if local != before:
|
|
print(f"removed helper {helper}")
|
|
|
|
local = replace_func(local, "getTurnstileToken", get_ts)
|
|
print("replaced getTurnstileToken")
|
|
local = replace_func(local, "fill_profile_and_submit", fill)
|
|
print("replaced fill_profile_and_submit")
|
|
local = replace_func(local, "create_browser_options", create_opts)
|
|
print("replaced create_browser_options")
|
|
|
|
# Simplify resolve_browser_path to zip style (auto -> None)
|
|
resolve_browser = '''def resolve_browser_path():
|
|
"""Zip style: only return path if config/env override exists.
|
|
|
|
browser_prefer=auto/system → None (DrissionPage system Chrome).
|
|
"""
|
|
override = str((config.get("browser_path") if isinstance(config, dict) else "") or "").strip()
|
|
if override:
|
|
norm = _normalize_browser_path(override)
|
|
if norm:
|
|
return norm
|
|
print(f" [browser] ignore invalid browser_path={override!r}", flush=True)
|
|
|
|
for env_key in ("BROWSER_PATH", "CHROME_PATH", "CHROMIUM_PATH"):
|
|
env_path = (os.environ.get(env_key) or "").strip()
|
|
if not env_path:
|
|
continue
|
|
norm = _normalize_browser_path(env_path)
|
|
if norm:
|
|
return norm
|
|
print(f" [browser] ignore invalid {env_key}={env_path!r}", flush=True)
|
|
|
|
prefer = str((config.get("browser_prefer") if isinstance(config, dict) else "") or "auto").strip().lower()
|
|
if prefer in ("", "default", "detect", "system"):
|
|
prefer = "auto"
|
|
env_pref = (os.environ.get("BROWSER_PREFER") or "").strip().lower()
|
|
if env_pref:
|
|
prefer = env_pref
|
|
|
|
if prefer in ("auto", "system"):
|
|
return None
|
|
if prefer in ("chrome", "google-chrome", "google"):
|
|
order = list(CHROME_CANDIDATES) + list(CHROMIUM_CANDIDATES)
|
|
elif prefer in ("chromium", "chromeium"):
|
|
order = list(CHROMIUM_CANDIDATES) + list(CHROME_CANDIDATES)
|
|
else:
|
|
return None
|
|
return _first_existing_browser(order)
|
|
|
|
|
|
'''
|
|
local = replace_func(local, "resolve_browser_path", resolve_browser)
|
|
print("replaced resolve_browser_path")
|
|
|
|
# Force stealth off by default in apply_stealth_patches
|
|
local = re.sub(
|
|
r'mode = str\(\(config\.get\("stealth_mode"\).*?or "\w+"\)\.strip\(\)\.lower\(\)',
|
|
'mode = str((config.get("stealth_mode") if isinstance(config, dict) else "") or "off").strip().lower()',
|
|
local,
|
|
count=1,
|
|
)
|
|
|
|
# Ensure fill_profile / other call sites don't reference removed helpers
|
|
# getTurnstileToken(..., max_rounds=) still ok
|
|
if "_turnstile_ui_state" in local or "_click_turnstile_widget" in local:
|
|
print("WARN: leftover helper references:")
|
|
for i, line in enumerate(local.splitlines(), 1):
|
|
if "_turnstile_ui_state" in line or "_click_turnstile_widget" in line or "_read_turnstile_token" in line:
|
|
print(f" L{i}: {line.strip()[:120]}")
|
|
|
|
LOCAL.write_text(local, encoding="utf-8")
|
|
print(f"wrote {LOCAL} ({len(local.splitlines())} lines)")
|
|
|
|
# syntax check
|
|
import py_compile
|
|
|
|
py_compile.compile(str(LOCAL), doraise=True)
|
|
print("syntax OK")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|