Overwrite previous grok-keygen contents with the local registration/CPA protocol codebase (secrets and runtime data excluded).
197 lines
6.7 KiB
Python
197 lines
6.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Batch mint CPA xai-*.json from register accounts_cli.txt.
|
|
|
|
Default: headed Chromium + turnstilePatch (headless is Cloudflare-blocked on
|
|
accounts.x.ai). Token poll is source of truth; consent Allow uses real click.
|
|
|
|
Example (from grok_reg project root):
|
|
export DISPLAY=:0
|
|
uv run python -u scripts/backfill_cpa_xai_from_accounts.py \\
|
|
--limit 1 --probe
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import shutil
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
_ROOT = Path(__file__).resolve().parents[1]
|
|
if str(_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(_ROOT))
|
|
|
|
from cpa_xai import existing_cpa_emails, mint_and_export, parse_accounts_file # noqa: E402
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(description=__doc__)
|
|
ap.add_argument(
|
|
"--accounts",
|
|
default=str(_ROOT / "accounts_cli.txt"),
|
|
)
|
|
ap.add_argument(
|
|
"--out-dir",
|
|
default=str(_ROOT / "cpa_auths"),
|
|
help="Primary output under register machine",
|
|
)
|
|
ap.add_argument(
|
|
"--cpa-dir",
|
|
default="",
|
|
help="Optional CPA hot-load auth-dir; files are copied here after success",
|
|
)
|
|
ap.add_argument("--limit", type=int, default=0, help="0 = all missing")
|
|
ap.add_argument("--offset", type=int, default=0)
|
|
ap.add_argument("--email", default="", help="Only this email")
|
|
ap.add_argument("--skip-existing", action="store_true", default=True)
|
|
ap.add_argument("--no-skip-existing", action="store_false", dest="skip_existing")
|
|
ap.add_argument(
|
|
"--headless",
|
|
action="store_true",
|
|
default=False,
|
|
help="Headless Chromium (usually blocked by Cloudflare on accounts.x.ai)",
|
|
)
|
|
ap.add_argument(
|
|
"--headed",
|
|
action="store_true",
|
|
default=True,
|
|
help="Show browser (default; required for stable device consent)",
|
|
)
|
|
ap.add_argument("--probe", action="store_true", default=True)
|
|
ap.add_argument("--no-probe", action="store_false", dest="probe")
|
|
ap.add_argument("--probe-chat", action="store_true", default=False)
|
|
ap.add_argument(
|
|
"--proxy",
|
|
default="",
|
|
help="Outbound proxy. Empty → read register config.json cpa_proxy/proxy, else env",
|
|
)
|
|
ap.add_argument(
|
|
"--config",
|
|
default=str(_ROOT / "config.json"),
|
|
help="register config.json for cpa_proxy/proxy defaults",
|
|
)
|
|
ap.add_argument("--timeout", type=float, default=300.0)
|
|
ap.add_argument("--sleep", type=float, default=3.0, help="Sleep between accounts")
|
|
ap.add_argument(
|
|
"--fail-log",
|
|
default=str(_ROOT / "cpa_auths" / "backfill_failed.jsonl"),
|
|
help="Append failures JSONL",
|
|
)
|
|
ap.add_argument(
|
|
"--force-standalone",
|
|
action="store_true",
|
|
default=True,
|
|
help="Always open fresh Chromium (default)",
|
|
)
|
|
args = ap.parse_args()
|
|
|
|
if args.headless:
|
|
args.headed = False
|
|
else:
|
|
args.headless = False
|
|
|
|
# Resolve proxy: CLI > config cpa_proxy/proxy > env
|
|
if not args.proxy:
|
|
try:
|
|
cfg_path = Path(args.config)
|
|
if cfg_path.is_file():
|
|
cfg = json.loads(cfg_path.read_text(encoding="utf-8"))
|
|
if isinstance(cfg, dict):
|
|
cfg = {
|
|
k: v
|
|
for k, v in cfg.items()
|
|
if not (isinstance(k, str) and (k.startswith("//") or k.startswith("#")))
|
|
}
|
|
args.proxy = (cfg.get("cpa_proxy") or cfg.get("proxy") or "").strip()
|
|
if not args.cpa_dir:
|
|
args.cpa_dir = (cfg.get("cpa_hotload_dir") or "").strip()
|
|
except Exception as e: # noqa: BLE001
|
|
print(f"warn: read config proxy failed: {e}", flush=True)
|
|
if not args.proxy:
|
|
args.proxy = (
|
|
os.environ.get("https_proxy")
|
|
or os.environ.get("HTTPS_PROXY")
|
|
or os.environ.get("http_proxy")
|
|
or ""
|
|
).strip()
|
|
print(f"proxy={args.proxy or '(none)'}", flush=True)
|
|
|
|
accounts = parse_accounts_file(args.accounts)
|
|
if args.email:
|
|
accounts = [a for a in accounts if a.email.lower() == args.email.lower()]
|
|
accounts = accounts[args.offset :]
|
|
|
|
have = set()
|
|
if args.skip_existing:
|
|
have |= {e.lower() for e in existing_cpa_emails(args.out_dir)}
|
|
if args.cpa_dir:
|
|
have |= {e.lower() for e in existing_cpa_emails(args.cpa_dir)}
|
|
|
|
todo = []
|
|
for a in accounts:
|
|
if args.skip_existing and a.email.lower() in have:
|
|
continue
|
|
todo.append(a)
|
|
if args.limit and len(todo) >= args.limit:
|
|
break
|
|
|
|
print(f"accounts total={len(parse_accounts_file(args.accounts))} todo={len(todo)} out={args.out_dir}")
|
|
Path(args.out_dir).mkdir(parents=True, exist_ok=True)
|
|
if args.cpa_dir:
|
|
Path(args.cpa_dir).mkdir(parents=True, exist_ok=True)
|
|
|
|
ok_n = fail_n = 0
|
|
results = []
|
|
for i, acc in enumerate(todo, 1):
|
|
print(f"\n=== [{i}/{len(todo)}] {acc.email} ===", flush=True)
|
|
|
|
def log(msg: str, _email=acc.email) -> None:
|
|
print(f"[{time.strftime('%H:%M:%S')}] [{_email}] {msg}", flush=True)
|
|
|
|
r = mint_and_export(
|
|
email=acc.email,
|
|
password=acc.password,
|
|
auth_dir=args.out_dir,
|
|
page=None,
|
|
proxy=args.proxy or None,
|
|
headless=args.headless,
|
|
probe=args.probe,
|
|
probe_chat=args.probe_chat,
|
|
browser_timeout_sec=args.timeout,
|
|
force_standalone=args.force_standalone,
|
|
sso=acc.sso or None,
|
|
prefer_protocol=True,
|
|
log=log,
|
|
)
|
|
results.append(r)
|
|
if r.get("ok") and r.get("path"):
|
|
ok_n += 1
|
|
# mirror into CPA auth-dir
|
|
if args.cpa_dir:
|
|
src = Path(r["path"])
|
|
dst = Path(args.cpa_dir) / src.name
|
|
shutil.copy2(src, dst)
|
|
os.chmod(dst, 0o600)
|
|
print(f"copied -> {dst}", flush=True)
|
|
else:
|
|
fail_n += 1
|
|
if args.fail_log:
|
|
Path(args.fail_log).parent.mkdir(parents=True, exist_ok=True)
|
|
with open(args.fail_log, "a", encoding="utf-8") as f:
|
|
f.write(json.dumps(r, ensure_ascii=False) + "\n")
|
|
if args.sleep and i < len(todo):
|
|
time.sleep(args.sleep)
|
|
|
|
print(f"\n=== done ok={ok_n} fail={fail_n} ===", flush=True)
|
|
summary = Path(args.out_dir) / f"backfill_summary_{int(time.time())}.json"
|
|
summary.write_text(json.dumps(results, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
print(f"summary {summary}")
|
|
return 0 if ok_n > 0 or not todo else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|