You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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__, **>...||, ). 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.
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.
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).
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 broaderMessageEntitycoverage (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=HTMLTelegramChannel::flush_chunks(crates/zeph-channels/src/telegram.rs:1289-1307) sends the accumulated LLM text viaanswer_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 inzeph-channels). The regular path (telegram.rs:1202-1213) correctly routes text throughmarkdown_to_telegram()withParseMode::MarkdownV2. Any&,<,>, or literal*/_in a guest response either breaks Telegram's HTML parser (message rejected) or renders as garbage.Key design decisions
markdown_to_telegram+ParseMode::MarkdownV2. No second HTML converter/escaper is introduced (DRY). The guest-mode fix is a unification, not a new path.||spoiler||,__underline__,**>...||,). teloxide-core 0.13 already has the entity kinds (Underline/Spoiler/Blockquote/ExpandableBlockquote/CustomEmoji) andSendMessage::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.sendRichMessageis 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 atelegram_api_ext.rsraw-HTTP extension.Scope / Phases
Phase 1 — MVP (this issue):
flush_chunksguest path throughmarkdown_to_telegramand sendparse_mode="MarkdownV2"; apply the length-warning check to the formatted string; skip empty-after-formatting output.>(currently only the first line is quoted —markdown.rs:199).**>...||when a blockquote exceedsexpandable_blockquote_min_lines.[telegram] expandable_blockquote_min_lines: u32 = 10with--initand--migrate-configwiring.Phase 2 — follow-up PR (out of scope here): spoiler via a source convention (
||text||), custom emoji via a config-mappedcustom_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)
flush_chunksformats viamarkdown_to_telegramand sendsparse_mode="MarkdownV2"; no raw text is ever sent with any parse mode.>.expandable_blockquote_min_linesrender as**>...||(collapsible in the client).expandable_blockquote_min_linesconfig field added, defaulting to 10, with--initand--migrate-configintegration.& < > * _) escaping in the guest path.markdown.rstests remain green (no regressions).unwrap()/expect()in changed code; single formatting path preserved (no HTML converter)..local/testing/playbooks/telegram.md.coverage-status.mdupdated.Non-Goals
Message.rich_message/RichBlock*).MessageEntityemission (deferred; if ever added, offsets must be UTF-16 code units and must not coexist withparse_mode).Environment
crates/zeph-channels(markdown.rs,telegram.rs,telegram_api_ext.rs),crates/zeph-config/src/telegram.rsteloxide = "0.17",teloxide-core 0.13.0(covers up to Bot API 9.1; 10.x gap methods live intelegram_api_ext.rs)Evidence / References
crates/zeph-channels/src/telegram.rs:1289-1307(raw text +parse_mode="HTML", unescaped)crates/zeph-channels/src/telegram.rs:1202-1213crates/zeph-channels/src/markdown.rs:199-206MessageEntityKind::{Underline,Spoiler,Blockquote,ExpandableBlockquote,CustomEmoji};SendMessage::entitiesTelegramApiExtto native teloxide Bot API types — relevant to Phase 3 routing); sibling sub-specsspecs/007-channels/007-1-telegram-guest-mode.md,007-2-telegram-bot-to-bot.md