Skip to content

feat(telegram): rich.py renderer for Bot API 10.1 (chat@4.31 4662309 — TG2/4)#164

Merged
patrick-chinchill merged 2 commits into
mainfrom
feat/4.31-telegram-rich-renderer
Jun 20, 2026
Merged

feat(telegram): rich.py renderer for Bot API 10.1 (chat@4.31 4662309 — TG2/4)#164
patrick-chinchill merged 2 commits into
mainfrom
feat/4.31-telegram-rich-renderer

Conversation

@patrick-chinchill

@patrick-chinchill patrick-chinchill commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

What

Port of upstream packages/adapter-telegram/src/rich.ts (chat@4.31.0, commit 4662309) — the largest module of the 4.31 Wave C Telegram work. Net-new upstream (no rich.ts existed 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.py and tests/test_telegram_rich.py. Consumes the TG1 rich-message wire types from chat_sdk.adapters.telegram.types (already on main). Does not touch the adapter (TG4), types.py, or CHANGELOG.md.

Surface

  • TELEGRAM_RICH_MESSAGE_LIMIT = 32768
  • truncate_rich_markdown(markdown) — code-point-aware truncation (list(str) mirrors JS Array.from, so a slice never splits a non-BMP code point / surrogate pair). Reuses StreamingMarkdownRenderer.push/.finish so the truncated prefix stays valid markdown, with ... appended; mirrors upstream's shrink-loop.
  • rich_message_to_markdown / rich_message_to_text / rich_message_media

Faithfulness notes

  • Escape char-class /[!-/:-@[- + "" + {-~]/ported exactly asre.compile(r"[!-/:-@[- + "" + {-~]"); replacement re.sub(..., r"\\\g<0>") (JS "\\$&").
  • Backtick run-sizing: inline max(1, longest_run+1), fence max(3, longest_run+1) — default-of-1/3 semantics preserved (no max(runs or [1]) idiom).
  • replaceAllstr.replace (all-occurrences); Array.from(x).lengthlen(x).
  • The recursive text()/plain()/block()/plain_block()/media() walkers are translated as match statements with OR-patterns — this both mirrors the upstream switch (fall-through cases) and lets pyrefly narrow the recursive TelegramRichBlock/TelegramRichText TypeAlias union (a plain if value["type"] in (...) does not narrow).

Tests

Ports the rich.test.ts it() blocks plus adversarial coverage (per docs/SELF_REVIEW.md, this is a substitution pass):

  • full-punctuation escape sweep — every in-class char escaped, out-of-class (digits, letters, non-ASCII, whitespace) untouched, range-gap chars (0-9, A-Z, a-z) left literal;
  • all-backtick fence growth (longest run wins, +1);
  • surrogate-pair truncation boundary (a long emoji run never splits a code point, no lone surrogate leaks).

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: pass
  • pyrefly check: 0 errors on rich.py (src) and on test_telegram_rich.py
  • audit_test_quality.py: 0 hard failures
  • pytest: 15/15 new tests pass; full suite 5041 passed, 4 skipped

Wave 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 (→ RichMedia dataclass), and truncate_rich_markdown / TELEGRAM_RICH_MESSAGE_LIMIT.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added Telegram rich-message rendering supporting Markdown and plain text conversion
    • Implemented media extraction from rich-message structures
    • Added message truncation with Telegram character limit enforcement
  • Tests

    • Added comprehensive test coverage for rich-message rendering, conversion, and truncation behavior

…— 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.
@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e3023199-e58e-456f-ac26-1f5b2b5d7ea1

📥 Commits

Reviewing files that changed from the base of the PR and between 8939d97 and 287d143.

📒 Files selected for processing (2)
  • src/chat_sdk/adapters/telegram/rich.py
  • tests/test_telegram_rich.py

📝 Walkthrough

Walkthrough

Adds src/chat_sdk/adapters/telegram/rich.py, a new Telegram Bot API 10.1 rich-message renderer that converts TelegramRichBlock/TelegramRichText trees into Markdown and plain text, extracts media attachments into a RichMedia dataclass, and truncates output to the Telegram character limit. A comprehensive pytest suite in tests/test_telegram_rich.py covers all public entry points, edge cases, and adversarial inputs.

Changes

Telegram Rich Message Renderer

Layer / File(s) Summary
Module constants, JS-truthiness helper, and truncation
src/chat_sdk/adapters/telegram/rich.py, tests/test_telegram_rich.py
Defines the Telegram character limit constant, compiled regexes, JS-trim whitespace set, _present to replicate JS truthiness for optional fields, and truncate_rich_markdown with code-point-aware slicing and StreamingMarkdownRenderer-based re-rendering. Tests cover over-limit truncation, trailing-line preservation, non-BMP emoji splitting, no-op under-limit behavior, ...-suffix reserve, and exact-boundary pass-through.
Text-level Markdown escaping and recursive rendering
src/chat_sdk/adapters/telegram/rich.py, tests/test_telegram_rich.py
Implements _escape_text, _inline_code with variable backtick-fence sizing and boundary padding, _code_block with fence widening and language sanitization, _link_destination, and the recursive _text/_plain pair covering all TelegramRichText formatting variants. Tests cover punctuation sweeps, fence sizing boundary conditions, empty code, link escaping, mention/hashtag round-trips, and Markdown character escaping.
Block-level Markdown and plain-text rendering
src/chat_sdk/adapters/telegram/rich.py, tests/test_telegram_rich.py
Adds caption helpers, checkbox-aware list-item rendering, table composition, _quote, and the main _block dispatcher covering headings, paragraphs, fenced code, dividers, lists, blockquotes with JS-truthiness-aware optional credits, collage/slideshow, tables, details, maps, and media captions. _plain_block provides the parallel plain-text equivalents. Tests validate structured block-to-Markdown conversion, plain-text entity display, styled-span and table plain output, and credit: [] JS-truthiness fidelity.
RichMedia dataclass and _media tree traversal
src/chat_sdk/adapters/telegram/rich.py, tests/test_telegram_rich.py
Defines the RichMedia dataclass with type discriminator and optional metadata, and _media which recursively walks block trees to collect animation (MIME-mapped to image/video), photo (largest size), video, audio, and voice-note attachments. Tests verify list recursion, largest-photo selection, and animation MIME classification.
Public entry points and test module setup
src/chat_sdk/adapters/telegram/rich.py, tests/test_telegram_rich.py
Adds rich_message_to_markdown, rich_message_to_text, and rich_message_media which join rendered blocks, apply JS-trim whitespace stripping, and return RichMedia lists. Includes the test module header with shared imports.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 A new file hops in, tall and bright,
Rich blocks and Markdown, rendered right!
Backtick fences grow when needed more,
JS truthiness guarded at the door.
Emoji safe from splitting mid-byte,
The rabbit ships it — pure delight! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(telegram): rich.py renderer for Bot API 10.1 (chat@4.31 4662309 — TG2/4)' clearly identifies the main change: introducing a rich.py renderer module for Telegram adapters, and specifies the version/commit context.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

)


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:

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +52 to +63
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
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

Comment on lines +134 to +135
case "custom_emoji":
return markdown["alternative_text"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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)
@patrick-chinchill patrick-chinchill marked this pull request as ready for review June 20, 2026 01:42

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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 ")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@patrick-chinchill patrick-chinchill merged commit 01e5e6e into main Jun 20, 2026
9 checks passed
@patrick-chinchill patrick-chinchill deleted the feat/4.31-telegram-rich-renderer branch June 21, 2026 19:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant