Skip to main content

rustigram_types/
rich_message.rs

1use serde::{Deserialize, Serialize};
2
3use crate::file::{Animation, Audio, PhotoSize, Video, Voice};
4use crate::user::User;
5
6// ─── RichText ─────────────────────────────────────────────────────────────────
7
8/// Rich formatted text — a recursive sum type that mirrors the Telegram
9/// `RichText` union from Bot API 10.1.
10///
11/// A `RichText` value is either:
12/// - a plain [`String`] (leaf node),
13/// - an [`Array`](RichText::Array) of nested `RichText` values, or
14/// - one of the typed inline-formatting variants listed below.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(untagged)]
17pub enum RichText {
18    /// Plain text without any formatting.
19    Plain(String),
20    /// A sequence of rich-text nodes rendered consecutively.
21    Array(Vec<RichText>),
22    /// Bold formatting.
23    Bold(Box<RichTextBold>),
24    /// Italic formatting.
25    Italic(Box<RichTextItalic>),
26    /// Underline formatting.
27    Underline(Box<RichTextUnderline>),
28    /// Strikethrough formatting.
29    Strikethrough(Box<RichTextStrikethrough>),
30    /// Spoiler — text hidden until tapped.
31    Spoiler(Box<RichTextSpoiler>),
32    /// A date/time entity.
33    DateTime(Box<RichTextDateTime>),
34    /// A mention by user object.
35    TextMention(Box<RichTextTextMention>),
36    /// Subscript text.
37    Subscript(Box<RichTextSubscript>),
38    /// Superscript text.
39    Superscript(Box<RichTextSuperscript>),
40    /// Highlighted/marked text.
41    Marked(Box<RichTextMarked>),
42    /// Inline monospace code.
43    Code(Box<RichTextCode>),
44    /// A custom emoji.
45    CustomEmoji(Box<RichTextCustomEmoji>),
46    /// An inline LaTeX mathematical expression.
47    MathematicalExpression(Box<RichTextMathematicalExpression>),
48    /// A hyperlink.
49    Url(Box<RichTextUrl>),
50    /// An e-mail address link.
51    EmailAddress(Box<RichTextEmailAddress>),
52    /// A telephone number link.
53    PhoneNumber(Box<RichTextPhoneNumber>),
54    /// A bank card number.
55    BankCardNumber(Box<RichTextBankCardNumber>),
56    /// A `@username` mention.
57    Mention(Box<RichTextMention>),
58    /// A `#hashtag`.
59    Hashtag(Box<RichTextHashtag>),
60    /// A `$cashtag`.
61    Cashtag(Box<RichTextCashtag>),
62    /// A `/bot_command`.
63    BotCommand(Box<RichTextBotCommand>),
64    /// An in-document anchor definition.
65    Anchor(Box<RichTextAnchor>),
66    /// A link targeting an in-document anchor.
67    AnchorLink(Box<RichTextAnchorLink>),
68    /// A footnote body.
69    Footnote(Box<RichTextFootnote>),
70    /// A reference to a footnote.
71    Reference(Box<RichTextReference>),
72}
73
74/// Bold text (`**text**` / `<b>text</b>`).
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct RichTextBold {
77    /// Always `"bold"`.
78    #[serde(rename = "type")]
79    pub kind: String,
80    /// The contained rich text.
81    pub text: RichText,
82}
83
84/// Italic text (`*text*` / `<i>text</i>`).
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct RichTextItalic {
87    /// Always `"italic"`.
88    #[serde(rename = "type")]
89    pub kind: String,
90    /// The contained rich text.
91    pub text: RichText,
92}
93
94/// Underlined text (`<u>text</u>`).
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct RichTextUnderline {
97    /// Always `"underline"`.
98    #[serde(rename = "type")]
99    pub kind: String,
100    /// The contained rich text.
101    pub text: RichText,
102}
103
104/// Strikethrough text (`~~text~~` / `<s>text</s>`).
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct RichTextStrikethrough {
107    /// Always `"strikethrough"`.
108    #[serde(rename = "type")]
109    pub kind: String,
110    /// The contained rich text.
111    pub text: RichText,
112}
113
114/// Spoiler text (`||text||` / `<tg-spoiler>text</tg-spoiler>`).
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct RichTextSpoiler {
117    /// Always `"spoiler"`.
118    #[serde(rename = "type")]
119    pub kind: String,
120    /// The contained rich text.
121    pub text: RichText,
122}
123
124/// A date/time entity rendered according to the client's locale.
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct RichTextDateTime {
127    /// Always `"date_time"`.
128    #[serde(rename = "type")]
129    pub kind: String,
130    /// The display text.
131    pub text: RichText,
132    /// The Unix timestamp associated with the entity.
133    pub unix_time: i64,
134    /// Format string controlling how the date/time is rendered.
135    pub date_time_format: String,
136}
137
138/// A mention of a Telegram user by their `User` object.
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct RichTextTextMention {
141    /// Always `"text_mention"`.
142    #[serde(rename = "type")]
143    pub kind: String,
144    /// The display text.
145    pub text: RichText,
146    /// The mentioned user.
147    pub user: User,
148}
149
150/// Subscript text (`<sub>text</sub>`).
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct RichTextSubscript {
153    /// Always `"subscript"`.
154    #[serde(rename = "type")]
155    pub kind: String,
156    /// The contained rich text.
157    pub text: RichText,
158}
159
160/// Superscript text (`<sup>text</sup>`).
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct RichTextSuperscript {
163    /// Always `"superscript"`.
164    #[serde(rename = "type")]
165    pub kind: String,
166    /// The contained rich text.
167    pub text: RichText,
168}
169
170/// Highlighted/marked text (`==text==` / `<mark>text</mark>`).
171#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct RichTextMarked {
173    /// Always `"marked"`.
174    #[serde(rename = "type")]
175    pub kind: String,
176    /// The contained rich text.
177    pub text: RichText,
178}
179
180/// Inline monospace/code text (`` `text` `` / `<code>text</code>`).
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct RichTextCode {
183    /// Always `"code"`.
184    #[serde(rename = "type")]
185    pub kind: String,
186    /// The contained rich text.
187    pub text: RichText,
188}
189
190/// A custom emoji (`![alt](tg://emoji?id=...)`).
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct RichTextCustomEmoji {
193    /// Always `"custom_emoji"`.
194    #[serde(rename = "type")]
195    pub kind: String,
196    /// Unique identifier of the custom emoji.
197    pub custom_emoji_id: String,
198    /// Fallback emoji string for clients that do not support custom emoji.
199    pub alternative_text: String,
200}
201
202/// An inline LaTeX mathematical expression (`$expr$` / `<tg-math>expr</tg-math>`).
203#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct RichTextMathematicalExpression {
205    /// Always `"mathematical_expression"`.
206    #[serde(rename = "type")]
207    pub kind: String,
208    /// The LaTeX source of the expression.
209    pub expression: String,
210}
211
212/// A hyperlink (`[text](url)` / `<a href="url">text</a>`).
213#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct RichTextUrl {
215    /// Always `"url"`.
216    #[serde(rename = "type")]
217    pub kind: String,
218    /// The display text.
219    pub text: RichText,
220    /// The target URL.
221    pub url: String,
222}
223
224/// An e-mail address link (`[text](mailto:addr)` / `<a href="mailto:addr">text</a>`).
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct RichTextEmailAddress {
227    /// Always `"email_address"`.
228    #[serde(rename = "type")]
229    pub kind: String,
230    /// The display text.
231    pub text: RichText,
232    /// The raw e-mail address.
233    pub email_address: String,
234}
235
236/// A telephone number link (`[text](tel:+nnn)` / `<a href="tel:+nnn">text</a>`).
237#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct RichTextPhoneNumber {
239    /// Always `"phone_number"`.
240    #[serde(rename = "type")]
241    pub kind: String,
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    /// Always `"bank_card_number"`.
252    #[serde(rename = "type")]
253    pub kind: String,
254    /// The display text.
255    pub text: RichText,
256    /// The raw bank card number.
257    pub bank_card_number: String,
258}
259
260/// A `@username` mention.
261#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct RichTextMention {
263    /// Always `"mention"`.
264    #[serde(rename = "type")]
265    pub kind: String,
266    /// The display text.
267    pub text: RichText,
268    /// The target username (without the leading `@`).
269    pub username: String,
270}
271
272/// A `#hashtag`.
273#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct RichTextHashtag {
275    /// Always `"hashtag"`.
276    #[serde(rename = "type")]
277    pub kind: String,
278    /// The display text.
279    pub text: RichText,
280    /// The hashtag value (without the leading `#`).
281    pub hashtag: String,
282}
283
284/// A `$cashtag`.
285#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct RichTextCashtag {
287    /// Always `"cashtag"`.
288    #[serde(rename = "type")]
289    pub kind: String,
290    /// The display text.
291    pub text: RichText,
292    /// The cashtag value (without the leading `$`).
293    pub cashtag: String,
294}
295
296/// A bot command (e.g. `/start`).
297#[derive(Debug, Clone, Serialize, Deserialize)]
298pub struct RichTextBotCommand {
299    /// Always `"bot_command"`.
300    #[serde(rename = "type")]
301    pub kind: String,
302    /// The display text.
303    pub text: RichText,
304    /// The command string including the leading `/`.
305    pub bot_command: String,
306}
307
308/// An in-document anchor definition (`<a name="id"></a>`).
309#[derive(Debug, Clone, Serialize, Deserialize)]
310pub struct RichTextAnchor {
311    /// Always `"anchor"`.
312    #[serde(rename = "type")]
313    pub kind: String,
314    /// The anchor name.
315    pub name: String,
316}
317
318/// A link to an in-document anchor (`<a href="#id">text</a>`).
319///
320/// If `anchor_name` is empty the link scrolls back to the top of the message.
321#[derive(Debug, Clone, Serialize, Deserialize)]
322pub struct RichTextAnchorLink {
323    /// Always `"anchor_link"`.
324    #[serde(rename = "type")]
325    pub kind: String,
326    /// The display text.
327    pub text: RichText,
328    /// The target anchor name; empty string scrolls to the top.
329    pub anchor_name: String,
330}
331
332/// The body of a footnote.
333#[derive(Debug, Clone, Serialize, Deserialize)]
334pub struct RichTextFootnote {
335    /// Always `"footnote"`.
336    #[serde(rename = "type")]
337    pub kind: String,
338    /// The footnote content.
339    pub text: RichText,
340    /// The footnote identifier.
341    pub name: String,
342}
343
344/// A reference to a previously defined footnote.
345#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct RichTextReference {
347    /// Always `"reference"`.
348    #[serde(rename = "type")]
349    pub kind: String,
350    /// The display text (typically the footnote superscript label).
351    pub text: RichText,
352    /// The footnote identifier being referenced.
353    pub footnote_name: String,
354}
355
356// ─── RichBlock helpers ────────────────────────────────────────────────────────
357
358/// Caption (and optional credit) for a media block.
359#[derive(Debug, Clone, Serialize, Deserialize)]
360pub struct RichBlockCaption {
361    /// The caption text.
362    pub text: RichText,
363    /// Optional credit line (HTML `<cite>`).
364    #[serde(skip_serializing_if = "Option::is_none")]
365    pub credit: Option<RichText>,
366}
367
368/// A single cell inside a [`RichBlockTable`].
369#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct RichBlockTableCell {
371    /// The cell content; omit to leave the cell empty/invisible.
372    #[serde(skip_serializing_if = "Option::is_none")]
373    pub text: Option<RichText>,
374    /// `true` if this is a header cell (`<th>`).
375    #[serde(skip_serializing_if = "Option::is_none")]
376    pub is_header: Option<bool>,
377    /// Number of columns the cell spans.
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub colspan: Option<u32>,
380    /// Number of rows the cell spans.
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub rowspan: Option<u32>,
383    /// Horizontal text alignment: `"left"`, `"center"`, or `"right"`.
384    pub align: String,
385    /// Vertical text alignment: `"top"`, `"middle"`, or `"bottom"`.
386    pub valign: String,
387}
388
389/// A single item inside a [`RichBlockList`].
390#[derive(Debug, Clone, Serialize, Deserialize)]
391pub struct RichBlockListItem {
392    /// The bullet or number label rendered by the client.
393    pub label: String,
394    /// The nested content of this list item.
395    pub blocks: Vec<RichBlock>,
396    /// `true` if the item has a checkbox.
397    #[serde(skip_serializing_if = "Option::is_none")]
398    pub has_checkbox: Option<bool>,
399    /// `true` if the checkbox is checked.
400    #[serde(skip_serializing_if = "Option::is_none")]
401    pub is_checked: Option<bool>,
402    /// For ordered lists — the explicit numeric value of this item.
403    #[serde(skip_serializing_if = "Option::is_none")]
404    pub value: Option<i64>,
405    /// For ordered lists — the label type: `"a"`, `"A"`, `"i"`, `"I"`, or `"1"`.
406    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
407    pub kind: Option<String>,
408}
409
410// ─── RichBlock ────────────────────────────────────────────────────────────────
411
412/// A block-level element in a rich message.
413///
414/// This is the top-level building block for `RichMessage::blocks`.
415#[derive(Debug, Clone, Serialize, Deserialize)]
416#[serde(tag = "type", rename_all = "snake_case")]
417pub enum RichBlock {
418    /// A text paragraph (`<p>`).
419    Paragraph(RichBlockParagraph),
420    /// A section heading (`<h1>`…`<h6>`).
421    #[serde(rename = "heading")]
422    SectionHeading(RichBlockSectionHeading),
423    /// A preformatted / code block (`<pre><code>`).
424    Pre(RichBlockPreformatted),
425    /// A footer (`<footer>`).
426    Footer(RichBlockFooter),
427    /// A horizontal rule / divider (`<hr/>`).
428    Divider(RichBlockDivider),
429    /// A block-level LaTeX expression (`<tg-math-block>`).
430    MathematicalExpression(RichBlockMathematicalExpression),
431    /// An in-document anchor (`<a name="…"></a>`).
432    Anchor(RichBlockAnchor),
433    /// An ordered or unordered list (`<ul>` / `<ol>`).
434    List(RichBlockList),
435    /// A block quotation (`<blockquote>`).
436    Blockquote(RichBlockBlockQuotation),
437    /// A pull quotation (`<aside>`).
438    Pullquote(RichBlockPullQuotation),
439    /// A multi-media collage (`<tg-collage>`).
440    Collage(RichBlockCollage),
441    /// A media slideshow (`<tg-slideshow>`).
442    Slideshow(RichBlockSlideshow),
443    /// A table (`<table>`).
444    Table(RichBlockTable),
445    /// A collapsible details block (`<details>`).
446    Details(RichBlockDetails),
447    /// An embedded map (`<tg-map>`).
448    Map(RichBlockMap),
449    /// A looping animation / GIF (`<video loop>`).
450    Animation(RichBlockAnimation),
451    /// An audio file (`<audio>`).
452    Audio(RichBlockAudio),
453    /// A photo (`<photo>`).
454    Photo(RichBlockPhoto),
455    /// A video (`<video>`).
456    Video(RichBlockVideo),
457    /// A voice note (`<audio>` in voice-note context).
458    VoiceNote(RichBlockVoiceNote),
459    /// A `Thinking…` placeholder used during AI streaming drafts.
460    ///
461    /// Only valid in [`sendRichMessageDraft`](https://core.telegram.org/bots/api#sendrichmessagedraft) calls.
462    Thinking(RichBlockThinking),
463}
464
465/// A text paragraph (`<p>`).
466#[derive(Debug, Clone, Serialize, Deserialize)]
467pub struct RichBlockParagraph {
468    /// The paragraph text.
469    pub text: RichText,
470}
471
472/// A section heading, corresponding to `<h1>`…`<h6>`.
473#[derive(Debug, Clone, Serialize, Deserialize)]
474pub struct RichBlockSectionHeading {
475    /// The heading text.
476    pub text: RichText,
477    /// Font size level 1–6 (1 = largest, 6 = smallest).
478    pub size: u8,
479}
480
481/// A preformatted text block (`<pre><code>`).
482#[derive(Debug, Clone, Serialize, Deserialize)]
483pub struct RichBlockPreformatted {
484    /// The preformatted text.
485    pub text: RichText,
486    /// Optional syntax-highlight language identifier.
487    #[serde(skip_serializing_if = "Option::is_none")]
488    pub language: Option<String>,
489}
490
491/// A footer block (`<footer>`).
492#[derive(Debug, Clone, Serialize, Deserialize)]
493pub struct RichBlockFooter {
494    /// The footer text.
495    pub text: RichText,
496}
497
498/// A horizontal rule / divider (`<hr/>`).
499///
500/// Has no content fields.
501#[derive(Debug, Clone, Serialize, Deserialize)]
502pub struct RichBlockDivider {}
503
504/// A block-level mathematical expression in LaTeX format (`<tg-math-block>`).
505#[derive(Debug, Clone, Serialize, Deserialize)]
506pub struct RichBlockMathematicalExpression {
507    /// The raw LaTeX source.
508    pub expression: String,
509}
510
511/// An in-document anchor (`<a name="…"></a>`).
512#[derive(Debug, Clone, Serialize, Deserialize)]
513pub struct RichBlockAnchor {
514    /// The anchor name.
515    pub name: String,
516}
517
518/// An ordered or unordered list (`<ul>` / `<ol>`).
519#[derive(Debug, Clone, Serialize, Deserialize)]
520pub struct RichBlockList {
521    /// The list items.
522    pub items: Vec<RichBlockListItem>,
523}
524
525/// A block quotation (`<blockquote>`).
526#[derive(Debug, Clone, Serialize, Deserialize)]
527pub struct RichBlockBlockQuotation {
528    /// Nested block content.
529    pub blocks: Vec<RichBlock>,
530    /// Optional attribution credit.
531    #[serde(skip_serializing_if = "Option::is_none")]
532    pub credit: Option<RichText>,
533}
534
535/// A pull quotation with centred text (`<aside>`).
536#[derive(Debug, Clone, Serialize, Deserialize)]
537pub struct RichBlockPullQuotation {
538    /// The quotation text.
539    pub text: RichText,
540    /// Optional attribution credit.
541    #[serde(skip_serializing_if = "Option::is_none")]
542    pub credit: Option<RichText>,
543}
544
545/// A multi-media collage (`<tg-collage>`).
546#[derive(Debug, Clone, Serialize, Deserialize)]
547pub struct RichBlockCollage {
548    /// The media elements of the collage.
549    pub blocks: Vec<RichBlock>,
550    /// Optional caption.
551    #[serde(skip_serializing_if = "Option::is_none")]
552    pub caption: Option<RichBlockCaption>,
553}
554
555/// A media slideshow (`<tg-slideshow>`).
556#[derive(Debug, Clone, Serialize, Deserialize)]
557pub struct RichBlockSlideshow {
558    /// The media elements of the slideshow.
559    pub blocks: Vec<RichBlock>,
560    /// Optional caption.
561    #[serde(skip_serializing_if = "Option::is_none")]
562    pub caption: Option<RichBlockCaption>,
563}
564
565/// A table (`<table>`).
566#[derive(Debug, Clone, Serialize, Deserialize)]
567pub struct RichBlockTable {
568    /// A 2-D array of cells (rows × columns).
569    pub cells: Vec<Vec<RichBlockTableCell>>,
570    /// `true` if the table has visible borders.
571    #[serde(skip_serializing_if = "Option::is_none")]
572    pub is_bordered: Option<bool>,
573    /// `true` if alternate rows are shaded.
574    #[serde(skip_serializing_if = "Option::is_none")]
575    pub is_striped: Option<bool>,
576    /// Optional table caption.
577    #[serde(skip_serializing_if = "Option::is_none")]
578    pub caption: Option<RichText>,
579}
580
581/// A collapsible details / disclosure block (`<details>`).
582#[derive(Debug, Clone, Serialize, Deserialize)]
583pub struct RichBlockDetails {
584    /// The always-visible summary.
585    pub summary: RichText,
586    /// Nested block content shown when expanded.
587    pub blocks: Vec<RichBlock>,
588    /// `true` if the block is expanded by default.
589    #[serde(skip_serializing_if = "Option::is_none")]
590    pub is_open: Option<bool>,
591}
592
593/// An embedded map (`<tg-map>`).
594#[derive(Debug, Clone, Serialize, Deserialize)]
595pub struct RichBlockMap {
596    /// Latitude of the map centre.
597    pub latitude: f64,
598    /// Longitude of the map centre.
599    pub longitude: f64,
600    /// Zoom level (13–20).
601    pub zoom: u8,
602    /// Expected rendered width in pixels.
603    pub width: u32,
604    /// Expected rendered height in pixels.
605    pub height: u32,
606    /// Optional caption.
607    #[serde(skip_serializing_if = "Option::is_none")]
608    pub caption: Option<RichBlockCaption>,
609}
610
611/// A looping animation / GIF block (`<video loop>`).
612#[derive(Debug, Clone, Serialize, Deserialize)]
613pub struct RichBlockAnimation {
614    /// The animation file.
615    pub animation: Animation,
616    /// `true` if the animation should play automatically.
617    #[serde(skip_serializing_if = "Option::is_none")]
618    pub need_autoplay: Option<bool>,
619    /// `true` if a spoiler overlay is shown before the first tap.
620    #[serde(skip_serializing_if = "Option::is_none")]
621    pub has_spoiler: Option<bool>,
622    /// Optional caption.
623    #[serde(skip_serializing_if = "Option::is_none")]
624    pub caption: Option<RichBlockCaption>,
625}
626
627/// An audio file block (`<audio>`).
628#[derive(Debug, Clone, Serialize, Deserialize)]
629pub struct RichBlockAudio {
630    /// The audio file.
631    pub audio: Audio,
632    /// Optional caption.
633    #[serde(skip_serializing_if = "Option::is_none")]
634    pub caption: Option<RichBlockCaption>,
635}
636
637/// A photo block (`<photo>`).
638#[derive(Debug, Clone, Serialize, Deserialize)]
639pub struct RichBlockPhoto {
640    /// All available sizes of the photo.
641    pub photo: Vec<PhotoSize>,
642    /// `true` if a spoiler overlay is shown before the first tap.
643    #[serde(skip_serializing_if = "Option::is_none")]
644    pub has_spoiler: Option<bool>,
645    /// Optional caption.
646    #[serde(skip_serializing_if = "Option::is_none")]
647    pub caption: Option<RichBlockCaption>,
648}
649
650/// A video block (`<video>`).
651#[derive(Debug, Clone, Serialize, Deserialize)]
652pub struct RichBlockVideo {
653    /// The video file.
654    pub video: Video,
655    /// `true` if the video should play automatically.
656    #[serde(skip_serializing_if = "Option::is_none")]
657    pub need_autoplay: Option<bool>,
658    /// `true` if the video loops back to the start when it ends.
659    #[serde(skip_serializing_if = "Option::is_none")]
660    pub is_looped: Option<bool>,
661    /// `true` if a spoiler overlay is shown before the first tap.
662    #[serde(skip_serializing_if = "Option::is_none")]
663    pub has_spoiler: Option<bool>,
664    /// Optional caption.
665    #[serde(skip_serializing_if = "Option::is_none")]
666    pub caption: Option<RichBlockCaption>,
667}
668
669/// A voice note block (`<audio>` in voice-note context).
670#[derive(Debug, Clone, Serialize, Deserialize)]
671pub struct RichBlockVoiceNote {
672    /// The voice note file.
673    pub voice_note: Voice,
674    /// Optional caption.
675    #[serde(skip_serializing_if = "Option::is_none")]
676    pub caption: Option<RichBlockCaption>,
677}
678
679/// A `Thinking…` placeholder for use while a bot streams an AI response.
680///
681/// Only valid inside [`sendRichMessageDraft`](https://core.telegram.org/bots/api#sendrichmessagedraft) calls.
682/// See <https://t.me/addemoji/AIActions> for recommended custom emoji.
683#[derive(Debug, Clone, Serialize, Deserialize)]
684pub struct RichBlockThinking {
685    /// The placeholder display text (may include custom emoji).
686    pub text: RichText,
687}
688
689// ─── RichMessage ──────────────────────────────────────────────────────────────
690
691/// A complete rich formatted message as received from the Bot API.
692///
693/// Carried in `Message::rich_message`.
694#[derive(Debug, Clone, Serialize, Deserialize)]
695pub struct RichMessage {
696    /// The ordered list of top-level blocks forming the message body.
697    pub blocks: Vec<RichBlock>,
698    /// `true` if the message must be rendered right-to-left.
699    #[serde(skip_serializing_if = "Option::is_none")]
700    pub is_rtl: Option<bool>,
701}
702
703// ─── InputRichMessage ─────────────────────────────────────────────────────────
704
705/// Describes a rich message to be sent.
706///
707/// Exactly one of `html` or `markdown` must be set.
708#[derive(Debug, Clone, Serialize, Deserialize)]
709pub struct InputRichMessage {
710    /// Rich message content encoded as HTML.
711    ///
712    /// Mutually exclusive with [`markdown`](Self::markdown).
713    #[serde(skip_serializing_if = "Option::is_none")]
714    pub html: Option<String>,
715    /// Rich message content encoded as Markdown.
716    ///
717    /// Mutually exclusive with [`html`](Self::html).
718    #[serde(skip_serializing_if = "Option::is_none")]
719    pub markdown: Option<String>,
720    /// Pass `true` to render the message right-to-left.
721    #[serde(skip_serializing_if = "Option::is_none")]
722    pub is_rtl: Option<bool>,
723    /// Pass `true` to disable automatic entity detection (URLs, mentions, etc.).
724    #[serde(skip_serializing_if = "Option::is_none")]
725    pub skip_entity_detection: Option<bool>,
726}
727
728impl InputRichMessage {
729    /// Creates an `InputRichMessage` from an HTML string.
730    pub fn from_html(html: impl Into<String>) -> Self {
731        Self {
732            html: Some(html.into()),
733            markdown: None,
734            is_rtl: None,
735            skip_entity_detection: None,
736        }
737    }
738
739    /// Creates an `InputRichMessage` from a Markdown string.
740    pub fn from_markdown(markdown: impl Into<String>) -> Self {
741        Self {
742            html: None,
743            markdown: Some(markdown.into()),
744            is_rtl: None,
745            skip_entity_detection: None,
746        }
747    }
748
749    /// Sets the right-to-left rendering flag.
750    #[must_use]
751    pub fn rtl(mut self, v: bool) -> Self {
752        self.is_rtl = Some(v);
753        self
754    }
755
756    /// Disables automatic entity detection.
757    #[must_use]
758    pub fn skip_entity_detection(mut self, v: bool) -> Self {
759        self.skip_entity_detection = Some(v);
760        self
761    }
762}
763
764/// Rich message content to be sent as the result of an inline / guest / Web App query.
765#[derive(Debug, Clone, Serialize, Deserialize)]
766pub struct InputRichMessageContent {
767    /// The rich message to be sent.
768    pub rich_message: InputRichMessage,
769}