feat(telegram): rich.py renderer for Bot API 10.1 (chat@4.31 4662309 — TG2/4)#164
Conversation
…— TG2/4)
Port packages/adapter-telegram/src/rich.ts character-for-character: the
rich-message -> Markdown / plain-text renderer and media extractor for the
Bot API 10.1 rich-message wire types introduced in TG1.
- TELEGRAM_RICH_MESSAGE_LIMIT = 32768
- truncate_rich_markdown: code-point-aware (list(str) == Array.from), reuses
StreamingMarkdownRenderer.push/finish so the truncated prefix stays valid
markdown; never splits a non-BMP code point.
- escape_text: MARKDOWN_PUNCTUATION /[!-/:-@[-`{-~]/ backslash-escape pass.
- inline/code fence run-sizing: max(1, longest_run+1) / max(3, longest_run+1).
- recursive text()/plain()/block()/plain_block()/media() walkers over every
TelegramRichText / TelegramRichBlock variant, via match statements that
mirror the upstream switch and let pyrefly narrow the recursive union.
Tests port the rich.test.ts it() blocks plus adversarial coverage: a full
punctuation escape sweep (in-class escaped, out-of-class untouched), all-
backtick fence growth, and a surrogate-pair truncation-boundary check. Each
fails on a plausible mutation. Test fixtures annotated to concrete TG1 variant
TypedDicts; pyrefly 0 on src and the test file.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdds ChangesTelegram Rich Message Renderer
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| ) | ||
|
|
||
|
|
||
| def _text(markdown: TelegramRichText) -> str: |
| return "" | ||
|
|
||
|
|
||
| def _plain(markdown: TelegramRichText) -> str: |
| return "\n".join(f"> {line}" for line in value.split("\n")) | ||
|
|
||
|
|
||
| def _block(value: TelegramRichBlock) -> str: |
| return "" | ||
|
|
||
|
|
||
| def _plain_block(value: TelegramRichBlock) -> str: |
There was a problem hiding this comment.
Code Review
This pull request introduces a Python port of the Telegram rich-message renderer, converting structured rich messages into Markdown or plain text and extracting media attachments. Feedback on the implementation highlights two issues: first, string length checks during truncation should use UTF-16 code units instead of Python's default Unicode code point length to match the Telegram Bot API's limits and avoid message rejection with non-BMP characters; second, the alternative text for custom emojis should be escaped to prevent Markdown-sensitive characters from being parsed as formatting.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| characters = list(markdown) | ||
| if len(characters) <= TELEGRAM_RICH_MESSAGE_LIMIT: | ||
| return markdown | ||
|
|
||
| end = TELEGRAM_RICH_MESSAGE_LIMIT - 3 | ||
| while end > 0: | ||
| renderer = StreamingMarkdownRenderer() | ||
| renderer.push("".join(characters[:end])) | ||
| rendered = f"{renderer.finish()}..." | ||
| if len(rendered) <= TELEGRAM_RICH_MESSAGE_LIMIT: | ||
| return rendered | ||
| end -= len(rendered) - TELEGRAM_RICH_MESSAGE_LIMIT |
There was a problem hiding this comment.
In Python, len(string) returns the number of Unicode code points, whereas in JavaScript/TypeScript, string.length returns the number of UTF-16 code units. Since the Telegram Bot API measures message lengths and entity offsets in UTF-16 code units, using len() directly on strings with non-BMP characters (such as emojis) can lead to underestimating the actual length of the message. This might cause Telegram to reject the message for exceeding the 32,768 character limit even if truncate_rich_markdown believes it is within the limit.
To fix this, we should measure the string length in UTF-16 code units using len(s.encode('utf-16-le')) // 2.
| characters = list(markdown) | |
| if len(characters) <= TELEGRAM_RICH_MESSAGE_LIMIT: | |
| return markdown | |
| end = TELEGRAM_RICH_MESSAGE_LIMIT - 3 | |
| while end > 0: | |
| renderer = StreamingMarkdownRenderer() | |
| renderer.push("".join(characters[:end])) | |
| rendered = f"{renderer.finish()}..." | |
| if len(rendered) <= TELEGRAM_RICH_MESSAGE_LIMIT: | |
| return rendered | |
| end -= len(rendered) - TELEGRAM_RICH_MESSAGE_LIMIT | |
| def utf16_len(s: str) -> int: | |
| return len(s.encode('utf-16-le')) // 2 | |
| if utf16_len(markdown) <= TELEGRAM_RICH_MESSAGE_LIMIT: | |
| return markdown | |
| characters = list(markdown) | |
| end = min(len(characters), TELEGRAM_RICH_MESSAGE_LIMIT - 3) | |
| while end > 0: | |
| renderer = StreamingMarkdownRenderer() | |
| renderer.push("".join(characters[:end])) | |
| rendered = f"{renderer.finish()}..." | |
| rendered_len = utf16_len(rendered) | |
| if rendered_len <= TELEGRAM_RICH_MESSAGE_LIMIT: | |
| return rendered | |
| end -= rendered_len - TELEGRAM_RICH_MESSAGE_LIMIT |
| case "custom_emoji": | ||
| return markdown["alternative_text"] |
There was a problem hiding this comment.
The alternative_text of a custom_emoji is returned as-is without escaping. If the alternative text contains any Markdown-sensitive characters (such as *, _, [, etc.), they will be parsed as Markdown formatting instead of plain text. We should escape the alternative text using _escape_text before returning it.
| case "custom_emoji": | |
| return markdown["alternative_text"] | |
| case "custom_emoji": | |
| return _escape_text(markdown["alternative_text"]) |
Render optional rich-text fields (credit/caption/cell text) when the value is an empty list []. Upstream gates these with `value.x ? ... : ""`; JS arrays are always truthy, so credit: [] renders a blank credit, while the prior Python-truthiness gate wrongly skipped it. Add a _present() helper that renders for everything except None/absent and the empty string "". Strip with the exact ECMAScript WhiteSpace+LineTerminator set (JS_WHITESPACE) instead of Python's broader Unicode-whitespace strip(), matching JS trimEnd()/trim() character-for-character. Add regression tests, each verified to fail on its mutation: - empty-list credit renders (truthiness trap) - no-backtick pre block emits a 3-backtick fence (max(3,...)/else 3) - truncation reserves room for the ellipsis (end = LIMIT - 3) - exact-limit-length input passes through unchanged (<= boundary)
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 287d143260
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| checked = "" | ||
| if value.get("has_checkbox"): | ||
| checked = "[x] " if value.get("is_checked") else "[ ] " | ||
| content = "\n\n".join(_block(b) for b in value["blocks"]).replace("\n", "\n ") |
There was a problem hiding this comment.
Use a single continuation space in list items
When a rich list item contains multiple nested blocks or any block with embedded newlines, this replacement turns a\n\nb into a\n \n b, so the rendered item becomes - a\n \n b; the upstream renderer only adds one continuation space, and the same two-space replacement in _plain_item makes those extra spaces visible in rich_message_to_text snippets. This changes multiline list round-trips and text output for Telegram rich lists.
Useful? React with 👍 / 👎.
What
Port of upstream
packages/adapter-telegram/src/rich.ts(chat@4.31.0, commit4662309) — the largest module of the 4.31 Wave C Telegram work. Net-new upstream (norich.tsexisted in 4.30). This is a regex/parse-heavy module ported character-for-character.Strict lane scope: adds only
src/chat_sdk/adapters/telegram/rich.pyandtests/test_telegram_rich.py. Consumes the TG1 rich-message wire types fromchat_sdk.adapters.telegram.types(already on main). Does not touch the adapter (TG4),types.py, orCHANGELOG.md.Surface
TELEGRAM_RICH_MESSAGE_LIMIT = 32768truncate_rich_markdown(markdown)— code-point-aware truncation (list(str)mirrors JSArray.from, so a slice never splits a non-BMP code point / surrogate pair). ReusesStreamingMarkdownRenderer.push/.finishso the truncated prefix stays valid markdown, with...appended; mirrors upstream's shrink-loop.rich_message_to_markdown/rich_message_to_text/rich_message_mediaFaithfulness notes
/[!-/:-@[-+ "" +{-~]/ported exactly asre.compile(r"[!-/:-@[-+ "" +{-~]"); replacementre.sub(..., r"\\\g<0>")(JS"\\$&").max(1, longest_run+1), fencemax(3, longest_run+1)— default-of-1/3 semantics preserved (nomax(runs or [1])idiom).replaceAll→str.replace(all-occurrences);Array.from(x).length→len(x).text()/plain()/block()/plain_block()/media()walkers are translated asmatchstatements with OR-patterns — this both mirrors the upstreamswitch(fall-through cases) and lets pyrefly narrow the recursiveTelegramRichBlock/TelegramRichTextTypeAliasunion (a plainif value["type"] in (...)does not narrow).Tests
Ports the
rich.test.tsit()blocks plus adversarial coverage (perdocs/SELF_REVIEW.md, this is a substitution pass):0-9,A-Z,a-z) left literal;+1);Each adversarial test fails on a plausible mutation (wrong range bound, missing
+1, UTF-16-unit truncation). The escape test asserts on the renderer's emitted Markdown string (the load-bearing port) rather than round-tripping through the Python markdown parser, which is intentionally not full CommonMark (no angle-bracket link destinations / fence widening).Gauntlet
ruff check/ruff format --check: passpyrefly check: 0 errors onrich.py(src) and ontest_telegram_rich.pyaudit_test_quality.py: 0 hard failurespytest: 15/15 new tests pass; full suite 5041 passed, 4 skippedWave C follow-ups
TG3 (format converter) and TG4 (adapter wiring) consume the public surface here:
rich_message_to_markdown,rich_message_to_text,rich_message_media(→RichMediadataclass), andtruncate_rich_markdown/TELEGRAM_RICH_MESSAGE_LIMIT.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Tests