Skip to main content

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 (`![alt](tg://emoji?id=...)`).
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}