From ae591ed1421b892db716e08aef76381df2a4b757 Mon Sep 17 00:00:00 2001 From: Mohammad Ibrahim Saleem Date: Wed, 14 Oct 2026 00:50:45 -0500 Subject: [PATCH] fix(telegram): do not treat a bot-created topic's root as a reply to the bot Telegram sets reply_to_message on every plain message in a forum topic to the topic's forum_topic_created service message. When the bot created the topic, that message is authored by the bot, so all chatter in the topic passed the reply-to-bot addressing check. Ignore replies to the topic creation message; explicit replies to the bot's ordinary messages remain addressed. --- src/agentchat-providers/src/telegram.rs | 31 +++++++++++++++++++++---- tests/channels/telegram/test_events.py | 9 +++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/agentchat-providers/src/telegram.rs b/src/agentchat-providers/src/telegram.rs index 160352e..ee596ca 100644 --- a/src/agentchat-providers/src/telegram.rs +++ b/src/agentchat-providers/src/telegram.rs @@ -228,10 +228,11 @@ pub fn normalize(update: &Value, bot: i64, username: &str) -> Option { ("", &[][..]) }; let address = strip_bot_address(text, entities, bot, username); - if chat.kind != "private" - && !address.is_addressed - && message["reply_to_message"]["from"]["id"].as_i64() != Some(bot) - { + let reply = &message["reply_to_message"]; + // Plain topic messages reply to the topic's creation message, which the bot may own. + let replies_to_bot = + reply["from"]["id"].as_i64() == Some(bot) && reply.get("forum_topic_created").is_none(); + if chat.kind != "private" && !address.is_addressed && !replies_to_bot { return None; } let files = attachments(message); @@ -741,6 +742,28 @@ mod tests { assert_eq!(a.text, "😀 hi"); } #[test] + fn bot_created_forum_topic_root_is_not_a_reply_to_the_bot() { + let update = |reply: Value| { + json!({"update_id": 1, "message": { + "message_id": 20, + "chat": {"id": -100, "type": "supergroup", "is_forum": true}, + "from": {"id": 5, "is_bot": false, "first_name": "Alice"}, + "is_topic_message": true, + "message_thread_id": 7, + "reply_to_message": reply, + "text": "just chatting", + }}) + }; + // Telegram links every plain message in a topic to its creation message. + let root = json!({ + "message_id": 7, "from": {"id": 1, "is_bot": true}, + "forum_topic_created": {"name": "Support", "icon_color": 7322096}, + }); + assert!(normalize(&update(root), 1, "bot").is_none()); + let answer = json!({"message_id": 8, "from": {"id": 1, "is_bot": true}, "text": "hi"}); + assert!(normalize(&update(answer), 1, "bot").is_some()); + } + #[test] fn malformed_utf16_and_expanding_casefold_are_safe() { let address = strip_bot_address( "@İ", diff --git a/tests/channels/telegram/test_events.py b/tests/channels/telegram/test_events.py index cf2721a..fa6bb42 100644 --- a/tests/channels/telegram/test_events.py +++ b/tests/channels/telegram/test_events.py @@ -53,6 +53,15 @@ def test_text_mention_and_reply_to_bot_are_addressed(): assert read(group_update("thanks", reply_to_message=reply)).text == "thanks" +def test_topic_chatter_under_a_bot_created_topic_is_not_addressed(): + # Plain topic messages carry the topic's creation message as reply_to_message. + root = {"message_id": 7, "from": dict(BOT), "forum_topic_created": {"name": "Support"}} + topic = {"is_topic_message": True, "message_thread_id": 7} + assert read(group_update("just chatting", reply_to_message=root, **topic)) is None + answer = {"message_id": 8, "from": dict(BOT), "text": "hi"} + assert read(group_update("thanks", reply_to_message=answer, **topic)).text == "thanks" + + @pytest.mark.parametrize( "update", [