diff --git a/docs/reference/transports/telegram.md b/docs/reference/transports/telegram.md index 294258f..ebf2ec8 100644 --- a/docs/reference/transports/telegram.md +++ b/docs/reference/transports/telegram.md @@ -76,6 +76,11 @@ Explicit invocation includes any of: - Replying to a bot message. - Built-in or plugin slash commands (for example `/agent`, `/model`, `/reasoning`, `/file`, `/trigger`). +Note: In forum topics, some Telegram clients include `reply_to_message` on every +message, pointing at the topic’s root service message (`message_id == +message_thread_id`). Takopi treats those as implicit topic references, not +explicit replies, so they do not trigger mentions-only mode. + Commands: - `/trigger` shows the current mode and defaults. diff --git a/src/takopi/telegram/parsing.py b/src/takopi/telegram/parsing.py index 328d84b..e1cc487 100644 --- a/src/takopi/telegram/parsing.py +++ b/src/takopi/telegram/parsing.py @@ -110,6 +110,11 @@ def _parse_incoming_message( media_group_id = msg.media_group_id thread_id = msg.message_thread_id is_topic_message = msg.is_topic_message + if thread_id is not None and reply_to_message_id == thread_id: + reply_to_message_id = None + reply_to_text = None + reply_to_is_bot = None + reply_to_username = None return TelegramIncomingMessage( transport="telegram", chat_id=msg_chat_id, diff --git a/src/takopi/telegram/trigger_mode.py b/src/takopi/telegram/trigger_mode.py index 3f02fd0..6f70ab8 100644 --- a/src/takopi/telegram/trigger_mode.py +++ b/src/takopi/telegram/trigger_mode.py @@ -43,12 +43,17 @@ def should_trigger_run( needle = f"@{bot_username}" if needle in lowered: return True - if msg.reply_to_is_bot: + implicit_topic_reply = ( + msg.thread_id is not None and msg.reply_to_message_id == msg.thread_id + ) + + if msg.reply_to_is_bot and not implicit_topic_reply: return True if ( bot_username and msg.reply_to_username and msg.reply_to_username.lower() == bot_username + and not implicit_topic_reply ): return True command_id, _ = _parse_slash_command(text) diff --git a/tests/test_telegram_incoming.py b/tests/test_telegram_incoming.py index f5ad310..382f512 100644 --- a/tests/test_telegram_incoming.py +++ b/tests/test_telegram_incoming.py @@ -57,6 +57,34 @@ def test_parse_incoming_update_maps_fields() -> None: assert msg.raw["message_id"] == 10 +def test_parse_incoming_update_ignores_implicit_topic_reply() -> None: + update = Update( + update_id=1, + message=Message( + message_id=187, + message_thread_id=163, + is_topic_message=True, + text="Hello", + chat=Chat(id=123, type="supergroup", is_forum=True), + from_=User(id=99), + reply_to_message=MessageReply( + message_id=163, + from_=User(id=77, is_bot=True, username="TakopiBot"), + ), + ), + ) + + msg = parse_incoming_update(update, chat_id=123) + assert msg is not None + assert isinstance(msg, TelegramIncomingMessage) + assert msg.thread_id == 163 + assert msg.is_topic_message is True + assert msg.reply_to_message_id is None + assert msg.reply_to_text is None + assert msg.reply_to_is_bot is None + assert msg.reply_to_username is None + + def test_parse_incoming_update_filters_non_matching_chat() -> None: update = Update( update_id=1, diff --git a/tests/test_telegram_trigger_mode.py b/tests/test_telegram_trigger_mode.py index a227ae9..1c390d8 100644 --- a/tests/test_telegram_trigger_mode.py +++ b/tests/test_telegram_trigger_mode.py @@ -83,6 +83,32 @@ def test_should_trigger_run_reply_to_bot() -> None: ) +def test_should_trigger_run_ignores_implicit_topic_reply_to_root() -> None: + runtime = _runtime() + msg = TelegramIncomingMessage( + transport="telegram", + chat_id=1, + message_id=187, + text="hello", + reply_to_message_id=163, + reply_to_text=None, + reply_to_is_bot=True, + reply_to_username="TakopiBot", + sender_id=1, + thread_id=163, + is_topic_message=True, + chat_type="supergroup", + is_forum=True, + ) + assert not should_trigger_run( + msg, + bot_username=None, + runtime=runtime, + command_ids=set(), + reserved_chat_commands=set(RESERVED_CHAT_COMMANDS), + ) + + def test_should_trigger_run_known_commands() -> None: runtime = _runtime() assert should_trigger_run(