rustigram_types/rich_message.rs
1use serde::{Deserialize, Serialize};
2
3use crate::chat::Location;
4use crate::file::{
5 Animation, Audio, InputMediaAnimation, InputMediaAudio, InputMediaPhoto, InputMediaVideo,
6 InputMediaVoiceNote, PhotoSize, Video, Voice,
7};
8use crate::user::User;
9
10// ─── RichText ─────────────────────────────────────────────────────────────────
11
12/// Rich formatted text — a recursive sum type that mirrors the Telegram
13/// `RichText` union from Bot API 10.1.
14///
15/// A `RichText` value is either:
16/// - a plain [`String`] (leaf node),
17/// - an [`Array`](RichText::Array) of nested `RichText` values, or
18/// - a single formatted [`Node`](RichText::Node).
19///
20/// These three untagged variants only ever have to tell a string from an array
21/// from an object, which serde does reliably. The choice *between* formatted
22/// node kinds is made by [`RichTextNode`]'s `type` tag.
23///
24/// This split matters: when all 25 node kinds were untagged variants of this
25/// enum they were structurally identical, so serde matched the first one and
26/// every kind of formatted text deserialized as bold.
27#[derive(Debug, Clone, Serialize, Deserialize)]
28#[serde(untagged)]
29pub enum RichText {
30 /// Plain text without any formatting.
31 Plain(String),
32 /// A sequence of rich-text nodes rendered consecutively.
33 Array(Vec<RichText>),
34 /// A single formatted node.
35 Node(Box<RichTextNode>),
36}
37
38/// A single formatted rich-text node, dispatched on its `type` discriminant.
39///
40/// An unrecognised `type` is a deserialization error rather than a silent
41/// fallback. That is deliberate — silently mistyping formatted text is the bug
42/// this enum exists to fix — but it does mean a rich-text kind introduced by a
43/// future Bot API version will fail to parse until it is added here.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45#[serde(tag = "type", rename_all = "snake_case")]
46pub enum RichTextNode {
47 /// Bold formatting.
48 Bold(RichTextBold),
49
50 /// Italic formatting.
51 Italic(RichTextItalic),
52
53 /// Underline formatting.
54 Underline(RichTextUnderline),
55
56 /// Strikethrough formatting.
57 Strikethrough(RichTextStrikethrough),
58
59 /// Spoiler — text hidden until tapped.
60 Spoiler(RichTextSpoiler),
61
62 /// A date/time entity.
63 DateTime(RichTextDateTime),
64
65 /// A mention by user object.
66 TextMention(RichTextTextMention),
67
68 /// Subscript text.
69 Subscript(RichTextSubscript),
70
71 /// Superscript text.
72 Superscript(RichTextSuperscript),
73
74 /// Highlighted/marked text.
75 Marked(RichTextMarked),
76
77 /// Inline monospace code.
78 Code(RichTextCode),
79
80 /// A custom emoji.
81 CustomEmoji(RichTextCustomEmoji),
82
83 /// An inline LaTeX mathematical expression.
84 MathematicalExpression(RichTextMathematicalExpression),
85
86 /// A hyperlink.
87 Url(RichTextUrl),
88
89 /// An e-mail address link.
90 EmailAddress(RichTextEmailAddress),
91
92 /// A telephone number link.
93 PhoneNumber(RichTextPhoneNumber),
94
95 /// A bank card number.
96 BankCardNumber(RichTextBankCardNumber),
97
98 /// A `@username` mention.
99 Mention(RichTextMention),
100
101 /// A `#hashtag`.
102 Hashtag(RichTextHashtag),
103
104 /// A `$cashtag`.
105 Cashtag(RichTextCashtag),
106
107 /// A `/bot_command`.
108 BotCommand(RichTextBotCommand),
109
110 /// An in-document anchor definition.
111 Anchor(RichTextAnchor),
112
113 /// A link targeting an in-document anchor.
114 AnchorLink(RichTextAnchorLink),
115
116 /// A reference to a footnote.
117 Reference(RichTextReference),
118 /// A link to a reference.
119 ReferenceLink(RichTextReferenceLink),
120}
121
122/// Bold text (`**text**` / `<b>text</b>`).
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct RichTextBold {
125 /// The contained rich text.
126 pub text: RichText,
127}
128
129/// Italic text (`*text*` / `<i>text</i>`).
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct RichTextItalic {
132 /// The contained rich text.
133 pub text: RichText,
134}
135
136/// Underlined text (`<u>text</u>`).
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct RichTextUnderline {
139 /// The contained rich text.
140 pub text: RichText,
141}
142
143/// Strikethrough text (`~~text~~` / `<s>text</s>`).
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct RichTextStrikethrough {
146 /// The contained rich text.
147 pub text: RichText,
148}
149
150/// Spoiler text (`||text||` / `<tg-spoiler>text</tg-spoiler>`).
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct RichTextSpoiler {
153 /// The contained rich text.
154 pub text: RichText,
155}
156
157/// A date/time entity rendered according to the client's locale.
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct RichTextDateTime {
160 /// The display text.
161 pub text: RichText,
162 /// The Unix timestamp associated with the entity.
163 pub unix_time: i64,
164 /// Format string controlling how the date/time is rendered.
165 pub date_time_format: String,
166}
167
168/// A mention of a Telegram user by their `User` object.
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct RichTextTextMention {
171 /// The display text.
172 pub text: RichText,
173 /// The mentioned user.
174 pub user: User,
175}
176
177/// Subscript text (`<sub>text</sub>`).
178#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct RichTextSubscript {
180 /// The contained rich text.
181 pub text: RichText,
182}
183
184/// Superscript text (`<sup>text</sup>`).
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct RichTextSuperscript {
187 /// The contained rich text.
188 pub text: RichText,
189}
190
191/// Highlighted/marked text (`==text==` / `<mark>text</mark>`).
192#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct RichTextMarked {
194 /// The contained rich text.
195 pub text: RichText,
196}
197
198/// Inline monospace/code text (`` `text` `` / `<code>text</code>`).
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct RichTextCode {
201 /// The contained rich text.
202 pub text: RichText,
203}
204
205/// A custom emoji (``).
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct RichTextCustomEmoji {
208 /// Unique identifier of the custom emoji.
209 pub custom_emoji_id: String,
210 /// Fallback emoji string for clients that do not support custom emoji.
211 pub alternative_text: String,
212}
213
214/// An inline LaTeX mathematical expression (`$expr$` / `<tg-math>expr</tg-math>`).
215#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct RichTextMathematicalExpression {
217 /// The LaTeX source of the expression.
218 pub expression: String,
219}
220
221/// A hyperlink (`[text](url)` / `<a href="url">text</a>`).
222#[derive(Debug, Clone, Serialize, Deserialize)]
223pub struct RichTextUrl {
224 /// The display text.
225 pub text: RichText,
226 /// The target URL.
227 pub url: String,
228}
229
230/// An e-mail address link (`[text](mailto:addr)` / `<a href="mailto:addr">text</a>`).
231#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct RichTextEmailAddress {
233 /// The display text.
234 pub text: RichText,
235 /// The raw e-mail address.
236 pub email_address: String,
237}
238
239/// A telephone number link (`[text](tel:+nnn)` / `<a href="tel:+nnn">text</a>`).
240#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct RichTextPhoneNumber {
242 /// The display text.
243 pub text: RichText,
244 /// The raw phone number.
245 pub phone_number: String,
246}
247
248/// A bank card number.
249#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct RichTextBankCardNumber {
251 /// The display text.
252 pub text: RichText,
253 /// The raw bank card number.
254 pub bank_card_number: String,
255}
256
257/// A `@username` mention.
258#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct RichTextMention {
260 /// The display text.
261 pub text: RichText,
262 /// The target username (without the leading `@`).
263 pub username: String,
264}
265
266/// A `#hashtag`.
267#[derive(Debug, Clone, Serialize, Deserialize)]
268pub struct RichTextHashtag {
269 /// The display text.
270 pub text: RichText,
271 /// The hashtag value (without the leading `#`).
272 pub hashtag: String,
273}
274
275/// A `$cashtag`.
276#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct RichTextCashtag {
278 /// The display text.
279 pub text: RichText,
280 /// The cashtag value (without the leading `$`).
281 pub cashtag: String,
282}
283
284/// A bot command (e.g. `/start`).
285#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct RichTextBotCommand {
287 /// The display text.
288 pub text: RichText,
289 /// The command string including the leading `/`.
290 pub bot_command: String,
291}
292
293/// An in-document anchor definition (`<a name="id"></a>`).
294#[derive(Debug, Clone, Serialize, Deserialize)]
295pub struct RichTextAnchor {
296 /// The anchor name.
297 pub name: String,
298}
299
300/// A link to an in-document anchor (`<a href="#id">text</a>`).
301///
302/// If `anchor_name` is empty the link scrolls back to the top of the message.
303#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct RichTextAnchorLink {
305 /// The display text.
306 pub text: RichText,
307 /// The target anchor name; empty string scrolls to the top.
308 pub anchor_name: String,
309}
310
311/// A link to a reference.
312#[derive(Debug, Clone, Serialize, Deserialize)]
313pub struct RichTextReferenceLink {
314 /// The link text.
315 pub text: RichText,
316 /// The name of the reference.
317 pub reference_name: String,
318}
319
320/// A reference to a previously defined footnote.
321#[derive(Debug, Clone, Serialize, Deserialize)]
322pub struct RichTextReference {
323 /// The display text (typically the footnote superscript label).
324 pub text: RichText,
325 /// The footnote identifier being referenced.
326 #[serde(rename = "name")]
327 pub footnote_name: String,
328}
329
330// ─── RichBlock helpers ────────────────────────────────────────────────────────
331
332/// Caption (and optional credit) for a media block.
333#[derive(Debug, Clone, Serialize, Deserialize)]
334pub struct RichBlockCaption {
335 /// The caption text.
336 pub text: RichText,
337 /// Optional credit line (HTML `<cite>`).
338 #[serde(skip_serializing_if = "Option::is_none")]
339 pub credit: Option<RichText>,
340}
341
342/// A single cell inside a [`RichBlockTable`].
343#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct RichBlockTableCell {
345 /// The cell content; omit to leave the cell empty/invisible.
346 #[serde(skip_serializing_if = "Option::is_none")]
347 pub text: Option<RichText>,
348 /// `true` if this is a header cell (`<th>`).
349 #[serde(skip_serializing_if = "Option::is_none")]
350 pub is_header: Option<bool>,
351 /// Number of columns the cell spans.
352 #[serde(skip_serializing_if = "Option::is_none")]
353 pub colspan: Option<u32>,
354 /// Number of rows the cell spans.
355 #[serde(skip_serializing_if = "Option::is_none")]
356 pub rowspan: Option<u32>,
357 /// Horizontal text alignment: `"left"`, `"center"`, or `"right"`.
358 pub align: String,
359 /// Vertical text alignment: `"top"`, `"middle"`, or `"bottom"`.
360 pub valign: String,
361}
362
363/// A single item inside a [`RichBlockList`].
364#[derive(Debug, Clone, Serialize, Deserialize)]
365pub struct RichBlockListItem {
366 /// The bullet or number label rendered by the client.
367 pub label: String,
368 /// The nested content of this list item.
369 pub blocks: Vec<RichBlock>,
370 /// `true` if the item has a checkbox.
371 #[serde(skip_serializing_if = "Option::is_none")]
372 pub has_checkbox: Option<bool>,
373 /// `true` if the checkbox is checked.
374 #[serde(skip_serializing_if = "Option::is_none")]
375 pub is_checked: Option<bool>,
376 /// For ordered lists — the explicit numeric value of this item.
377 #[serde(skip_serializing_if = "Option::is_none")]
378 pub value: Option<i64>,
379 /// For ordered lists — the label type: `"a"`, `"A"`, `"i"`, `"I"`, or `"1"`.
380 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
381 pub kind: Option<String>,
382}
383
384// ─── RichBlock ────────────────────────────────────────────────────────────────
385
386/// A block-level element in a rich message.
387///
388/// This is the top-level building block for `RichMessage::blocks`.
389#[derive(Debug, Clone, Serialize, Deserialize)]
390#[serde(tag = "type", rename_all = "snake_case")]
391pub enum RichBlock {
392 /// A text paragraph (`<p>`).
393 Paragraph(RichBlockParagraph),
394 /// A section heading (`<h1>`…`<h6>`).
395 #[serde(rename = "heading")]
396 SectionHeading(RichBlockSectionHeading),
397 /// A preformatted / code block (`<pre><code>`).
398 Pre(RichBlockPreformatted),
399 /// A footer (`<footer>`).
400 Footer(RichBlockFooter),
401 /// A horizontal rule / divider (`<hr/>`).
402 Divider(RichBlockDivider),
403 /// A block-level LaTeX expression (`<tg-math-block>`).
404 MathematicalExpression(RichBlockMathematicalExpression),
405 /// An in-document anchor (`<a name="…"></a>`).
406 Anchor(RichBlockAnchor),
407 /// An ordered or unordered list (`<ul>` / `<ol>`).
408 List(RichBlockList),
409 /// A block quotation (`<blockquote>`).
410 Blockquote(RichBlockBlockQuotation),
411 /// A pull quotation (`<aside>`).
412 Pullquote(RichBlockPullQuotation),
413 /// A multi-media collage (`<tg-collage>`).
414 Collage(RichBlockCollage),
415 /// A media slideshow (`<tg-slideshow>`).
416 Slideshow(RichBlockSlideshow),
417 /// A table (`<table>`).
418 Table(RichBlockTable),
419 /// A collapsible details block (`<details>`).
420 Details(RichBlockDetails),
421 /// An embedded map (`<tg-map>`).
422 Map(RichBlockMap),
423 /// A looping animation / GIF (`<video loop>`).
424 Animation(RichBlockAnimation),
425 /// An audio file (`<audio>`).
426 Audio(RichBlockAudio),
427 /// A photo (`<photo>`).
428 Photo(RichBlockPhoto),
429 /// A video (`<video>`).
430 Video(RichBlockVideo),
431 /// A voice note (`<audio>` in voice-note context).
432 VoiceNote(RichBlockVoiceNote),
433 /// A `Thinking…` placeholder used during AI streaming drafts.
434 ///
435 /// Only valid in [`sendRichMessageDraft`](https://core.telegram.org/bots/api#sendrichmessagedraft) calls.
436 Thinking(RichBlockThinking),
437}
438
439/// A text paragraph (`<p>`).
440#[derive(Debug, Clone, Serialize, Deserialize)]
441pub struct RichBlockParagraph {
442 /// The paragraph text.
443 pub text: RichText,
444}
445
446/// A section heading, corresponding to `<h1>`…`<h6>`.
447#[derive(Debug, Clone, Serialize, Deserialize)]
448pub struct RichBlockSectionHeading {
449 /// The heading text.
450 pub text: RichText,
451 /// Font size level 1–6 (1 = largest, 6 = smallest).
452 pub size: u8,
453}
454
455/// A preformatted text block (`<pre><code>`).
456#[derive(Debug, Clone, Serialize, Deserialize)]
457pub struct RichBlockPreformatted {
458 /// The preformatted text.
459 pub text: RichText,
460 /// Optional syntax-highlight language identifier.
461 #[serde(skip_serializing_if = "Option::is_none")]
462 pub language: Option<String>,
463}
464
465/// A footer block (`<footer>`).
466#[derive(Debug, Clone, Serialize, Deserialize)]
467pub struct RichBlockFooter {
468 /// The footer text.
469 pub text: RichText,
470}
471
472/// A horizontal rule / divider (`<hr/>`).
473///
474/// Has no content fields.
475#[derive(Debug, Clone, Serialize, Deserialize)]
476pub struct RichBlockDivider {}
477
478/// A block-level mathematical expression in LaTeX format (`<tg-math-block>`).
479#[derive(Debug, Clone, Serialize, Deserialize)]
480pub struct RichBlockMathematicalExpression {
481 /// The raw LaTeX source.
482 pub expression: String,
483}
484
485/// An in-document anchor (`<a name="…"></a>`).
486#[derive(Debug, Clone, Serialize, Deserialize)]
487pub struct RichBlockAnchor {
488 /// The anchor name.
489 pub name: String,
490}
491
492/// An ordered or unordered list (`<ul>` / `<ol>`).
493#[derive(Debug, Clone, Serialize, Deserialize)]
494pub struct RichBlockList {
495 /// The list items.
496 pub items: Vec<RichBlockListItem>,
497}
498
499/// A block quotation (`<blockquote>`).
500#[derive(Debug, Clone, Serialize, Deserialize)]
501pub struct RichBlockBlockQuotation {
502 /// Nested block content.
503 pub blocks: Vec<RichBlock>,
504 /// Optional attribution credit.
505 #[serde(skip_serializing_if = "Option::is_none")]
506 pub credit: Option<RichText>,
507}
508
509/// A pull quotation with centred text (`<aside>`).
510#[derive(Debug, Clone, Serialize, Deserialize)]
511pub struct RichBlockPullQuotation {
512 /// The quotation text.
513 pub text: RichText,
514 /// Optional attribution credit.
515 #[serde(skip_serializing_if = "Option::is_none")]
516 pub credit: Option<RichText>,
517}
518
519/// A multi-media collage (`<tg-collage>`).
520#[derive(Debug, Clone, Serialize, Deserialize)]
521pub struct RichBlockCollage {
522 /// The media elements of the collage.
523 pub blocks: Vec<RichBlock>,
524 /// Optional caption.
525 #[serde(skip_serializing_if = "Option::is_none")]
526 pub caption: Option<RichBlockCaption>,
527}
528
529/// A media slideshow (`<tg-slideshow>`).
530#[derive(Debug, Clone, Serialize, Deserialize)]
531pub struct RichBlockSlideshow {
532 /// The media elements of the slideshow.
533 pub blocks: Vec<RichBlock>,
534 /// Optional caption.
535 #[serde(skip_serializing_if = "Option::is_none")]
536 pub caption: Option<RichBlockCaption>,
537}
538
539/// A table (`<table>`).
540#[derive(Debug, Clone, Serialize, Deserialize)]
541pub struct RichBlockTable {
542 /// A 2-D array of cells (rows × columns).
543 pub cells: Vec<Vec<RichBlockTableCell>>,
544 /// `true` if the table has visible borders.
545 #[serde(skip_serializing_if = "Option::is_none")]
546 pub is_bordered: Option<bool>,
547 /// `true` if alternate rows are shaded.
548 #[serde(skip_serializing_if = "Option::is_none")]
549 pub is_striped: Option<bool>,
550 /// Optional table caption.
551 #[serde(skip_serializing_if = "Option::is_none")]
552 pub caption: Option<RichText>,
553}
554
555/// A collapsible details / disclosure block (`<details>`).
556#[derive(Debug, Clone, Serialize, Deserialize)]
557pub struct RichBlockDetails {
558 /// The always-visible summary.
559 pub summary: RichText,
560 /// Nested block content shown when expanded.
561 pub blocks: Vec<RichBlock>,
562 /// `true` if the block is expanded by default.
563 #[serde(skip_serializing_if = "Option::is_none")]
564 pub is_open: Option<bool>,
565}
566
567/// An embedded map (`<tg-map>`).
568#[derive(Debug, Clone, Serialize, Deserialize)]
569pub struct RichBlockMap {
570 /// Location of the map centre.
571 ///
572 /// Telegram sends this as a nested `location` object. It was previously
573 /// modelled as flat `latitude`/`longitude` fields here, which meant every
574 /// real map block failed to deserialize with "missing field `latitude`".
575 pub location: crate::chat::Location,
576 /// Zoom level (13–20).
577 pub zoom: u8,
578 /// Expected rendered width in pixels.
579 pub width: u32,
580 /// Expected rendered height in pixels.
581 pub height: u32,
582 /// Optional caption.
583 #[serde(skip_serializing_if = "Option::is_none")]
584 pub caption: Option<RichBlockCaption>,
585}
586
587/// A looping animation / GIF block (`<video loop>`).
588#[derive(Debug, Clone, Serialize, Deserialize)]
589pub struct RichBlockAnimation {
590 /// The animation file.
591 pub animation: Animation,
592 /// `true` if a spoiler overlay is shown before the first tap.
593 #[serde(skip_serializing_if = "Option::is_none")]
594 pub has_spoiler: Option<bool>,
595 /// Optional caption.
596 #[serde(skip_serializing_if = "Option::is_none")]
597 pub caption: Option<RichBlockCaption>,
598}
599
600/// An audio file block (`<audio>`).
601#[derive(Debug, Clone, Serialize, Deserialize)]
602pub struct RichBlockAudio {
603 /// The audio file.
604 pub audio: Audio,
605 /// Optional caption.
606 #[serde(skip_serializing_if = "Option::is_none")]
607 pub caption: Option<RichBlockCaption>,
608}
609
610/// A photo block (`<photo>`).
611#[derive(Debug, Clone, Serialize, Deserialize)]
612pub struct RichBlockPhoto {
613 /// All available sizes of the photo.
614 pub photo: Vec<PhotoSize>,
615 /// `true` if a spoiler overlay is shown before the first tap.
616 #[serde(skip_serializing_if = "Option::is_none")]
617 pub has_spoiler: Option<bool>,
618 /// Optional caption.
619 #[serde(skip_serializing_if = "Option::is_none")]
620 pub caption: Option<RichBlockCaption>,
621}
622
623/// A video block (`<video>`).
624#[derive(Debug, Clone, Serialize, Deserialize)]
625pub struct RichBlockVideo {
626 /// The video file.
627 pub video: Video,
628 /// `true` if a spoiler overlay is shown before the first tap.
629 #[serde(skip_serializing_if = "Option::is_none")]
630 pub has_spoiler: Option<bool>,
631 /// Optional caption.
632 #[serde(skip_serializing_if = "Option::is_none")]
633 pub caption: Option<RichBlockCaption>,
634}
635
636/// A voice note block (`<audio>` in voice-note context).
637#[derive(Debug, Clone, Serialize, Deserialize)]
638pub struct RichBlockVoiceNote {
639 /// The voice note file.
640 pub voice_note: Voice,
641 /// Optional caption.
642 #[serde(skip_serializing_if = "Option::is_none")]
643 pub caption: Option<RichBlockCaption>,
644}
645
646/// A `Thinking…` placeholder for use while a bot streams an AI response.
647///
648/// Only valid inside [`sendRichMessageDraft`](https://core.telegram.org/bots/api#sendrichmessagedraft) calls.
649/// See <https://t.me/addemoji/AIActions> for recommended custom emoji.
650#[derive(Debug, Clone, Serialize, Deserialize)]
651pub struct RichBlockThinking {
652 /// The placeholder display text (may include custom emoji).
653 pub text: RichText,
654}
655
656// ─── RichMessage ──────────────────────────────────────────────────────────────
657
658/// A complete rich formatted message as received from the Bot API.
659///
660/// Carried in `Message::rich_message`.
661#[derive(Debug, Clone, Serialize, Deserialize)]
662pub struct RichMessage {
663 /// The ordered list of top-level blocks forming the message body.
664 pub blocks: Vec<RichBlock>,
665 /// `true` if the message must be rendered right-to-left.
666 #[serde(skip_serializing_if = "Option::is_none")]
667 pub is_rtl: Option<bool>,
668}
669
670// ─── InputRichBlock helpers ───────────────────────────────────────────────────
671
672/// An item of a list to be sent, contained in an [`InputRichBlockList`].
673#[derive(Debug, Clone, Serialize, Deserialize)]
674pub struct InputRichBlockListItem {
675 /// The content of the item.
676 pub blocks: Vec<InputRichBlock>,
677 /// Pass `true` if the item has a checkbox.
678 #[serde(skip_serializing_if = "Option::is_none")]
679 pub has_checkbox: Option<bool>,
680 /// Pass `true` if the item has a checked checkbox.
681 #[serde(skip_serializing_if = "Option::is_none")]
682 pub is_checked: Option<bool>,
683 /// For ordered lists, the numeric value of the item label.
684 #[serde(skip_serializing_if = "Option::is_none")]
685 pub value: Option<i64>,
686 /// For ordered lists, the type of the item label: `"a"`, `"A"`, `"i"`, `"I"`, or `"1"`.
687 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
688 pub kind: Option<String>,
689}
690
691// ─── InputRichBlock ───────────────────────────────────────────────────────────
692
693/// A block in a rich formatted message to be sent.
694///
695/// This is the input-side counterpart to [`RichBlock`] — used to build
696/// `InputRichMessage::blocks` rather than to read a received `RichMessage`.
697#[derive(Debug, Clone, Serialize, Deserialize)]
698#[serde(tag = "type", rename_all = "snake_case")]
699pub enum InputRichBlock {
700 /// A text paragraph, corresponding to the HTML tag `<p>`.
701 Paragraph(InputRichBlockParagraph),
702 /// A section heading, corresponding to the HTML tags `<h1>`…`<h6>`.
703 #[serde(rename = "heading")]
704 SectionHeading(InputRichBlockSectionHeading),
705 /// A preformatted text block, corresponding to the nested HTML tags `<pre>` and `<code>`.
706 #[serde(rename = "pre")]
707 Preformatted(InputRichBlockPreformatted),
708 /// A footer, corresponding to the HTML tag `<footer>`.
709 Footer(InputRichBlockFooter),
710 /// A divider, corresponding to the HTML tag `<hr/>`.
711 Divider(InputRichBlockDivider),
712 /// A block with a mathematical expression in LaTeX format, corresponding
713 /// to the custom HTML tag `<tg-math-block>`.
714 MathematicalExpression(InputRichBlockMathematicalExpression),
715 /// A block with an anchor, corresponding to the HTML tag `<a>` with the attribute `name`.
716 Anchor(InputRichBlockAnchor),
717 /// A list of blocks, corresponding to the HTML tag `<ul>` or `<ol>` with nested `<li>` tags.
718 List(InputRichBlockList),
719 /// A block quotation, corresponding to the HTML tag `<blockquote>`.
720 #[serde(rename = "blockquote")]
721 BlockQuotation(InputRichBlockBlockQuotation),
722 /// A quotation with centered text, loosely corresponding to the HTML tag `<aside>`.
723 #[serde(rename = "pullquote")]
724 PullQuotation(InputRichBlockPullQuotation),
725 /// A collage, corresponding to the custom HTML tag `<tg-collage>`.
726 Collage(InputRichBlockCollage),
727 /// A slideshow, corresponding to the custom HTML tag `<tg-slideshow>`.
728 Slideshow(InputRichBlockSlideshow),
729 /// A table, corresponding to the HTML tag `<table>`.
730 Table(InputRichBlockTable),
731 /// An expandable block for details disclosure, corresponding to the HTML tag `<details>`.
732 Details(InputRichBlockDetails),
733 /// A block with a map, corresponding to the custom HTML tag `<tg-map>`.
734 Map(InputRichBlockMap),
735 /// A block with an animation, corresponding to the HTML tag `<video>`.
736 Animation(InputRichBlockAnimation),
737 /// A block with a music file, corresponding to the HTML tag `<audio>`.
738 Audio(InputRichBlockAudio),
739 /// A block with a photo, corresponding to the HTML tag `<img>`.
740 Photo(InputRichBlockPhoto),
741 /// A block with a video, corresponding to the HTML tag `<video>`.
742 Video(InputRichBlockVideo),
743 /// A block with a voice note, corresponding to the HTML tag `<audio>`.
744 VoiceNote(InputRichBlockVoiceNote),
745 /// A `Thinking…` placeholder, corresponding to the custom HTML tag `<tg-thinking>`.
746 ///
747 /// May be used only in [`sendRichMessageDraft`](https://core.telegram.org/bots/api#sendrichmessagedraft)
748 /// calls — it cannot be received in messages. See <https://t.me/addemoji/AIActions>
749 /// for recommended custom emoji.
750 Thinking(InputRichBlockThinking),
751}
752
753/// A text paragraph (`<p>`).
754#[derive(Debug, Clone, Serialize, Deserialize)]
755pub struct InputRichBlockParagraph {
756 /// Text of the block.
757 pub text: RichText,
758}
759
760/// A section heading (`<h1>`…`<h6>`).
761#[derive(Debug, Clone, Serialize, Deserialize)]
762pub struct InputRichBlockSectionHeading {
763 /// Text of the block.
764 pub text: RichText,
765 /// Relative size of the text font; 1–6, `1` is the largest, `6` is the smallest.
766 pub size: u8,
767}
768
769/// A preformatted text block (`<pre><code>`).
770#[derive(Debug, Clone, Serialize, Deserialize)]
771pub struct InputRichBlockPreformatted {
772 /// Text of the block.
773 pub text: RichText,
774 /// The programming language of the text.
775 #[serde(skip_serializing_if = "Option::is_none")]
776 pub language: Option<String>,
777}
778
779/// A footer (`<footer>`).
780#[derive(Debug, Clone, Serialize, Deserialize)]
781pub struct InputRichBlockFooter {
782 /// Text of the block.
783 pub text: RichText,
784}
785
786/// A divider (`<hr/>`).
787#[derive(Debug, Clone, Serialize, Deserialize, Default)]
788pub struct InputRichBlockDivider {}
789
790/// A mathematical expression in LaTeX format (`<tg-math-block>`).
791#[derive(Debug, Clone, Serialize, Deserialize)]
792pub struct InputRichBlockMathematicalExpression {
793 /// The mathematical expression in LaTeX format.
794 pub expression: String,
795}
796
797/// An anchor (`<a name="…">`).
798#[derive(Debug, Clone, Serialize, Deserialize)]
799pub struct InputRichBlockAnchor {
800 /// The name of the anchor.
801 pub name: String,
802}
803
804/// A list of blocks (`<ul>` / `<ol>`).
805#[derive(Debug, Clone, Serialize, Deserialize)]
806pub struct InputRichBlockList {
807 /// Items of the list.
808 pub items: Vec<InputRichBlockListItem>,
809}
810
811/// A block quotation (`<blockquote>`).
812#[derive(Debug, Clone, Serialize, Deserialize)]
813pub struct InputRichBlockBlockQuotation {
814 /// Content of the block.
815 pub blocks: Vec<InputRichBlock>,
816 /// Credit of the block.
817 #[serde(skip_serializing_if = "Option::is_none")]
818 pub credit: Option<RichText>,
819}
820
821/// A quotation with centered text (`<aside>`).
822#[derive(Debug, Clone, Serialize, Deserialize)]
823pub struct InputRichBlockPullQuotation {
824 /// Text of the block.
825 pub text: RichText,
826 /// Credit of the block.
827 #[serde(skip_serializing_if = "Option::is_none")]
828 pub credit: Option<RichText>,
829}
830
831/// A collage (`<tg-collage>`).
832#[derive(Debug, Clone, Serialize, Deserialize)]
833pub struct InputRichBlockCollage {
834 /// Elements of the collage.
835 pub blocks: Vec<InputRichBlock>,
836 /// Caption of the block.
837 #[serde(skip_serializing_if = "Option::is_none")]
838 pub caption: Option<RichBlockCaption>,
839}
840
841/// A slideshow (`<tg-slideshow>`).
842#[derive(Debug, Clone, Serialize, Deserialize)]
843pub struct InputRichBlockSlideshow {
844 /// Elements of the slideshow.
845 pub blocks: Vec<InputRichBlock>,
846 /// Caption of the block.
847 #[serde(skip_serializing_if = "Option::is_none")]
848 pub caption: Option<RichBlockCaption>,
849}
850
851/// A table (`<table>`).
852#[derive(Debug, Clone, Serialize, Deserialize)]
853pub struct InputRichBlockTable {
854 /// Cells of the table.
855 pub cells: Vec<Vec<RichBlockTableCell>>,
856 /// Pass `true` if the table has borders.
857 #[serde(skip_serializing_if = "Option::is_none")]
858 pub is_bordered: Option<bool>,
859 /// Pass `true` if the table is striped.
860 #[serde(skip_serializing_if = "Option::is_none")]
861 pub is_striped: Option<bool>,
862 /// Caption of the table.
863 #[serde(skip_serializing_if = "Option::is_none")]
864 pub caption: Option<RichText>,
865}
866
867/// An expandable details disclosure block (`<details>`).
868#[derive(Debug, Clone, Serialize, Deserialize)]
869pub struct InputRichBlockDetails {
870 /// Always shown summary of the block.
871 pub summary: RichText,
872 /// Content of the block.
873 pub blocks: Vec<InputRichBlock>,
874 /// Pass `true` if the content of the block is visible by default.
875 #[serde(skip_serializing_if = "Option::is_none")]
876 pub is_open: Option<bool>,
877}
878
879/// A map (`<tg-map>`).
880///
881/// The map's width and height must not exceed 10000 in total. The width and
882/// height ratio must be at most 20.
883#[derive(Debug, Clone, Serialize, Deserialize)]
884pub struct InputRichBlockMap {
885 /// Location of the center of the map.
886 pub location: Location,
887 /// Map zoom level; 0–24.
888 pub zoom: u8,
889 /// Map width; 0–10000.
890 pub width: u32,
891 /// Map height; 0–10000.
892 pub height: u32,
893 /// Caption of the block.
894 #[serde(skip_serializing_if = "Option::is_none")]
895 pub caption: Option<RichBlockCaption>,
896}
897
898/// An animation block (`<video>`). Caption on the inner media is ignored —
899/// use this block's own `caption` instead.
900#[derive(Debug, Clone, Serialize, Deserialize)]
901pub struct InputRichBlockAnimation {
902 /// The animation.
903 pub animation: InputMediaAnimation,
904 /// Caption of the block.
905 #[serde(skip_serializing_if = "Option::is_none")]
906 pub caption: Option<RichBlockCaption>,
907}
908
909/// A music file block (`<audio>`). Caption on the inner media is ignored —
910/// use this block's own `caption` instead.
911#[derive(Debug, Clone, Serialize, Deserialize)]
912pub struct InputRichBlockAudio {
913 /// The audio.
914 pub audio: InputMediaAudio,
915 /// Caption of the block.
916 #[serde(skip_serializing_if = "Option::is_none")]
917 pub caption: Option<RichBlockCaption>,
918}
919
920/// A photo block (`<img>`). Caption on the inner media is ignored — use this
921/// block's own `caption` instead.
922#[derive(Debug, Clone, Serialize, Deserialize)]
923pub struct InputRichBlockPhoto {
924 /// The photo.
925 pub photo: InputMediaPhoto,
926 /// Caption of the block.
927 #[serde(skip_serializing_if = "Option::is_none")]
928 pub caption: Option<RichBlockCaption>,
929}
930
931/// A video block (`<video>`). Caption on the inner media is ignored — use
932/// this block's own `caption` instead.
933#[derive(Debug, Clone, Serialize, Deserialize)]
934pub struct InputRichBlockVideo {
935 /// The video.
936 pub video: InputMediaVideo,
937 /// Caption of the block.
938 #[serde(skip_serializing_if = "Option::is_none")]
939 pub caption: Option<RichBlockCaption>,
940}
941
942/// A voice note block (`<audio>` in voice-note context). Caption on the inner
943/// media is ignored — use this block's own `caption` instead.
944#[derive(Debug, Clone, Serialize, Deserialize)]
945pub struct InputRichBlockVoiceNote {
946 /// The voice note.
947 pub voice_note: InputMediaVoiceNote,
948 /// Caption of the block.
949 #[serde(skip_serializing_if = "Option::is_none")]
950 pub caption: Option<RichBlockCaption>,
951}
952
953/// A `Thinking…` placeholder for use while a bot streams an AI response.
954///
955/// Only valid inside [`sendRichMessageDraft`](https://core.telegram.org/bots/api#sendrichmessagedraft)
956/// calls, therefore it can't be received in messages. See
957/// <https://t.me/addemoji/AIActions> for examples of custom emoji recommended
958/// for use in this block.
959#[derive(Debug, Clone, Serialize, Deserialize)]
960pub struct InputRichBlockThinking {
961 /// Text of the block. May include custom emoji — see
962 /// <https://t.me/addemoji/AIActions> for recommended examples.
963 pub text: RichText,
964}
965
966// ─── InputRichMessageMedia ──────────────────────────────────────────────────
967
968/// The media to be sent, embedded in an outgoing rich message.
969///
970/// Referenced from `InputRichMessage.html`/`.markdown` via
971/// `tg://photo?id=`, `tg://video?id=`, and `tg://audio?id=` links.
972#[derive(Debug, Clone, Serialize, Deserialize)]
973#[serde(tag = "type", rename_all = "snake_case")]
974pub enum InputRichMessageMediaKind {
975 /// An animation (GIF or silent H.264).
976 Animation(InputMediaAnimation),
977 /// An audio file treated as music.
978 Audio(InputMediaAudio),
979 /// A photo.
980 Photo(InputMediaPhoto),
981 /// A video.
982 Video(InputMediaVideo),
983 /// A voice message.
984 VoiceNote(InputMediaVoiceNote),
985}
986
987/// Describes a media element embedded in an outgoing rich message.
988#[derive(Debug, Clone, Serialize, Deserialize)]
989pub struct InputRichMessageMedia {
990 /// Unique identifier of the media, referenced from a `tg://photo?id=`,
991 /// `tg://video?id=`, or `tg://audio?id=` link. 1–64 characters; only
992 /// `A-Z`, `a-z`, `0-9`, `_` and `-` are allowed.
993 pub id: String,
994 /// The media to be sent. Everything except the media itself and its
995 /// properties is ignored.
996 pub media: InputRichMessageMediaKind,
997}
998
999// ─── InputRichMessage ─────────────────────────────────────────────────────────
1000
1001/// Describes a rich message to be sent.
1002///
1003/// Exactly one of `html`, `markdown`, or `blocks` must be set.
1004#[derive(Debug, Clone, Serialize, Deserialize)]
1005pub struct InputRichMessage {
1006 /// Rich message content encoded as HTML.
1007 ///
1008 /// Mutually exclusive with [`markdown`](Self::markdown) and [`blocks`](Self::blocks).
1009 /// Use [`media`](Self::media) to specify the media used in the message.
1010 #[serde(skip_serializing_if = "Option::is_none")]
1011 pub html: Option<String>,
1012 /// Rich message content encoded as Markdown.
1013 ///
1014 /// Mutually exclusive with [`html`](Self::html) and [`blocks`](Self::blocks).
1015 /// Use [`media`](Self::media) to specify the media used in the message.
1016 #[serde(skip_serializing_if = "Option::is_none")]
1017 pub markdown: Option<String>,
1018 /// Content of the rich message described as a list of blocks.
1019 ///
1020 /// Mutually exclusive with [`html`](Self::html) and [`markdown`](Self::markdown).
1021 #[serde(skip_serializing_if = "Option::is_none")]
1022 pub blocks: Option<Vec<InputRichBlock>>,
1023 /// Media referenced from [`html`](Self::html) or [`markdown`](Self::markdown)
1024 /// via `tg://photo?id=`, `tg://video?id=`, and `tg://audio?id=` links.
1025 #[serde(skip_serializing_if = "Option::is_none")]
1026 pub media: Option<Vec<InputRichMessageMedia>>,
1027 /// Pass `true` to render the message right-to-left.
1028 #[serde(skip_serializing_if = "Option::is_none")]
1029 pub is_rtl: Option<bool>,
1030 /// Pass `true` to disable automatic entity detection (URLs, mentions, etc.).
1031 #[serde(skip_serializing_if = "Option::is_none")]
1032 pub skip_entity_detection: Option<bool>,
1033}
1034
1035impl InputRichMessage {
1036 /// Creates an `InputRichMessage` from an HTML string.
1037 pub fn from_html(html: impl Into<String>) -> Self {
1038 Self {
1039 html: Some(html.into()),
1040 markdown: None,
1041 blocks: None,
1042 media: None,
1043 is_rtl: None,
1044 skip_entity_detection: None,
1045 }
1046 }
1047
1048 /// Creates an `InputRichMessage` from a Markdown string.
1049 pub fn from_markdown(markdown: impl Into<String>) -> Self {
1050 Self {
1051 html: None,
1052 markdown: Some(markdown.into()),
1053 blocks: None,
1054 media: None,
1055 is_rtl: None,
1056 skip_entity_detection: None,
1057 }
1058 }
1059
1060 /// Creates an `InputRichMessage` from a list of explicit blocks.
1061 pub fn from_blocks(blocks: Vec<InputRichBlock>) -> Self {
1062 Self {
1063 html: None,
1064 markdown: None,
1065 blocks: Some(blocks),
1066 media: None,
1067 is_rtl: None,
1068 skip_entity_detection: None,
1069 }
1070 }
1071
1072 /// Sets the media referenced by `tg://photo?id=`, `tg://video?id=`, and
1073 /// `tg://audio?id=` links in [`html`](Self::html) or [`markdown`](Self::markdown).
1074 #[must_use]
1075 pub fn media(mut self, media: Vec<InputRichMessageMedia>) -> Self {
1076 self.media = Some(media);
1077 self
1078 }
1079
1080 /// Sets the right-to-left rendering flag.
1081 #[must_use]
1082 pub fn rtl(mut self, v: bool) -> Self {
1083 self.is_rtl = Some(v);
1084 self
1085 }
1086
1087 /// Disables automatic entity detection.
1088 #[must_use]
1089 pub fn skip_entity_detection(mut self, v: bool) -> Self {
1090 self.skip_entity_detection = Some(v);
1091 self
1092 }
1093}
1094
1095/// Rich message content to be sent as the result of an inline / guest / Web App query.
1096#[derive(Debug, Clone, Serialize, Deserialize)]
1097pub struct InputRichMessageContent {
1098 /// The rich message to be sent.
1099 pub rich_message: InputRichMessage,
1100}