Provider Adapter: Telegram Messaging (заглушка)
Назначение
Доставка канала Telegram (../domain/channel.md) через Telegram Bot API.
Заглушка: реализация отложена — контракт зафиксирован, отправка активируется per-канал флагом. Привязку
UserId → telegram chat-id ведёт адаптер (bot deep-link /start); уведомление хранит только
RecipientUserId. Provider input (bot-updates) — недоверенный внешний ввод.
Границы Adapter-а
| Поле | Значение |
|---|
| Provider | Telegram Bot API |
| Сервис | notifications |
| Capability | messaging (telegram) |
| Adapter class | TelegramChannelProvider |
| Typed client | ITelegramBotClient + IMessengerHandleStore (chat-id по UserId) |
| Внутренняя операция | dispatcher при Channel = Telegram, Status = Pending |
| Реализация | заглушка (отложено) — до включения канала отправка не выполняется и возвращается код NTF_PROVIDER_NOT_IMPLEMENTED |
Контракты Provider-а
| Операция | Direction | Internal model | Provider-specific model |
|---|
sendMessage | outbound | NotificationDispatchRequest | TelegramSendMessage (chat_id/text/reply_markup) |
bot updates (/start, block) | callback | привязка / отвязка messenger-handle | TelegramUpdate (подписанный webhook secret-token) |
Модель Internal NotificationDispatchRequest
Дом — ../domain/notification.md. Ключевые: NotificationId (idempotency),
RecipientUserId → chat-id из IMessengerHandleStore (RecipientAddress.MessengerHandle при наличии),
RenderedTitle+RenderedBody → text (Markdown/HTML по Format), DeepLink → inline-кнопка.
Модель Provider-Specific TelegramSendMessage
| Поле | Тип | Обязательность | Provider format | Validation | Internal mapping |
|---|
chat_id | long | Да | telegram chat id | привязка есть (иначе skip) | IMessengerHandleStore.Get(RecipientUserId) |
text | string | Да | ≤ 4096, Markdown/HTML | не пустое | RenderedTitle+RenderedBody |
reply_markup | объект? | Нет | inline keyboard | — | DeepLink (кнопка) |
disable_notification | bool | Нет | — | тихие часы/Low | Urgency/quiet-hours |
Безопасность
| Контроль | Правило |
|---|
| Authentication | bot-token Telegram |
| Signature validation | webhook — заголовок X-Telegram-Bot-Api-Secret-Token + allow-list IP |
| Secrets | bot-token/webhook secret-token — secret-store, ротация |
| PII | тело — Sensitive (маска в логах); chat-id — Sensitive, не логируется |
| Tenant binding | привязка chat-id к RecipientUserId; отправка только в привязанный chat |
Resilience И Устойчивость
| Аспект | Правило |
|---|
| Timeout | 5с к Bot API |
| Retry | 5xx/429(retry_after)/timeout → перевод в Failed → повтор retry-worker-ом; 403 blocked → unlink handle, не retry |
| Circuit breaker | размыкание на клиенте провайдера |
| Idempotency | локальный dedup по NotificationId (Bot API без client-ref — адаптер хранит отметку) |
| Rate limit | ≤ ~30 msg/s глобально + ChannelConfig.RateLimitPerMinute |
Обработка Callback
| Шаг | Правило |
|---|
| Verify | secret-token заголовка + IP allow-list |
| Normalize | TelegramUpdate → привязка handle (/start deep-link с UserId) / отвязка (block) |
| Validate | deep-link payload = валидный UserId; типизированные ошибки |
| Dispatch | привязка/отвязка chat-id в IMessengerHandleStore |
| Store | handle в store (безопасно); статус доставки — на NotificationEntity |
Маппинг Ошибок
| Provider code | Internal error | Повторяемость | Видно пользователю |
|---|
| 5xx / timeout | NTF_PROVIDER_TRANSIENT | Да | Нет |
429 (retry_after) | NTF_PROVIDER_RATE_LIMITED | Да | Нет |
| 403 bot blocked | NTF_TELEGRAM_BLOCKED | Нет (unlink) | Нет |
| нет привязки chat-id | NTF_MESSENGER_NOT_LINKED | Нет (skip) | Нет |
| не активирован (заглушка) | NTF_PROVIDER_NOT_IMPLEMENTED | Нет | Нет |
Поток доставки
Happy path и все ветки отказа отправки уведомления Channel = Telegram. Статусы и переходы — по
notification.md (INV-NTF-5/7); коды — таблица «Маппинг Ошибок».
- Выбор к отправке. Dispatcher берёт свежее
Pending, retry-worker — Failed с наступившим
NextAttemptAt и Attempts < MaxAttempts; уведомление Channel = Telegram. Канал выключен
(ChannelConfig.IsEnabled = false, заглушка, INV-CHN-2) → попытки нет, статус Pending, исход
NTF_PROVIDER_NOT_IMPLEMENTED (skip, метрика channel-disabled).
- Резолв chat-id. chat-id, привязанный к
RecipientUserId (реестр привязок ведёт адаптер;
RecipientAddress.MessengerHandle при наличии). Привязки нет → NTF_MESSENGER_NOT_LINKED: канал
пропускается (non-retriable); получатель
покрыт другими разрешёнными каналами (in-app — всегда).
- Сборка сообщения.
RenderedTitle+RenderedBody → text (≤ 4096, Markdown/HTML по Format;
усечение по MaxBodyLength, если задан, INV-CHN-3), DeepLink → inline-кнопка; тихие часы/Urgency = Low
→ disable_notification.
- Отправка (синхронно, timeout 5с). Bot API без client-ref — идемпотентность обеспечивает локальный
dedup по
NotificationId (адаптер хранит отметку): повтор той же отправки не шлёт второе сообщение.
- Классификация ответа:
- успех →
Pending/Failed → Sent, ProviderMessageId; вклад в NotificationDispatchedEvent(Sent)
(топик pin.notifications.dispatched.v1);
5xx/timeout → NTF_PROVIDER_TRANSIENT (retriable): → Failed, Attempts++,
NextAttemptAt = now + backoff (ChannelConfig.RetryBackoff);
429 с retry_after → NTF_PROVIDER_RATE_LIMITED (retriable): дефер не раньше retry_after;
403 blocked → NTF_TELEGRAM_BLOCKED (non-retriable): терминальный Failed, unlink chat-id.
- Порог повторов.
Attempts >= MaxAttempts (INV-NTF-7) → терминальный Failed +
NotificationDispatchedEvent(Failed) + алерт; запись выбывает из retry-цикла.
Обратный вызов и согласованность
Bot-updates — асинхронная привязка/отвязка handle, не delivery-receipt:
/start c deep-link payload UserId → после проверки secret-token заголовка + IP allow-list и валидности
UserId chat-id привязывается; канал становится доставляем.
- block → отвязка chat-id; отправки до повторного
/start завершаются NTF_MESSENGER_NOT_LINKED.
- Повтор того же update идемпотентен; неизвестный/невалидный
UserId игнорируется (лог webhook-received).
- Окно согласованности: сообщение, ушедшее до обработки block, считается
Sent; отвязка влияет только на
будущие отправки.
Пред- и постусловия
- Предусловие: канал
Telegram включён; есть уведомление Channel = Telegram в Pending/Failed(due);
для доставки — привязан chat-id (/start).
- Постусловие: уведомление в терминальном
Sent (есть ProviderMessageId) либо Failed (терминально
после MaxAttempts или 403-unlink); на финальном исходе эмитирован NotificationDispatchedEvent. Без
привязки канал пропущен, прочие каналы не затронуты.
Кросс-сервисные ожидания и гарантии
| Направление | Контракт |
|---|
Ожидает от ChannelConfig (channel.md) | IsEnabled, ProviderKey, MaxAttempts, RetryBackoff, RateLimitPerMinute, MaxBodyLength |
| Ожидает от рендера шаблона | непустой RenderedBody (INV-NTF-2); опц. RenderedTitle/DeepLink; Format (Markdown/HTML) |
| Ожидает от привязки | chat-id по RecipientUserId (deep-link /start); отсутствует → skip |
| Предоставляет агрегату | переход статуса Sent/Failed, ProviderMessageId, LastError (безопасный код) |
| Предоставляет наружу | вклад в NotificationDispatchedEvent (ключ партиционирования NotificationId, ContractVersion = 1) |
| Реплики | своих реплик нет; недоступность Bot API — transient-retry, не потеря |
Acceptance Criteria
| # | Given / When / Then |
|---|
| AC1 | Given канал Telegram выключен, When dispatcher берёт Pending, Then попытки нет, статус Pending, метрика channel-disabled (NTF_PROVIDER_NOT_IMPLEMENTED). |
| AC2 | Given привязан chat-id и канал включён, When отправка успешна, Then Sent + ProviderMessageId + вклад в NotificationDispatchedEvent(Sent). |
| AC3 | Given 5xx/429(retry_after), When обработка ответа, Then Failed, Attempts++, NextAttemptAt по backoff (не раньше retry_after); повтор до успеха/MaxAttempts. |
| AC4 | Given MaxAttempts исчерпан, When очередная ошибка, Then терминальный Failed + NotificationDispatchedEvent(Failed) + алерт. |
| AC5 | Given 403 blocked, When ответ провайдера, Then терминальный Failed + unlink chat-id (NTF_TELEGRAM_BLOCKED), без повторов. |
| AC6 | Given нет привязки chat-id, When отправка, Then канал пропущен (NTF_MESSENGER_NOT_LINKED), in-app доставлен независимо. |
| AC7 | Given повтор той же отправки (NotificationId), When локальный dedup, Then у провайдера одно сообщение (без дубля). |
Наблюдаемость
| Тип | Правило |
|---|
| Logs | send-started/sent/blocked/not-linked/provider-error/webhook-received (тело/chat-id — маска) |
| Metrics | notifications_telegram_sent_total, notifications_telegram_error_rate, notifications_telegram_linked_total, circuit-state |
| Traces | correlationId, NotificationId, provider request id |
| Redaction | bot-token/secret и raw тело/chat-id — исключены |
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы