src/telegram/normalize.ts — Telegram Update → TelegramEvent
Pure mapper from the raw Telegram Bot API Update (telegraf re-exports the
@telegraf/types definitions) into a unified discriminated union that the
rest of the system consumes. No I/O, no telegraf instance required.
Public surface
| Export | Shape | Purpose |
|---|---|---|
TelegramEvent | TextEvent | PhotoEvent | DocumentEvent | CallbackEvent | The union |
TelegramEventKind | 'text' | 'photo' | 'document' | 'callback' | Discriminator |
normalize(update) | (Update) => TelegramEvent | null | The mapper |
null means “ignore quietly” — the webhook still 200s but no event is
forwarded.
Envelope
Every event carries:
| Field | Type | Source |
|---|---|---|
userId | number | message.from.id / callback_query.from.id |
username | string | null | from.username (Telegram omits for users who haven’t set one) |
firstName | string | null | from.first_name |
chatId | number | message.chat.id / callback_query.message.chat.id |
messageId | number | For callbacks: the bot message the button was attached to (needed for edit_message/answerCallbackQuery) |
Kind-specific fields
text:text: stringphoto:fileId, width, height, caption?—fileIdis the largest size (Telegram returns multiple), selected bywidth * heightarea.document:fileId, mimeType?, fileName?, fileSize?, caption?— any field Telegram omits surfaces asnull.callback:callbackData: string, callbackQueryId: string— onlydatacallbacks are surfaced; game callbacks (nodata) returnnull.
Dropped update kinds
normalize() returns null for:
edited_message,channel_post,edited_channel_postmessage_reaction,message_reaction_countinline_query,chosen_inline_resultshipping_query,pre_checkout_querypoll,poll_answermy_chat_member,chat_member,chat_join_request,chat_boost,removed_chat_boost- messages whose payload we don’t surface (e.g. stickers, voice notes, video)
- callbacks attached to inaccessible messages (Telegram’s
date === 0sentinel) or inline-mode callbacks with nomessage
This matches the allowed_updates whitelist sent during webhook registration
(see scripts/register-webhook.ts), so in
practice we only have to handle these for defense-in-depth.
Tests
test/unit/normalize.test.ts — 16 cases
covering each kind, the largest-photo selection rule, optional-field nulling,
all the documented drop paths, and the null-username fallback.
See also
- docs/plan/phase-1-minimal-telegram-bridge.md — original deliverable
- docs/plan/data-and-contracts.md — downstream
chat_messages.payloadshape (Phase 4)