From 9459057d7f570b8674d28a100490a106b348ff9c Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sun, 14 Jun 2026 04:22:22 -0700 Subject: [PATCH] fix(telegram): guard rich details math crash (#46102) --- gateway/platforms/telegram.py | 28 +++++++++ tests/gateway/test_telegram_rich_messages.py | 62 ++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/gateway/platforms/telegram.py b/gateway/platforms/telegram.py index 314f7249e8a..381535ab8ac 100644 --- a/gateway/platforms/telegram.py +++ b/gateway/platforms/telegram.py @@ -953,6 +953,32 @@ class TelegramAdapter(BasePlatformAdapter): """ return inspect.iscoroutinefunction(getattr(self._bot, "do_api_request", None)) + _RICH_DETAILS_RE = re.compile(r"]*>.*?", 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() ) diff --git a/tests/gateway/test_telegram_rich_messages.py b/tests/gateway/test_telegram_rich_messages.py index 54827111a75..d13dc43ef03 100644 --- a/tests/gateway/test_telegram_rich_messages.py +++ b/tests/gateway/test_telegram_rich_messages.py @@ -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 = ( + "
Complex proof\n\n" + "$$\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}$$\n\n" + "And inline \\(\\alpha + \\beta\\)\n" + "
" +) # 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", + "
Notes\nNo equations here.\n
", + ) + + 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()