Тема
ТЗ — Telegram-канал (cms/telegram)
Слой: 🟡 функц-модуль · Зрелость доноров: ★★ · Донор: masha, notal Статус: ТЗ к разработке
Назначение и возможности
Бот Telegram для двух аудиторий: уведомления команде (новый заказ/лид) и клиентам (статусы). Привязка чата по одноразовому коду через deep-link, короткие команды бота, канал шины уведомлений.
- Уведомления команде в общий чат/канал: новый заказ, новый лид, критичные события;
- уведомления клиенту в личный чат: статус заказа, ответ на обращение (после привязки);
- привязка
chat_idк пользователю/сотруднику по одноразовому коду: пользователь переходит по deep-linkt.me/<bot>?start=<код>(кнопка «Привязать Telegram» в кабинете/админке генерирует код и ссылку), бот получает/start <код>как обычное сообщение и создаёт привязку; код одноразовый — сгорает сразу после первого успешного использования, повторный переход по той же ссылке уже не работает; - команды бота: краткий статус заказа, список последних уведомлений;
- provides: notification-channel telegram для
cms/notifications-bus; - вебхук бота принимается через
cms/webhooks-in(верификация, единая точка входа).
Зависимости и выключение
requires: cms/notifications-bus, cms/webhooks-in · provides: notification-channel telegram
Поведение при выключении: шина уведомлений пропускает канал telegram при рассылке; привязки chat_id сохраняются в БД, бот не отвечает на команды до включения модуля.
Стоимость внешнего API: Telegram Bot API бесплатен — тарифных лимитов и списаний за сообщение нет (в отличие от cms/messengers, где WhatsApp/Viber/MAX тарифицируются провайдером за сообщение/диалоговое окно). Единственное ограничение — rate-limit самого Bot API (см. «Производительность и кеш» и «Крайние случаи»), поэтому у модуля нет учёта расхода бюджета и алерта по исчерпанию квоты — это осознанное отличие от cms/sms и cms/messengers.
Модель данных
| Таблица | Ключевые поля | Примечание |
|---|---|---|
cms_telegram_bindings | id, user_id, chat_id, bind_code, bind_code_used_at, bound_at, is_active | привязка чата к пользователю (одноразовый deep-link, is_active=false после блокировки бота — см. крайние случаи) |
cms_telegram_messages | id, chat_id, direction, payload (json), status | журнал исходящих/входящих сообщений |
user_id — FK constrained()->index(); уникальный индекс на chat_id в cms_telegram_bindings. direction/status в cms_telegram_messages — PHP Enum; payload — json()->nullable() + cast array; BRIN-индекс по created_at (журнал растёт линейно). bind_code_used_at — nullable, устанавливается атомарно при первом успешном /start, дальнейшие попытки с тем же кодом отклоняются.
ПДн-паспорт: chat_id и user_id-привязка — ПДн (косвенно идентифицируют человека через Telegram-аккаунт); payload в cms_telegram_messages может содержать текст уведомления (номер заказа, имя) — ПДн. Ретеншн: cms_telegram_messages хранится 90 дней (config telegram.message_log_retention_days), джоба очистки по ScheduleRegistrar; cms_telegram_bindings хранится, пока действует привязка (удаляется при отвязке/уничтожении аккаунта пользователя). Модуль реализует хуки ядра «выгрузить всё по субъекту» и «забыть по запросу» (152-ФЗ) для обеих таблиц.
Входные и выходные данные
Входы (whitelist-принцип: всё не перечисленное ниже — отклоняется):
| Источник | Поля | Чем валидируется |
|---|---|---|
Вебхук Telegram Bot API (cms/webhooks-in, /api/v1/webhooks-in/telegram) | update_id, message.chat.id, message.text, message.from.id | секретный path/токен вебхука (cms/webhooks-in), whitelist типов update (только текстовые сообщения/команды), неизвестные типы отклоняются без ошибки |
Deep-link /start <код> (входящее сообщение боту) | chat_id, bind_code | сверка с cms_telegram_bindings.bind_code, TTL bind_code_ttl_minutes, одноразовость (bind_code_used_at is null) |
| Команда бота | chat_id, command (whitelist: /status, /history) | whitelist команд, chat_id обязан быть привязан и is_active=true |
| Filament admin CRUD привязок | user_id, chat_id | FormRequest, permission telegram.manage |
Вызов от cms/notifications-bus (канал telegram) | user_id, template, payload | provides-контракт notification-channel, whitelist шаблонов уведомлений ядра |
cms:telegram:import-legacy --source=<профиль> | экспорт донора (masha/notal): привязки чатов | маппинг по профилю, ключ external_id, --dry-run с отчётом расхождений |
Выходы:
| Потребитель | Данные | Формат |
|---|---|---|
Telegram Bot API (sendMessage) | текст уведомления по шаблону | HTTP POST JSON (Bot API), только из очереди telegram |
cms/notifications-bus | статус доставки канала (успех/фейл) | возврат сервис-вызова + событие |
| Filament (журнал) | cms_telegram_messages | таблица со статусами, keyset-пагинация |
cms/health | доступность Bot API, актуальность вебхука | cms:telegram:doctor --json |
| Подписчики шины | TelegramChatBound / TelegramMessageDelivered / TelegramCommandReceived | payload события |
Настройки (группа telegram)
| Ключ | Тип | Дефолт | affectsPageCache | Описание |
|---|---|---|---|---|
telegram.bot_token | string | — | нет | Токен бота (хранится в .env → config()) |
telegram.team_chat_id | string | — | нет | Chat ID группы для уведомлений команде |
telegram.bind_code_ttl_minutes | int | 10 | нет | Срок действия кода привязки |
telegram.rate_limit_per_minute | int | 20 | нет | Лимит исходящих сообщений в минуту (защита от лимитов Bot API); достижение — сообщение остаётся в очереди, не теряется |
telegram.message_log_retention_days | int | 90 | нет | Ретеншн журнала cms_telegram_messages (ПДн, 152-ФЗ) |
telegram.outbound_enabled | bool | true | нет | Kill-switch: аварийно отключить исходящую отправку без выключения модуля целиком — вебхук и входящие команды продолжают обрабатываться |
API
Отдельного публичного API нет — приём событий бота через cms/webhooks-in (/api/v1/webhooks-in/telegram), административный CRUD привязок через Filament.
Компоненты
Filament: список привязанных пользователей команды (со статусом is_active и подсказкой «бот заблокирован — предложите привязать заново» для неактивных), журнал отправленных сообщений, виджет здоровья (статус токена/вебхука из cms:telegram:doctor). Демо-контент: у модуля нет блоков/виджетов витрины (только канал уведомлений) — в галерею /_gallery не участвует, отдельный демо-сидер не нужен.
Команды: cms:telegram:generate-bind-code --json, cms:telegram:doctor --json (проверка токена и доступности вебхука — при обнаружении проблемы отчёт содержит конкретную команду восстановления: невалидный токен → «обновите TELEGRAM_BOT_TOKEN в .env»; потерянный вебхук → cms:telegram:webhook:register), cms:telegram:webhook:register --json (переустановка вебхука на текущий домен/секрет), cms:telegram:import-legacy --source=<профиль> --dry-run --json (миграция привязок из донора).
События и обмен
| Событие | Когда | Payload |
|---|---|---|
TelegramChatBound | пользователь привязал чат по коду | user_id, chat_id |
TelegramMessageDelivered | сообщение доставлено | chat_id, template |
TelegramCommandReceived | получена команда от пользователя боту | chat_id, command |
TelegramChatBlocked | Bot API вернул 403 при отправке, привязка деактивирована | user_id, chat_id |
Слушает: вебхуки от cms/webhooks-in (тип telegram), запросы канала telegram от cms/notifications-bus. Provides-контракт notification-channel telegram — потребляется шиной уведомлений наравне с push/webpush/sms/messengers.
Таблица взаимодействий:
| Сущность/модуль | Канал | Направление | Что происходит |
|---|---|---|---|
cms/webhooks-in | вебхук (внешний канал) | Telegram → telegram | принимает update, верифицирует секретный path, кладёт в очередь telegram на разбор |
cms/notifications-bus | provides-контракт notification-channel telegram + сервис-вызов (requires) | notifications-bus → telegram | шина резолвит канал через DI, вызывает отправку по шаблону |
cms/notifications-bus | событие TelegramMessageDelivered | telegram → шина/подписчики | статус доставки фиксируется для отчётности шины |
очередь telegram | очередь | telegram → воркер | асинхронный разбор входящего update и асинхронная отправка исходящего |
cms/health | health-чек из манифеста | telegram → health | доступность Bot API (401/сеть), актуальность вебхука — cms:telegram:doctor |
cms/audit (если включён) | событие безопасности | telegram → audit | привязка/отвязка чата, ротация токена — аудируемые действия |
cms/attack-monitor (если включён) | событие безопасности | telegram → attack-monitor | всплеск неудачных /start (подбор кода) — сигнал для мониторинга |
Фоновая работа
Именованная очередь telegram для отправки сообщений (внешний HTTP-вызов к Bot API — только из очереди) и разбора входящих команд после верификации вебхука; джобы идемпотентны (повторный update_id от Telegram — при таймауте ответа Telegram может прислать тот же апдейт снова — не создаёт вторую привязку и не публикует команду дважды).
Метрики и алерты (видны в Pulse/health): telegram.outbound_sent_total, telegram.outbound_failed_total, telegram.webhook_lag_seconds (время с последнего принятого апдейта). Алерты: telegram.bot_token_invalid (Bot API отвечает 401), telegram.webhook_stale (нет апдейтов дольше настраиваемого порога при активных привязках).
Мини-ранбук:
| Симптом | Что проверить | Команда |
|---|---|---|
| Уведомления не уходят, health красный (401) | токен отозван/невалиден | обновить TELEGRAM_BOT_TOKEN в .env, перевыпустив в BotFather, затем cms:telegram:doctor --json |
| Входящие апдейты не приходят (вебхук потерян) | смена домена/сертификата, Telegram не достучался | cms:telegram:doctor --json покажет webhook_stale и предложит cms:telegram:webhook:register --json |
| Сообщение конкретному пользователю не доставляется (403) | пользователь заблокировал бота | привязка деактивирована автоматически (TelegramChatBlocked), ретрай не требуется — проверить cms_telegram_bindings.is_active |
Очередь telegram отстаёт | нагрузка/сбой воркера | размер очереди в cms/health, перезапуск воркера |
Бэкап/рестор: в бэкап попадают cms_telegram_bindings (привязки не восстанавливаются автоматически — это не денормализованные данные) и cms_telegram_messages (журнал). После рестора ничего не пересчитывается, но вебхук нужно переустановить командой cms:telegram:webhook:register, так как Telegram мог сбросить регистрацию при смене окружения/IP.
Производительность и кеш
Ожидаемые объёмы: сотни–низкие тысячи исходящих уведомлений в день на активный сайт (заказы, лиды, статусы), входящих команд — на порядок меньше (десятки в день). Горячий путь — публикация уведомления через cms/notifications-bus: чтение привязки по индексу user_id/chat_id (1 запрос), сам HTTP-вызов к Bot API — только в очереди, не на горячем пути страницы. Бюджет запросов: чтение привязки — 1 индексный запрос; токен и team_chat_id — из config(), 0 запросов. Критичные индексы (см. «Модель данных»): уникальный chat_id в cms_telegram_bindings, BRIN по created_at в cms_telegram_messages (журнал растёт линейно, полнотекстовый поиск не нужен). Теги кеша: telegram:bindings — инвалидируются событием TelegramChatBound/ TelegramChatBlocked; журнал сообщений в кеш не попадает (персональные данные).
Безопасность
Вебхук бота верифицируется (секретный путь/токен Telegram через cms/webhooks-in), неавторизованные запросы отклоняются. Токен бота — только в .env → config(), не хранится в таблице настроек в открытом виде. Код привязки одноразовый, доступен по deep-link и действует только bind_code_ttl_minutes. Команды бота не дают доступа к чужим данным (chat_id проверяется на привязку к конкретному пользователю и is_active=true). Rate-limit на генерацию кода привязки (защита от подбора/спама кодов) и на исходящую отправку (telegram.rate_limit_per_minute).
Матрица ролей:
| Роль | telegram.view | telegram.manage |
|---|---|---|
| Администратор клиента | ✅ | ✅ |
| Менеджер | ✅ (журнал, свои привязки) | — |
| Редактор | — | — |
| Studio | ✅ | ✅ (+ ручная переустановка вебхука, ротация токена) |
ПДн-паспорт — см. «Модель данных». В логи и аналитику не попадают текст сообщений сверх необходимого для журнала; chat_id в логах не выводится в открытом виде вне Filament-журнала с ограниченным доступом.
UX-требования
Для пользователя/клиента: привязка — одна кнопка «Привязать Telegram» в кабинете, которая открывает t.me/<bot>?start=<код> без ручного копирования кода; сообщения от бота — короткие читаемые шаблоны без служебного мусора (id событий, стектрейсов); частота уведомлений ограничена rate_limit_per_minute — не более одного сообщения на одно и то же событие (без дублей при ретраях шины).
Для админа/оператора: пустой список привязок сопровождается подсказкой «отправьте сотруднику код привязки — cms:telegram:generate-bind-code»; массовое действие — отозвать привязку у нескольких пользователей разом; ошибки — на человеческом языке («бот заблокирован пользователем», а не «Telegram API error 403»); статус токена и вебхука виден прямо в Filament-дашборде модуля (индикатор из cms:telegram:doctor), без захода в консоль.
Крайние случаи и типовые баги
- Токен отозван владельцем через BotFather → все вызовы Bot API падают 401;
cms:telegram:doctorфиксируетbot_token_invalid, канал telegram помечается недоступным в шине уведомлений, остальные каналы (sms/email/push) продолжают работать без блокировки. - Вебхук потерян (смена домена/сертификата) →
cms:telegram:doctorобнаруживаетwebhook_staleи в отчёте прямо предлагает команду восстановленияcms:telegram:webhook:register; до восстановления входящие команды не разбираются, исходящие уведомления продолжают уходить (независимое направление Bot API). - Пользователь заблокировал бота → Bot API возвращает 403 при отправке; джоба не уходит в бесконечный retry с backoff — привязка деактивируется (
is_active=false, событиеTelegramChatBlocked) после первого 403, шина уведомлений переключается на fallback-канал, в интерфейсе видно «бот заблокирован — привяжите заново». - Повторный переход по deep-link (код кликнут дважды/переслан) →
bind_codeодноразовый, сгорает после первого успешногоTelegramChatBound; повторное использование отклоняется с понятной ошибкой, вторая привязка не создаётся. - Код привязки истёк → переход по устаревшей ссылке → бот отвечает «код истёк, запросите новый в кабинете», привязка не создаётся.
- Гонка: повторная генерация кода тому же пользователю → предыдущий код инвалидируется новым (действует только последний), иначе путаница, каким кодом привязываться.
- Групповой чат
team_chat_idудалён/бот исключён из группы → отправка тоже вернёт 403/400; health помечаетteam_chat_unavailableотдельным алертом, не затрагивая персональные привязки клиентов. cms/webhooks-inвыключен (жёсткийrequires, не suggests) → self-test на enable модуля проваливается, если webhooks-in недоступен изначально; если его выключили после того, как telegram уже enabled, модуль деградирует частично: исходящие уведомления продолжают уходить, входящие команды перестают обрабатываться, health помечает канал как частично недоступный.- Повторная доставка апдейта Telegram (тот же
update_idпри таймауте ответа) → обработка идемпотентна поupdate_id, повторная команда/привязка не выполняется дважды. - Массовая рассылка упирается в rate-limit Bot API (глобально ~30 сообщений/сек, ~20/мин в один чат) → очередь
telegramтроттлит поrate_limit_per_minute, превышение оставляет сообщения в очереди, не теряет их и не превращается в 429 без обработки. - ⚠️ Противоречие: измерение
site_idотсутствует в модели. Правило измерений §4 стандарта требует, чтобы контентные/пользовательские сущности несли nullablesite_id/locale/city_idи корректно работали в мультисайт-режиме, ноcms_telegram_bindingsиtelegram.team_chat_id— общие на всю инсталляцию: приcms/multisiteвсе сайты делят одного бота и один командный чат, привязка пользователя не различает сайт. Предложение: добавить nullablesite_idвcms_telegram_bindingsи сделатьtelegram.team_chat_idper-site настройкой (группа настроек ядра уже поддерживает site-scope); до решения — зафиксировать ограничение «один бот на инсталляцию» явно вdocs/module.md.
Донорский код
| Что взять | Путь |
|---|---|
| Бот-обвязка, привязка чата, команды | masha, notal |
Миграция legacy: cms:telegram:import-legacy --source=masha|notal — маппинг таблиц привязок чатов донора на cms_telegram_bindings по ключу external_id (идентификатор пользователя в доноре); идемпотентна (повторный прогон обновляет, не дублирует), --dry-run выводит отчёт расхождений перед применением. Прогон на копии донорских данных — часть приёмки модуля (стандарт §16).
Тесты и приёмка
- [ ] Контрактный тест: код привязки одноразовый, действует только
bind_code_ttl_minutes, повторное использование отклоняется; - [ ] Deep-link
/start <код>создаёт привязку и сжигает код атомарно (гонка двух одновременных запросов не создаёт дублей); - [ ] Вебхук бота верифицируется (секретный путь/токен Telegram), неавторизованные запросы отклоняются;
- [ ] Повторная доставка одного
update_idне создаёт вторую привязку и не публикует команду дважды; - [ ] Bot API 403 при отправке деактивирует привязку и не уходит в бесконечный retry (проверка backoff-политики);
- [ ] Отправка в
team_chat_idне зависит от привязки конкретного пользователя (групповой канал); - [ ] Токен бота — только в
.env→config(), не хранится в таблице настроек в открытом виде; - [ ]
telegram.outbound_enabled=falseостанавливает исходящую отправку, не трогая приём вебхука; - [ ] Деградация при выключении/потере
cms/webhooks-inне ломает остальные каналы шины уведомлений; - [ ] Команды бота не дают доступа к чужим данным (
chat_idпроверяется на привязку иis_active); - [ ] Ретеншн
cms_telegram_messages(90 дней) и хуки «выгрузить/забыть по субъекту» покрыты тестами; - [ ]
cms:telegram:import-legacy --dry-runдаёт отчёт расхождений и не пишет в БД без явного запуска; - [ ] Контрактный набор
cms-testingзелёный, testbench-изоляция пакета, feature-тест на каждый роут; - [ ] Тестовая БД только
telegram_test;migrate:fresh/refresh/reset,db:wipeзапрещены.