Files
climperor/web/cloudflare/_trigger_player_sync.py
T
vosonandCursor f5b7011c45 Ship Steam login, D1 player sync, and cached「我」dashboard.
Players get a fast TTL-backed homepage (local profile / Cloudflare D1) with dense UI polish; login unlocks /home without blocking on every OpenDota refresh.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-01 01:24:30 +08:00

265 lines
8.7 KiB
Python

"""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())