Перейти к содержанию

Tutorial: демо-бот на webhook, задействующий всю платформу

Цель — поднять живого Telegram-бота на webhook и увидеть все механизмы платформы в одном потоке: публичный шлюз-адаптер, дверь eventgate, контракт, мост bridge↔Inngest, durable-шаг движка, fan-out, и — наглядно — разделение «кто РЕШАЕТ что ответить» и «кто ПИШЕТ» через шину. Бот отвечает на любое сообщение; /remind показывает задержку, переживающую рестарт.

Webhook вместо пулинга: ничего не опрашивается постоянно — сервисы спят, пока Telegram не пришлёт апдейт.

Что произойдёт

Telegram ──webhook POST──▶ [tg-gate] ──POST /events (X-Api-Key)──▶ eventgate ──▶ tg.message.received.v1
  (домен + секрет)          TG Update→контракт (+bot)    схема+ключ+дедуп          │ один факт — N читателей
                                                          ┌─────────────────────────┼──────────────┐
                                                          ▼                         ▼
                                          bridge(IN) → Inngest [tg-handler]   [tg-stats] (Bento)
                                          РЕШАЕТ: echo / /remind(step.Sleep)  no-code счётчик
                                                          │ POST /publish (мост, OUT)
                                                          ▼  tg.reply.requested.v1 (+bot)
                                          [tg-sender] consume → выбирает токен по bot → Telegram sendMessage
                                          ПИШЕТ                       │
                                                                      ▼  tg.message.sent.v1

Каждый блок — отдельный механизм. «Решает» (движок, durable) и «пишет» (egress) — разные участники, общаются только фактом tg.reply.requested.v1.

Что понадобится

  • Поднятые стеки bus, eventgate, functions (см. как задеплоить).
  • Токен бота от @BotFather.
  • Публичный домен для tg-gate (Telegram требует HTTPS).
  • NATS_INTERNAL_PASSWORD и один ключ для eventgate.

Исходники: stacks/integrations/ (tg-gate, tg-sender, tg-stats) и stacks/functions/telegram.go (durable-функция движка).

Шаг 1. Контракт: три события (+ bot)

Subject Кто публикует Кто читает
tg.message.received.v1 tg-gate tg-handler и tg-stats
tg.reply.requested.v1 tg-handler tg-sender
tg.message.sent.v1 tg-sender (факт-итог)

Поле bot (логическое имя) есть в received и reply.requested — чтобы в мультибот-режиме tg-sender знал, чьим токеном слать ответ. eventgate валидирует вход по схеме; стрим TELEGRAM (subjects tg.>) объявлен как код.

Шаг 2. Вход: webhook + тонкий адаптер

Telegram шлёт апдейты на POST /tg/{bot} сервиса tg-gate (публичный домен). Шлюз: проверяет секрет (X-Telegram-Bot-Api-Secret-Token из setWebhook), мапит Telegram-Update в наш факт {bot, chatId, text, …} и кладёт его через eventgate (ключ + схема + дедуп по update_id).

Почему адаптер, а не сразу в eventgate: формат Telegram ≠ наш контракт. Чтобы гейт остался generic, а внутренний факт — чистым, маппинг живёт в тонком шлюзе. Это общий приём: вебхук-источник, чей payload не совпадает с твоим контрактом, заворачивается тонким адаптером. tg-gate при этом не держит бот-токенов — он только принимает; меньше секретов на публичном краю.

Шаг 3. Fan-out: один факт — несколько читателей

tg.message.received.v1 читают независимо, каждый своим durable-консьюмером:

  • tg-handler (движок) — решает, что ответить;
  • tg-stats (no-code Bento) — считает символы/слова в лог.

Добавить потребителя — это просто ещё один консьюмер, отправителя не трогаем.

Шаг 4. Движок РЕШАЕТ: durable-шаг

tg-handlerstacks/functions, сеть engine) срабатывает на событие. Шину он не знает — факт ему завёл мост, ответ он отдаёт мосту (POST /publish). Логика: обычный текст → Эхо: <текст>; /remind <сек> <текст> → подтверждение, step.Sleep(сек), затем ответ. step.Sleep durable: пауза переживает рестарт воркера. Функция кладёт tg.reply.requested.v1 с тем же bot.

Шаг 5. Egress ПИШЕТ: отдельный участник

tg-sender слушает tg.reply.requested.v1, по полю bot берёт нужный токен и шлёт sendMessage, затем публикует tg.message.sent.v1. Это и есть разделение: кто решает, не знает, как и чем пишется — связь только через факт. Поменять канал отправки (другой бот, другой мессенджер) = поменять egress, не трогая логику. traceparent проходит весь путь → один трейс в Jaeger.

Шаг 6. Подними и проверь

  1. eventgate: добавь ключ источника и передеплой: EVENTGATE_KEYS={"<ключ>":["tg.message.received.v1"]}.
  2. functions: INNGEST_SIGNING_KEY (как в bus) — поднимет tg-handler.
  3. integrations: переменные из .env.example:
  4. EVENTGATE_KEY=<ключ>, NATS_INTERNAL_PASSWORD;
  5. TG_WEBHOOK_SECRETS={"demo":"<секрет>"} (для tg-gate);
  6. TG_BOT_TOKENS={"demo":"<токен>"} (для tg-sender).
  7. Повесь домен на сервис tg-gate (в Coolify) и зарегистрируй webhook:
    curl "https://api.telegram.org/bot<ТОКЕН>/setWebhook" \
      -d "url=https://<домен>/tg/demo" \
      -d "secret_token=<секрет>"
    
  8. Напиши боту привет → «Эхо: привет»; /remind 30 чай → подтверждение, через 30 c — «чай».

Проверь по дороге:

P=<NATS_INTERNAL_PASSWORD>
N() { docker run --rm --network event-bus natsio/nats-box \
      nats -s nats://nats:4222 --user internal --password "$P" "$@"; }

N stream get TELEGRAM --last-for tg.message.received.v1   # вход появился
N stream get TELEGRAM --last-for tg.message.sent.v1       # ответ ушёл

Шаг 7. Где здесь DLQ

Если Telegram недоступен, tg-sender делает NAK — JetStream повторит (до MaxDeliver). Неустранимый сбой доводки факта в движок мост уводит в dlq.>. Разбор — наблюдаемость и DLQ.

Что ты узнал

  • webhook вместо пулинга: источник снаружи → публичный адаптер tg-gate.
  • Адаптер заворачивает чужой формат в твой контракт; eventgate остаётся generic.
  • Один факт читают независимо несколько участников (fan-out).
  • Мост — единственный клей к движку; Inngest и воркер — не на шине.
  • step.Sleepdurable: переживает рестарт.
  • Решает ≠ пишет: движок и egress общаются только фактом — каждый сменяем.
  • Мультибот: один tg-gate/tg-sender на много ботов через поле bot.

Дальше