fix(send): auto-forward message_thread_id under Threaded Mode + callback ctx#4
Open
adriangalilea wants to merge 2 commits into
Open
Conversation
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.
efb8105 to
ba710b9
Compare
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_idbut notis_topic_message— the latter remains exclusive to forum-supergroup topics.Two cumulative gaps in
@gramio/contextsmake replies land in the wrong thread:Gap 1 —
SendMixinguards onis_topic_messageEvery
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()isfalse, so the auto-forward skips and replies fall into the chat's general thread.Gap 2 —
CallbackQueryContexthas nothreadIdCallbackQueryContextexposeschatId(this.message?.chat?.id) soSendMixincan route to the right chat, but never exposedthreadId. Inside a callback handler,this.threadIdisundefinedand SendMixin can't auto-forward at all — even when the original button was tapped inside a thread.Fix
SendMixin: drop theisTopicMessage?.()clause.threadIdis 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.CallbackQueryContext: add `get threadId() { return this.message?.threadId }` — symmetric with the existingget chatId(). Completes theSendMixinMetadatacontract that the class already partly implemented.SendMixinMetadata.isTopicMessagefield is removed (no longer referenced).Behaviour table
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,/settingsmenu navigation, and a streaming demo all stay in their originating thread across both forum-supergroup topics and private-chat threaded mode.