From 59b214a9127e58aa9ebda17e4c8cac3d8f1dceda Mon Sep 17 00:00:00 2001 From: kyssta-exe Date: Sat, 25 Jul 2026 07:14:25 +0000 Subject: [PATCH] fix(pairing): add user_id-based approve for undelivered pairing codes (#70651) Add PairingStore.approve_by_user_id() and a --user-id / -u flag to 'hermes pairing approve' so operators can approve pending requests by user_id when the pairing code was never delivered (e.g. dropped DM, rate-limited reply on Weixin/WeChat bridges). Closes #70651 --- gateway/pairing.py | 47 +++++++++++++++++++++++++++++++ hermes_cli/pairing.py | 13 +++++---- hermes_cli/subcommands/pairing.py | 10 +++++-- 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/gateway/pairing.py b/gateway/pairing.py index bf6212f97f3f9..a645ba408f811 100644 --- a/gateway/pairing.py +++ b/gateway/pairing.py @@ -536,6 +536,53 @@ def approve_code(self, platform: str, code: str) -> Optional[dict]: "user_name": matched_entry.get("user_name", ""), } + def approve_by_user_id(self, platform: str, user_id: str) -> Optional[dict]: + """ + Approve a pending pairing request by user ID. + + Alternative to ``approve_code`` for platforms where the pairing code + was never delivered to the operator (e.g. dropped DM, rate-limited + reply). Looks up the pending entry by ``user_id`` instead of the + hashed code. + + Returns ``{user_id, user_name}`` on success, ``None`` if no pending + entry matches the user ID or the platform is locked out. + """ + with self._lock: + self._cleanup_expired(platform) + user_id = user_id.strip() + + # Lockout check — same as approve_code to ensure consistency. + if self._is_locked_out(platform): + return None + + pending = self._load_json(self._pending_path(platform)) + + matched_key = None + matched_entry = None + for entry_id, entry in pending.items(): + if not isinstance(entry, dict): + continue + if entry.get("user_id") == user_id: + matched_key = entry_id + matched_entry = entry + break + + if matched_key is None: + return None + + del pending[matched_key] + self._save_json(self._pending_path(platform), pending) + + # Add to approved list + self._approve_user(platform, matched_entry["user_id"], + matched_entry.get("user_name", "")) + + return { + "user_id": matched_entry["user_id"], + "user_name": matched_entry.get("user_name", ""), + } + def list_pending(self, platform: str = None) -> list: """List pending pairing requests, optionally filtered by platform. diff --git a/hermes_cli/pairing.py b/hermes_cli/pairing.py index 101a1d10bc776..e699a1f1279b8 100644 --- a/hermes_cli/pairing.py +++ b/hermes_cli/pairing.py @@ -18,7 +18,7 @@ def pairing_command(args): if action == "list": _cmd_list(store) elif action == "approve": - _cmd_approve(store, args.platform, args.code) + _cmd_approve(store, args.platform, args.code, getattr(args, "user_id_mode", False)) elif action == "revoke": _cmd_revoke(store, args.platform, args.user_id) elif action == "clear-pending": @@ -61,12 +61,15 @@ def _cmd_list(store): print() -def _cmd_approve(store, platform: str, code: str): - """Approve a pairing code.""" +def _cmd_approve(store, platform: str, code: str, user_id_mode: bool = False): + """Approve a pairing code, or a pending request by user ID with --user-id.""" platform = platform.lower().strip() - code = code.upper().strip() - result = store.approve_code(platform, code) + if user_id_mode: + result = store.approve_by_user_id(platform, code.strip()) + else: + code = code.upper().strip() + result = store.approve_code(platform, code) if result: uid = result["user_id"] name = result.get("user_name") or "" diff --git a/hermes_cli/subcommands/pairing.py b/hermes_cli/subcommands/pairing.py index 55b022ed6db91..26dd178e10bdf 100644 --- a/hermes_cli/subcommands/pairing.py +++ b/hermes_cli/subcommands/pairing.py @@ -21,12 +21,16 @@ def build_pairing_parser(subparsers, *, cmd_pairing: Callable) -> None: pairing_sub.add_parser("list", help="Show pending + approved users") pairing_approve_parser = pairing_sub.add_parser( - "approve", help="Approve a pairing code" + "approve", help="Approve a pairing code or a pending request by user ID" ) pairing_approve_parser.add_argument( - "platform", help="Platform name (telegram, discord, slack, whatsapp)" + "platform", help="Platform name (telegram, discord, slack, whatsapp, weixin)" + ) + pairing_approve_parser.add_argument("code", help="Pairing code to approve (or user ID with --user-id)") + pairing_approve_parser.add_argument( + "--user-id", "-u", action="store_true", dest="user_id_mode", + help="Interpret the argument as a user ID (for when the pairing code was never delivered)" ) - pairing_approve_parser.add_argument("code", help="Pairing code to approve") pairing_revoke_parser = pairing_sub.add_parser("revoke", help="Revoke user access") pairing_revoke_parser.add_argument("platform", help="Platform name")