Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions gateway/pairing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
13 changes: 8 additions & 5 deletions hermes_cli/pairing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down Expand Up @@ -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 ""
Expand Down
10 changes: 7 additions & 3 deletions hermes_cli/subcommands/pairing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading