ForcePilot/backend/package/yuxi/channel/extensions/mattermost/gateway.py

134 lines
5.1 KiB
Python
Raw Normal View History

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", "")