From 1289dbf70791f78c7123a90550950fa4d1bfd9d1 Mon Sep 17 00:00:00 2001 From: Markov Date: Thu, 26 Mar 2026 13:12:07 +0100 Subject: [PATCH] feat: migrate bridge to events API (project_id, EventOut format) --- bridge.py | 31 +++++++++++++++----------- tracker_client.py | 56 ++++------------------------------------------- 2 files changed, 22 insertions(+), 65 deletions(-) diff --git a/bridge.py b/bridge.py index 9c6707a..6b40ef9 100644 --- a/bridge.py +++ b/bridge.py @@ -44,22 +44,29 @@ async def on_tracker_event(event: dict): return if event_type == "message.new": - msg = event.get("data", {}) - author = msg.get("author", {}) - logger.info("message.new from %s: %s", author.get("slug", "?"), (msg.get("content", ""))[:50]) + data = event.get("data", {}) + actor = data.get("actor") or {} + payload = data.get("payload") or {} + evt_type = data.get("type", "") + content = payload.get("content", "") + + logger.info("message.new type=%s from %s: %s", evt_type, actor.get("slug", "?"), content[:50]) + + # Only forward chat messages (not task_comment, task_status, etc.) + if evt_type not in ("chat_message",): + return # Skip messages from bridge itself (avoid echo) - author_slug = author.get("slug", "") - if author_slug == "bridge": + actor_slug = actor.get("slug", "") + if actor_slug == "bridge": return # Skip messages that bridge sent on behalf of Telegram users # (they contain "[Username]" prefix) - text_content = msg.get("content", "") - if text_content.startswith("[") and "] " in text_content[:50]: + if content.startswith("[") and "] " in content[:50]: return - project_id = msg.get("project_id") + project_id = data.get("project_id") if not project_id: return @@ -67,13 +74,11 @@ async def on_tracker_event(event: dict): if not topic_id: return - author_name = author.get("name", author.get("slug", "???")) - text = msg.get("content", "") - if not text: + if not content: return - # Format: "Author: message" - tg_text = f"{_escape_html(author_name)}: {_escape_html(text)}" + actor_name = actor.get("name", actor.get("slug", "???")) + tg_text = f"{_escape_html(actor_name)}: {_escape_html(content)}" # Silent if any human is online in web silent = any(s for s in _online_members if s not in ("coder", "architect", "bridge")) await _send_to_topic(topic_id, tg_text, silent=silent) diff --git a/tracker_client.py b/tracker_client.py index 27dd099..a875b19 100644 --- a/tracker_client.py +++ b/tracker_client.py @@ -5,7 +5,6 @@ import json import logging from typing import Callable, Awaitable -import httpx import websockets from config import Config @@ -21,7 +20,6 @@ class TrackerClient: self.on_message = on_message self._ws = None self._running = False - self._project_chat_ids: dict[str, str] = {} # project_uuid -> chat_id self._send_queue: asyncio.Queue = asyncio.Queue() async def connect(self): @@ -35,7 +33,6 @@ class TrackerClient: self._ws = ws logger.info("Connected to Tracker WS") - # Run reader, writer and heartbeat concurrently await asyncio.gather( self._reader(ws), self._writer(ws), @@ -84,7 +81,6 @@ class TrackerClient: if event_type == "auth.ok": logger.info("Authenticated as bridge") - await self._cache_projects() return if event_type == "error": @@ -100,63 +96,19 @@ class TrackerClient: if event_type in ("message.new", "task.updated", "task.created", "project.created"): await self.on_message(event) - async def _cache_projects(self): - """Fetch all projects and cache their chat_ids.""" - try: - async with httpx.AsyncClient() as client: - resp = await client.get( - f"{self.config.tracker_url}/api/v1/projects", - headers={"Authorization": f"Bearer {self.config.bridge_token}"}, - ) - if resp.status_code == 200: - for p in resp.json(): - pid = p.get("id") - cid = p.get("chat_id") - if pid and cid: - self._project_chat_ids[pid] = cid - logger.info("Cached %d project chat_ids", len(self._project_chat_ids)) - except Exception as e: - logger.error("Failed to cache projects: %s", e) - - async def get_chat_id(self, project_uuid: str) -> str | None: - """Get chat_id for a project, fetching if not cached.""" - if project_uuid in self._project_chat_ids: - return self._project_chat_ids[project_uuid] - - # Try to fetch - try: - async with httpx.AsyncClient() as client: - resp = await client.get( - f"{self.config.tracker_url}/api/v1/projects/{project_uuid}", - headers={"Authorization": f"Bearer {self.config.bridge_token}"}, - ) - if resp.status_code == 200: - cid = resp.json().get("chat_id") - if cid: - self._project_chat_ids[project_uuid] = cid - return cid - except Exception as e: - logger.error("Failed to fetch project %s: %s", project_uuid[:8], e) - return None - - async def send_message(self, project_uuid: str, text: str): - """Send a chat message to Tracker.""" + async def send_message(self, project_id: str, text: str): + """Send a chat message to Tracker via WS chat.send.""" if not self._ws: logger.warning("Not connected to Tracker") return - chat_id = await self.get_chat_id(project_uuid) - if not chat_id: - logger.warning("No chat_id for project %s", project_uuid[:8]) - return - payload = { "type": "chat.send", - "chat_id": chat_id, + "project_id": project_id, "content": text, } await self._send_queue.put(payload) - logger.info("Queued to Tracker: project=%s chat=%s", project_uuid[:8], chat_id[:8]) + logger.info("Queued to Tracker: project=%s", project_id[:8]) def stop(self): self._running = False