Skip to content

ТЗ — Telegram-канал (cms/telegram)

Слой: 🟡 функц-модуль · Зрелость доноров: ★★ · Донор: masha, notal Статус: ТЗ к разработке

Назначение и возможности

Бот Telegram для двух аудиторий: уведомления команде (новый заказ/лид) и клиентам (статусы). Привязка чата по одноразовому коду через deep-link, короткие команды бота, канал шины уведомлений.

  • Уведомления команде в общий чат/канал: новый заказ, новый лид, критичные события;
  • уведомления клиенту в личный чат: статус заказа, ответ на обращение (после привязки);
  • привязка chat_id к пользователю/сотруднику по одноразовому коду: пользователь переходит по deep-link t.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_bindingsid, user_id, chat_id, bind_code, bind_code_used_at, bound_at, is_activeпривязка чата к пользователю (одноразовый deep-link, is_active=false после блокировки бота — см. крайние случаи)
cms_telegram_messagesid, chat_id, direction, payload (json), statusжурнал исходящих/входящих сообщений

user_id — FK constrained()->index(); уникальный индекс на chat_id в cms_telegram_bindings. direction/status в cms_telegram_messages — PHP Enum; payloadjson()->nullable() + cast array; BRIN-индекс по created_at (журнал растёт линейно). bind_code_used_atnullable, устанавливается атомарно при первом успешном /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_idFormRequest, permission telegram.manage
Вызов от cms/notifications-bus (канал telegram)user_id, template, payloadprovides-контракт 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 / TelegramCommandReceivedpayload события

Настройки (группа telegram)

КлючТипДефолтaffectsPageCacheОписание
telegram.bot_tokenstringнетТокен бота (хранится в .envconfig())
telegram.team_chat_idstringнетChat ID группы для уведомлений команде
telegram.bind_code_ttl_minutesint10нетСрок действия кода привязки
telegram.rate_limit_per_minuteint20нетЛимит исходящих сообщений в минуту (защита от лимитов Bot API); достижение — сообщение остаётся в очереди, не теряется
telegram.message_log_retention_daysint90нетРетеншн журнала cms_telegram_messages (ПДн, 152-ФЗ)
telegram.outbound_enabledbooltrueнет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
TelegramChatBlockedBot 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-busprovides-контракт notification-channel telegram + сервис-вызов (requires)notifications-bus → telegramшина резолвит канал через DI, вызывает отправку по шаблону
cms/notifications-busсобытие TelegramMessageDeliveredtelegram → шина/подписчикистатус доставки фиксируется для отчётности шины
очередь telegramочередьtelegram → воркерасинхронный разбор входящего update и асинхронная отправка исходящего
cms/healthhealth-чек из манифеста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), неавторизованные запросы отклоняются. Токен бота — только в .envconfig(), не хранится в таблице настроек в открытом виде. Код привязки одноразовый, доступен по deep-link и действует только bind_code_ttl_minutes. Команды бота не дают доступа к чужим данным (chat_id проверяется на привязку к конкретному пользователю и is_active=true). Rate-limit на генерацию кода привязки (защита от подбора/спама кодов) и на исходящую отправку (telegram.rate_limit_per_minute).

Матрица ролей:

Рольtelegram.viewtelegram.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 стандарта требует, чтобы контентные/пользовательские сущности несли nullable site_id/locale/city_id и корректно работали в мультисайт-режиме, но cms_telegram_bindings и telegram.team_chat_id — общие на всю инсталляцию: при cms/multisite все сайты делят одного бота и один командный чат, привязка пользователя не различает сайт. Предложение: добавить nullable site_id в cms_telegram_bindings и сделать telegram.team_chat_id per-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 не зависит от привязки конкретного пользователя (групповой канал);
  • [ ] Токен бота — только в .envconfig(), не хранится в таблице настроек в открытом виде;
  • [ ] 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 запрещены.

Внутренняя база знаний студии