fix(telegram): guard rich details math crash (#46102)

This commit is contained in:
Teknium 2026-06-14 04:22:22 -07:00 committed by GitHub
parent cf7d5932f8
commit 9459057d7f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 90 additions and 0 deletions

View file

@ -953,6 +953,32 @@ class TelegramAdapter(BasePlatformAdapter):
"""
return inspect.iscoroutinefunction(getattr(self._bot, "do_api_request", None))
_RICH_DETAILS_RE = re.compile(r"<details\b[^>]*>.*?</details>", re.IGNORECASE | re.DOTALL)
_RICH_MATH_IN_DETAILS_RE = re.compile(
r"(\$\$.*?\$\$|"
r"\\\[.*?\\\]|"
r"\\\(.*?\\\)|"
r"\\(?:sum|frac|alpha|beta|gamma|delta|theta|lambda|mu|pi|sigma|"
r"int|prod|sqrt|lim|infty|begin\{(?:equation|align|matrix|cases)\}))",
re.IGNORECASE | re.DOTALL,
)
def _has_telegram_desktop_details_math_crash_shape(self, content: str) -> bool:
"""Return True for rich-message details+math content that crashes TDesktop.
Telegram Desktop 6.9.1 can crash while rendering Bot API 10.1 rich
messages containing math inside a collapsible details block
(telegramdesktop/tdesktop#30808). The Bot API accepts the payload, so
Hermes must skip rich delivery up front and use the legacy MarkdownV2
path until affected Desktop clients age out.
"""
if not content:
return False
for details_block in self._RICH_DETAILS_RE.findall(content):
if self._RICH_MATH_IN_DETAILS_RE.search(details_block):
return True
return False
def _should_attempt_rich(
self, content: str, metadata: Optional[Dict[str, Any]] = None
) -> bool:
@ -962,6 +988,7 @@ class TelegramAdapter(BasePlatformAdapter):
and not (metadata or {}).get("expect_edits")
and content
and content.strip()
and not self._has_telegram_desktop_details_math_crash_shape(content)
and self._content_fits_rich_limits(content)
and self._bot_supports_rich()
)
@ -1194,6 +1221,7 @@ class TelegramAdapter(BasePlatformAdapter):
and not getattr(self, "_rich_draft_disabled", False)
and content
and content.strip()
and not self._has_telegram_desktop_details_math_crash_shape(content)
and self._content_fits_rich_limits(content)
and self._bot_supports_rich()
)

View file

@ -24,6 +24,12 @@ from telegram.error import BadRequest, NetworkError, TimedOut
# Content exercising rich-only constructs: a heading, a real Markdown table,
# and a task list. Pipes / brackets must survive untouched into the payload.
RICH_CONTENT = "## Results\n\n| Case | Status |\n|---|---|\n| rich | ✅ |\n\n- [x] table renders"
DANGEROUS_DETAILS_MATH = (
"<details><summary>Complex proof</summary>\n\n"
"$$\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}$$\n\n"
"And inline \\(\\alpha + \\beta\\)\n"
"</details>"
)
# PTB 22.6's real unknown-endpoint errors: do_api_request can raise
# EndPointNotFound for Bot API 404s, and the request layer can wrap that same
@ -105,6 +111,48 @@ async def test_rich_happy_path_sends_raw_markdown():
adapter._bot.send_message.assert_not_called()
@pytest.mark.asyncio
async def test_details_with_math_skips_rich_send_to_avoid_tdesktop_crash():
adapter = _make_adapter()
result = await adapter.send("12345", DANGEROUS_DETAILS_MATH)
assert result.success is True
bot = adapter._bot
assert bot is not None
bot.do_api_request.assert_not_called()
bot.send_message.assert_awaited()
@pytest.mark.asyncio
async def test_details_without_math_still_uses_rich_send():
adapter = _make_adapter()
result = await adapter.send(
"12345",
"<details><summary>Notes</summary>\nNo equations here.\n</details>",
)
assert result.success is True
bot = adapter._bot
assert bot is not None
bot.do_api_request.assert_awaited_once()
bot.send_message.assert_not_called()
@pytest.mark.asyncio
async def test_math_outside_details_still_uses_rich_send():
adapter = _make_adapter()
result = await adapter.send("12345", "Outside details: $$x^2 + y^2$$")
assert result.success is True
bot = adapter._bot
assert bot is not None
bot.do_api_request.assert_awaited_once()
bot.send_message.assert_not_called()
@pytest.mark.asyncio
async def test_rich_messages_opt_out_uses_legacy_send_path():
adapter = _make_adapter(extra={"rich_messages": False})
@ -393,6 +441,20 @@ async def test_rich_gate_tolerates_minimal_bot_without_raw_endpoint():
# ── Streaming drafts: sendRichMessageDraft ─────────────────────────────
@pytest.mark.asyncio
async def test_details_with_math_skips_rich_draft_to_avoid_tdesktop_crash():
adapter = _make_adapter()
bot = adapter._bot
assert bot is not None
bot.do_api_request = AsyncMock(return_value=True)
result = await adapter.send_draft("12345", draft_id=7, content=DANGEROUS_DETAILS_MATH)
assert result.success is True
bot.do_api_request.assert_not_called()
bot.send_message_draft.assert_awaited_once()
@pytest.mark.asyncio
async def test_rich_draft_happy_path_sends_raw_markdown():
adapter = _make_adapter()