diff --git a/gateway/config.py b/gateway/config.py index 3ee460dd88d52..a028bef2b7765 100644 --- a/gateway/config.py +++ b/gateway/config.py @@ -1074,7 +1074,7 @@ def load_gateway_config() -> GatewayConfig: if isinstance(group_allowed_chats, list): group_allowed_chats = ",".join(str(v) for v in group_allowed_chats) os.environ["TELEGRAM_GROUP_ALLOWED_CHATS"] = str(group_allowed_chats) - for _telegram_extra_key in ("guest_mode", "disable_link_previews", "command_menu"): + for _telegram_extra_key in ("guest_mode", "disable_link_previews"): if _telegram_extra_key in telegram_cfg: plat_data = platforms_data.setdefault(Platform.TELEGRAM.value, {}) if not isinstance(plat_data, dict): diff --git a/gateway/platforms/telegram.py b/gateway/platforms/telegram.py index e69600dbbf21f..76288d4596d97 100644 --- a/gateway/platforms/telegram.py +++ b/gateway/platforms/telegram.py @@ -1527,28 +1527,11 @@ def _polling_error_callback(error: Exception) -> None: BotCommandScopeDefault, BotCommandScopeChat, ) - from hermes_cli.commands import ( - telegram_menu_commands, - telegram_quick_menu_commands, - ) + from hermes_cli.commands import telegram_menu_commands # Telegram allows up to 100 commands but has an undocumented # payload size limit (~4KB total). Limit to 30 core commands # to stay well under the threshold while covering all categories. - if self.config.extra.get("command_menu") == "quick_commands_only": - # Fetch quick_commands via the gateway runner reference if - # available; otherwise fall back to PlatformConfig.extra. - _qc = self.config.extra.get("quick_commands") - if not isinstance(_qc, dict) or not _qc: - _runner_ref = getattr(self, "_runner_ref", None) - _runner = _runner_ref() if callable(_runner_ref) else None - _gw_cfg = getattr(_runner, "config", None) if _runner else None - _qc = getattr(_gw_cfg, "quick_commands", {}) or {} - menu_commands, hidden_count = telegram_quick_menu_commands( - _qc, - max_commands=MAX_COMMANDS_PER_SCOPE, - ) - else: - menu_commands, hidden_count = telegram_menu_commands(max_commands=MAX_COMMANDS_PER_SCOPE) + menu_commands, hidden_count = telegram_menu_commands(max_commands=MAX_COMMANDS_PER_SCOPE) bot_commands = [BotCommand(name, desc) for name, desc in menu_commands] # Register for all scopes independently — Telegram picks the # narrowest matching scope per chat type (forum topics fall @@ -4449,12 +4432,11 @@ def _clean_bot_trigger_text(self, text: Optional[str]) -> Optional[str]: return cleaned or text def _should_process_message(self, message: Message, *, is_command: bool = False) -> bool: - """Apply Telegram group trigger rules and user allowlist. + """Apply Telegram group trigger rules. - DMs and group messages are both subject to TELEGRAM_ALLOWED_USERS - allowlist check. The chat also passes the ``allowed_chats`` whitelist - (when set), or ``guest_mode`` is enabled and the bot is explicitly - mentioned. Group/supergroup messages are additionally accepted when: + DMs remain unrestricted. Group/supergroup messages are accepted when: + - the chat passes the ``allowed_chats`` whitelist (when set), or + ``guest_mode`` is enabled and the bot is explicitly mentioned - the chat is explicitly allowlisted in ``free_response_chats`` - ``require_mention`` is disabled - the message replies to the bot @@ -4471,18 +4453,6 @@ def _should_process_message(self, message: Message, *, is_command: bool = False) mentioning the bot (``@botname /command``), both of which are recognised as mentions by :meth:`_message_mentions_bot`. """ - # Enforce TELEGRAM_ALLOWED_USERS allowlist for ALL message types - # (DMs and groups). Previously only callback actions were gated, - # leaving inbound messages unblocked (issue #23778). - _user = getattr(message, "from_user", None) - _user_id = str(getattr(_user, "id", "")) if _user else "" - if not self._is_callback_user_authorized(_user_id): - logger.warning( - "[%s] Unauthorized user %s — message dropped", - self.name, _user_id, - ) - return False - if not self._is_group_chat(message): return True @@ -5432,25 +5402,16 @@ async def _clear_reactions(self, chat_id: str, message_id: str) -> bool: return False async def on_processing_start(self, event: MessageEvent) -> None: - """Add an in-progress reaction and pin the message when processing begins.""" + """Add an in-progress reaction when message processing begins.""" + if not self._reactions_enabled(): + return chat_id = getattr(event.source, "chat_id", None) message_id = getattr(event, "message_id", None) if chat_id and message_id: - if self._reactions_enabled(): - await self._set_reaction(chat_id, message_id, "\U0001f440") - # Pin the incoming message for the duration of the turn - if self._bot: - try: - await self._bot.pin_chat_message( - chat_id=int(chat_id), - message_id=int(message_id), - disable_notification=True, - ) - except Exception: - logger.debug("[Telegram] Failed to pin message %s in chat %s", message_id, chat_id) + await self._set_reaction(chat_id, message_id, "\U0001f440") async def on_processing_complete(self, event: MessageEvent, outcome: ProcessingOutcome) -> None: - """Swap the in-progress reaction for a final success/failure reaction and unpin. + """Swap the in-progress reaction for a final success/failure reaction. Unlike Discord (additive reactions), Telegram's set_message_reaction replaces all existing reactions in one call — no remove step needed. @@ -5462,25 +5423,17 @@ async def on_processing_complete(self, event: MessageEvent, outcome: ProcessingO another agent run to swap it to 👍/👎 — which never happens if the cancellation was the last activity in the chat. """ + if not self._reactions_enabled(): + return chat_id = getattr(event.source, "chat_id", None) message_id = getattr(event, "message_id", None) if not (chat_id and message_id): return - if self._reactions_enabled(): - if outcome == ProcessingOutcome.CANCELLED: - await self._clear_reactions(chat_id, message_id) - else: - await self._set_reaction( - chat_id, - message_id, - "\U0001f44d" if outcome == ProcessingOutcome.SUCCESS else "\U0001f44e", - ) - # Unpin the message when processing is complete - if self._bot: - try: - await self._bot.unpin_chat_message( - chat_id=int(chat_id), - message_id=int(message_id), - ) - except Exception: - logger.debug("[Telegram] Failed to unpin message %s in chat %s", message_id, chat_id) + if outcome == ProcessingOutcome.CANCELLED: + await self._clear_reactions(chat_id, message_id) + else: + await self._set_reaction( + chat_id, + message_id, + "\U0001f44d" if outcome == ProcessingOutcome.SUCCESS else "\U0001f44e", + ) diff --git a/hermes_cli/commands.py b/hermes_cli/commands.py index 610621aed6f65..9fc0472f51210 100644 --- a/hermes_cli/commands.py +++ b/hermes_cli/commands.py @@ -740,42 +740,6 @@ def telegram_menu_commands(max_commands: int = 100) -> tuple[list[tuple[str, str return all_commands[:max_commands], hidden_count -def telegram_quick_menu_commands( - quick_commands: Mapping[str, Any] | None, - max_commands: int = 100, -) -> tuple[list[tuple[str, str]], int]: - """Return Telegram BotCommands for profile-defined quick commands only. - - Specialist Telegram bots often use ``quick_commands`` as their whole - user-facing interface. This helper lets a profile opt into a focused - Telegram slash menu without exposing every generic Hermes command. - - ``show_in_telegram_menu: false`` hides a quick command from the native - menu while leaving gateway dispatch unchanged. - """ - if not isinstance(quick_commands, Mapping): - return [], 0 - - menu: list[tuple[str, str]] = [] - seen: set[str] = set() - for raw_name, raw_config in quick_commands.items(): - if not isinstance(raw_name, str) or not isinstance(raw_config, Mapping): - continue - if raw_config.get("show_in_telegram_menu") is False: - continue - name = _sanitize_telegram_name(raw_name) - if not name or name in seen: - continue - desc = str(raw_config.get("description") or f"Run /{raw_name}") - if len(desc) > 40: - desc = desc[:37] + "..." - menu.append((name, desc)) - seen.add(name) - - hidden_count = max(0, len(menu) - max_commands) - return menu[:max_commands], hidden_count - - def discord_skill_commands( max_slots: int, reserved_names: set[str], diff --git a/tests/gateway/test_config.py b/tests/gateway/test_config.py index ea00a44b1d6d0..da7673011fe87 100644 --- a/tests/gateway/test_config.py +++ b/tests/gateway/test_config.py @@ -270,25 +270,6 @@ def test_bridges_quick_commands_from_config_yaml(self, tmp_path, monkeypatch): assert config.quick_commands == {"limits": {"type": "exec", "command": "echo ok"}} - def test_bridges_telegram_command_menu_from_config_yaml(self, tmp_path, monkeypatch): - hermes_home = tmp_path / ".hermes" - hermes_home.mkdir() - config_path = hermes_home / "config.yaml" - config_path.write_text( - "telegram:\n" - " command_menu: quick_commands_only\n", - encoding="utf-8", - ) - - monkeypatch.setenv("HERMES_HOME", str(hermes_home)) - - config = load_gateway_config() - - assert ( - config.platforms[Platform.TELEGRAM].extra["command_menu"] - == "quick_commands_only" - ) - def test_bridges_group_sessions_per_user_from_config_yaml(self, tmp_path, monkeypatch): hermes_home = tmp_path / ".hermes" hermes_home.mkdir() diff --git a/tests/hermes_cli/test_commands.py b/tests/hermes_cli/test_commands.py index 7fefd7470c75a..6de778347e13c 100644 --- a/tests/hermes_cli/test_commands.py +++ b/tests/hermes_cli/test_commands.py @@ -26,7 +26,6 @@ slack_subcommand_map, telegram_bot_commands, telegram_menu_commands, - telegram_quick_menu_commands, ) @@ -1154,47 +1153,6 @@ def test_empty_sanitized_names_excluded(self, tmp_path, monkeypatch): # No empty string in menu names assert "" not in menu_names - def test_quick_menu_commands_include_profile_quick_commands_only(self): - menu, hidden = telegram_quick_menu_commands( - { - "agent-health": { - "type": "exec", - "command": "echo ok", - "description": "Show agent health", - }, - "hidden": { - "type": "exec", - "command": "echo hidden", - "description": "Hidden command", - "show_in_telegram_menu": False, - }, - } - ) - - assert hidden == 0 - assert menu == [("agent_health", "Show agent health")] - - def test_quick_menu_commands_sanitize_dedupe_and_trim_descriptions(self): - menu, hidden = telegram_quick_menu_commands( - { - "agent-health": { - "description": "A" * 80, - }, - "agent_health": { - "description": "Duplicate after sanitization", - }, - "+++": { - "description": "Sanitizes to empty", - }, - }, - max_commands=1, - ) - - assert hidden == 0 - assert len(menu) == 1 - assert menu[0][0] == "agent_health" - assert menu[0][1] == ("A" * 37) + "..." - # --------------------------------------------------------------------------- # Backward-compat aliases diff --git a/tools/send_message_tool.py b/tools/send_message_tool.py index 302ddb16b4c6d..892346fb9f00b 100644 --- a/tools/send_message_tool.py +++ b/tools/send_message_tool.py @@ -804,7 +804,7 @@ async def _send_telegram(token, chat_id, message, media_files=None, thread_id=No instead, bypassing MarkdownV2 conversion. """ try: - from telegram import Bot, MessageEntity + from telegram import Bot from telegram.constants import ParseMode # Auto-detect HTML tags — if present, skip MarkdownV2 and send as HTML. @@ -825,14 +825,6 @@ async def _send_telegram(token, chat_id, message, media_files=None, thread_id=No formatted = message send_parse_mode = ParseMode.MARKDOWN_V2 - # Detect @username patterns and create mention entities so - # require_mention on the receiving bot's Gateway can trigger. - _MENTION_RE = re.compile(r'@([a-zA-Z][a-zA-Z0-9_]{4,31})') - _entities = [ - MessageEntity(type="mention", offset=m.start(), length=len(m.group())) - for m in _MENTION_RE.finditer(formatted) - ] - # Honour a configured proxy (telegram.proxy_url in config.yaml, exported # as TELEGRAM_PROXY env var by load_gateway_config). Without this, the # standalone send path bypasses the proxy and times out in regions @@ -897,7 +889,7 @@ async def _send_telegram(token, chat_id, message, media_files=None, thread_id=No last_msg = await _send_telegram_message_with_retry( bot, chat_id=int_chat_id, text=formatted, - parse_mode=send_parse_mode, entities=_entities, **text_kwargs + parse_mode=send_parse_mode, **text_kwargs ) except Exception as md_error: # Thread not found — retry without message_thread_id so the @@ -931,7 +923,7 @@ async def _send_telegram(token, chat_id, message, media_files=None, thread_id=No last_msg = await _send_telegram_message_with_retry( bot, chat_id=int_chat_id, text=plain, - parse_mode=None, entities=_entities, **text_kwargs + parse_mode=None, **text_kwargs ) else: raise diff --git a/website/docs/reference/slash-commands.md b/website/docs/reference/slash-commands.md index 249db462b8b9b..d545c8242bd0e 100644 --- a/website/docs/reference/slash-commands.md +++ b/website/docs/reference/slash-commands.md @@ -143,11 +143,6 @@ quick_commands: Then type `/status`, `/deploy`, or `/inbox` in the CLI or a messaging platform. Quick commands are resolved at dispatch time and may not appear in every built-in autocomplete/help table. -For specialist Telegram bots, set `telegram.command_menu: quick_commands_only` -to make Telegram's native slash menu show only profile-defined quick commands. -Set `show_in_telegram_menu: false` on a quick command to keep it callable but -hide it from the Telegram menu. - String-only prompt shortcuts are not supported as quick commands. Put longer reusable prompts in a skill, or use `type: alias` to point at an existing slash command. ### Custom model aliases diff --git a/website/docs/user-guide/configuration.md b/website/docs/user-guide/configuration.md index ba1a64c20a517..40f924aa209bd 100644 --- a/website/docs/user-guide/configuration.md +++ b/website/docs/user-guide/configuration.md @@ -1433,28 +1433,6 @@ Usage: type `/status`, `/disk`, `/update`, `/gpu`, or `/restart` in the CLI or a - **Type** — supported types are `exec` and `alias`; other types show an error - **Works everywhere** — CLI, Telegram, Discord, Slack, WhatsApp, Signal, Email, Home Assistant -Telegram profiles can opt into a focused BotCommand menu that shows only -profile-defined quick commands: - -```yaml -telegram: - command_menu: quick_commands_only - -quick_commands: - health: - type: exec - command: scripts/health.sh - description: Show service health - internal-debug: - type: exec - command: scripts/debug.sh - description: Internal debug helper - show_in_telegram_menu: false -``` - -`show_in_telegram_menu: false` hides a quick command from Telegram's native -slash menu while leaving the command callable. - String-only prompt shortcuts are not valid quick commands. For reusable prompt workflows, create a skill or alias to an existing slash command. ## Human Delay