187 lines
5.3 KiB
Python
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()
|