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