# 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) **Параметры:** - Зарегистрированных пользователей: 300–400 - Частота использования: экстренная кнопка — событие редкое - Типичная частота: ~1 нажатие/неделю по всей базе **Сценарии:** | Сценарий | Сигналов/мин | Telegram msg/мин | Статус | |---|---|---|---| | Нормальный (1 пользователь) | 1 | 1 | Лимит: 20/мин ✅ | | Групповая тревога (5 человек за минуту) | 5 | 5 | Лимит: 20/мин ✅ | | Массовая тревога (20 за минуту) | 20 | 20 | На грани лимита ⚠️ | | Абсурдный worst case (все 400) | 400 | 400 | Далеко за лимитом ❌ | **Вывод:** реалистичный пик — 5–10 одновременных сигналов. «Все 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:00–12: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+ одновременных сигналов/мин.** Для 300–400 пользователей это нереалистичный сценарий. Агрегатор — 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: ``` Реализовано: `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/день — триггер для включения агрегатора.