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-handler (в stacks/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. Подними и проверь¶
- eventgate: добавь ключ источника и передеплой:
EVENTGATE_KEYS={"<ключ>":["tg.message.received.v1"]}. - functions:
INNGEST_SIGNING_KEY(как в bus) — подниметtg-handler. - integrations: переменные из
.env.example: EVENTGATE_KEY=<ключ>,NATS_INTERNAL_PASSWORD;TG_WEBHOOK_SECRETS={"demo":"<секрет>"}(для tg-gate);TG_BOT_TOKENS={"demo":"<токен>"}(для tg-sender).- Повесь домен на сервис
tg-gate(в Coolify) и зарегистрируй webhook:curl "https://api.telegram.org/bot<ТОКЕН>/setWebhook" \ -d "url=https://<домен>/tg/demo" \ -d "secret_token=<секрет>" - Напиши боту
привет→ «Эхо: привет»;/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.Sleep— durable: переживает рестарт.- Решает ≠ пишет: движок и egress общаются только фактом — каждый сменяем.
- Мультибот: один
tg-gate/tg-senderна много ботов через полеbot.
Дальше¶
- Свой участник — добавить участника.
- Новое событие — изменить контракт.
- Принципы — архитектура.