Skip to content

fix(telegram): HTML mode, message splitting, and access control#23

Open
arne1101 wants to merge 1 commit intoghostwright:mainfrom
arne1101:fix/telegram-reliability
Open

fix(telegram): HTML mode, message splitting, and access control#23
arne1101 wants to merge 1 commit intoghostwright:mainfrom
arne1101:fix/telegram-reliability

Conversation

@arne1101
Copy link
Copy Markdown

@arne1101 arne1101 commented Apr 1, 2026

Fixes #14

Summary

Four reliability fixes for the Telegram channel. These were found and validated in a real deployment.

1. Non-blocking connect() - fixes the startup hang (closes #14)

connect() awaited bot.launch() directly. Telegraf's launch() runs an infinite polling loop that only resolves when bot.stop() is called, so connect() never returned. This blocked router.connectAll(), which meant the scheduler, /trigger endpoint, and the "is ready" log never ran.

Fix: Use Telegraf's onLaunch callback, which fires after getMe() succeeds but before the polling loop starts. This lets connect() resolve as soon as the bot is authenticated with Telegram, without waiting for polling to end.

This is more robust than a fire-and-forget approach: fire-and-forget would set connectionState = "connected" before Telegram validates the token, hiding auth errors. With the callback, a bad token still surfaces as a rejected promise.

2. Switch to HTML send mode with plain-text fallback

sendMessage() used MarkdownV2 with a custom escapeMarkdownV2() function. The escaper fails on edge cases -- parentheses inside URLs, certain punctuation patterns -- causing a 400: can't parse entities error that swallows the response silently.

Fix: Switched to HTML mode with a markdownToTelegramHtml() converter. If the HTML send still fails, the chunk is retried as plain text so the user always gets a response.

3. Message splitting for the 4096-char limit

Telegram rejects messages over 4096 characters with a 400 error. The original code sent the full response in one call with no error handling, so long responses disappeared without a trace.

Fix: Added splitMessage() which chunks at newlines near the 4000-char limit, with a hard split as fallback. Long responses are now delivered as sequential chunks.

4. try/catch in command handlers

/start, /status, /help called ctx.reply() without wrapping it, causing unhandled promise rejections in Telegraf on any transient send error.

Fix: Each ctx.reply() call is now wrapped in try/catch. Failures are silently ignored since the command responses are non-critical.

5. allowed_user_ids access control

No way to restrict which Telegram users can interact with the bot. Anyone who discovers the bot can send it messages.

Fix: New optional allowed_user_ids: number[] field in channels.yaml. When set, users not on the list receive "Unauthorized." and the message handler is never invoked. Applies to text messages, callback queries, and all slash commands. Empty list or absent field = allow all (preserves existing behavior).

Utility functions (splitMessage, stripHtml, markdownToTelegramHtml) are extracted to a new telegram-utils.ts to keep telegram.ts within the 300-line guideline.

Test plan

  • bun test - 848 tests, all pass (78 existing tests unchanged + 38 new tests for the Telegram channel)
  • bun run lint - clean
  • bun run typecheck - clean
  • New tests cover: non-blocking connect(), HTML mode verification, message splitting, plain-text fallback, isAllowed() logic (all allowed, whitelist pass, whitelist block), command handler authorization, splitMessage() edge cases, stripHtml(), markdownToTelegramHtml() conversions

…ss control

Four bugs/gaps in the Telegram channel fixed:

1. HTML send mode with plain-text fallback: sendMessage() was using MarkdownV2
   with a custom escaper that failed on parentheses and other edge cases.
   Switched to HTML mode with a markdownToTelegramHtml() converter. On any
   send failure (malformed HTML), retries the chunk as plain text so the user
   always gets a response.

2. Message splitting: Telegram rejects messages over 4096 characters with no
   error surfaced to the user. Added splitMessage() which chunks at newlines
   near the 4000-char limit (hard split as fallback).

3. try/catch in command handlers: /start, /status, /help called ctx.reply()
   without error handling, which caused "Unhandled promise rejection" in
   Telegraf on any transient network error.

4. allowed_user_ids access control: new optional config field that whitelists
   Telegram user IDs. When set, any user not on the list gets "Unauthorized."
   and the message handler is never invoked. Applies to text messages,
   callback queries, and all slash commands. Config schema updated in
   schemas.ts and wired through in index.ts.

Utility functions (splitMessage, stripHtml, markdownToTelegramHtml) extracted
to telegram-utils.ts to keep telegram.ts under the 300-line guideline.

All 848 tests pass. bun run lint and bun run typecheck clean.
@mcheemaa mcheemaa self-requested a review April 2, 2026 03:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Telegram channel blocks startup: bot.launch() never resolves

1 participant