From 1f971ab2b83f518e277e66be6380f625c5c6ea85 Mon Sep 17 00:00:00 2001 From: alberto <914199+alblez@users.noreply.github.com> Date: Fri, 24 Apr 2026 18:40:59 -0500 Subject: [PATCH] fix(telegram): accept /cmd@botname from bot menu in groups Telegram groups emit a single bot_command entity covering the whole /cmd@botname span with no accompanying mention entity, so the existing mention gate in _message_mentions_bot dropped slash commands sent via the bot-menu autocomplete whenever require_mention is enabled. Recognise bot_command entities whose @botname suffix matches the bot username (case-insensitive) as a direct mention, and keep rejecting commands addressed at other bots. Fixes #15415. --- gateway/platforms/telegram.py | 20 ++++++++++ tests/gateway/test_telegram_group_gating.py | 41 +++++++++++++++++++-- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/gateway/platforms/telegram.py b/gateway/platforms/telegram.py index be1bf494c5..8742be7c20 100644 --- a/gateway/platforms/telegram.py +++ b/gateway/platforms/telegram.py @@ -2328,6 +2328,26 @@ class TelegramAdapter(BasePlatformAdapter): user = getattr(entity, "user", None) if user and getattr(user, "id", None) == bot_id: return True + elif entity_type == "bot_command" and expected: + # Telegram's official group-disambiguation form for slash + # commands (``/cmd@botname``) is emitted as a single + # ``bot_command`` entity covering the whole span — there + # is no accompanying ``mention`` entity. Treat it as a + # direct address to this bot when the ``@botname`` suffix + # matches. This is the form Telegram's own command menu + # autocomplete produces in groups, so dropping it at the + # mention gate would break /new, /reset, /help, ... for + # every group that has ``require_mention`` enabled (#15415). + offset = int(getattr(entity, "offset", -1)) + length = int(getattr(entity, "length", 0)) + if offset < 0 or length <= 0: + continue + command_text = source_text[offset:offset + length] + at_index = command_text.find("@") + if at_index < 0: + continue + if command_text[at_index:].strip().lower() == expected: + return True return False def _message_matches_mention_patterns(self, message: Message) -> bool: diff --git a/tests/gateway/test_telegram_group_gating.py b/tests/gateway/test_telegram_group_gating.py index 0381cf6f46..ababe5ec61 100644 --- a/tests/gateway/test_telegram_group_gating.py +++ b/tests/gateway/test_telegram_group_gating.py @@ -59,6 +59,17 @@ def _mention_entity(text, mention="@hermes_bot"): return SimpleNamespace(type="mention", offset=offset, length=len(mention)) +def _bot_command_entity(text, command): + """Entity Telegram emits for a ``/cmd`` or ``/cmd@botname`` token. + + Telegram parses slash commands server-side. For ``/cmd@botname`` the + client does NOT emit a separate ``mention`` entity — the whole span + is a single ``bot_command`` entity. + """ + offset = text.index(command) + return SimpleNamespace(type="bot_command", offset=offset, length=len(command)) + + def test_group_messages_can_be_opened_via_config(): adapter = _make_adapter(require_mention=False) @@ -73,12 +84,34 @@ def test_group_messages_can_require_direct_trigger_via_config(): assert adapter._should_process_message(_group_message("replying", reply_to_bot=True)) is True # Commands must also respect require_mention when it is enabled assert adapter._should_process_message(_group_message("/status"), is_command=True) is False - # But commands with @mention still pass (Telegram emits a MENTION entity - # for /cmd@botname — the bot menu and python-telegram-bot's CommandHandler - # rely on this same mechanism) + # Telegram's group command menu sends ``/cmd@botname`` as a single + # ``bot_command`` entity spanning the whole token (no separate mention + # entity). We must accept it so the menu works when require_mention is on. assert adapter._should_process_message( - _group_message("/status@hermes_bot", entities=[_mention_entity("/status@hermes_bot")]) + _group_message( + "/status@hermes_bot", + entities=[_bot_command_entity("/status@hermes_bot", "/status@hermes_bot")], + ), + is_command=True, ) is True + # A bot_command entity addressed at a different bot must not satisfy + # the mention gate — Telegram groups can host multiple bots that + # register the same command name. + assert adapter._should_process_message( + _group_message( + "/status@other_bot", + entities=[_bot_command_entity("/status@other_bot", "/status@other_bot")], + ), + is_command=True, + ) is False + # Bare ``/status`` (no @botname) must still be dropped in groups with + # require_mention=True — Telegram delivers it only when the bot's + # privacy mode is off, and even then we should not respond unless the + # user explicitly addressed the bot. + assert adapter._should_process_message( + _group_message("/status", entities=[_bot_command_entity("/status", "/status")]), + is_command=True, + ) is False # And commands still pass unconditionally when require_mention is disabled adapter_no_mention = _make_adapter(require_mention=False) assert adapter_no_mention._should_process_message(_group_message("/status"), is_command=True) is True