from __future__ import annotations import re import time from collections import OrderedDict from threading import Lock from typing import Any _PAIRING_PREFIX_PATTERN = re.compile(r"^(twitch:|user:)", re.IGNORECASE) def strip_pairing_prefix(user_id: str) -> str: return _PAIRING_PREFIX_PATTERN.sub("", user_id).strip() class PairingStore: MAX_PENDING = 500 PENDING_TTL = 300.0 MAX_APPROVED = 2000 APPROVED_TTL = 86400.0 def __init__(self): self._approved: OrderedDict[str, float] = OrderedDict() self._pending: OrderedDict[str, float] = OrderedDict() self._lock = Lock() self._on_approve: Any = None self._on_approve_channel: str = "" def set_on_approve(self, callback: Any, channel: str = "") -> None: self._on_approve = callback self._on_approve_channel = channel def _normalize(self, user_id: str) -> str: return strip_pairing_prefix(user_id) def is_approved(self, user_id: str) -> bool: uid = self._normalize(user_id) with self._lock: if uid not in self._approved: return False ts = self._approved[uid] if time.monotonic() - ts >= self.APPROVED_TTL: del self._approved[uid] return False self._approved.move_to_end(uid) return True def is_pending(self, user_id: str) -> bool: with self._lock: return self._normalize(user_id) in self._pending def add_pending(self, user_id: str, user_name: str, channel: str) -> None: uid = self._normalize(user_id) with self._lock: self._pending[uid] = time.monotonic() self._pending.move_to_end(uid) self._evict_expired_pending() def approve(self, user_id: str, user_name: str = "") -> bool: uid = self._normalize(user_id) with self._lock: if uid in self._pending: del self._pending[uid] self._approved[uid] = time.monotonic() self._approved.move_to_end(uid) self._evict_expired_approved() if self._on_approve: try: self._on_approve(user_name or uid, self._on_approve_channel) except Exception: pass return True def reject(self, user_id: str) -> bool: uid = self._normalize(user_id) with self._lock: if uid in self._pending: del self._pending[uid] return True return False def remove_approval(self, user_id: str) -> bool: uid = self._normalize(user_id) with self._lock: if uid in self._approved: del self._approved[uid] return True return False def get_approved_users(self) -> list[str]: with self._lock: return list(self._approved.keys()) def get_pending_users(self) -> list[str]: with self._lock: self._evict_expired_pending() return list(self._pending.keys()) def _evict_expired_pending(self) -> None: now = time.monotonic() expired = [uid for uid, ts in self._pending.items() if now - ts >= self.PENDING_TTL] for uid in expired: del self._pending[uid] def _evict_expired_approved(self) -> None: now = time.monotonic() expired = [uid for uid, ts in self._approved.items() if now - ts >= self.APPROVED_TTL] for uid in expired: del self._approved[uid] while len(self._approved) > self.MAX_APPROVED: self._approved.popitem(last=False) def __len__(self) -> int: with self._lock: return len(self._approved) @property def pending_count(self) -> int: with self._lock: return len(self._pending) def check_pairing_policy( pairings: PairingStore, user_id: str, content: str | None = None, ) -> bool: return pairings.is_approved(user_id) def format_pairing_notification(user_name: str, approval_code: str) -> str: return f"{user_name} wants to interact. Approve with: !approve {approval_code}"