# Team Board — Telegram Bridge ## Назначение **Telegram Bridge** — микросервис для интеграции Team Board с Telegram. Обеспечивает двустороннюю синхронизацию сообщений между проектными чатами Tracker и топиками Telegram группы. **Основной файл:** `bridge.py` **Архитектура:** Standalone Python сервис --- ## Архитектура ``` Telegram Bot API ↔ Bridge ↔ Tracker WebSocket ↓ Topic Mapping (JSON файл хранение) ``` ### Компоненты: 1. **bridge.py** — основной сервис с двусторонней синхронизацией 2. **config.py** — конфигурация через environment переменные 3. **topic_map.py** — персистентное маппинг Telegram топик ↔ Tracker проект 4. **tracker_client.py** — WebSocket клиент для подключения к Tracker --- ## Конфигурация **Файл:** `.env` ```bash # Telegram Bot TELEGRAM_BOT_TOKEN=1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZ TELEGRAM_GROUP_ID=-1001234567890 # Tracker TRACKER_URL=https://dev.team.uix.su TRACKER_WS_URL=wss://dev.team.uix.su/ws BRIDGE_TOKEN=tb-bridge-dev-token ``` ### Переменные окружения: - **TELEGRAM_BOT_TOKEN** — токен бота от @BotFather - **TELEGRAM_GROUP_ID** — ID группы с топиками (отрицательное число) - **TRACKER_URL** — HTTP URL Tracker API - **TRACKER_WS_URL** — WebSocket URL для подключения к Tracker - **BRIDGE_TOKEN** — Bearer токен агента типа "bridge" в Tracker --- ## Маппинг топиков **Файл:** `data/topic_map.json` ### Структура: ```json { "123": "550e8400-e29b-41d4-a716-446655440000", "456": "6ba7b810-9dad-11d1-80b4-00c04fd430c8" } ``` **Формат:** `{telegram_topic_id: tracker_project_uuid}` ### Класс TopicMap: ```python class TopicMap: def set(topic_id: int, project_uuid: str) # создать маппинг def get_project(topic_id: int) -> str | None # получить проект по топику def get_topic(project_uuid: str) -> int | None # получить топик по проекту def remove_by_topic(topic_id: int) # удалить маппинг def all() -> dict[int, str] # все маппинги ``` **Персистентность:** автоматическое сохранение в JSON файл при изменениях --- ## Telegram → Tracker ### Обработка сообщений: ```python async def handle_telegram_message(update: Update, context: ContextTypes.DEFAULT_TYPE): # Фильтрация по группе и наличию topic if message.chat_id != config.group_id or not message.message_thread_id: return # Поиск соответствующего проекта project_uuid = topic_map.get_project(message.message_thread_id) if not project_uuid: return # Формат сообщения: [Имя пользователя] текст text = f"[{user.full_name}] {message.text}" await tracker.send_message(project_uuid, text) ``` ### Telegram команды: #### `/link ` Привязать текущий топик к проекту **Использование:** ``` /link team-board ``` **Логика:** 1. Проверка, что команда выполнена в топике 2. Поиск проекта по slug через Tracker API 3. Создание маппинга в TopicMap 4. Подтверждение в чате #### `/unlink` Отвязать текущий топик от проекта **Логика:** 1. Проверка выполнения в топике 2. Удаление маппинга из TopicMap 3. Подтверждение в чате #### `/bridge_status` Показать статус bridge и все активные маппинги **Пример ответа:** ``` Привязки топик → проект: • topic 123 → 550e8400... • topic 456 → 6ba7b810... ``` --- ## Tracker → Telegram ### Поддерживаемые события: #### `message.new` Новое сообщение в проектном чате **Фильтрация:** - Пропуск сообщений от самого bridge (избежание эха) - Пропуск сообщений с префиксом `[Username]` (отправленных bridge'ом) **Формат в Telegram:** ``` [Автор]: текст сообщения ``` **Silent режим:** если есть онлайн участники-люди в web интерфейсе #### `task.created` Создание новой задачи **Формат в Telegram:** ``` 📋 Новая задача XX-123: Название задачи ``` #### `project.created` Создание нового проекта **Автоматическое действие:** 1. Создание топика в Telegram с именем проекта 2. Автоматический маппинг топик → проект 3. Уведомление о привязке #### `agent.status` Изменение статуса участника **Отслеживание:** обновление множества онлайн участников для silent режима --- ## TrackerClient **Файл:** `tracker_client.py` ### WebSocket подключение: ```python class TrackerClient: async def connect() # подключение с авто-переподключением async def send_message(project_uuid, text) # отправка сообщения в проект def stop() # остановка клиента ``` ### Особенности: - **Auto-reconnect:** переподключение каждые 5 секунд при разрыве - **Heartbeat:** отправка heartbeat каждые 25 секунд - **Queue система:** очередь сообщений до успешного подключения - **Project caching:** кэширование project_uuid → chat_id маппингов ### Аутентификация: ```python url = f"{config.tracker_ws_url}?token={config.bridge_token}" ``` **Тип участника:** `bridge` в Tracker системе --- ## Работа с проектами ### Кэширование chat_id: ```python self._project_chat_ids: dict[str, str] = {} # project_uuid -> chat_id ``` **Загрузка при подключении:** ```python GET /api/v1/projects Authorization: Bearer {bridge_token} ``` **Lazy loading:** автоматическая подгрузка chat_id при необходимости ### Отправка сообщений: 1. Получение chat_id для проекта 2. Формирование WebSocket сообщения: ```json { "type": "chat.send", "chat_id": "project_chat_uuid", "content": "[Username] message text" } ``` --- ## Silent режим **Логика:** если в системе есть онлайн участники-люди (не агенты/bridge), уведомления в Telegram отправляются беззвучно **Отслеживание онлайн:** ```python _online_members: set[str] = set() # member slugs # При получении agent.status события: if status == "online": _online_members.add(slug) else: _online_members.discard(slug) # При отправке в Telegram: silent = any(s for s in _online_members if s not in ("coder", "architect", "bridge")) ``` **Исключения:** агенты `coder`, `architect`, `bridge` не считаются "людьми" --- ## HTML Escaping Все сообщения в Telegram экранируются для HTML: ```python def _escape_html(text: str) -> str: return text.replace("&", "&").replace("<", "<").replace(">", ">") ``` **Формат отправки:** `parse_mode="HTML"` --- ## Предотвращение эха ### Проблема: Bridge отправляет сообщение в Tracker → Tracker рассылает `message.new` event → Bridge получает и отправляет в Telegram → возможное дублирование ### Решения: #### 1. Пропуск сообщений от bridge: ```python author_slug = author.get("slug", "") if author_slug == "bridge": return ``` #### 2. Пропуск сообщений с Telegram префиксом: ```python if text_content.startswith("[") and "] " in text_content[:50]: return ``` #### 3. Исключение собственной сессии в WebSocket: Tracker автоматически не отправляет события отправителю --- ## Логирование **Уровень:** INFO **Формат:** `%(asctime)s [%(name)s] %(levelname)s: %(message)s` ### Ключевые логи: ``` Connected to Tracker WS Authenticated as bridge TG->Tracker: topic=123 project=550e8400 Auto-created topic 456 for project team-board (6ba7b810) Linked topic 123 -> project team-board (550e8400) ``` --- ## Запуск и развёртывание ### Зависимости: ```txt python-telegram-bot[webhooks] websockets httpx python-dotenv ``` **Файл:** `requirements.txt` ### Запуск: ```bash python bridge.py ``` ### Архитектура событий: ```python async def main(): # Запуск Telegram polling await app.updater.start_polling() # Запуск Tracker WebSocket (блокирует) await tracker.connect() ``` **Concurrent execution:** Telegram bot и Tracker WebSocket клиент работают параллельно --- ## Обработка ошибок ### Telegram API: ```python try: await _bot.send_message(...) except Exception as e: logger.error("Failed to send to topic %d: %s", topic_id, e) ``` ### Tracker WebSocket: ```python try: # WebSocket операции except websockets.ConnectionClosed: logger.warning("Tracker WS closed, reconnecting in 5s...") await asyncio.sleep(5) ``` ### API запросы: ```python try: async with httpx.AsyncClient() as client: resp = await client.get(...) if resp.status_code != 200: return None except Exception as e: logger.error("API error: %s", e) ``` **Стратегия:** логирование ошибок + graceful degradation без остановки сервиса --- ## Масштабирование ### Ограничения: - Один bridge instance на Telegram группу - JSON файл для маппингов (не подходит для кластера) - In-memory кэш проектов ### Потенциальные улучшения: - Redis для маппингов и кэша - Database-backed конфигурация - Multiple bot support - Rate limiting для Telegram API --- ## Безопасность ### Аутентификация: - Bridge токен для Tracker API/WebSocket - Фильтрация по group_id в Telegram - Только авторизованные команды ### Данные: - Маппинги хранятся локально в JSON - Логирование без чувствительной информации - Автоматическое переподключение при разрыве Этот документ описывает Telegram Bridge на основе исходного кода в `/root/projects/team-board/bridge/` на дату создания спецификации.