from __future__ import annotations import logging import secrets import time logger = logging.getLogger(__name__) CODE_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" CODE_LENGTH = 6 CODE_TTL_SECONDS = 600 MAX_PENDING = 3 REQUEST_COOLDOWN_S = 3600 class ILinkPairing: def __init__(self): self._pending: dict[str, dict] = {} self._paired: dict[str, float] = {} self._last_request: dict[str, float] = {} def generate_code(self, user_id: str) -> str | None: now = time.time() last = self._last_request.get(user_id, 0) if now - last < REQUEST_COOLDOWN_S: return None if len(self._pending) >= MAX_PENDING: return None code = "".join(secrets.choice(CODE_CHARS) for _ in range(CODE_LENGTH)) self._pending[user_id] = {"code": code, "created_at": now} self._last_request[user_id] = now return code def verify(self, user_id: str, code: str) -> bool: entry = self._pending.get(user_id) if not entry: return False if time.time() - entry["created_at"] > CODE_TTL_SECONDS: del self._pending[user_id] return False if entry["code"] == code.upper().strip(): del self._pending[user_id] self._paired[user_id] = time.time() return True return False def is_paired(self, user_id: str) -> bool: return user_id in self._paired def revoke(self, user_id: str) -> None: self._paired.pop(user_id, None) self._pending.pop(user_id, None)