Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions gateway/platforms/telegram.py
Original file line number Diff line number Diff line change
Expand Up @@ -4799,14 +4799,21 @@ 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.

Protected regions (code blocks, inline code) are extracted first so
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
Expand All @@ -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.
Expand Down
60 changes: 60 additions & 0 deletions tests/gateway/test_telegram_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
16 changes: 14 additions & 2 deletions tools/send_message_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still sends the result through ParseMode.MARKDOWN_V2 immediately below, so rewrite_tables=False cannot yield a native Telegram table: the formatter escapes table pipes and this path never invokes sendRichMessage. Please use an explicit rich endpoint with fallback if native standalone table rendering is the intended behavior.

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
Expand Down
Loading