fix(gpc): query local OTP helper by phone

This commit is contained in:
朴圣佑
2026-05-07 12:12:57 +08:00
parent 250a709f20
commit 2236730374
6 changed files with 227 additions and 38 deletions
+109 -24
View File
@@ -74,6 +74,11 @@ def extract_gopay_otp(text: str, require_keywords: bool = True) -> Optional[str]
return None
def normalize_phone_key(value: object) -> str:
digits = re.sub(r"\D+", "", str(value or ""))
return f"+{digits}" if digits else ""
def mac_message_time_to_datetime(value: int | float | None) -> dt.datetime:
if not value:
return dt.datetime.now(dt.timezone.utc)
@@ -113,24 +118,52 @@ def read_recent_messages(db_path: Path, after_rowid: int = 0, limit: int = 80) -
try:
conn = sqlite3.connect(str(copied))
conn.row_factory = sqlite3.Row
rows = conn.execute(
"""
SELECT
message.ROWID AS rowid,
message.guid AS guid,
message.text AS text,
message.date AS date,
message.service AS service,
handle.id AS handle
FROM message
LEFT JOIN handle ON message.handle_id = handle.ROWID
WHERE message.ROWID > ?
AND message.text IS NOT NULL
ORDER BY message.ROWID DESC
LIMIT ?
""",
(max(0, int(after_rowid or 0)), max(1, int(limit or 80))),
).fetchall()
params = (max(0, int(after_rowid or 0)), max(1, int(limit or 80)))
try:
rows = conn.execute(
"""
SELECT
message.ROWID AS rowid,
message.guid AS guid,
message.text AS text,
message.date AS date,
message.service AS service,
message.destination_caller_id AS destination_caller_id,
message.account AS account,
handle.id AS handle,
chat.last_addressed_handle AS last_addressed_handle,
chat.account_login AS account_login
FROM message
LEFT JOIN handle ON message.handle_id = handle.ROWID
LEFT JOIN chat_message_join ON chat_message_join.message_id = message.ROWID
LEFT JOIN chat ON chat.ROWID = chat_message_join.chat_id
WHERE message.ROWID > ?
AND message.text IS NOT NULL
ORDER BY message.ROWID DESC
LIMIT ?
""",
params,
).fetchall()
except sqlite3.OperationalError:
rows = conn.execute(
"""
SELECT
message.ROWID AS rowid,
message.guid AS guid,
message.text AS text,
message.date AS date,
message.service AS service,
message.account AS account,
handle.id AS handle
FROM message
LEFT JOIN handle ON message.handle_id = handle.ROWID
WHERE message.ROWID > ?
AND message.text IS NOT NULL
ORDER BY message.ROWID DESC
LIMIT ?
""",
params,
).fetchall()
return [dict(row) for row in rows]
finally:
try:
@@ -140,8 +173,17 @@ def read_recent_messages(db_path: Path, after_rowid: int = 0, limit: int = 80) -
shutil.rmtree(copied.parent, ignore_errors=True)
def get_record_phone(row: dict) -> str:
for key in ("destination_caller_id", "last_addressed_handle", "account_login", "account"):
phone = normalize_phone_key(row.get(key))
if phone:
return phone
return ""
def make_otp_record(row: dict, otp: str) -> dict:
received_at = mac_message_time_to_datetime(row.get("date")).isoformat()
phone_e164 = get_record_phone(row)
return {
"otp": otp,
"code": otp,
@@ -149,6 +191,8 @@ def make_otp_record(row: dict, otp: str) -> dict:
"rowid": int(row.get("rowid") or 0),
"sender": str(row.get("handle") or ""),
"service": str(row.get("service") or ""),
"account_phone": phone_e164,
"phone_e164": phone_e164,
"received_at": received_at,
"message_text": str(row.get("text") or ""),
}
@@ -189,19 +233,59 @@ def parse_timestamp_ms(value: object) -> int:
return int(numeric)
def select_otp_record(state: dict, after_ms: int = 0) -> Optional[dict]:
def record_matches_phone(record: dict, phone: str = "") -> bool:
wanted = normalize_phone_key(phone)
if not wanted:
return True
return normalize_phone_key(record.get("phone_e164") or record.get("account_phone")) == wanted
def select_otp_record(state: dict, after_ms: int = 0, phone: str = "") -> Optional[dict]:
records = state.get("otps")
if not isinstance(records, list):
records = []
if after_ms > 0:
for record in records:
if isinstance(record, dict) and parse_timestamp_ms(record.get("received_at")) >= after_ms:
if (
isinstance(record, dict)
and record_matches_phone(record, phone)
and parse_timestamp_ms(record.get("received_at")) >= after_ms
):
return record
return None
record = state.get("last_otp") or None
if isinstance(record, dict):
if isinstance(record, dict) and record_matches_phone(record, phone):
return record
return records[0] if records and isinstance(records[0], dict) else None
for record in records:
if isinstance(record, dict) and record_matches_phone(record, phone):
return record
return None
def consume_otp_record(phone: str = "", record: Optional[dict] = None) -> None:
wanted = normalize_phone_key(phone)
consumed_message_id = str((record or {}).get("message_id") or "").strip()
consumed_rowid = int((record or {}).get("rowid") or 0)
def should_keep(item: object) -> bool:
if not isinstance(item, dict):
return False
if wanted:
return not record_matches_phone(item, wanted)
if consumed_message_id:
return str(item.get("message_id") or "").strip() != consumed_message_id
if consumed_rowid:
return int(item.get("rowid") or 0) != consumed_rowid
return False
with STATE_LOCK:
records = STATE.get("otps")
if not isinstance(records, list):
records = []
STATE["otps"] = [item for item in records if should_keep(item)]
last_otp = STATE.get("last_otp")
if isinstance(last_otp, dict) and not should_keep(last_otp):
STATE["last_otp"] = STATE["otps"][0] if STATE["otps"] else None
def scan_once(db_path: Path, require_keywords: bool = True) -> None:
@@ -263,14 +347,15 @@ class HelperHandler(BaseHTTPRequestHandler):
query = parse_qs(parsed.query)
consume = str(query.get("consume", ["0"])[0]).strip().lower() in {"1", "true", "yes"}
after_ms = parse_timestamp_ms(query.get("after_ms", query.get("after", ["0"]))[0])
phone = str((query.get("phone") or query.get("phone_e164") or query.get("phone_number") or [""])[0]).strip()
state = get_state()
record = select_otp_record(state, after_ms=after_ms)
record = select_otp_record(state, after_ms=after_ms, phone=phone)
if not record:
write_json(self, 200, {"ok": True, "otp": "", "code": "", "status": "waiting", "message": "未查询到验证码"})
return
payload = {"ok": True, "status": "found", **record}
if consume:
update_state(last_otp=None)
consume_otp_record(phone=phone, record=record)
write_json(self, 200, payload)
return
write_json(self, 404, {"ok": False, "error": "not_found"})