"""Probe D1 for an account, enqueue login_refresh, wait, probe again. Uses Cloudflare Global API Key env (no secret echo). """ from __future__ import annotations import json import os import subprocess import sys import time import urllib.error import urllib.request from pathlib import Path HERE = Path(__file__).resolve().parent WORKER = HERE / "player-sync" ACCOUNT_ID = "510534f7f6284344aadaf2f5a0794d48" QUEUE_ID = "371f11c7b4114f9b99ab10062a38ecd7" TARGET = int(os.environ.get("CLIMPEROR_SYNC_ACCOUNT_ID", "143712136")) def _cf_env() -> dict[str, str]: env = os.environ.copy() email = env.get("CLOUDFLARE_EMAIL") or env.get("KEYZOO_ASSET_META_USERNAME") key = env.get("CLOUDFLARE_API_KEY") or env.get( "KEYZOO_ASSET_SECRET_GLOBAL_API_KEY" ) if not email or not key: raise SystemExit("missing Cloudflare credentials") env["CLOUDFLARE_EMAIL"] = email env["CLOUDFLARE_API_KEY"] = key env["CLOUDFLARE_ACCOUNT_ID"] = ACCOUNT_ID return env def d1_query(sql: str, env: dict[str, str]) -> list: cmd = ( f'npx --yes wrangler@3 d1 execute climperor-users --remote --json ' f'--command "{sql}"' ) r = subprocess.run( cmd, cwd=str(WORKER), env=env, shell=True, capture_output=True, text=True, ) if r.returncode != 0: print(r.stderr or r.stdout, file=sys.stderr) raise SystemExit(r.returncode) try: data = json.loads(r.stdout) except json.JSONDecodeError: print(r.stdout) return [] if isinstance(data, list) and data: return data[0].get("results") or [] return [] class CfApiError(RuntimeError): def __init__(self, code: int, body: str): super().__init__(f"CF API {code}") self.code = code self.body = body def cf_api(method: str, path: str, env: dict[str, str], body: dict | None = None) -> dict: url = f"https://api.cloudflare.com/client/v4{path}" data = None if body is None else json.dumps(body).encode("utf-8") req = urllib.request.Request( url, data=data, method=method, headers={ "X-Auth-Email": env["CLOUDFLARE_EMAIL"], "X-Auth-Key": env["CLOUDFLARE_API_KEY"], "Content-Type": "application/json", "User-Agent": "climperor-trigger-sync", }, ) try: with urllib.request.urlopen(req, timeout=60) as resp: return json.loads(resp.read().decode("utf-8")) except urllib.error.HTTPError as e: raw = e.read().decode("utf-8", errors="replace") raise CfApiError(e.code, raw[:1200]) from e def probe(env: dict[str, str], label: str) -> None: print(f"\n=== {label} account_id={TARGET} ===", flush=True) users = d1_query( f"SELECT account_id, personaname, last_login_at FROM users " f"WHERE account_id={TARGET}", env, ) print("users:", json.dumps(users, ensure_ascii=False), flush=True) jobs = d1_query( f"SELECT kind, status, substr(COALESCE(error,''),1,160) AS error, " f"updated_at FROM sync_jobs WHERE account_id={TARGET} " f"ORDER BY updated_at DESC LIMIT 5", env, ) print("sync_jobs:", json.dumps(jobs, ensure_ascii=False), flush=True) matches = d1_query( f"SELECT COUNT(*) AS n, " f"SUM(CASE WHEN r2_key IS NOT NULL AND r2_key != '' THEN 1 ELSE 0 END) AS r2 " f"FROM player_matches WHERE account_id={TARGET}", env, ) print("matches:", json.dumps(matches, ensure_ascii=False), flush=True) stats = d1_query( f"SELECT scope, sample, winrate FROM player_stats " f"WHERE account_id={TARGET}", env, ) print("stats:", json.dumps(stats, ensure_ascii=False), flush=True) heroes = d1_query( f"SELECT COUNT(*) AS n FROM player_heroes WHERE account_id={TARGET}", env, ) print("heroes:", json.dumps(heroes, ensure_ascii=False), flush=True) def worker_http_sync(env: dict[str, str], msg: dict) -> str: """POST Worker fetch handler. Returns 'http'.""" print("Worker HTTP sync …", flush=True) try: cf_api( "POST", f"/accounts/{ACCOUNT_ID}/workers/scripts/climperor-player-sync/subdomain", env, {"enabled": True}, ) except CfApiError as e: print(f"enable subdomain: {e.code} {e.body[:400]}", flush=True) sub_name = "" try: sub = cf_api("GET", f"/accounts/{ACCOUNT_ID}/workers/subdomain", env) sub_name = ((sub.get("result") or {}).get("subdomain") or "").strip() except CfApiError as e: print(f"get subdomain: {e.code} {e.body[:400]}", flush=True) if not sub_name: raise SystemExit("cannot resolve workers.dev subdomain") url = f"https://climperor-player-sync.{sub_name}.workers.dev/" print(f"POST {url}", flush=True) data = json.dumps(msg).encode("utf-8") req = urllib.request.Request( url, data=data, method="POST", headers={ "Content-Type": "application/json", "User-Agent": "climperor-trigger-sync", }, ) try: with urllib.request.urlopen(req, timeout=120) as resp: raw = resp.read().decode("utf-8", errors="replace") print(f"worker HTTP {resp.status} len={len(raw)}", flush=True) print(raw[:800], flush=True) return "http" except urllib.error.HTTPError as e: raw = e.read().decode("utf-8", errors="replace") print(f"worker HTTP {e.code}: {raw[:800]}", file=sys.stderr) raise SystemExit(1) from e except urllib.error.URLError as e: print(f"worker URL error: {e}", file=sys.stderr) raise SystemExit(1) from e def enqueue(env: dict[str, str]) -> str: """Enqueue or HTTP-sync. Returns 'queue' | 'http'.""" steamid = str(TARGET + 76561197960265728) msg = { "kind": "login_refresh", "account_id": TARGET, "steamid": steamid, "personaname": "refining", } if os.environ.get("CLIMPEROR_FORCE_HTTP_SYNC", "").strip() in ( "1", "true", "yes", ): return worker_http_sync(env, msg) print("\nenqueue login_refresh …", flush=True) # https://developers.cloudflare.com/queues/configuration/javascript-apis/#producer # HTTP: POST /accounts/:account_id/queues/:queue_id/messages attempts = [ ( f"/accounts/{ACCOUNT_ID}/queues/{QUEUE_ID}/messages", {"body": msg}, ), ( f"/accounts/{ACCOUNT_ID}/queues/{QUEUE_ID}/messages", {"messages": [{"body": json.dumps(msg)}]}, ), ( f"/accounts/{ACCOUNT_ID}/queues/{QUEUE_ID}/messages/batch", {"messages": [{"body": msg}]}, ), ( f"/accounts/{ACCOUNT_ID}/queues/{QUEUE_ID}/messages/batch", {"messages": [{"body": json.dumps(msg)}]}, ), ] for path, body in attempts: try: out = cf_api("POST", path, env, body) except CfApiError as e: print(f"try {path.split('/')[-1]} HTTP {e.code}: {e.body[:400]}", flush=True) continue ok = bool(out.get("success")) print(f"try {path.split('/')[-1]} success={ok}", flush=True) if ok: return "queue" print(json.dumps(out, ensure_ascii=False)[:600], flush=True) # Last resort: enable workers.dev and POST the Worker fetch handler. return worker_http_sync(env, msg) def check_worker(env: dict[str, str]) -> None: out = cf_api("GET", f"/accounts/{ACCOUNT_ID}/workers/scripts", env) names = [r.get("id") or r.get("name") for r in (out.get("result") or [])] print("workers:", ", ".join(n for n in names if n)[:500], flush=True) has = "climperor-player-sync" in names print(f"climperor-player-sync deployed={has}", flush=True) def main() -> int: env = _cf_env() check_worker(env) probe(env, "before") mode = enqueue(env) waits = 2 if mode == "http" else 8 for i in range(1, waits + 1): delay = 3 if mode == "http" else 8 time.sleep(delay) probe(env, f"after wait #{i} ({i * delay}s)") users = d1_query( f"SELECT account_id FROM users WHERE account_id={TARGET}", env ) stats = d1_query( f"SELECT scope FROM player_stats WHERE account_id={TARGET}", env ) filled = d1_query( f"SELECT scope, sample FROM player_stats WHERE account_id={TARGET} " f"AND sample > 0", env, ) if users and filled: print("\nSYNC OK — user + non-empty stats", flush=True) return 0 print("\nSYNC PENDING/FAILED — no non-empty stats after waits", flush=True) return 1 if __name__ == "__main__": raise SystemExit(main())