这是一个批量整理提交,包含以下主要改动: 1. 删除多处冗余的空行和未使用的导入 2. 修复文件末尾缺少换行符的问题 3. 调整部分模块的导入顺序与代码排版 4. 修复部分配置默认值与策略逻辑 5. 新增多个功能模块与辅助工具 6. 完善异常处理与日志记录 7. 修复速率限制、消息缓存、权限校验等逻辑bug 8. 废弃部分旧有API与配置项并添加警告提示
133 lines
4.1 KiB
Python
133 lines
4.1 KiB
Python
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}"
|