Files
miniapp_qr_portable_bundle/miniapp_qr_poc.py
Logic 29d1ad71ab
Some checks failed
docker-cicd / build-and-push (push) Failing after 16s
Initial commit
2026-03-21 18:47:00 +08:00

187 lines
5.3 KiB
Python

import argparse
import base64
import json
import os
import re
import subprocess
import urllib.request
from datetime import datetime
from pathlib import Path
from zoneinfo import ZoneInfo
API_URL = "https://jsh.szsentry.com/api//mine/one_see_secret_word"
BASE_DIR = Path(__file__).resolve().parent
HELPER_JS = BASE_DIR / "js" / "21D38FE5F73FD4DF47B5E7E2FB120D83.js"
QR_JS = BASE_DIR / "js" / "0C723952F73FD4DF6A145155C4220D83.js"
FOUND_IDS_FILE = BASE_DIR / "found_ids.txt"
FOUND_ID_PATTERN = re.compile(r"ID:\s*(\d+)\s*\|\s*Village:\s*(.+)")
DEFAULT_TIMEZONE = ZoneInfo(os.getenv("MINIAPP_QR_TIMEZONE", "Asia/Shanghai"))
def fetch(member_id: int):
body = json.dumps({"member_id": member_id}).encode()
req = urllib.request.Request(
API_URL,
data=body,
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=20) as resp:
return json.loads(resp.read().decode("utf-8"))
def get_command(qr_hex: str, dt: datetime | None = None) -> bytes:
if dt is None:
dt = datetime.now(DEFAULT_TIMEZONE)
elif dt.tzinfo is None:
dt = dt.replace(tzinfo=DEFAULT_TIMEZONE)
else:
dt = dt.astimezone(DEFAULT_TIMEZONE)
c = [
f"{dt.year % 100:02d}",
f"{dt.month:02d}",
f"{dt.day:02d}",
f"{dt.hour:02d}",
f"{dt.minute:02d}",
f"{dt.second:02d}",
]
m = [int(x, 16) for x in c]
s = m[0] ^ m[2]
s = (((s + m[4]) ^ m[1]) + m[3]) ^ m[5]
s &= 0xFF
pairs = re.findall(r"[0-9A-Fa-f]{2}", qr_hex)
g = [f"{(int(x, 16) ^ s):02X}" for x in pairs]
f = ("".join(c) + "".join(g)).upper()
d = [ord(ch) for ch in f]
h = [0x03] + d + [0x04]
chk = 0
for x in h:
chk ^= x
chk_hex = f"{chk:02X}"
h.extend(ord(ch) for ch in chk_hex)
h.append(0x0D)
return bytes(h)
def render_with_bundled_js(qr_hex: str) -> str:
js = f'''
const helper = require({json.dumps(str(HELPER_JS))});
const qr = require({json.dumps(str(QR_JS))});
const out = qr.QR(helper.getCommand({json.dumps(qr_hex)}).toString());
console.log(out);
'''
cp = subprocess.run(["node", "-e", js], capture_output=True, text=True)
if cp.returncode != 0:
raise RuntimeError(f"node failed\nstdout:\n{cp.stdout}\nstderr:\n{cp.stderr}")
return cp.stdout.strip()
def save_data_url(data_url: str, out_path: str):
m = re.match(r"data:.*?;base64,(.*)", data_url)
if not m:
raise ValueError("unexpected data URL")
raw = base64.b64decode(m.group(1))
with open(out_path, "wb") as f:
f.write(raw)
def load_found_ids(path: str | Path = FOUND_IDS_FILE) -> list[dict]:
path = Path(path)
items = []
seen = set()
if not path.exists():
return items
with path.open("r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
m = FOUND_ID_PATTERN.match(line)
if not m:
continue
member_id = int(m.group(1))
if member_id in seen:
continue
seen.add(member_id)
village_name = m.group(2).strip()
items.append(
{
"member_id": member_id,
"village_name": village_name,
"label": f"{member_id} | {village_name}",
}
)
return items
def summarize_response(resp: dict) -> dict:
data = resp.get("data") or {}
return {
"status": resp.get("status"),
"msg": resp.get("msg"),
"member_id": data.get("member_id"),
"village_name": data.get("village_name"),
"uuid": data.get("uuid"),
"qr_prefix": str(data.get("qr", ""))[:40],
}
def generate_member_qr(member_id: int, include_data_url: bool = False) -> dict:
resp = fetch(member_id)
data = resp.get("data") or {}
qr_hex = data.get("qr")
if not qr_hex:
raise ValueError(f"member_id={member_id} did not return qr data")
payload = get_command(qr_hex)
result = summarize_response(resp)
result.update(
{
"qr_hex": qr_hex,
"payload_len": len(payload),
"payload_hex_prefix": payload.hex()[:160],
}
)
if include_data_url:
result["data_url"] = render_with_bundled_js(qr_hex)
return result
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--member-id", type=int)
ap.add_argument("--qr")
ap.add_argument("--json-out")
ap.add_argument("--gif-out")
args = ap.parse_args()
resp = None
qr_hex = args.qr
if args.member_id is not None:
resp = fetch(args.member_id)
if args.json_out:
with open(args.json_out, "w", encoding="utf-8") as f:
json.dump(resp, f, ensure_ascii=False, indent=2)
qr_hex = resp["data"]["qr"]
if not qr_hex:
raise SystemExit("provide --member-id or --qr")
payload = get_command(qr_hex)
print("payload_len=", len(payload))
print("payload_hex_prefix=", payload.hex()[:160])
if args.gif_out:
data_url = render_with_bundled_js(qr_hex)
save_data_url(data_url, args.gif_out)
print("saved", args.gif_out)
if resp:
print(json.dumps(summarize_response(resp), ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()