400 lines
12 KiB
Markdown
400 lines
12 KiB
Markdown
# 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 <project_slug>`
|
||
Привязать текущий топик к проекту
|
||
|
||
**Использование:**
|
||
```
|
||
/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/` на дату создания спецификации. |