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