baton/docs/adr/ADR-004-telegram-strategy.md

185 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ADR-004: Стратегия отправки в Telegram (прямой vs агрегатор)
**Дата:** 2026-03-20
**Статус:** Accepted
**Автор:** Architect Agent (Kin pipeline, BATON-003)
**Решения:** #1009, #1010, #1011, #1014, #1017, #1020
---
## Контекст
При нажатии кнопки SOS бэкенд должен доставить уведомление в Telegram-группу через Bot API. Вопрос: отправлять каждый сигнал отдельным `sendMessage` (direct) или буферизовать и отправлять пачками (aggregator)?
### Rate limits Telegram Bot API
| Ограничение | Значение |
|---|---|
| Один чат (любой тип) | ~1 msg/сек |
| Группа | 20 msg/минуту |
| Глобально | ~30 msg/сек |
При превышении: HTTP 429 + `parameters.retry_after`.
---
## Оценка пиковой нагрузки (#1017)
**Параметры:**
- Зарегистрированных пользователей: 300400
- Частота использования: экстренная кнопка — событие редкое
- Типичная частота: ~1 нажатие/неделю по всей базе
**Сценарии:**
| Сценарий | Сигналов/мин | Telegram msg/мин | Статус |
|---|---|---|---|
| Нормальный (1 пользователь) | 1 | 1 | Лимит: 20/мин ✅ |
| Групповая тревога (5 человек за минуту) | 5 | 5 | Лимит: 20/мин ✅ |
| Массовая тревога (20 за минуту) | 20 | 20 | На грани лимита ⚠️ |
| Абсурдный worst case (все 400) | 400 | 400 | Далеко за лимитом ❌ |
**Вывод:** реалистичный пик — 510 одновременных сигналов. «Все 400 нажали» — нереалистичный сценарий (разные страны, разные часовые пояса, разные ситуации). Лимит 20 msg/мин покрывает 99.9% случаев.
**Решение #1017:** оценки нагрузки в ADR могут быть «drastically wrong» → архитектура должна позволять включить агрегатор без переписывания. Код агрегатора УЖЕ реализован в `telegram.py:51-121` — он просто не активирован.
---
## Варианты
### Вариант A: Прямой sendMessage (1 сигнал = 1 сообщение)
```
[PWA] POST /api/signal
→ [Backend] save_signal() в SQLite
→ [Backend] POST api.telegram.org/sendMessage
→ [Telegram Group]
```
**Плюсы:**
- Минимальная задержка: сигнал → сообщение < 1 секунды
- Простейшая логика: нет буфера, нет фоновых задач, нет lock
- Отладка тривиальна: 1 HTTP вызов, 1 ответ
- Прозрачность: каждый сигнал виден отдельно в Telegram
**Минусы:**
- При 20+ сигналов/мин 429 от Telegram потеря или задержка сообщений
- Каждый сигнал = отдельное уведомление в группе (шум при массовом событии)
### Вариант B: Агрегатор (буфер 10 секунд → batch message)
```
[PWA] POST /api/signal
→ [Backend] add_signal() в in-memory buffer
→ [Background task: 10 sec flush loop]
→ [Backend] POST sendMessage (пакетное сообщение)
```
**Плюсы:**
- Гарантированное соблюдение rate limits: 6 msg/мин max (1 каждые 10 сек)
- Компактные сообщения: "🚨 3 сигнала [12:05:0012:05:08]" вместо 3 отдельных
- Масштабируется до 1000+ сигналов/мин
**Минусы:**
- Задержка: до 10 секунд между сигналом и сообщением
- Сложность: asyncio.Lock, background task, graceful shutdown, buffer management
- In-memory буфер: при crash uvicorn сигналы в буфере потеряны (SQLite сохраняет, но Telegram не получит)
- Один uvicorn worker обязателен (буфер в памяти, не shared)
### Вариант C: Внешняя очередь (Redis/RabbitMQ)
**Плюсы:**
- Масштабируется горизонтально
- Буфер переживёт перезапуск процесса
**Минусы:**
- Дополнительная зависимость (Redis) для 1 нажатия/неделю
- Deployment complexity растёт кратно
- Абсурдный overkill для 300-400 пользователей
---
## Решение
**Выбран Вариант A: Прямой sendMessage для v1**
Агрегатор (Вариант B) оставлен в коде (`telegram.py:SignalAggregator`) но НЕ активирован. При росте нагрузки включается изменением 2 строк в `main.py`.
---
## Обоснование
1. **#1020: агрегатор нужен только при 20+ одновременных сигналов/мин.** Для 300400 пользователей это нереалистичный сценарий. Агрегатор premature optimization.
2. **Задержка неприемлема для экстренного приложения.** 10-секундный буфер = 10 секунд ожидания, пока спасатель увидит сигнал. При прямой отправке < 1 секунды.
3. **Код агрегатора уже написан.** `telegram.py:51-121` содержит полный `SignalAggregator` с lock, flush, rate limiting. Включение в v2:
```python
# main.py lifespan: раскомментировать 2 строки
aggregator = SignalAggregator(interval=10)
asyncio.create_task(aggregator.run())
```
4. **#1017 учтён:** если оценка нагрузки окажется «drastically wrong» — переключение на агрегатор не требует переписывания архитектуры.
---
## Безопасность Telegram-интеграции
### Webhook secret validation (#1010)
Каждый входящий запрос на `POST /api/webhook/telegram` ОБЯЗАН содержать заголовок:
```
X-Telegram-Bot-Api-Secret-Token: <WEBHOOK_SECRET>
```
Реализовано: `middleware.py:verify_webhook_secret()` — FastAPI dependency, 403 при несовпадении.
### HTTPS обязателен (#1011)
- Telegram принимает webhook ТОЛЬКО на HTTPS URL
- Поддерживаемые порты: 443, 80, 88, 8443
- TLS 1.2 минимум
- CA-signed сертификат (Let's Encrypt достаточен)
### setWebhook vs getUpdates (#1009)
Telegram Bot API: `setWebhook` и `getUpdates` взаимоисключающи. Baton использует ТОЛЬКО `setWebhook`. Вызов `getUpdates` автоматически удалит webhook.
---
## Формат сообщения в Telegram (v1)
### Прямая отправка (1 сигнал = 1 сообщение)
```
🚨 SOS от Алиса
📍 55.7558, 37.6173 (±15м)
🕐 2026-03-20 14:05:23 UTC
```
Без геолокации:
```
🚨 SOS от Алиса
📍 Геолокация недоступна
🕐 2026-03-20 14:05:23 UTC
```
Если имя неизвестно (UUID не зарегистрирован):
```
🚨 SOS от 550e8400
📍 Геолокация недоступна
🕐 2026-03-20 14:05:23 UTC
```
---
## Последствия
1. **Один uvicorn worker достаточен для v1.** Агрегатор не активен → нет shared state → но для консистентности рекомендуется 1 worker.
2. **При 429 от Telegram:** текущий код (`telegram.py:22-25`) обрабатывает retry_after корректно. Сигнал не теряется — он уже в SQLite. Telegram-сообщение задержится на `retry_after` секунд.
3. **Переход на v2 (агрегатор):** изменение 3 строк в `main.py` + изменение signal endpoint (вместо прямого sendMessage → `aggregator.add_signal()`). Оценка: 30 минут работы dev-агента.
4. **Мониторинг:** логировать 429 ответы от Telegram. Если частота > 1/день — триггер для включения агрегатора.