from __future__ import annotations import asyncio import threading from dataclasses import dataclass from yuxi.utils.logging_config import logger @dataclass class PairRequest: phone_number: str pairing_code: str | None = None expires_at: float | None = None @property def is_expired(self) -> bool: if self.expires_at is None: return False return asyncio.get_event_loop().time() > self.expires_at class PairingManager: def __init__(self): self._pending: dict[str, PairRequest] = {} self._paired: set[str] = set() self._lock = threading.Lock() def create_pair_request(self, phone_number: str, code: str, timeout: float = 300) -> PairRequest: req = PairRequest( phone_number=phone_number, pairing_code=code, expires_at=asyncio.get_event_loop().time() + timeout, ) with self._lock: self._pending[phone_number] = req logger.info(f"PairingManager: pair request created for {phone_number}") return req def confirm_pair(self, phone_number: str) -> bool: with self._lock: req = self._pending.get(phone_number) if req is None or req.is_expired: logger.warning(f"PairingManager: no valid request for {phone_number}") return False del self._pending[phone_number] self._paired.add(phone_number) logger.info(f"PairingManager: confirmed pair for {phone_number}") return True def is_paired(self, phone_number: str) -> bool: with self._lock: return phone_number in self._paired def revoke_pair(self, phone_number: str) -> bool: with self._lock: removed_pending = self._pending.pop(phone_number, None) is not None removed_paired = phone_number in self._paired self._paired.discard(phone_number) return removed_pending or removed_paired def get_pending_phones(self) -> list[str]: with self._lock: return list(self._pending.keys()) def cleanup_expired(self) -> int: count = 0 with self._lock: expired = [phone for phone, req in self._pending.items() if req.is_expired] for phone in expired: del self._pending[phone] count += 1 if count: logger.info(f"PairingManager: cleaned {count} expired requests") return count