diff --git a/gateway/platforms/telegram.py b/gateway/platforms/telegram.py index 3cf241966784..4285c43f6fc5 100644 --- a/gateway/platforms/telegram.py +++ b/gateway/platforms/telegram.py @@ -4799,7 +4799,7 @@ async def get_chat_info(self, chat_id: str) -> Dict[str, Any]: ) return {"name": str(chat_id), "type": "dm", "error": str(e)} - def format_message(self, content: str) -> str: + def format_message(self, content: str, *, rewrite_tables: bool = True) -> str: """ Convert standard markdown to Telegram MarkdownV2 format. @@ -4807,6 +4807,13 @@ def format_message(self, content: str) -> str: their contents are never modified. Standard markdown constructs (headers, bold, italic, links) are translated to MarkdownV2 syntax, and all remaining special characters are escaped. + + Args: + content: Raw markdown text to convert. + rewrite_tables: When True (default), GFM pipe tables are + rewritten into Telegram-friendly bullet groups. Pass + False for the rich-message path (sendRichMessage) where + Telegram renders tables natively. """ if not content: return content @@ -4825,7 +4832,10 @@ def _ph(value: str) -> str: # 0) Rewrite GFM-style pipe tables into Telegram-friendly row groups # before the normal MarkdownV2 conversions run. - text = _wrap_markdown_tables(text) + # Skipped on the rich-message path where Telegram renders tables + # natively — rewriting would destroy the pipe syntax. + if rewrite_tables: + text = _wrap_markdown_tables(text) # 1) Protect fenced code blocks (``` ... ```) # Per MarkdownV2 spec, \ and ` inside pre/code must be escaped. diff --git a/tests/gateway/test_telegram_format.py b/tests/gateway/test_telegram_format.py index 1d3a2375a78d..9c0638eae040 100644 --- a/tests/gateway/test_telegram_format.py +++ b/tests/gateway/test_telegram_format.py @@ -1012,3 +1012,63 @@ def test_guest_mode_allows_mention_in_caption_outside_allowed_chats(self): message.caption_entities = [_guest_mention_entity(text)] assert adapter._should_process_message(message) is True + + +# --------------------------------------------------------------------------- +# rewrite_tables parameter tests +# --------------------------------------------------------------------------- + +class TestFormatMessageRewriteTables: + """Verify format_message(rewrite_tables=...) controls table rewriting.""" + + def _make_adapter(self): + adapter = TelegramAdapter.__new__(TelegramAdapter) + return adapter + + def test_rewrite_tables_true_rewrites_pipe_tables(self): + """Default behavior: pipe tables are rewritten into bullet groups.""" + adapter = self._make_adapter() + table = "| Name | Value |\n| --- | --- |\n| foo | bar |" + result = adapter.format_message(table) + # Should NOT contain pipe table syntax — rewritten to bullets + assert "| --- |" not in result + assert "foo" in result + assert "bar" in result + + def test_rewrite_tables_false_preserves_pipe_syntax(self): + """Rich-message path: pipe table syntax is preserved for native rendering.""" + adapter = self._make_adapter() + table = "| Name | Value |\n| --- | --- |\n| foo | bar |" + result = adapter.format_message(table, rewrite_tables=False) + # Pipe table structure should survive MarkdownV2 escaping + # Pipes are escaped to \|, dashes to \- in MarkdownV2 + assert "\\|" in result or "|" in result + # The separator row should still be present (escaped dashes) + assert "\\-" in result or "-" in result + + def test_rewrite_tables_default_is_true(self): + """Backward compatibility: default is True (legacy behavior).""" + adapter = self._make_adapter() + table = "| A | B |\n| --- | --- |\n| 1 | 2 |" + result_default = adapter.format_message(table) + result_explicit = adapter.format_message(table, rewrite_tables=True) + assert result_default == result_explicit + + def test_rewrite_tables_false_no_bullet_groups(self): + """With rewrite_tables=False, no bullet-group markers appear.""" + adapter = self._make_adapter() + table = "| Col1 | Col2 |\n| --- | --- |\n| val1 | val2 |" + result = adapter.format_message(table, rewrite_tables=False) + # Bullet groups use "•" or similar — should not appear + assert "•" not in result + + def test_rewrite_tables_false_code_blocks_untouched(self): + """Code blocks should still be protected regardless of rewrite_tables.""" + adapter = self._make_adapter() + content = "```\n| A | B |\n| --- | --- |\n| 1 | 2 |\n```\n\nSome text" + result_true = adapter.format_message(content, rewrite_tables=True) + result_false = adapter.format_message(content, rewrite_tables=False) + # Code block content should be identical in both modes + # (tables inside code blocks are never rewritten) + assert "A" in result_true + assert "A" in result_false diff --git a/tools/send_message_tool.py b/tools/send_message_tool.py index a37f9eb62a29..97ea1326fed2 100644 --- a/tools/send_message_tool.py +++ b/tools/send_message_tool.py @@ -963,11 +963,23 @@ async def _send_telegram(token, chat_id, message, media_files=None, thread_id=No formatted = message send_parse_mode = ParseMode.HTML else: - # Reuse the gateway adapter's format_message for markdown→MarkdownV2 + # Reuse the gateway adapter's format_message for markdown→MarkdownV2. + # When rich messages are enabled, skip table rewriting — Telegram + # renders pipe tables natively via sendRichMessage; rewriting them + # into bullet groups would destroy the syntax. + _rewrite_tables = True + try: + from hermes_cli.config import load_config + _cfg = load_config() + _tg_extra = (_cfg.get("platforms", {}).get("telegram", {}) + .get("extra", {})) + _rewrite_tables = not _tg_extra.get("rich_messages", False) + except Exception: + pass try: from gateway.platforms.telegram import TelegramAdapter _adapter = TelegramAdapter.__new__(TelegramAdapter) - formatted = _adapter.format_message(message) + formatted = _adapter.format_message(message, rewrite_tables=_rewrite_tables) except Exception: # Fallback: send as-is if formatting unavailable formatted = message