Skip to content

feat(channels): adopt Telegram Bot API 10.1/10.2 rich-text formatting and fix guest-mode escaping #6541

Description

@bug-ops

Motivation

Telegram Bot API 10.1 (June 2026) and 10.2 (14 July 2026) introduced substantially expanded rich-text formatting for bots ("obscenely rich text formatting"): structured rich messages (sendRichMessage + InputRichBlock*), a raised 32,768-character limit, and broader MessageEntity coverage (spoiler, underline, blockquote, expandable/collapsible blockquote, custom emoji).

Zeph's Telegram formatting lives entirely in crates/zeph-channels/src/markdown.rs (markdown_to_telegram / TelegramRenderer) and today emits only a subset of MarkdownV2: bold, italic, strikethrough, inline/block code, links, bullet lists, and a single-line blockquote. It supports none of the new inline features and none of the structured rich blocks.

Separately, while scoping this work an existing correctness bug was confirmed in the guest-mode response path.

Bug (bundled): guest-mode responses are sent raw with parse_mode=HTML

TelegramChannel::flush_chunks (crates/zeph-channels/src/telegram.rs:1289-1307) sends the accumulated LLM text via answer_guest_query(&query_id, &text, Some("HTML"))parse_mode="HTML", but the text is never converted to HTML and never HTML-escaped (no HTML escaper exists in zeph-channels). The regular path (telegram.rs:1202-1213) correctly routes text through markdown_to_telegram() with ParseMode::MarkdownV2. Any &, <, >, or literal */_ in a guest response either breaks Telegram's HTML parser (message rejected) or renders as garbage.

Key design decisions

  • One formatting path. All outbound Telegram text goes through markdown_to_telegram + ParseMode::MarkdownV2. No second HTML converter/escaper is introduced (DRY). The guest-mode fix is a unification, not a new path.
  • Source signal, not emit format. The LLM emits CommonMark, which has no underline/spoiler/collapsible-quote/custom-emoji concept. A feature is only reachable when derivable from an existing CommonMark construct (multi-line quote -> expandable) or via an agreed source convention. This is orthogonal to markup-vs-entities.
  • MarkdownV2 markup for MVP; entities deferred. MarkdownV2 already expresses every inline feature (||spoiler||, __underline__, **>...||, ![g](tg://emoji?id=N)). teloxide-core 0.13 already has the entity kinds (Underline/Spoiler/Blockquote/ExpandableBlockquote/CustomEmoji) and SendMessage::entities, so entity emission remains a future option needing no teloxide upgrade — but it requires UTF-16 offset math and is out of scope here.
  • sendRichMessage is a separate epic. It is absent from teloxide-core 0.13 and needs native teloxide types (preferred; aligns with refactor(channels): migrate TelegramApiExt to native teloxide Bot API 10.0 types #3732) or a telegram_api_ext.rs raw-HTTP extension.

Scope / Phases

Phase 1 — MVP (this issue):

  • Fix guest-mode escaping: route flush_chunks guest path through markdown_to_telegram and send parse_mode="MarkdownV2"; apply the length-warning check to the formatted string; skip empty-after-formatting output.
  • Fix the latent multi-line blockquote bug: prefix every line of a blockquote with > (currently only the first line is quoted — markdown.rs:199).
  • Add expandable (collapsible) blockquote: emit **>...|| when a blockquote exceeds expandable_blockquote_min_lines.
  • Add [telegram] expandable_blockquote_min_lines: u32 = 10 with --init and --migrate-config wiring.

Phase 2 — follow-up PR (out of scope here): spoiler via a source convention (||text||), custom emoji via a config-mapped custom_emoji_id, optional underline.

Phase 3 — separate epic (out of scope here): sendRichMessage/InputRichBlock* structured blocks (tables, checklists, headings, collages, math, footnotes, details, thinking), 32,768-char limit, ephemeral messages.

Acceptance Criteria (Phase 1)

  • Guest-mode flush_chunks formats via markdown_to_telegram and sends parse_mode="MarkdownV2"; no raw text is ever sent with any parse mode.
  • Guest and regular paths produce identical formatted output for identical input (parity unit test).
  • Multi-line blockquotes prefix every line with >.
  • Blockquotes over expandable_blockquote_min_lines render as **>...|| (collapsible in the client).
  • expandable_blockquote_min_lines config field added, defaulting to 10, with --init and --migrate-config integration.
  • Unit tests added: guest/regular parity, multi-line quote, expandable quote, special-char (& < > * _) escaping in the guest path.
  • Existing markdown.rs tests remain green (no regressions).
  • No unwrap()/expect() in changed code; single formatting path preserved (no HTML converter).
  • Live test: a guest @mention response containing HTML/MarkdownV2 special characters renders correctly; documented in .local/testing/playbooks/telegram.md.
  • coverage-status.md updated.

Non-Goals

  • No inbound rich-text parsing (Message.rich_message / RichBlock*).
  • No HTML formatting path.
  • No structured MessageEntity emission (deferred; if ever added, offsets must be UTF-16 code units and must not coexist with parse_mode).
  • No ephemeral-message methods.
  • No teloxide upgrade in this issue.

Environment

  • Component: crates/zeph-channels (markdown.rs, telegram.rs, telegram_api_ext.rs), crates/zeph-config/src/telegram.rs
  • teloxide = "0.17", teloxide-core 0.13.0 (covers up to Bot API 9.1; 10.x gap methods live in telegram_api_ext.rs)

Evidence / References

Metadata

Metadata

Assignees

Labels

P1High ROI, low complexity — do next sprintchannelszeph-channels crate (Telegram)enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions