from __future__ import annotations import secrets import time from dataclasses import dataclass, field PAIRING_CODE_LENGTH = 6 PAIRING_REQUEST_TTL_S = 300 @dataclass class PairingRequest: user_id: str code: str created_at: float = field(default_factory=time.monotonic) approved: bool = False denied: bool = False @property def expired(self) -> bool: return time.monotonic() - self.created_at > PAIRING_REQUEST_TTL_S @dataclass class PairingResult: needs_pairing: bool code: str = "" reason: str = "" class MattermostPairingManager: def __init__(self): self._pending: dict[str, PairingRequest] = {} def check_or_request(self, user_id: str) -> PairingResult: existing = self._pending.get(user_id) if existing and not existing.expired: if existing.approved: return PairingResult(needs_pairing=False, reason="already approved") return PairingResult( needs_pairing=True, code=existing.code, reason="pairing pending", ) code = self._generate_code() self._pending[user_id] = PairingRequest(user_id=user_id, code=code) self._cleanup_expired() return PairingResult(needs_pairing=True, code=code, reason="new pairing request") def approve(self, user_id: str, code: str) -> bool: req = self._pending.get(user_id) if req is None or req.expired: return False if req.code != code: return False req.approved = True return True def deny(self, user_id: str) -> bool: req = self._pending.get(user_id) if req is None or req.expired: return False req.denied = True return True def is_approved(self, user_id: str) -> bool: req = self._pending.get(user_id) if req is None: return False if req.expired: del self._pending[user_id] return False return req.approved def list_pending(self) -> list[PairingRequest]: self._cleanup_expired() return [r for r in self._pending.values() if not r.approved and not r.denied] def list_approved(self) -> list[PairingRequest]: self._cleanup_expired() return [r for r in self._pending.values() if r.approved] @staticmethod def _generate_code() -> str: return secrets.token_hex(PAIRING_CODE_LENGTH // 2)[:PAIRING_CODE_LENGTH].upper() def _cleanup_expired(self) -> None: self._pending = {uid: r for uid, r in self._pending.items() if not r.expired} def build_pairing_notification(user_id: str, pairing_code: str) -> str: return ( f"**ForcePilot DM 配对请求**\n\n" f"检测到来自 `{user_id}` 的私聊消息,但该用户未在您的允许列表中。\n\n" f"配对码:**`{pairing_code}`**\n\n" f"请管理员确认此配对请求以允许该用户与 Bot 对话。\n" f"配对码有效期:{PAIRING_REQUEST_TTL_S // 60} 分钟。" )