bridge/tracker_client.py

81 lines
2.5 KiB
Python

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