fix(telegram): convert markdown to HTML before splitting to avoid message length overflow
Cherry-picked from #3316 (himax12). When streaming completes in send_delta(), the code was splitting raw markdown text by 4000, then converting to HTML. The markdown-to-HTML conversion adds 10-33% characters, which could push the result over Telegram's 4096 character limit. The fix converts markdown to HTML first, then splits by 4096 (actual Telegram limit), ensuring the edited message always fits. Fixes #3315
This commit is contained in:
@@ -561,14 +561,22 @@ class TelegramChannel(BaseChannel):
|
||||
await self._remove_reaction(chat_id, int(reply_to_message_id))
|
||||
except ValueError:
|
||||
pass
|
||||
chunks = split_message(buf.text, TELEGRAM_MAX_MESSAGE_LEN)
|
||||
primary_text = chunks[0] if chunks else buf.text
|
||||
thread_kwargs = {}
|
||||
if message_thread_id := meta.get("message_thread_id"):
|
||||
thread_kwargs["message_thread_id"] = message_thread_id
|
||||
html = _markdown_to_telegram_html(buf.text)
|
||||
if len(html) <= 4096:
|
||||
primary_html = html
|
||||
extra_html_chunks = []
|
||||
else:
|
||||
html_chunks = split_message(html, 4096)
|
||||
primary_html = html_chunks[0]
|
||||
extra_html_chunks = html_chunks[1:]
|
||||
try:
|
||||
html = _markdown_to_telegram_html(primary_text)
|
||||
await self._call_with_retry(
|
||||
self._app.bot.edit_message_text,
|
||||
chat_id=int_chat_id, message_id=buf.message_id,
|
||||
text=html, parse_mode="HTML",
|
||||
text=primary_html, parse_mode="HTML",
|
||||
)
|
||||
except BadRequest as e:
|
||||
# Only fall back to plain text on actual HTML parse/format errors.
|
||||
@@ -583,7 +591,7 @@ class TelegramChannel(BaseChannel):
|
||||
await self._call_with_retry(
|
||||
self._app.bot.edit_message_text,
|
||||
chat_id=int_chat_id, message_id=buf.message_id,
|
||||
text=primary_text,
|
||||
text=primary_html,
|
||||
)
|
||||
except Exception as e2:
|
||||
if self._is_not_modified_error(e2):
|
||||
@@ -591,10 +599,13 @@ class TelegramChannel(BaseChannel):
|
||||
else:
|
||||
logger.warning("Final stream edit failed: {}", e2)
|
||||
raise # Let ChannelManager handle retry
|
||||
# If final content exceeds Telegram limit, keep the first chunk in
|
||||
# the edited stream message and send the rest as follow-up messages.
|
||||
for extra_chunk in chunks[1:]:
|
||||
await self._send_text(int_chat_id, extra_chunk)
|
||||
for extra_html_chunk in extra_html_chunks:
|
||||
await self._call_with_retry(
|
||||
self._app.bot.send_message,
|
||||
chat_id=int_chat_id, text=extra_html_chunk,
|
||||
parse_mode="HTML",
|
||||
**thread_kwargs,
|
||||
)
|
||||
self._stream_bufs.pop(chat_id, None)
|
||||
return
|
||||
|
||||
|
||||
@@ -467,8 +467,45 @@ async def test_send_delta_stream_end_falls_back_on_bad_request() -> None:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_delta_stream_end_splits_oversized_reply() -> None:
|
||||
"""Final streamed reply exceeding Telegram limit is split into chunks."""
|
||||
from nanobot.channels.telegram import TELEGRAM_MAX_MESSAGE_LEN
|
||||
"""Final streamed reply exceeding Telegram limit is split into chunks.
|
||||
|
||||
The fix converts markdown to HTML first, then splits by 4096 (actual Telegram
|
||||
limit), ensuring the edited message always fits within Telegram's constraint.
|
||||
Previously, the code split by 4000 (TELEGRAM_MAX_MESSAGE_LEN) before HTML
|
||||
conversion, which could still overflow when HTML tags were added.
|
||||
"""
|
||||
channel = TelegramChannel(
|
||||
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]),
|
||||
MessageBus(),
|
||||
)
|
||||
channel._app = _FakeApp(lambda: None)
|
||||
channel._app.bot.edit_message_text = AsyncMock()
|
||||
channel._app.bot.send_message = AsyncMock(return_value=SimpleNamespace(message_id=99))
|
||||
|
||||
oversized = "x" * (4000 + 500)
|
||||
channel._stream_bufs["123"] = _StreamBuf(text=oversized, message_id=7, last_edit=0.0)
|
||||
|
||||
await channel.send_delta("123", "", {"_stream_end": True})
|
||||
|
||||
channel._app.bot.edit_message_text.assert_called_once()
|
||||
edit_text = channel._app.bot.edit_message_text.call_args.kwargs.get("text", "")
|
||||
assert len(edit_text) <= 4096, f"edit_text length {len(edit_text)} exceeds Telegram's 4096 limit"
|
||||
|
||||
channel._app.bot.send_message.assert_called_once()
|
||||
send_text = channel._app.bot.send_message.call_args.kwargs.get("text", "")
|
||||
assert len(send_text) <= 4096
|
||||
assert "123" not in channel._stream_bufs
|
||||
|
||||
|
||||
async def test_send_delta_stream_end_html_expansion_does_not_overflow() -> None:
|
||||
"""Markdown that expands when converted to HTML is still split correctly.
|
||||
|
||||
This is the actual bug from issue #3315: markdown like **bold** expands to
|
||||
<b>bold</b>, adding ~33% characters. A 3600-char message with heavy markdown
|
||||
could become 4800+ chars after HTML conversion, exceeding 4096 limit.
|
||||
The fix converts to HTML first, THEN splits by 4096.
|
||||
"""
|
||||
from nanobot.channels.telegram import _markdown_to_telegram_html
|
||||
|
||||
channel = TelegramChannel(
|
||||
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]),
|
||||
@@ -478,14 +515,21 @@ async def test_send_delta_stream_end_splits_oversized_reply() -> None:
|
||||
channel._app.bot.edit_message_text = AsyncMock()
|
||||
channel._app.bot.send_message = AsyncMock(return_value=SimpleNamespace(message_id=99))
|
||||
|
||||
oversized = "x" * (TELEGRAM_MAX_MESSAGE_LEN + 500)
|
||||
channel._stream_bufs["123"] = _StreamBuf(text=oversized, message_id=7, last_edit=0.0)
|
||||
markdown_text = "**bold** " * 400 # 3600 chars raw, expands ~33% to 4800 HTML
|
||||
raw_len = len(markdown_text)
|
||||
html_len = len(_markdown_to_telegram_html(markdown_text))
|
||||
assert html_len > 4096, f"Test precondition failed: HTML should exceed 4096 (was {html_len})"
|
||||
|
||||
channel._stream_bufs["123"] = _StreamBuf(text=markdown_text, message_id=7, last_edit=0.0)
|
||||
|
||||
await channel.send_delta("123", "", {"_stream_end": True})
|
||||
|
||||
channel._app.bot.edit_message_text.assert_called_once()
|
||||
edit_text = channel._app.bot.edit_message_text.call_args.kwargs.get("text", "")
|
||||
assert len(edit_text) <= TELEGRAM_MAX_MESSAGE_LEN
|
||||
assert len(edit_text) <= 4096, (
|
||||
f"HTML text length {len(edit_text)} exceeds Telegram's 4096 limit. "
|
||||
f"Raw was {raw_len}, HTML was {html_len}."
|
||||
)
|
||||
|
||||
channel._app.bot.send_message.assert_called_once()
|
||||
assert "123" not in channel._stream_bufs
|
||||
|
||||
Reference in New Issue
Block a user