ForcePilot/backend/package/yuxi/channels/adapters/twitch/pairing.py
Kris 59cd13cf84 feat(twitch): 实现完整的Twitch聊天适配器模块
新增Twitch IRC协议相关的全套实现,包括:
1. 基础工具类:令牌处理、消息格式化、速率限制、消息去重
2. 核心适配器组件:IRC解析器、消息归一化、外发消息处理
3. API客户端:Helix API封装、认证提供者
4. 配置与部署:配置校验、设置向导
5. 辅助功能:配对管理、健康检查、目标解析等
2026-05-12 00:50:10 +08:00

113 lines
3.3 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
def __init__(self):
self._approved: dict[str, float] = {}
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:
with self._lock:
return self._normalize(user_id) in self._approved
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()
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 __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}"