Skip to content

feat(scheduler): add telegram as a delivery channel#85

Open
tiuro wants to merge 1 commit intoghostwright:mainfrom
tiuro:scheduler-telegram-delivery
Open

feat(scheduler): add telegram as a delivery channel#85
tiuro wants to merge 1 commit intoghostwright:mainfrom
tiuro:scheduler-telegram-delivery

Conversation

@tiuro
Copy link
Copy Markdown

@tiuro tiuro commented Apr 22, 2026

Summary

Adds Telegram as a third delivery channel for the scheduler, alongside the existing Slack and "none" options. Uses raw Bot API fetch() rather than injecting a TelegramChannel into DeliveryContext, which keeps the blast radius to two files and avoids the executor → service → index wiring cascade.

Motivation

Running two Phantom instances (one on a GPU host, one on a TrueNAS box) where Telegram is the primary channel. Slack was introducing multi-workspace friction for our setup; Telegram via the existing bot config "just works". Been stable in production for ~2 weeks across both instances with zero delivery incidents.

Design choices

  1. Raw fetch, not Telegraf. channels/telegram.ts already owns the Telegraf long-polling instance. Calling bot.telegram.sendMessage() from the scheduler would share the same Telegraf client across two concurrency contexts. Raw fetch against the sendMessage endpoint sidesteps this entirely. It also means the scheduler path has zero coupling to the channels layer for delivery.

  2. Env reuse. No new env vars. The two already-required vars (TELEGRAM_BOT_TOKEN, OWNER_TELEGRAM_USER_ID) are read directly in the delivery function rather than threaded through DeliveryContext, which would otherwise require changes in executor.ts + service.ts + index.ts.

  3. Target format. "owner" (symbolic, resolves to OWNER_TELEGRAM_USER_ID) or any numeric chat_id (positive user ids, negative group ids). isValidTelegramTarget regex-validates at creation time, matching the existing Slack validator pattern.

  4. 4096-char defensive truncation with a trailing indicator. Telegram hard-limits messages at 4096 chars, so operators see content was cut rather than chasing a silent API error.

  5. No parse_mode. Scheduler task outputs are generally plain text. Avoiding MarkdownV2 escaping keeps the delivery code trivial and deterministic.

Changes

  • src/scheduler/types.ts — add "telegram" to JobDeliverySchema.channel enum, isValidTelegramTarget() helper, update target description
  • src/scheduler/delivery.ts — add deliverTelegram() function, wire into deliverResult(), extend DeliveryOutcome union with two dropped:telegram_* variants

Test plan

  • Production validation (2 weeks). Running on 2 Phantom instances since 2026-04-21 via bind-mount patches. Scheduler fires morning-brief + weekly-digest jobs through channel=telegram, target=owner; delivery_status stamped "delivered" in scheduled_jobs rows, zero conflict with the Telegraf polling instance.
  • Unit tests not included in this revision — the forker's environment doesn't have bun locally. Happy to add src/scheduler/__tests__/delivery-telegram.test.ts covering happy path (target=owner, explicit numeric target), missing-token env, missing-owner env, API 4xx response, and network error in a follow-up commit if the maintainers want that before merge.

Adds Telegram as a third delivery channel for the scheduler, alongside
the existing Slack and "none" options. Uses raw Bot API fetch() rather
than injecting a TelegramChannel into DeliveryContext, which keeps the
blast radius to two files and avoids the executor -> service -> index
wiring cascade.

Design choices:

1. Raw fetch, not Telegraf. channels/telegram.ts already owns the
   Telegraf long-polling instance. Calling bot.telegram.sendMessage()
   from the scheduler would share the same Telegraf client across two
   concurrency contexts. Raw fetch against the sendMessage endpoint
   sidesteps this entirely.

2. Env reuse. No new env vars. The two already-required vars
   (TELEGRAM_BOT_TOKEN, OWNER_TELEGRAM_USER_ID) are read directly in
   the delivery function rather than threaded through DeliveryContext.

3. Target format. "owner" (resolves to OWNER_TELEGRAM_USER_ID) or
   any numeric chat_id (positive user ids, negative group ids).
   isValidTelegramTarget regex-validates at creation time.

4. 4096-char defensive truncation with trailing indicator. Telegram
   hard-limits messages at 4096 chars.

5. No parse_mode. Scheduler task outputs are generally plain text.
   Avoiding MarkdownV2 escaping keeps delivery code deterministic.

Changes:
- src/scheduler/types.ts: add "telegram" to channel enum,
  isValidTelegramTarget() helper, update target description.
- src/scheduler/delivery.ts: add deliverTelegram() function, wire into
  deliverResult(), extend DeliveryOutcome union with two
  dropped:telegram_* variants.

Production validation:
Running in production on 2 Phantom instances (GPU host + TrueNAS) since
2026-04-21. Scheduler fires morning-brief + weekly jobs via telegram,
delivery_status stamped "delivered" in scheduled_jobs rows. Zero
conflict with Telegraf polling instance.

Tests skipped in the forker's environment (no bun toolchain locally);
patch validated in production instead. Unit tests for deliverTelegram
(mock fetch, target variants, env-missing cases) are straightforward
follow-ups happy to add in a revision if preferred.
@mcheemaa
Copy link
Copy Markdown
Member

Lets add some tests and screenshots if you can. Thank youu

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.

2 participants