Skip to Content
Telegram MCPComponentsTelegram Dispatch

src/telegram/dispatch.ts — Inbound event composer

Builds the Phase 1 inbound pipeline: record the user, then run the next handler.

Public surface

ExportShapePurpose
createDispatcher(deps)(DispatchDeps) => (TelegramEvent) => Promise<void>Builder
DispatchDeps{ db: Queryable, next: (event) => Promise<void> }DI

Behavior

  1. findOrCreatePendingUser(db, {telegramUserId, username, firstName}) — upserts the row. Idempotent for repeat messages; refreshes username / first_name if Telegram supplied new values.
  2. If the upsert succeeds, call next(event). The bootstrapped admin row is not clobbered — bootstrapAdmin and findOrCreatePendingUser use compatible ON CONFLICT clauses that only touch the fields each one owns.
  3. If the upsert throws, the error propagates out. The webhook handler catches it and logs without re-throwing (see telegramWebhook) — next is not invoked, so the user never receives a reply.

Why this composer exists

The user-recording step has to run for every inbound event, regardless of kind. Inlining it into the echo handler would mean Phase 3’s auth gate and Phase 6’s agent forwarder each need to re-implement it. Splitting it as a composer keeps the “always record” guarantee in one place and lets later phases swap out next without losing it.

Phase 3 extension point

Phase 3 inserts an auth gate between the upsert and next:

record → check status → (active? → next) | (pending? → "approval pending" reply) | (blocked? → drop)

The composer’s shape is intentional: it leaves room for that branching without restructuring the call sites.

Tests

test/unit/dispatch.test.ts — 4 cases:

  • Ordering: db before next (proven via call-order log)
  • null username/firstName passes through unchanged
  • Non-text events (e.g. photo) still touch the table
  • DB failure short-circuits — next is never called