from __future__ import annotations import asyncio import hashlib import logging import time from collections.abc import Callable, Awaitable from dataclasses import dataclass, field from typing import Any logger = logging.getLogger(__name__) @dataclass class InteractionContext: interaction_id: str interaction_type: str chat_type: str chat_id: str user_id: str user_name: str data: dict[str, Any] msg_id: str = "" timestamp: float = field(default_factory=time.time) InteractionCallback = Callable[[InteractionContext], Awaitable[bool]] class InteractionRegistry: def __init__(self): self._callbacks: dict[str, InteractionCallback] = {} def register(self, action_id: str, callback: InteractionCallback) -> None: self._callbacks[action_id] = callback logger.debug("InteractionRegistry: registered %s", action_id) def unregister(self, action_id: str) -> None: self._callbacks.pop(action_id, None) async def dispatch(self, ctx: InteractionContext) -> bool: if not ctx.interaction_id: return False for action_id, callback in self._callbacks.items(): if ctx.interaction_id == action_id or ctx.interaction_id.startswith(action_id): try: return await callback(ctx) except Exception: logger.exception("InteractionRegistry: callback failed for %s", action_id) return False class InteractionBuilder: @staticmethod def make_confirm_button( action_id: str, label: str = "确认", style: int = 1, ) -> dict: return { "type": 2, "style": style, "label": label, "data": action_id, } @staticmethod def make_action_row(buttons: list[dict]) -> dict: return {"type": 1, "components": buttons} @staticmethod def make_select_menu( action_id: str, placeholder: str = "请选择", options: list[dict] | None = None, min_values: int = 1, max_values: int = 1, ) -> dict: return { "type": 3, "custom_id": action_id, "placeholder": placeholder, "options": options or [], "min_values": min_values, "max_values": max_values, } @staticmethod def make_modal( action_id: str, title: str, fields: list[dict], ) -> dict: return { "type": 4, "custom_id": action_id, "title": title, "components": fields, } @dataclass class InteractionHandler: interaction_id: str handler_type: str chat_id: str user_id: str created_at: float = field(default_factory=time.time) expires_at: float = 300.0 resolved: bool = False result: Any = None def __post_init__(self): self.expires_at = self.created_at + 300.0 class InteractionSessionManager: def __init__(self, max_sessions: int = 1000): self._sessions: dict[str, InteractionHandler] = {} self._max_sessions = max_sessions self._lock = asyncio.Lock() async def create( self, handler_type: str, chat_id: str, user_id: str, ttl: float = 300.0, ) -> InteractionHandler: async with self._lock: self._cleanup_expired() if len(self._sessions) >= self._max_sessions: oldest = min( self._sessions.values(), key=lambda h: h.created_at, default=None, ) if oldest: self._sessions.pop(oldest.interaction_id, None) iid = hashlib.sha256(f"{chat_id}:{user_id}:{handler_type}:{time.time()}".encode()).hexdigest()[:16] handler = InteractionHandler( interaction_id=iid, handler_type=handler_type, chat_id=chat_id, user_id=user_id, ) handler.expires_at = time.time() + ttl self._sessions[iid] = handler return handler async def get(self, interaction_id: str) -> InteractionHandler | None: async with self._lock: handler = self._sessions.get(interaction_id) if handler is None: return None if time.time() >= handler.expires_at: self._sessions.pop(interaction_id, None) return None return handler async def resolve(self, interaction_id: str, result: Any = None) -> bool: async with self._lock: handler = self._sessions.get(interaction_id) if handler is None: return False handler.resolved = True handler.result = result self._sessions.pop(interaction_id, None) return True async def cancel(self, interaction_id: str) -> bool: async with self._lock: if interaction_id in self._sessions: self._sessions.pop(interaction_id, None) return True return False def _cleanup_expired(self) -> None: now = time.time() expired = [iid for iid, h in self._sessions.items() if now >= h.expires_at] for iid in expired: self._sessions.pop(iid, None)