Skip to content

fix(send): auto-forward message_thread_id under Threaded Mode + callback ctx#4

Open
adriangalilea wants to merge 2 commits into
gramiojs:mainfrom
adriangalilea:fix/auto-thread-private-chat-threaded-mode
Open

fix(send): auto-forward message_thread_id under Threaded Mode + callback ctx#4
adriangalilea wants to merge 2 commits into
gramiojs:mainfrom
adriangalilea:fix/auto-thread-private-chat-threaded-mode

Conversation

@adriangalilea
Copy link
Copy Markdown

@adriangalilea adriangalilea commented May 10, 2026

Problem

Telegram added Threaded Mode for private chats (BotFather → Bot Settings → Threaded Mode), letting users hold multiple parallel conversations with a bot. Messages in those threads carry message_thread_id but not is_topic_message — the latter remains exclusive to forum-supergroup topics.

Two cumulative gaps in @gramio/contexts make replies land in the wrong thread:

Gap 1 — SendMixin guards on is_topic_message

Every send* method has:

```ts
if (this.threadId && this.isTopicMessage?.() && !params.message_thread_id)
params.message_thread_id = this.threadId
```

In Threaded Mode private chats, isTopicMessage() is false, so the auto-forward skips and replies fall into the chat's general thread.

Gap 2 — CallbackQueryContext has no threadId

CallbackQueryContext exposes chatId (this.message?.chat?.id) so SendMixin can route to the right chat, but never exposed threadId. Inside a callback handler, this.threadId is undefined and SendMixin can't auto-forward at all — even when the original button was tapped inside a thread.

Fix

  1. SendMixin: drop the isTopicMessage?.() clause. threadId is set iff the incoming message belongs to a thread; the secondary guard was redundant for forum supergroups and harmful for Threaded Mode. Caller-override (params.message_thread_id) still wins.
  2. CallbackQueryContext: add `get threadId() { return this.message?.threadId }` — symmetric with the existing get chatId(). Completes the SendMixinMetadata contract that the class already partly implemented.

SendMixinMetadata.isTopicMessage field is removed (no longer referenced).

Behaviour table

Scenario `threadId` `isTopicMessage()` Before After
Forum-supergroup topic message set true forwards ✓ forwards ✓
Forum-supergroup General undef false skips ✓ skips ✓
Top-level chat undef false skips ✓ skips ✓
Private-chat Threaded Mode set false skips ✗ forwards ✓
Callback inside any thread undef before / set after n/a skips ✗ forwards ✓

Affected methods

17 send methods + sendMessageDraft's draft/message param builders.

Validation

Tested in production against a bot with BotFather Threaded Mode enabled — /start, plain echoes, /settings menu navigation, and a streaming demo all stay in their originating thread across both forum-supergroup topics and private-chat threaded mode.

Telegram introduced "Threaded Mode" for private chats via @Botfather
(Bot Settings → Threaded Mode). Messages in those private-chat threads
carry `message_thread_id` but NOT `is_topic_message` — the latter
remains exclusive to forum-supergroup topics.

The existing `this.isTopicMessage?.()` clause in every send method
therefore skips auto-threading in private-chat threaded mode. Replies
land in the general thread of the chat instead of the user's topic,
breaking the use case Telegram explicitly designed Threaded Mode for
(parallel conversations with bots, especially AI chatbots).

The guard is also unnecessary in the original forum-supergroup case:
`threadId` is only set when the incoming message belongs to a thread,
and `is_topic_message` is set on every such message in forum
supergroups. Dropping the guard:

- preserves existing forum-supergroup behaviour (threadId is set →
  forwarded)
- fixes private-chat threaded mode (threadId is set → now forwarded)
- preserves caller override (`message_thread_id` in params still wins)

Affects 17 send-style methods plus sendMessageDraft's draft+message
param builders. SendMixinMetadata's `isTopicMessage` field is removed
(no longer referenced).
adriangalilea added a commit to adriangalilea/ts-utils that referenced this pull request May 10, 2026
…of plugins

Two threading gaps in @gramio/contexts (PR gramiojs/contexts#4 open):

  1. SendMixin's `isTopicMessage()` guard skipped auto-thread under
     BotFather Threaded Mode (private-chat threads set
     `message_thread_id` without `is_topic_message`).
  2. CallbackQueryContext never exposed `threadId`, so SendMixin saw
     `undefined` inside callback handlers — even when the original
     button was tapped inside a thread.

Both fixes shipped on adriangalilea/contexts. Pinned here via
pnpm.overrides + onlyBuiltDeps allowlist. With the fork, every
`ctx.send` / `ctx.sendDocument` / `ctx.reply` etc auto-forwards
`message_thread_id` whenever the incoming ctx had one. Including
callback handlers.

Workarounds removed:
  - `inThread(ctx)` helper deleted from bot/kit (replaced by
    SendMixin's native auto-forward)
  - `ctx.say.send/.reply/.edit` no longer wrap with inThread; pass
    through to `ctx.send` / `ctx.editText` / `ctx.answer` directly
  - menu + access-control + tests/bot-integration drop their
    `...inThread(ctx)` spreads everywhere
  - llm-stream KEEPS its `threadId` capture because it calls
    `bot.api.sendMessage` directly (bypasses SendMixin)

BREAKING: `inThread` export removed from bot/kit. Anyone who imported
it can delete the call; SendMixin handles it now.

Docs in README and src/bot/CLAUDE.md updated. Override gets dropped
once upstream merges (gramiojs/contexts#4).
CallbackQueryContext only exposed `chatId` (derived from
`this.message.chat.id`). The thread was not surfaced at the same level,
so the SendMixin (which reads `this.threadId`) saw `undefined` when
auto-forwarding inside a callback handler — meaning replies and new
sends from a button tap landed in the chat's general thread instead
of the thread the button was tapped in.

Add `get threadId()` returning `this.message?.threadId`. Symmetric
with `get chatId()`. The SendMixin now auto-forwards `message_thread_id`
on `ctx.send` / `ctx.reply` / `ctx.sendDocument` / ... inside callback
handlers, keeping the reply in the same thread as the tapped button.
@adriangalilea adriangalilea force-pushed the fix/auto-thread-private-chat-threaded-mode branch from efb8105 to ba710b9 Compare May 10, 2026 23:54
adriangalilea added a commit to adriangalilea/ts-utils that referenced this pull request May 10, 2026
`fix/auto-thread-private-chat-threaded-mode` is now the clean PR branch
(only the two fix commits; pinned in PR gramiojs/contexts#4).

`local-build/auto-thread-private-chat-threaded-mode` carries the same
two fix commits plus a `prepare` script so pnpm can build dist/ on git
install. ts-utils + downstream bots pin this branch via pnpm.overrides
until upstream merges.
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.

1 participant