#!/usr/bin/env python3 """notiMessage 本地调试转发服务 — 在 PC 浏览器查看手机抓取的消息。""" import json import re import threading import time from collections import deque from datetime import datetime from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from urllib.parse import urlparse HOST = "0.0.0.0" PORT = 8765 MAX_MESSAGES = 500 _messages = deque(maxlen=MAX_MESSAGES) _lock = threading.Lock() _last_dedup = {"key": None, "ts": 0.0} def _now_iso(): return datetime.now().strftime("%Y-%m-%d %H:%M:%S") def _resolve_group(payload): group = (payload.get("group") or payload.get("title") or "").strip() if group and "TLRPC$" not in group and "org.telegram.tgnet." not in group: return group app = payload.get("appName") or payload.get("packageName") or "" return app + " / 未分类" if app else "未分类" def _add_message(payload): item = dict(payload) item["group"] = _resolve_group(payload) item["receivedAt"] = _now_iso() # 本机 + 局域网双推时可能各成功一次,3 秒内同内容去重 dedup_key = "|".join([ str(payload.get("source") or ""), str(payload.get("packageName") or ""), str(payload.get("title") or ""), str(payload.get("content") or ""), ]) now = time.time() with _lock: if (_last_dedup["key"] == dedup_key and now - float(_last_dedup["ts"]) < 3.0): if _messages: return _messages[0] _last_dedup["key"] = dedup_key _last_dedup["ts"] = now _messages.appendleft(item) item["id"] = len(_messages) print("[{0}] [{1}] [{2}] {3} | {4}".format( _now_iso(), item["group"], payload.get("source", "?"), payload.get("appName", payload.get("packageName", "")), (payload.get("content", "") or "")[:80], )) return item def _json_response(handler, status, data): body = json.dumps(data, ensure_ascii=False).encode("utf-8") handler.send_response(status) handler.send_header("Content-Type", "application/json; charset=utf-8") handler.send_header("Access-Control-Allow-Origin", "*") handler.send_header("Content-Length", str(len(body))) handler.end_headers() handler.wfile.write(body) def _group_messages(messages): groups = {} for msg in messages: key = msg.get("group") or _resolve_group(msg) if key not in groups: groups[key] = { "key": key, "appName": msg.get("appName") or msg.get("packageName") or "", "count": 0, "latestAt": msg.get("receivedAt", ""), "messages": [], } g = groups[key] g["count"] += 1 g["messages"].append(msg) if (msg.get("receivedAt") or "") > (g.get("latestAt") or ""): g["latestAt"] = msg.get("receivedAt", "") result = sorted(groups.values(), key=lambda x: x.get("latestAt", ""), reverse=True) for g in result: g["messages"].sort(key=lambda m: m.get("receivedAt", ""), reverse=True) return result def _is_mmp_message(msg): source = (msg.get("source") or "").lower() content = msg.get("content") or "" title = msg.get("title") or "" if "tng_mmp" in source or "xposed_tng_mmp" in source: return True if "[MMP统计]" in content or "Money Packet" in title: return True lower = content.lower() return "receiverlist" in lower or ("claimedamount" in lower and "mmp" in lower) def _normalize_money(value): """把 1.23 / RM1.23 / {"amount":"0.71","cent":"71"} 统一成 float 或 None。""" if value is None: return None text = str(value).strip() if not text or text.lower() == "null": return None text = text.replace("RM", "").replace("rm", "").strip() if text.startswith("{") and "amount" in text: try: obj = json.loads(text) if isinstance(obj, dict): if obj.get("amount") not in (None, ""): return float(str(obj["amount"]).replace(",", "")) if obj.get("cent") not in (None, ""): return int(str(obj["cent"])) / 100.0 except Exception: return None try: return float(text.replace(",", "")) except ValueError: return None def _parse_mmp_content(content): """解析 TngMoneyPacketHook 转发文本 → {meta, claims[]}。""" text = (content or "").strip() meta = {} claims = [] if not text: return meta, claims lines = text.splitlines() head = lines[0] if lines else "" if head.startswith("[MMP统计]"): head = head[len("[MMP统计]"):].strip() # 新格式用 " | " 分隔;旧格式兼容空格拆 token if " | " in head: parts = [p.strip() for p in head.split(" | ") if p.strip()] else: parts = [] buf = "" for part in head.split(): if buf: buf += " " + part if buf.count("{") <= buf.count("}"): parts.append(buf) buf = "" continue if "=" in part and part.split("=", 1)[1].startswith("{") and part.count("{") > part.count("}"): buf = part continue parts.append(part) if buf: parts.append(buf) for part in parts: if "=" not in part: continue k, v = part.split("=", 1) meta[k.strip()] = v.strip() # 旧报文 sender=LIAO RUICHAO 被空格截断时,尝试从原文还原 if meta.get("sender") and "sender=" in head: raw_sender = head.split("sender=", 1)[1] for stop in (" | ", " total=", " via=", " group=", " packet=", " src="): if stop in raw_sender: raw_sender = raw_sender.split(stop, 1)[0] break raw_sender = raw_sender.strip() if raw_sender and len(raw_sender) > len(meta.get("sender") or ""): meta["sender"] = raw_sender if "total" in meta: total_num = _normalize_money(meta["total"]) meta["total"] = ("{0:.2f}".format(total_num) if total_num is not None else meta["total"]) # 发放时间:issued= / createTime= / issuedAt= for key in ("issued", "createTime", "issuedAt", "create"): if meta.get(key): meta["issued"] = meta[key] break for line in lines[1:]: line = line.strip() if not line or "->" not in line: continue left, right = line.split("->", 1) nick = left.strip() right = right.strip() claim_time = "" if right.endswith(")") and "(" in right and not right.startswith("{"): amt, _, rest = right.partition("(") amount_raw = amt.strip() claim_time = rest.rstrip(")").strip() else: amount_raw = right amount_num = _normalize_money(amount_raw) if nick: claims.append({ "nickname": nick, "amount": ("{0:.2f}".format(amount_num) if amount_num is not None else "0.00"), "amountValue": amount_num if amount_num is not None else 0.0, "claimTime": claim_time, }) return meta, claims def _claims_fingerprint(claims): rows = [] for c in claims: rows.append("{0}={1}".format(c.get("nickname") or "?", c.get("amount") or "0")) rows.sort() return "|".join(rows) def _aggregate_claims(claims): """同一快照内同昵称只保留一笔(取较大金额),红包每人只领一次,禁止累加导致翻倍。""" buckets = {} order = [] for c in claims: nick = c.get("nickname") or "?" try: amt = float(c.get("amountValue") if c.get("amountValue") is not None else _normalize_money(c.get("amount")) or 0.0) except (TypeError, ValueError): amt = 0.0 if nick not in buckets: buckets[nick] = {"nickname": nick, "amount": amt, "count": 1, "claimTime": c.get("claimTime") or ""} order.append(nick) continue b = buckets[nick] b["count"] += 1 if amt >= b["amount"]: b["amount"] = amt if c.get("claimTime"): b["claimTime"] = c.get("claimTime") or b["claimTime"] elif c.get("claimTime") and not b["claimTime"]: b["claimTime"] = c.get("claimTime") or "" result = [] for nick in order: b = buckets[nick] result.append({ "nickname": nick, "amount": round(b["amount"], 4), "amountText": "{0:.2f}".format(b["amount"]), "claimCount": b["count"], "claimTime": b.get("claimTime") or "", }) result.sort(key=lambda x: x["amount"], reverse=True) for i, row in enumerate(result, 1): row["rank"] = i return result def _mmp_packets(): with _lock: msgs = [m for m in _messages if _is_mmp_message(m)] # 每个红包只保留「最新一次完整领取榜」快照,避免多个红包因缺 packetId 被揉在一起 packets = {} for msg in msgs: meta, claims = _parse_mmp_content(msg.get("content") or "") if not claims: continue packet_id = meta.get("packet") or meta.get("packetId") or "" if not packet_id or packet_id in ("unknown", "TNG 红包", "TNG Money Packet"): # 用领取名单指纹区分不同红包 packet_id = "红包-" + _claims_fingerprint(claims)[:48] key = str(packet_id) issued = meta.get("issued") or "" item = { "packetId": key, "title": msg.get("title") or "TNG 红包", "sender": meta.get("sender") or "", "group": meta.get("group") or "", "total": meta.get("total") or "", "via": meta.get("via") or "", "issuedAt": issued, "fetchedAt": msg.get("receivedAt") or "", "latestAt": issued or (msg.get("receivedAt") or ""), "snapshots": 1, "claims": claims, "rawMessages": [{ "id": msg.get("id"), "receivedAt": msg.get("receivedAt"), "content": msg.get("content"), "source": msg.get("source"), }], } existing = packets.get(key) # 优先保留「有发放时间 + 领取更全」的快照;时间戳用发放时间排序 def _score(it): return ( 1 if it.get("issuedAt") else 0, len(it.get("claims") or []), it.get("fetchedAt") or "", ) if existing is None or _score(item) >= _score(existing): if existing is not None: item["snapshots"] = int(existing.get("snapshots") or 1) + 1 item["rawMessages"] = existing.get("rawMessages", []) + item["rawMessages"] if not item.get("issuedAt") and existing.get("issuedAt"): item["issuedAt"] = existing["issuedAt"] item["latestAt"] = item["issuedAt"] or item.get("fetchedAt") or "" packets[key] = item else: existing["snapshots"] = int(existing.get("snapshots") or 1) + 1 existing["rawMessages"].append(item["rawMessages"][0]) if not existing.get("issuedAt") and issued: existing["issuedAt"] = issued existing["latestAt"] = issued result = [] for p in packets.values(): ranked = _aggregate_claims(p.get("claims") or []) p["leaderboard"] = ranked p["claimantCount"] = len(ranked) p["claimEventCount"] = len(p.get("claims") or []) try: p["sumClaimed"] = round(sum(x["amount"] for x in ranked), 4) except Exception: p["sumClaimed"] = 0 # 总额缺失或只抓到单笔金额时,用领取合计兜底 total_num = _normalize_money(p.get("total")) if total_num is None or (p["sumClaimed"] > 0 and abs(total_num - p["sumClaimed"]) > 0.001 and total_num <= max((x["amount"] for x in ranked), default=0) + 1e-9): p["total"] = "{0:.2f}".format(p["sumClaimed"]) elif total_num is not None: p["total"] = "{0:.2f}".format(total_num) del p["claims"] result.append(p) result.sort(key=lambda x: _mmp_time_sort_key(x.get("issuedAt") or x.get("latestAt") or ""), reverse=True) return result def _mmp_time_sort_key(text): """把 13/07/2026 11:54:44 / ISO 等统一成可比较字符串。""" s = (text or "").strip() m = re.match(r"^(\d{2})/(\d{2})/(\d{4})(?:\s+(\d{2}:\d{2}(?::\d{2})?))?", s) if m: return "{0}-{1}-{2} {3}".format(m.group(3), m.group(2), m.group(1), m.group(4) or "") return s def _mmp_html_page(): html = """