from __future__ import annotations import asyncio import logging from yuxi.channel.extensions.mattermost.client import MattermostClient from yuxi.channel.extensions.mattermost.config import MattermostConfigAdapter, normalize_mattermost_base_url from yuxi.channel.extensions.mattermost.errors import MattermostError, MattermostAuthError from yuxi.channel.extensions.mattermost.reconnect import ReconnectManager from yuxi.channel.extensions.mattermost.status import MattermostStatusAdapter from yuxi.channel.extensions.mattermost.websocket import MattermostWebSocketMonitor logger = logging.getLogger(__name__) GATEWAY_AUTH_BYPASS_PATHS = [ "/api/channels/mattermost/command", "/api/channels/mattermost/interactions", ] class MattermostGatewayAdapter: def __init__(self, config_adapter: MattermostConfigAdapter): self.config_adapter = config_adapter self._clients: dict[str, MattermostClient] = {} self._monitors: dict[str, MattermostWebSocketMonitor] = {} self._tasks: dict[str, asyncio.Task] = {} self._abort_events: dict[str, asyncio.Event] = {} self._status_adapters: dict[str, MattermostStatusAdapter] = {} self.bot_info: dict[str, dict] = {} async def start(self, ctx: object) -> object: account_id = getattr(ctx, "account_id", "default") if ctx else "default" account = await self.config_adapter.resolve_account(account_id) if not account.get("bot_token") or not account.get("base_url"): logger.warning("Mattermost account %s not configured, skipping", account_id) return {"status": "not_configured", "account_id": account_id} base_url = normalize_mattermost_base_url(account["base_url"]) client = MattermostClient( base_url=base_url, bot_token=account["bot_token"], allow_private_network=account.get("dangerously_allow_private_network", False), ) try: me = await client.fetch_me() self.bot_info[account_id] = me logger.info( "Mattermost bot %s (%s) connected to %s", me.get("username", "unknown"), me.get("id", "unknown"), base_url, ) except MattermostAuthError as e: logger.error("Mattermost auth failed for account %s: %s", account_id, e) await client.close() return {"status": "auth_failed", "account_id": account_id} except MattermostError as e: logger.error("Mattermost connection failed for account %s: %s", account_id, e) await client.close() return {"status": "connection_failed", "account_id": account_id} self._clients[account_id] = client self._status_adapters[account_id] = MattermostStatusAdapter(client, account_id) abort_event = asyncio.Event() self._abort_events[account_id] = abort_event reconnect_mgr = ReconnectManager(initial_delay_ms=2000, max_delay_ms=60000) async def ws_connect_loop(): monitor = MattermostWebSocketMonitor(client, account_id) self._monitors[account_id] = monitor monitor.bot_user_id = self.bot_info[account_id].get("id", "") monitor.bot_last_update_at = self.bot_info[account_id].get("update_at", 0) self._monitors[account_id] = monitor await reconnect_mgr.run_with_reconnect( connect_fn=monitor.connect, abort_event=abort_event, ) task = asyncio.create_task(ws_connect_loop()) self._tasks[account_id] = task return { "status": "started", "account_id": account_id, "base_url": base_url, "bot_user_id": me.get("id", ""), "bot_username": me.get("username", ""), } async def stop(self, ctx: object) -> None: account_id = getattr(ctx, "account_id", "default") if ctx else "default" abort = self._abort_events.pop(account_id, None) if abort: abort.set() task = self._tasks.pop(account_id, None) if task: task.cancel() try: await task except asyncio.CancelledError: pass monitor = self._monitors.pop(account_id, None) if monitor: await monitor.disconnect() client = self._clients.pop(account_id, None) if client: await client.close() self._status_adapters.pop(account_id, None) self.bot_info.pop(account_id, None) logger.info("Mattermost gateway stopped for account %s", account_id) def resolve_gateway_auth_bypass_paths(self, config: dict) -> list[str]: return GATEWAY_AUTH_BYPASS_PATHS def get_client(self, account_id: str = "default") -> MattermostClient | None: return self._clients.get(account_id) def get_bot_user_id(self, account_id: str = "default") -> str: info = self.bot_info.get(account_id, {}) return info.get("id", "") def get_bot_username(self, account_id: str = "default") -> str: info = self.bot_info.get(account_id, {}) return info.get("username", "")