fix(telegram): measure the rich-message cap in UTF-16 code units (#6174)

`_can_send_rich` checked `len(text)`, a code-point count, against the 32768
rich-message cap, while Telegram measures that cap in UTF-16 code units like
every other limit it enforces. An emoji-heavy reply that fits by code points
is still over the cap by code units, so the rich send was attempted, rejected
by the API, and the reply degraded to chunked plain text.

The check now reuses `_first_utf16_chunk`, the bounded scan the plain-text path
already uses, so eligibility costs the limit rather than the whole reply.
This commit is contained in:
Grapette.L 2026-10-02 22:31:40 +08:00 committed by GitHub
parent 3f2d8e8706
commit 23b366a101
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 32 additions and 1 deletions

View File

@ -376,7 +376,13 @@ class TelegramChannel(Channel):
# actually contains a rich construct. Structured command/error replies
# are plain text with none, so they stay plain and their newlines and
# <placeholder> tokens are not collapsed into one line.
return bool(self.config.get("rich_messages")) and 0 < len(text) <= TELEGRAM_MAX_RICH_MESSAGE_LENGTH and _has_rich_constructs(text)
if not self.config.get("rich_messages"):
return False
# Telegram measures this cap in UTF-16 code units like its other limits, while
# `len()` counts code points, so reuse the bounded scan the plain-text path
# uses: an emoji-heavy reply that fits by code points is still rejected.
_, over_limit = _first_utf16_chunk(text, TELEGRAM_MAX_RICH_MESSAGE_LENGTH)
return not over_limit and _has_rich_constructs(text)
async def _edit_rich_message(self, chat_id: int, message_id: int, text: str) -> bool:
"""Replace a streamed preview with a persistent Telegram Rich Message."""

View File

@ -140,3 +140,28 @@ def test_stream_update_clips_without_measuring_the_whole_reply(monkeypatch):
assert display.endswith("…")
# Bounded by the limit twice (measure against it, then clip one unit shorter), not by 200,000 characters.
assert measured <= 2 * (TELEGRAM_MAX_MESSAGE_LENGTH + 1)
def _rich_channel():
ch, bot = _channel_with_bot()
ch.config["rich_messages"] = True
return ch, bot
def test_rich_send_is_refused_when_the_reply_crosses_the_utf16_rich_limit():
"""The rich-message cap is measured in code units too, so an emoji-heavy reply cannot pass a code-point check."""
ch, _ = _rich_channel()
# Half the limit in emoji plus a bold construct: under the cap by code points, over it by code units.
text = EMOJI * (telegram.TELEGRAM_MAX_RICH_MESSAGE_LENGTH // 2) + "**x**"
assert len(text) < telegram.TELEGRAM_MAX_RICH_MESSAGE_LENGTH
assert utf16_units(text) > telegram.TELEGRAM_MAX_RICH_MESSAGE_LENGTH
assert ch._can_send_rich(text) is False
def test_rich_send_is_still_offered_inside_the_utf16_rich_limit():
ch, _ = _rich_channel()
text = EMOJI * 10 + "**x**"
assert utf16_units(text) == 25
assert ch._can_send_rich(text) is True