from __future__ import annotations import logging import secrets import time from .constants import ( PAIRING_CODE_TTL_SECONDS, PAIRING_MAX_ATTEMPTS, PAIRING_RATE_LIMIT_PER_PEER, ) logger = logging.getLogger(__name__) ID_LABEL = "flockUserId" _RATE_LIMIT_WINDOW = 60 _PAIRING_CODES: dict[str, tuple[str, float, int]] = {} _GENERATE_TIMESTAMPS: dict[str, list[float]] = {} def _cleanup_expired() -> None: now = time.monotonic() expired = [k for k, (_, ts, _) in _PAIRING_CODES.items() if now - ts > PAIRING_CODE_TTL_SECONDS] for k in expired: del _PAIRING_CODES[k] def _cleanup_rate_limits() -> None: now = time.monotonic() for key in list(_GENERATE_TIMESTAMPS): _GENERATE_TIMESTAMPS[key] = [ts for ts in _GENERATE_TIMESTAMPS[key] if now - ts < _RATE_LIMIT_WINDOW] if not _GENERATE_TIMESTAMPS[key]: del _GENERATE_TIMESTAMPS[key] async def generate_code(peer_id: str, config: dict, account_id: str = "default") -> str: _cleanup_expired() _cleanup_rate_limits() now = time.monotonic() recent = _GENERATE_TIMESTAMPS.get(peer_id, []) recent = [ts for ts in recent if now - ts < _RATE_LIMIT_WINDOW] if len(recent) >= PAIRING_RATE_LIMIT_PER_PEER: logger.warning("Flock pairing rate limit exceeded for peer %s", peer_id) raise RuntimeError( f"Rate limit exceeded: max {PAIRING_RATE_LIMIT_PER_PEER} codes " f"per {_RATE_LIMIT_WINDOW}s per peer" ) recent.append(now) _GENERATE_TIMESTAMPS[peer_id] = recent code = f"{secrets.randbelow(1_000_000):06d}" _PAIRING_CODES[peer_id] = (code, time.monotonic(), 0) logger.info("Flock pairing code generated for peer %s", peer_id) return code async def verify_code(peer_id: str, code: str, config: dict, account_id: str = "default") -> bool: _cleanup_expired() stored = _PAIRING_CODES.get(peer_id) if stored is None: return False stored_code, created_at, attempts = stored if time.monotonic() - created_at > PAIRING_CODE_TTL_SECONDS: _PAIRING_CODES.pop(peer_id, None) logger.info("Flock pairing code expired for peer %s", peer_id) return False attempts += 1 if attempts > PAIRING_MAX_ATTEMPTS: _PAIRING_CODES.pop(peer_id, None) logger.warning("Flock pairing max attempts exceeded for peer %s", peer_id) return False if not secrets.compare_digest(stored_code, code): _PAIRING_CODES[peer_id] = (stored_code, created_at, attempts) logger.info("Flock pairing code mismatch for peer %s (attempt %d/%d)", peer_id, attempts, PAIRING_MAX_ATTEMPTS) return False _PAIRING_CODES.pop(peer_id, None) logger.info("Flock pairing code verified for peer %s", peer_id) return True async def notify_approval(config: dict, peer_id: str, account_id: str = "default") -> None: logger.info("Flock pairing approved for peer %s (account=%s)", peer_id, account_id) try: from .config import _apply_env_overrides, _dict_to_account from .outbound import send_text as _ob_send_text account_data = config.get("accounts", {}).get(account_id, {}) account = _dict_to_account(account_data) account = _apply_env_overrides(account) if not account.bot_token: logger.warning("Cannot notify approval: no bot_token for account %s", account_id) return await _ob_send_text( peer_id, "您的配对已通过验证,现在可以与 Bot 私聊了", account_id=account_id, config=config, ) except Exception: logger.warning("Failed to send approval notification to peer %s", peer_id, exc_info=True) def normalize_allow_entry(entry: str) -> str: from .security import normalize_allow_entry as _sec_normalize return _sec_normalize(entry) id_label = ID_LABEL