"""WebSocket client for Tracker.""" import asyncio import json import logging from typing import Callable, Awaitable import websockets from websockets.asyncio.client import connect from config import Config logger = logging.getLogger(__name__) class TrackerClient: """Connects to Tracker WS as a bridge member.""" def __init__(self, config: Config, on_message: Callable[[dict], Awaitable[None]]): self.config = config self.on_message = on_message self._ws = None self._topic_map: dict[str, int] = {} # project_uuid -> topic_id self._running = False async def connect(self): """Connect and authenticate.""" url = f"{self.config.tracker_ws_url}?token={self.config.bridge_token}" self._running = True while self._running: try: async with connect(url) as ws: self._ws = ws logger.info("Connected to Tracker WS") async for raw in ws: try: event = json.loads(raw) await self._handle_event(event) except json.JSONDecodeError: logger.warning("Invalid JSON from Tracker: %s", raw[:200]) except websockets.ConnectionClosed: logger.warning("Tracker WS closed, reconnecting in 5s...") await asyncio.sleep(5) except Exception as e: logger.error("Tracker WS error: %s, reconnecting in 5s...", e) await asyncio.sleep(5) async def _handle_event(self, event: dict): """Route Tracker events.""" event_type = event.get("type", "") if event_type == "auth.ok": logger.info("Authenticated as bridge") return if event_type == "error": logger.error("Tracker error: %s", event.get("message")) return # Forward message events to Telegram if event_type in ("message.new", "task.updated", "task.created"): await self.on_message(event) async def send_message(self, project_uuid: str, text: str, author_name: str = None): """Send a chat message to Tracker.""" if not self._ws: logger.warning("Not connected to Tracker") return payload = { "type": "chat.send", "project_id": project_uuid, "text": text, } await self._ws.send(json.dumps(payload)) def stop(self): self._running = False