from __future__ import annotations from collections.abc import Callable, Awaitable from dataclasses import dataclass, field from typing import Any from yuxi.utils.logging_config import logger @dataclass class InteractionPayload: type: str = "" action_id: str = "" user: str = "" channel: str = "" ts: str = "" thread_ts: str = "" value: str = "" raw: dict[str, Any] | None = None @property def is_button(self) -> bool: return self.type == "button" @property def is_select(self) -> bool: return self.type == "static_select" @property def is_overflow(self) -> bool: return self.type == "overflow" @dataclass class InteractionRoute: action_id: str handler: Callable[[InteractionPayload], Awaitable[dict[str, Any]]] description: str = "" async def execute(self, payload: InteractionPayload) -> dict[str, Any]: try: return await self.handler(payload) except Exception as e: logger.error(f"Interaction route '{self.action_id}' failed: {e}") return {"error": str(e)} @dataclass class InteractionRouter: routes: dict[str, InteractionRoute] = field(default_factory=dict) def register(self, action_id: str, handler: Callable, description: str = "") -> None: self.routes[action_id] = InteractionRoute( action_id=action_id, handler=handler, description=description, ) def unregister(self, action_id: str) -> None: self.routes.pop(action_id, None) def get_route(self, action_id: str) -> InteractionRoute | None: if action_id in self.routes: return self.routes[action_id] for prefix, route in self.routes.items(): if prefix.endswith("*") and action_id.startswith(prefix[:-1]): return route return None async def dispatch(self, payload: InteractionPayload) -> dict[str, Any]: route = self.get_route(payload.action_id) if route: return await route.execute(payload) logger.debug(f"No route found for action_id: {payload.action_id}") return {"acknowledged": True, "routed": False} @property def registered_actions(self) -> list[str]: return list(self.routes.keys()) async def dispatch_interaction( client, payload: InteractionPayload, *, handlers: dict[str, Any] | None = None, ) -> dict[str, Any]: if not handlers: handlers = {} handler = handlers.get(payload.action_id) if handler: try: return await handler(payload) except Exception as e: logger.error(f"Interaction handler error for {payload.action_id}: {e}") return {"error": str(e)} logger.debug(f"No handler found for action_id: {payload.action_id}") return {"acknowledged": True} def parse_interaction_payload(raw_payload: dict[str, Any]) -> InteractionPayload: actions = raw_payload.get("actions", []) action = actions[0] if actions else {} return InteractionPayload( type=raw_payload.get("type", action.get("type", "")), action_id=action.get("action_id", ""), user=raw_payload.get("user", {}).get("id", ""), channel=raw_payload.get("channel", {}).get("id", ""), ts=raw_payload.get("message", {}).get("ts", ""), thread_ts=raw_payload.get("message", {}).get("thread_ts", ""), value=action.get("value", action.get("selected_option", {}).get("value", "")), raw=raw_payload, ) _external_menu_store: dict[str, list[dict[str, Any]]] = {} def store_external_options(request_id: str, options: list[dict[str, Any]]) -> None: _external_menu_store[request_id] = options def get_external_options(request_id: str) -> list[dict[str, Any]]: return _external_menu_store.pop(request_id, []) def clear_external_options() -> None: _external_menu_store.clear()