Repository navigation
fix(telegram): keep regular supergroup replies on the chat thread - #1063
tarasyarema wants to merge 2 commits into
Conversation
Outside forums, Telegram sets message_thread_id on a reply to the id of
the message it answers. The adapter treated that as a forum topic and
routed the reply to telegram:{chatId}:{messageThreadId}, so a bot
subscribed to the chat thread never received it. Only use
message_thread_id as a thread when is_topic_message is true.
Signed-off-by: Taras' Swarm <tarasyaremabcn@gmail.com>
|
@tarasyarema is attempting to deploy a commit to the Vercel Team on Vercel. A member of the Team first needs to authorize it. |
|
@cramforce hi any chance to check this in the near future? |
There was a problem hiding this comment.
Thanks for this. The core fix is right: gating message_thread_id on is_topic_message is the same rule grammY and Telegraf apply, and routing every message-to-thread conversion through one helper keeps the inbound and outbound paths consistent.
What I checked locally on d3eab4a
pnpm turbo run build --filter=@chat-adapter/telegram...: passestsc --noEmitinpackages/adapter-telegram: passesvitest runinpackages/adapter-telegram: 382 passedvitest run src/replies.test.tsinpackages/integration-tests: 9 passedbiome checkon the changed files: clean- With
src/index.tsrestored to the base version, both new regression tests fail and the forum case passes, so the tests do guard the fix.
Requested changes
- Document the behavior change for existing deployments (inline on the changeset). Thread IDs of the form
telegram:{chatId}:{n}that a bot already stored for a regular supergroup (subscriptions,thread.setState, anything keyed onthread.id) stop matching after this ships, and subscribing from a reply chain now subscribes to the whole chat. That is the intended outcome, but it should be spelled out for upgraders. - Ground the fix in a captured payload. The repo
AGENTS.mdasks forsample-messages.mdto be extended when fixing parsers. The existing captured sample, "Reply to a bot message in a group", has nomessage_thread_idat all, so the repo currently has no real payload showing the shape this PR fixes. Please add a sanitized regular-supergroup reply that carriesmessage_thread_idwithoutis_topic_message(and, if you have it, the matchingsendMessageresponse). - Tighten the
message_thread_idwording (inline). It's described in several places as "the id of the message it answers". The Bot API documents it as the id of "a message thread or forum topic". For a reply to a reply it's my understanding that this is the thread's root message rather than the direct parent, so the claim only holds for one level of reply. The code doesn't depend on it, but the docs, changeset,AGENTS.md, JSDoc and test comments do.
Nits (non-blocking)
- Test coverage only exercises the incoming webhook and
reply/postMessage.parseMessage, callback queries, andeditMessage/streaming also changed; aparseMessagecase and a callback-query case would be cheap to add. handleMessageReactionUpdatestill usesreactionUpdate.message_thread_idas-is (not in this diff, aroundsrc/index.ts:1488).MessageReactionUpdatedcarries nois_topic_message, so it can't use the helper. That's fine to leave for now, but the PR description's "wherever the adapter builds a thread ID" slightly overstates it.pnpm validatehasn't run (knip in particular). CI should cover it, just flagging that the checklist item is still open.
| "@chat-adapter/telegram": patch | ||
| --- | ||
|
|
||
| Keep replies in a regular supergroup on the chat's thread. Telegram sets `message_thread_id` on a reply outside forums, to the id of the message it answers, and the adapter turned that into a separate `telegram:{chatId}:{messageThreadId}` thread. A bot subscribed to the chat never saw the reply. The adapter now uses `message_thread_id` as a thread only when `is_topic_message` is `true`, so forum topics keep their own threads. |
There was a problem hiding this comment.
Please add an upgrade note. Bots that already stored telegram:{chatId}:{n} thread IDs for a regular supergroup (subscriptions, thread state) lose them after this ships: follow-ups now arrive on telegram:{chatId} and go to onNewMention/onNewMessage instead of onSubscribedMessage. Subscribing from inside a reply chain also now subscribes to the whole chat. One or two sentences here (and in the docs Threads section) would cover it. It's also worth a maintainer's call on whether patch is still the right bump for a change to thread IDs.
| * The forum topic a message belongs to, or `undefined` outside topics. | ||
| * | ||
| * Only a topic message (`is_topic_message`) gets its own thread. In a regular | ||
| * supergroup Telegram also sets `message_thread_id` on a reply, to the id of |
There was a problem hiding this comment.
Nit on accuracy: "to the id of the message it answers" is only true for a direct reply to a top-level message. The Bot API calls this "a message thread or forum topic", and for a reply to a reply I believe it's the root of the reply thread. Something like "Telegram also sets message_thread_id on replies, identifying the reply thread" stays correct either way. The same wording appears in the changeset, the docs page, AGENTS.md, and the two test comments.
| }); | ||
|
|
||
| it.each([ | ||
| { |
There was a problem hiding this comment.
Nit: both cases use message_thread_id === reply_to_message.message_id. A case where they differ (a reply to a reply, with message_thread_id set to the thread root) would show the routing doesn't depend on that coincidence.
| markdown: "hello", | ||
| }); | ||
|
|
||
| expect(posted.threadId).toBe("telegram:-100123"); |
There was a problem hiding this comment.
Nit: worth asserting the outgoing request too, as the integration test does, so this also proves the bot doesn't send message_thread_id back to a regular supergroup:
const body = JSON.parse(
String((mockFetch.mock.calls[1]?.[1] as RequestInit).body)
);
expect(body.message_thread_id).toBeUndefined();|
|
||
| ## Threads | ||
|
|
||
| Each Telegram chat is one thread, `telegram:{chatId}`. In a forum supergroup, each topic is its own thread, `telegram:{chatId}:{messageThreadId}`. |
There was a problem hiding this comment.
Nit: "Each Telegram chat is one thread" leaves out two existing forms: topics in private chats with the bot (the is_topic_message doc in types.ts covers these) and business threads, telegram:biz:{connectionId}:{chatId}. Either list them or phrase this as the default and link to the full list.
Hi @tarasyarema, thanks for the contribution, and we appreciate that you're keen to get this reviewed soon. Please don't tag individual maintainers to ask for a review, though. It won't make the review happen any sooner, and PRs that do this may be closed without notice. Open PRs are triaged by the team as capacity allows. A review with requested changes is already on this one, so addressing that feedback is the best way to move it forward. |
Bots that stored telegram:{chatId}:{n} thread IDs for a regular
supergroup lose them once replies move to telegram:{chatId}, so the
changeset and the docs Threads section now spell that out.
message_thread_id identifies the reply thread by its root message, so
it is not the id of the message a reply answers once replies nest. The
docs, AGENTS.md, JSDoc and test comments no longer claim it is.
New tests cover a reply to a reply, parseMessage, callback queries, and
the request reply sends to a regular supergroup.
Signed-off-by: Taras' Swarm <tarasyaremabcn@gmail.com>
Summary
In a regular (non-forum) supergroup, a reply to the bot never reached
onSubscribedMessage. Telegram setsmessage_thread_idon a reply outside forums, to the id of the message it answers, and leavesis_topic_messageunset. The adapter treated anymessage_thread_idas a forum topic, so the reply got its own thread ID,telegram:{chatId}:{messageThreadId}. The bot was subscribed totelegram:{chatId}, so the reply was dropped without an error.The adapter now treats
message_thread_idas a thread only whenis_topic_messageistrue. A newgetTopicThreadIdhelper applies that rule wherever the adapter builds a thread ID from a Telegram message: incoming messages, slash commands, callback queries,parseMessage, and the messages returned bypostMessage(whichreplyuses),editMessage, and streaming. For sent messages, the adapter still falls back to the target thread's topic when the Bot API response omitsis_topic_message. Forum topics keep their own threads.The mention-on-reply check already scopes its forum exclusion to
is_topic_message, so it needed no change.Also in this PR:
@chat-adapter/telegram.AGENTS.md.packages/integration-tests/src/replies.test.tsnow expects a regular supergroup reply ontelegram:{chatId}, with nomessage_thread_idon the bot's answer.Test plan
Checks ran scoped to the Telegram adapter. The full-repo
pnpm validatewas skipped: the sandbox that produced this PR has 2 CPUs and 4 GB of RAM, and two earlier runs of the full validate and the examples build ran out of resources and crashed.pnpm turbo run build --filter=@chat-adapter/telegram...passes.pnpm --filter @chat-adapter/telegram typecheckpasses.pnpm --filter @chat-adapter/telegram testpasses: 382 tests.biome checkon the changed files passes.vitest run src/replies.test.tsinpackages/integration-testspasses: 9 tests.mainand pass with this change. The forum topic case passes on both.Checklist
git commit -s)pnpm validatepasses (skipped for sandbox memory reasons; see Test plan)