Skip to main content

rustigram_types/
inline.rs

1use crate::chat::Location;
2use crate::user::User;
3use serde::{Deserialize, Serialize};
4
5/// An incoming inline query sent when a user types `@YourBot something` in any chat.
6///
7/// Respond with [`answerInlineQuery`](https://core.telegram.org/bots/api#answerinlinequery)
8/// within 10 seconds.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct InlineQuery {
11    /// Unique identifier for this query.
12    pub id: String,
13    /// The user who sent the query.
14    pub from: User,
15    /// Text of the query (up to 256 characters).
16    pub query: String,
17    /// Offset of the result to be returned.
18    pub offset: String,
19    /// Type of the chat from which the query was sent.
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub chat_type: Option<String>,
22    /// Sender's location, if the bot requests it.
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub location: Option<Location>,
25}
26
27/// The result a user chose from an inline query.
28///
29/// Delivered only when the bot has been granted access to inline feedback
30/// via [@BotFather](https://t.me/BotFather) under "Inline Feedback".
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct ChosenInlineResult {
33    /// Identifier of the chosen result.
34    pub result_id: String,
35    /// The user who chose the result.
36    pub from: User,
37    /// Sender's location, if the bot requests it.
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub location: Option<Location>,
40    /// Identifier of the sent inline message, if applicable.
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub inline_message_id: Option<String>,
43    /// The query used to obtain the result.
44    pub query: String,
45}
46
47#[derive(Debug, Clone, Serialize)]
48#[serde(tag = "type", rename_all = "snake_case")]
49/// One result to show in an inline query answer.
50///
51/// Up to 50 results can be returned per [`answerInlineQuery`](https://core.telegram.org/bots/api#answerinlinequery) call.
52/// Each variant corresponds to a different content type (article, photo,
53/// video, etc.). Cached variants re-use a previously uploaded Telegram
54/// `file_id` rather than a URL.
55///
56/// # Why two variants share a discriminant
57///
58/// Telegram gives a cached result the *same* `type` as its non-cached
59/// counterpart — a cached photo is `"photo"`, not `"cached_photo"` — and tells
60/// them apart by whether the payload carries `photo_file_id` or `photo_url`.
61/// So the tag alone cannot select a variant, which is why [`Deserialize`] is
62/// implemented by hand below rather than derived.
63pub enum InlineQueryResult {
64    /// A link to an article or web page.
65    Article(InlineQueryResultArticle),
66    /// A link to a photo.
67    Photo(InlineQueryResultPhoto),
68    /// A link to an animated GIF.
69    Gif(InlineQueryResultGif),
70    /// A link to a video animation (MPEG4 without sound).
71    Mpeg4Gif(InlineQueryResultMpeg4Gif),
72    /// A link to a video.
73    Video(InlineQueryResultVideo),
74    /// A link to an audio file.
75    Audio(InlineQueryResultAudio),
76    /// A link to a voice recording.
77    Voice(InlineQueryResultVoice),
78    /// A link to a general file.
79    Document(InlineQueryResultDocument),
80    /// A geographic location.
81    Location(InlineQueryResultLocation),
82    /// A venue.
83    Venue(InlineQueryResultVenue),
84    /// A contact.
85    Contact(InlineQueryResultContact),
86    /// A game.
87    Game(InlineQueryResultGame),
88    /// A photo from a Telegram `file_id`.
89    #[serde(rename = "photo")]
90    CachedPhoto(InlineQueryResultCachedPhoto),
91    /// A GIF from a Telegram `file_id`.
92    #[serde(rename = "gif")]
93    CachedGif(InlineQueryResultCachedGif),
94    /// An MPEG4 GIF from a Telegram `file_id`.
95    #[serde(rename = "mpeg4_gif")]
96    CachedMpeg4Gif(InlineQueryResultCachedMpeg4Gif),
97    /// A sticker from a Telegram `file_id`.
98    #[serde(rename = "sticker")]
99    CachedSticker(InlineQueryResultCachedSticker),
100    /// A document from a Telegram `file_id`.
101    #[serde(rename = "document")]
102    CachedDocument(InlineQueryResultCachedDocument),
103    /// A video from a Telegram `file_id`.
104    #[serde(rename = "video")]
105    CachedVideo(InlineQueryResultCachedVideo),
106    /// A voice message from a Telegram `file_id`.
107    #[serde(rename = "voice")]
108    CachedVoice(InlineQueryResultCachedVoice),
109    /// An audio file from a Telegram `file_id`.
110    #[serde(rename = "audio")]
111    CachedAudio(InlineQueryResultCachedAudio),
112}
113
114// ─── URL-based results ────────────────────────────────────────────────────────
115
116impl<'de> Deserialize<'de> for InlineQueryResult {
117    /// Selects a variant from the `type` tag, disambiguating cached results by
118    /// the presence of their `*_file_id` field.
119    ///
120    /// A derived `#[serde(tag = "type")]` implementation cannot do this: seven
121    /// discriminants map to two variants each, and serde would silently take
122    /// whichever was declared first. That is the same failure mode as an
123    /// untagged enum matching the wrong variant — it decodes successfully and
124    /// hands back the wrong thing.
125    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
126    where
127        D: serde::Deserializer<'de>,
128    {
129        use serde::de::Error as _;
130
131        let value = serde_json::Value::deserialize(deserializer)?;
132        let tag = value
133            .get("type")
134            .and_then(serde_json::Value::as_str)
135            .ok_or_else(|| D::Error::custom("InlineQueryResult is missing its `type` field"))?
136            .to_owned();
137        let cached = |field: &str| value.get(field).is_some();
138
139        macro_rules! into {
140            ($variant:ident, $ty:ty) => {
141                <$ty>::deserialize(value)
142                    .map(InlineQueryResult::$variant)
143                    .map_err(D::Error::custom)
144            };
145        }
146
147        match tag.as_str() {
148            "article" => into!(Article, InlineQueryResultArticle),
149            "location" => into!(Location, InlineQueryResultLocation),
150            "venue" => into!(Venue, InlineQueryResultVenue),
151            "contact" => into!(Contact, InlineQueryResultContact),
152            "game" => into!(Game, InlineQueryResultGame),
153            // Cached-only: Telegram has no URL-based sticker result.
154            "sticker" => into!(CachedSticker, InlineQueryResultCachedSticker),
155
156            "photo" if cached("photo_file_id") => into!(CachedPhoto, InlineQueryResultCachedPhoto),
157            "photo" => into!(Photo, InlineQueryResultPhoto),
158            "gif" if cached("gif_file_id") => into!(CachedGif, InlineQueryResultCachedGif),
159            "gif" => into!(Gif, InlineQueryResultGif),
160            "mpeg4_gif" if cached("mpeg4_file_id") => {
161                into!(CachedMpeg4Gif, InlineQueryResultCachedMpeg4Gif)
162            }
163            "mpeg4_gif" => into!(Mpeg4Gif, InlineQueryResultMpeg4Gif),
164            "video" if cached("video_file_id") => into!(CachedVideo, InlineQueryResultCachedVideo),
165            "video" => into!(Video, InlineQueryResultVideo),
166            "audio" if cached("audio_file_id") => into!(CachedAudio, InlineQueryResultCachedAudio),
167            "audio" => into!(Audio, InlineQueryResultAudio),
168            "voice" if cached("voice_file_id") => into!(CachedVoice, InlineQueryResultCachedVoice),
169            "voice" => into!(Voice, InlineQueryResultVoice),
170            "document" if cached("document_file_id") => {
171                into!(CachedDocument, InlineQueryResultCachedDocument)
172            }
173            "document" => into!(Document, InlineQueryResultDocument),
174
175            // Phrased as serde phrases it, so tooling that matches on
176            // "unknown variant" keeps working across the manual impl.
177            other => Err(D::Error::custom(format!(
178                "unknown variant `{other}`, expected a valid InlineQueryResult type"
179            ))),
180        }
181    }
182}
183
184/// A link to an article or web page.
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct InlineQueryResultArticle {
187    /// Unique identifier for this result (1–64 bytes).
188    pub id: String,
189    /// Title of the result.
190    pub title: String,
191    /// Content of the message to be sent.
192    pub input_message_content: InputMessageContent,
193    /// Inline keyboard attached to the message.
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub reply_markup: Option<crate::keyboard::InlineKeyboardMarkup>,
196    /// URL of the result.
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub url: Option<String>,
199    /// Short description of the result.
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub description: Option<String>,
202    /// URL of the thumbnail for the result.
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub thumbnail_url: Option<String>,
205    /// Thumbnail width in pixels.
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub thumbnail_width: Option<u32>,
208    /// Thumbnail height in pixels.
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub thumbnail_height: Option<u32>,
211}
212
213/// A link to a photo (JPEG, max 5 MB).
214#[derive(Debug, Clone, Serialize, Deserialize)]
215/// A photo result in an inline query.
216pub struct InlineQueryResultPhoto {
217    /// Unique identifier for this result (1–64 bytes).
218    pub id: String,
219    /// A valid URL of the photo.
220    pub photo_url: String,
221    /// URL of the thumbnail for the photo.
222    pub thumbnail_url: String,
223    /// Photo width in pixels.
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub photo_width: Option<u32>,
226    /// Photo height in pixels.
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub photo_height: Option<u32>,
229    /// Title for the result.
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub title: Option<String>,
232    /// Short description of the result.
233    #[serde(skip_serializing_if = "Option::is_none")]
234    pub description: Option<String>,
235    /// Caption of the photo (0–1024 characters after entities parsing).
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub caption: Option<String>,
238    /// Parse mode for the caption. See [formatting options](https://core.telegram.org/bots/api#formatting-options) for more details.
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub parse_mode: Option<crate::message::ParseMode>,
241    /// Special entities in the caption; alternative to `parse_mode`.
242    #[serde(skip_serializing_if = "Option::is_none")]
243    pub caption_entities: Option<Vec<crate::message::MessageEntity>>,
244    /// `true` if the caption must be shown above the photo.
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub show_caption_above_media: Option<bool>,
247    /// Inline keyboard attached to the message.
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub reply_markup: Option<crate::keyboard::InlineKeyboardMarkup>,
250    /// Content of the message to be sent instead of the photo.
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub input_message_content: Option<InputMessageContent>,
253}
254
255/// A link to an animated GIF file (max 1 MB).
256#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct InlineQueryResultGif {
258    /// Unique identifier for this result (1–64 bytes).
259    pub id: String,
260    /// A valid URL for the GIF file.
261    pub gif_url: String,
262    /// URL of the static or animated thumbnail for the result.
263    pub thumbnail_url: String,
264    /// Width of the GIF in pixels.
265    #[serde(skip_serializing_if = "Option::is_none")]
266    pub gif_width: Option<u32>,
267    /// Height of the GIF in pixels.
268    #[serde(skip_serializing_if = "Option::is_none")]
269    pub gif_height: Option<u32>,
270    /// Duration of the GIF in seconds.
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub gif_duration: Option<u32>,
273    /// MIME type of the thumbnail (`image/jpeg`, `image/gif`, or `video/mp4`).
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub thumbnail_mime_type: Option<String>,
276    /// Title for the result.
277    #[serde(skip_serializing_if = "Option::is_none")]
278    pub title: Option<String>,
279    /// Caption of the GIF (0–1024 characters after entities parsing).
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub caption: Option<String>,
282    /// Parse mode for the caption. See [formatting options](https://core.telegram.org/bots/api#formatting-options) for more details.
283    #[serde(skip_serializing_if = "Option::is_none")]
284    pub parse_mode: Option<crate::message::ParseMode>,
285    /// Special entities in the caption; alternative to `parse_mode`.
286    #[serde(skip_serializing_if = "Option::is_none")]
287    pub caption_entities: Option<Vec<crate::message::MessageEntity>>,
288    /// `true` if the caption must be shown above the GIF.
289    #[serde(skip_serializing_if = "Option::is_none")]
290    pub show_caption_above_media: Option<bool>,
291    /// Inline keyboard attached to the message.
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub reply_markup: Option<crate::keyboard::InlineKeyboardMarkup>,
294    /// Content of the message to be sent instead of the GIF.
295    #[serde(skip_serializing_if = "Option::is_none")]
296    pub input_message_content: Option<InputMessageContent>,
297}
298
299/// A link to a video animation (MPEG4 without sound, max 1 MB).
300#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct InlineQueryResultMpeg4Gif {
302    /// Unique identifier for this result (1–64 bytes).
303    pub id: String,
304    /// A valid URL for the MPEG4 file.
305    pub mpeg4_url: String,
306    /// URL of the static or animated thumbnail.
307    pub thumbnail_url: String,
308    /// Video width in pixels.
309    #[serde(skip_serializing_if = "Option::is_none")]
310    pub mpeg4_width: Option<u32>,
311    /// Video height in pixels.
312    #[serde(skip_serializing_if = "Option::is_none")]
313    pub mpeg4_height: Option<u32>,
314    /// Video duration in seconds.
315    #[serde(skip_serializing_if = "Option::is_none")]
316    pub mpeg4_duration: Option<u32>,
317    /// MIME type of the thumbnail (`image/jpeg`, `image/gif`, or `video/mp4`).
318    #[serde(skip_serializing_if = "Option::is_none")]
319    pub thumbnail_mime_type: Option<String>,
320    /// Title for the result.
321    #[serde(skip_serializing_if = "Option::is_none")]
322    pub title: Option<String>,
323    /// Caption of the MPEG4 (0–1024 characters after entities parsing).
324    #[serde(skip_serializing_if = "Option::is_none")]
325    pub caption: Option<String>,
326    /// Parse mode for the caption.
327    #[serde(skip_serializing_if = "Option::is_none")]
328    pub parse_mode: Option<crate::message::ParseMode>,
329    /// Special entities in the caption; alternative to `parse_mode`.
330    #[serde(skip_serializing_if = "Option::is_none")]
331    pub caption_entities: Option<Vec<crate::message::MessageEntity>>,
332    /// `true` if the caption must be shown above the animation.
333    #[serde(skip_serializing_if = "Option::is_none")]
334    pub show_caption_above_media: Option<bool>,
335    /// Inline keyboard attached to the message.
336    #[serde(skip_serializing_if = "Option::is_none")]
337    pub reply_markup: Option<crate::keyboard::InlineKeyboardMarkup>,
338    /// Content of the message to be sent instead of the animation.
339    #[serde(skip_serializing_if = "Option::is_none")]
340    pub input_message_content: Option<InputMessageContent>,
341}
342
343/// A link to a video file (`text/html` or `video/mp4`).
344#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct InlineQueryResultVideo {
346    /// Unique identifier for this result (1–64 bytes).
347    pub id: String,
348    /// A valid URL for the video file.
349    pub video_url: String,
350    /// MIME type of the video (`text/html` or `video/mp4`).
351    pub mime_type: String,
352    /// URL of the thumbnail for the video.
353    pub thumbnail_url: String,
354    /// Title for the result.
355    pub title: String,
356    /// Caption of the video (0–1024 characters after entities parsing).
357    #[serde(skip_serializing_if = "Option::is_none")]
358    pub caption: Option<String>,
359    /// Parse mode for the caption.
360    #[serde(skip_serializing_if = "Option::is_none")]
361    pub parse_mode: Option<crate::message::ParseMode>,
362    /// Special entities in the caption; alternative to `parse_mode`.
363    #[serde(skip_serializing_if = "Option::is_none")]
364    pub caption_entities: Option<Vec<crate::message::MessageEntity>>,
365    /// `true` if the caption must be shown above the video.
366    #[serde(skip_serializing_if = "Option::is_none")]
367    pub show_caption_above_media: Option<bool>,
368    /// Video width in pixels.
369    #[serde(skip_serializing_if = "Option::is_none")]
370    pub video_width: Option<u32>,
371    /// Video height in pixels.
372    #[serde(skip_serializing_if = "Option::is_none")]
373    pub video_height: Option<u32>,
374    /// Video duration in seconds.
375    #[serde(skip_serializing_if = "Option::is_none")]
376    pub video_duration: Option<u32>,
377    /// Short description of the result.
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub description: Option<String>,
380    /// Inline keyboard attached to the message.
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub reply_markup: Option<crate::keyboard::InlineKeyboardMarkup>,
383    /// Content of the message to be sent instead of the video.
384    #[serde(skip_serializing_if = "Option::is_none")]
385    pub input_message_content: Option<InputMessageContent>,
386}
387
388/// A link to an audio file (`.mp3` or `.m4a`).
389#[derive(Debug, Clone, Serialize, Deserialize)]
390pub struct InlineQueryResultAudio {
391    /// Unique identifier for this result (1–64 bytes).
392    pub id: String,
393    /// A valid URL for the audio file.
394    pub audio_url: String,
395    /// Title.
396    pub title: String,
397    /// Caption of the audio (0–1024 characters after entities parsing).
398    #[serde(skip_serializing_if = "Option::is_none")]
399    pub caption: Option<String>,
400    /// Parse mode for the caption.
401    #[serde(skip_serializing_if = "Option::is_none")]
402    pub parse_mode: Option<crate::message::ParseMode>,
403    /// Special entities in the caption; alternative to `parse_mode`.
404    #[serde(skip_serializing_if = "Option::is_none")]
405    pub caption_entities: Option<Vec<crate::message::MessageEntity>>,
406    /// Performer of the audio.
407    #[serde(skip_serializing_if = "Option::is_none")]
408    pub performer: Option<String>,
409    /// Audio duration in seconds.
410    #[serde(skip_serializing_if = "Option::is_none")]
411    pub audio_duration: Option<u32>,
412    /// Inline keyboard attached to the message.
413    #[serde(skip_serializing_if = "Option::is_none")]
414    pub reply_markup: Option<crate::keyboard::InlineKeyboardMarkup>,
415    /// Content of the message to be sent instead of the audio.
416    #[serde(skip_serializing_if = "Option::is_none")]
417    pub input_message_content: Option<InputMessageContent>,
418}
419
420/// A link to a voice recording in `.ogg` format encoded with OPUS.
421#[derive(Debug, Clone, Serialize, Deserialize)]
422pub struct InlineQueryResultVoice {
423    /// Unique identifier for this result (1–64 bytes).
424    pub id: String,
425    /// A valid URL for the voice recording.
426    pub voice_url: String,
427    /// Recording title.
428    pub title: String,
429    /// Caption of the voice recording (0–1024 characters after entities parsing).
430    #[serde(skip_serializing_if = "Option::is_none")]
431    pub caption: Option<String>,
432    /// Parse mode for the caption.
433    #[serde(skip_serializing_if = "Option::is_none")]
434    pub parse_mode: Option<crate::message::ParseMode>,
435    /// Special entities in the caption; alternative to `parse_mode`.
436    #[serde(skip_serializing_if = "Option::is_none")]
437    pub caption_entities: Option<Vec<crate::message::MessageEntity>>,
438    /// Recording duration in seconds.
439    #[serde(skip_serializing_if = "Option::is_none")]
440    pub voice_duration: Option<u32>,
441    /// Inline keyboard attached to the message.
442    #[serde(skip_serializing_if = "Option::is_none")]
443    pub reply_markup: Option<crate::keyboard::InlineKeyboardMarkup>,
444    /// Content of the message to be sent instead of the voice recording.
445    #[serde(skip_serializing_if = "Option::is_none")]
446    pub input_message_content: Option<InputMessageContent>,
447}
448
449/// A link to a general file (`application/pdf` or `application/zip`).
450#[derive(Debug, Clone, Serialize, Deserialize)]
451pub struct InlineQueryResultDocument {
452    /// Unique identifier for this result (1–64 bytes).
453    pub id: String,
454    /// Title for the result.
455    pub title: String,
456    /// Caption of the document (0–1024 characters after entities parsing).
457    #[serde(skip_serializing_if = "Option::is_none")]
458    pub caption: Option<String>,
459    /// Parse mode for the caption.
460    #[serde(skip_serializing_if = "Option::is_none")]
461    pub parse_mode: Option<crate::message::ParseMode>,
462    /// Special entities in the caption; alternative to `parse_mode`.
463    #[serde(skip_serializing_if = "Option::is_none")]
464    pub caption_entities: Option<Vec<crate::message::MessageEntity>>,
465    /// A valid URL for the file.
466    pub document_url: String,
467    /// MIME type of the document (`application/pdf` or `application/zip`).
468    pub mime_type: String,
469    /// Short description of the result.
470    #[serde(skip_serializing_if = "Option::is_none")]
471    pub description: Option<String>,
472    /// Inline keyboard attached to the message.
473    #[serde(skip_serializing_if = "Option::is_none")]
474    pub reply_markup: Option<crate::keyboard::InlineKeyboardMarkup>,
475    /// Content of the message to be sent instead of the document.
476    #[serde(skip_serializing_if = "Option::is_none")]
477    pub input_message_content: Option<InputMessageContent>,
478    /// URL of the thumbnail for the result.
479    #[serde(skip_serializing_if = "Option::is_none")]
480    pub thumbnail_url: Option<String>,
481    /// Thumbnail width in pixels.
482    #[serde(skip_serializing_if = "Option::is_none")]
483    pub thumbnail_width: Option<u32>,
484    /// Thumbnail height in pixels.
485    #[serde(skip_serializing_if = "Option::is_none")]
486    pub thumbnail_height: Option<u32>,
487}
488
489/// A geographic location on a map.
490#[derive(Debug, Clone, Serialize, Deserialize)]
491pub struct InlineQueryResultLocation {
492    /// Unique identifier for this result (1–64 bytes).
493    pub id: String,
494    /// Location latitude in degrees.
495    pub latitude: f64,
496    /// Location longitude in degrees.
497    pub longitude: f64,
498    /// Location title.
499    pub title: String,
500    /// Radius of uncertainty for the location in metres (0–1500).
501    #[serde(skip_serializing_if = "Option::is_none")]
502    pub horizontal_accuracy: Option<f64>,
503    /// Period in seconds during which the location can be updated (60–86400).
504    #[serde(skip_serializing_if = "Option::is_none")]
505    pub live_period: Option<u32>,
506    /// Direction of movement in degrees (1–360) for live locations.
507    #[serde(skip_serializing_if = "Option::is_none")]
508    pub heading: Option<u16>,
509    /// Maximum distance in metres for proximity alerts about approaching another chat member.
510    #[serde(skip_serializing_if = "Option::is_none")]
511    pub proximity_alert_radius: Option<u32>,
512    /// Inline keyboard attached to the message.
513    #[serde(skip_serializing_if = "Option::is_none")]
514    pub reply_markup: Option<crate::keyboard::InlineKeyboardMarkup>,
515    /// Content of the message to be sent instead of the location.
516    #[serde(skip_serializing_if = "Option::is_none")]
517    pub input_message_content: Option<InputMessageContent>,
518    /// URL of the thumbnail for the result.
519    #[serde(skip_serializing_if = "Option::is_none")]
520    pub thumbnail_url: Option<String>,
521    /// Thumbnail width in pixels.
522    #[serde(skip_serializing_if = "Option::is_none")]
523    pub thumbnail_width: Option<u32>,
524    /// Thumbnail height in pixels.
525    #[serde(skip_serializing_if = "Option::is_none")]
526    pub thumbnail_height: Option<u32>,
527}
528
529/// A venue.
530#[derive(Debug, Clone, Serialize, Deserialize)]
531pub struct InlineQueryResultVenue {
532    /// Unique identifier for this result (1–64 bytes).
533    pub id: String,
534    /// Venue latitude in degrees.
535    pub latitude: f64,
536    /// Venue longitude in degrees.
537    pub longitude: f64,
538    /// Venue title.
539    pub title: String,
540    /// Venue address.
541    pub address: String,
542    /// Foursquare identifier of the venue.
543    #[serde(skip_serializing_if = "Option::is_none")]
544    pub foursquare_id: Option<String>,
545    /// Foursquare type of the venue.
546    #[serde(skip_serializing_if = "Option::is_none")]
547    pub foursquare_type: Option<String>,
548    /// Google Places identifier of the venue.
549    #[serde(skip_serializing_if = "Option::is_none")]
550    pub google_place_id: Option<String>,
551    /// Google Places type of the venue.
552    #[serde(skip_serializing_if = "Option::is_none")]
553    pub google_place_type: Option<String>,
554    /// Inline keyboard attached to the message.
555    #[serde(skip_serializing_if = "Option::is_none")]
556    pub reply_markup: Option<crate::keyboard::InlineKeyboardMarkup>,
557    /// Content of the message to be sent instead of the venue.
558    #[serde(skip_serializing_if = "Option::is_none")]
559    pub input_message_content: Option<InputMessageContent>,
560    /// URL of the thumbnail for the result.
561    #[serde(skip_serializing_if = "Option::is_none")]
562    pub thumbnail_url: Option<String>,
563    /// Thumbnail width in pixels.
564    #[serde(skip_serializing_if = "Option::is_none")]
565    pub thumbnail_width: Option<u32>,
566    /// Thumbnail height in pixels.
567    #[serde(skip_serializing_if = "Option::is_none")]
568    pub thumbnail_height: Option<u32>,
569}
570
571/// A contact with a phone number.
572#[derive(Debug, Clone, Serialize, Deserialize)]
573pub struct InlineQueryResultContact {
574    /// Unique identifier for this result (1–64 bytes).
575    pub id: String,
576    /// Contact phone number.
577    pub phone_number: String,
578    /// Contact first name.
579    pub first_name: String,
580    /// Contact last name.
581    #[serde(skip_serializing_if = "Option::is_none")]
582    pub last_name: Option<String>,
583    /// Contact vCard (0–2048 bytes).
584    #[serde(skip_serializing_if = "Option::is_none")]
585    pub vcard: Option<String>,
586    /// Inline keyboard attached to the message.
587    #[serde(skip_serializing_if = "Option::is_none")]
588    pub reply_markup: Option<crate::keyboard::InlineKeyboardMarkup>,
589    /// Content of the message to be sent instead of the contact.
590    #[serde(skip_serializing_if = "Option::is_none")]
591    pub input_message_content: Option<InputMessageContent>,
592    /// URL of the thumbnail for the result.
593    #[serde(skip_serializing_if = "Option::is_none")]
594    pub thumbnail_url: Option<String>,
595    /// Thumbnail width in pixels.
596    #[serde(skip_serializing_if = "Option::is_none")]
597    pub thumbnail_width: Option<u32>,
598    /// Thumbnail height in pixels.
599    #[serde(skip_serializing_if = "Option::is_none")]
600    pub thumbnail_height: Option<u32>,
601}
602
603/// A game result.
604#[derive(Debug, Clone, Serialize, Deserialize)]
605pub struct InlineQueryResultGame {
606    /// Unique identifier for this result (1–64 bytes).
607    pub id: String,
608    /// Short name of the game.
609    pub game_short_name: String,
610    /// Inline keyboard attached to the message.
611    #[serde(skip_serializing_if = "Option::is_none")]
612    pub reply_markup: Option<crate::keyboard::InlineKeyboardMarkup>,
613}
614
615// ─── Cached (file_id-based) results ──────────────────────────────────────────
616
617/// A photo from a previously uploaded Telegram file.
618#[derive(Debug, Clone, Serialize, Deserialize)]
619pub struct InlineQueryResultCachedPhoto {
620    /// Unique identifier for this result (1–64 bytes).
621    pub id: String,
622    /// A valid Telegram `file_id` of the photo.
623    pub photo_file_id: String,
624    /// Title for the result.
625    #[serde(skip_serializing_if = "Option::is_none")]
626    pub title: Option<String>,
627    /// Short description of the result.
628    #[serde(skip_serializing_if = "Option::is_none")]
629    pub description: Option<String>,
630    /// Caption of the photo (0–1024 characters after entities parsing).
631    #[serde(skip_serializing_if = "Option::is_none")]
632    pub caption: Option<String>,
633    /// Parse mode for the caption.
634    #[serde(skip_serializing_if = "Option::is_none")]
635    pub parse_mode: Option<crate::message::ParseMode>,
636    /// Special entities in the caption; alternative to `parse_mode`.
637    #[serde(skip_serializing_if = "Option::is_none")]
638    pub caption_entities: Option<Vec<crate::message::MessageEntity>>,
639    /// `true` if the caption must be shown above the photo.
640    #[serde(skip_serializing_if = "Option::is_none")]
641    pub show_caption_above_media: Option<bool>,
642    /// Inline keyboard attached to the message.
643    #[serde(skip_serializing_if = "Option::is_none")]
644    pub reply_markup: Option<crate::keyboard::InlineKeyboardMarkup>,
645    /// Content of the message to be sent instead of the photo.
646    #[serde(skip_serializing_if = "Option::is_none")]
647    pub input_message_content: Option<InputMessageContent>,
648}
649
650/// An animated GIF from a previously uploaded Telegram file.
651#[derive(Debug, Clone, Serialize, Deserialize)]
652pub struct InlineQueryResultCachedGif {
653    /// Unique identifier for this result (1–64 bytes).
654    pub id: String,
655    /// A valid Telegram `file_id` of the GIF.
656    pub gif_file_id: String,
657    /// Title for the result.
658    #[serde(skip_serializing_if = "Option::is_none")]
659    pub title: Option<String>,
660    /// Caption of the GIF (0–1024 characters after entities parsing).
661    #[serde(skip_serializing_if = "Option::is_none")]
662    pub caption: Option<String>,
663    /// Parse mode for the caption. See [formatting options](https://core.telegram.org/bots/api#formatting-options) for more details.
664    #[serde(skip_serializing_if = "Option::is_none")]
665    pub parse_mode: Option<crate::message::ParseMode>,
666    /// Special entities in the caption; alternative to `parse_mode`.
667    #[serde(skip_serializing_if = "Option::is_none")]
668    pub caption_entities: Option<Vec<crate::message::MessageEntity>>,
669    /// `true` if the caption must be shown above the GIF.
670    #[serde(skip_serializing_if = "Option::is_none")]
671    pub show_caption_above_media: Option<bool>,
672    /// Inline keyboard attached to the message.
673    #[serde(skip_serializing_if = "Option::is_none")]
674    pub reply_markup: Option<crate::keyboard::InlineKeyboardMarkup>,
675    /// Content of the message to be sent instead of the GIF.
676    #[serde(skip_serializing_if = "Option::is_none")]
677    pub input_message_content: Option<InputMessageContent>,
678}
679
680/// An MPEG4 animation from a previously uploaded Telegram file.
681#[derive(Debug, Clone, Serialize, Deserialize)]
682pub struct InlineQueryResultCachedMpeg4Gif {
683    /// Unique identifier for this result (1–64 bytes).
684    pub id: String,
685    /// A valid Telegram `file_id` of the MPEG4 animation.
686    pub mpeg4_file_id: String,
687    /// Title for the result.
688    #[serde(skip_serializing_if = "Option::is_none")]
689    pub title: Option<String>,
690    /// Caption of the animation (0–1024 characters after entities parsing).
691    #[serde(skip_serializing_if = "Option::is_none")]
692    pub caption: Option<String>,
693    /// Parse mode for the caption.
694    #[serde(skip_serializing_if = "Option::is_none")]
695    pub parse_mode: Option<crate::message::ParseMode>,
696    /// Special entities in the caption; alternative to `parse_mode`.
697    #[serde(skip_serializing_if = "Option::is_none")]
698    pub caption_entities: Option<Vec<crate::message::MessageEntity>>,
699    /// `true` if the caption must be shown above the animation.
700    #[serde(skip_serializing_if = "Option::is_none")]
701    pub show_caption_above_media: Option<bool>,
702    /// Inline keyboard attached to the message.
703    #[serde(skip_serializing_if = "Option::is_none")]
704    pub reply_markup: Option<crate::keyboard::InlineKeyboardMarkup>,
705    /// Content of the message to be sent instead of the animation.
706    #[serde(skip_serializing_if = "Option::is_none")]
707    pub input_message_content: Option<InputMessageContent>,
708}
709
710/// A sticker from a previously uploaded Telegram file.
711#[derive(Debug, Clone, Serialize, Deserialize)]
712pub struct InlineQueryResultCachedSticker {
713    /// Unique identifier for this result (1–64 bytes).
714    pub id: String,
715    /// A valid Telegram `file_id` of the sticker.
716    pub sticker_file_id: String,
717    /// Inline keyboard attached to the message.
718    #[serde(skip_serializing_if = "Option::is_none")]
719    pub reply_markup: Option<crate::keyboard::InlineKeyboardMarkup>,
720    /// Content of the message to be sent instead of the sticker.
721    #[serde(skip_serializing_if = "Option::is_none")]
722    pub input_message_content: Option<InputMessageContent>,
723}
724
725/// A document from a previously uploaded Telegram file.
726#[derive(Debug, Clone, Serialize, Deserialize)]
727pub struct InlineQueryResultCachedDocument {
728    /// Unique identifier for this result (1–64 bytes).
729    pub id: String,
730    /// Title for the result.
731    pub title: String,
732    /// A valid Telegram `file_id` of the document.
733    pub document_file_id: String,
734    /// Short description of the result.
735    #[serde(skip_serializing_if = "Option::is_none")]
736    pub description: Option<String>,
737    /// Caption of the document (0–1024 characters after entities parsing).
738    #[serde(skip_serializing_if = "Option::is_none")]
739    pub caption: Option<String>,
740    /// Parse mode for the caption.
741    #[serde(skip_serializing_if = "Option::is_none")]
742    pub parse_mode: Option<crate::message::ParseMode>,
743    /// Special entities in the caption; alternative to `parse_mode`.
744    #[serde(skip_serializing_if = "Option::is_none")]
745    pub caption_entities: Option<Vec<crate::message::MessageEntity>>,
746    /// Inline keyboard attached to the message.
747    #[serde(skip_serializing_if = "Option::is_none")]
748    pub reply_markup: Option<crate::keyboard::InlineKeyboardMarkup>,
749    /// Content of the message to be sent instead of the document.
750    #[serde(skip_serializing_if = "Option::is_none")]
751    pub input_message_content: Option<InputMessageContent>,
752}
753
754/// A video from a previously uploaded Telegram file.
755#[derive(Debug, Clone, Serialize, Deserialize)]
756pub struct InlineQueryResultCachedVideo {
757    /// Unique identifier for this result (1–64 bytes).
758    pub id: String,
759    /// A valid Telegram `file_id` of the video.
760    pub video_file_id: String,
761    /// Title for the result.
762    pub title: String,
763    /// Short description of the result.
764    #[serde(skip_serializing_if = "Option::is_none")]
765    pub description: Option<String>,
766    /// Caption of the video (0–1024 characters after entities parsing).
767    #[serde(skip_serializing_if = "Option::is_none")]
768    pub caption: Option<String>,
769    /// Parse mode for the caption.
770    #[serde(skip_serializing_if = "Option::is_none")]
771    pub parse_mode: Option<crate::message::ParseMode>,
772    /// Special entities in the caption; alternative to `parse_mode`.
773    #[serde(skip_serializing_if = "Option::is_none")]
774    pub caption_entities: Option<Vec<crate::message::MessageEntity>>,
775    /// `true` if the caption must be shown above the video.
776    #[serde(skip_serializing_if = "Option::is_none")]
777    pub show_caption_above_media: Option<bool>,
778    /// Inline keyboard attached to the message.
779    #[serde(skip_serializing_if = "Option::is_none")]
780    pub reply_markup: Option<crate::keyboard::InlineKeyboardMarkup>,
781    /// Content of the message to be sent instead of the video.
782    #[serde(skip_serializing_if = "Option::is_none")]
783    pub input_message_content: Option<InputMessageContent>,
784}
785
786/// A voice message from a previously uploaded Telegram file.
787#[derive(Debug, Clone, Serialize, Deserialize)]
788pub struct InlineQueryResultCachedVoice {
789    /// Unique identifier for this result (1–64 bytes).
790    pub id: String,
791    /// A valid Telegram `file_id` of the voice message.
792    pub voice_file_id: String,
793    /// Title for the result.
794    pub title: String,
795    /// Caption of the voice message (0–1024 characters after entities parsing).
796    #[serde(skip_serializing_if = "Option::is_none")]
797    pub caption: Option<String>,
798    /// Parse mode for the caption.
799    #[serde(skip_serializing_if = "Option::is_none")]
800    pub parse_mode: Option<crate::message::ParseMode>,
801    /// Special entities in the caption; alternative to `parse_mode`.
802    #[serde(skip_serializing_if = "Option::is_none")]
803    pub caption_entities: Option<Vec<crate::message::MessageEntity>>,
804    /// Inline keyboard attached to the message.
805    #[serde(skip_serializing_if = "Option::is_none")]
806    pub reply_markup: Option<crate::keyboard::InlineKeyboardMarkup>,
807    /// Content of the message to be sent instead of the voice message.
808    #[serde(skip_serializing_if = "Option::is_none")]
809    pub input_message_content: Option<InputMessageContent>,
810}
811
812/// An audio file from a previously uploaded Telegram file.
813#[derive(Debug, Clone, Serialize, Deserialize)]
814pub struct InlineQueryResultCachedAudio {
815    /// Unique identifier for this result (1–64 bytes).
816    pub id: String,
817    /// A valid Telegram `file_id` of the audio file.
818    pub audio_file_id: String,
819    /// Caption of the audio (0–1024 characters after entities parsing).
820    #[serde(skip_serializing_if = "Option::is_none")]
821    pub caption: Option<String>,
822    /// Parse mode for the caption.
823    #[serde(skip_serializing_if = "Option::is_none")]
824    pub parse_mode: Option<crate::message::ParseMode>,
825    /// Special entities in the caption; alternative to `parse_mode`.
826    #[serde(skip_serializing_if = "Option::is_none")]
827    pub caption_entities: Option<Vec<crate::message::MessageEntity>>,
828    /// Inline keyboard attached to the message.
829    #[serde(skip_serializing_if = "Option::is_none")]
830    pub reply_markup: Option<crate::keyboard::InlineKeyboardMarkup>,
831    /// Content of the message to be sent instead of the audio.
832    #[serde(skip_serializing_if = "Option::is_none")]
833    pub input_message_content: Option<InputMessageContent>,
834}
835
836// ─── InputMessageContent ──────────────────────────────────────────────────────
837
838/// The content of a message sent as the result of an inline query.
839///
840/// # Variant order is load-bearing
841///
842/// This enum is `#[serde(untagged)]`, so serde takes the first variant that
843/// matches, and it ignores fields the variant does not declare. `Venue`
844/// requires everything `Location` requires (`latitude`, `longitude`) plus
845/// `title` and `address` — a strict superset — so `Venue` must be tried first.
846/// With `Location` first, every venue deserialized as a location and silently
847/// lost its title and address.
848///
849/// Adding a variant whose required fields are a superset of an existing one
850/// means placing it above that one. `Venue` and `Location` are the only such
851/// pair today; the others have disjoint required fields and their order is free.
852#[derive(Debug, Clone, Serialize, Deserialize)]
853#[serde(untagged)]
854pub enum InputMessageContent {
855    /// The message text.
856    Text(InputTextMessageContent),
857    /// A rich formatted message.
858    Rich(crate::rich_message::InputRichMessageContent),
859    /// A venue. Declared before [`Location`](Self::Location) — see the note above.
860    Venue(InputVenueMessageContent),
861    /// A location on a map.
862    Location(InputLocationMessageContent),
863    /// A contact.
864    Contact(InputContactMessageContent),
865    /// An invoice.
866    Invoice(InputInvoiceMessageContent),
867}
868
869/// A text message to send as an inline query result.
870#[derive(Debug, Clone, Serialize, Deserialize)]
871pub struct InputTextMessageContent {
872    /// Text of the message (1–4096 characters).
873    pub message_text: String,
874    /// Parse mode for the message text.
875    #[serde(skip_serializing_if = "Option::is_none")]
876    pub parse_mode: Option<crate::message::ParseMode>,
877    /// Special entities in the message text; alternative to `parse_mode`.
878    #[serde(skip_serializing_if = "Option::is_none")]
879    pub entities: Option<Vec<crate::message::MessageEntity>>,
880    /// Options for link preview generation.
881    #[serde(skip_serializing_if = "Option::is_none")]
882    pub link_preview_options: Option<crate::message::LinkPreviewOptions>,
883}
884
885/// A live location message to send as an inline query result.
886#[derive(Debug, Clone, Serialize, Deserialize)]
887pub struct InputLocationMessageContent {
888    /// Latitude in degrees.
889    pub latitude: f64,
890    /// Longitude in degrees.
891    pub longitude: f64,
892    /// Radius of uncertainty for the location in metres (0–1500).
893    #[serde(skip_serializing_if = "Option::is_none")]
894    pub horizontal_accuracy: Option<f64>,
895    /// Period in seconds during which the location can be updated (60–86400).
896    #[serde(skip_serializing_if = "Option::is_none")]
897    pub live_period: Option<u32>,
898    /// Direction of movement in degrees (1–360).
899    #[serde(skip_serializing_if = "Option::is_none")]
900    pub heading: Option<u16>,
901    /// Maximum distance in metres for proximity alerts.
902    #[serde(skip_serializing_if = "Option::is_none")]
903    pub proximity_alert_radius: Option<u32>,
904}
905
906/// A venue message to send as an inline query result.
907#[derive(Debug, Clone, Serialize, Deserialize)]
908pub struct InputVenueMessageContent {
909    /// Latitude in degrees.
910    pub latitude: f64,
911    /// Longitude in degrees.
912    pub longitude: f64,
913    /// Venue name.
914    pub title: String,
915    /// Venue address.
916    pub address: String,
917    /// Foursquare identifier of the venue.
918    #[serde(skip_serializing_if = "Option::is_none")]
919    pub foursquare_id: Option<String>,
920    /// Foursquare type of the venue.
921    #[serde(skip_serializing_if = "Option::is_none")]
922    pub foursquare_type: Option<String>,
923    /// Google Places identifier of the venue.
924    #[serde(skip_serializing_if = "Option::is_none")]
925    pub google_place_id: Option<String>,
926    /// Google Places type of the venue.
927    #[serde(skip_serializing_if = "Option::is_none")]
928    pub google_place_type: Option<String>,
929}
930
931/// A contact message to send as an inline query result.
932#[derive(Debug, Clone, Serialize, Deserialize)]
933pub struct InputContactMessageContent {
934    /// Contact phone number.
935    pub phone_number: String,
936    /// Contact first name.
937    pub first_name: String,
938    /// Contact last name.
939    #[serde(skip_serializing_if = "Option::is_none")]
940    pub last_name: Option<String>,
941    /// Contact vCard (0–2048 bytes).
942    #[serde(skip_serializing_if = "Option::is_none")]
943    pub vcard: Option<String>,
944}
945
946/// An invoice message to send as an inline query result.
947#[derive(Debug, Clone, Serialize, Deserialize)]
948pub struct InputInvoiceMessageContent {
949    /// Product name (1–32 characters).
950    pub title: String,
951    /// Product description (1–255 characters).
952    pub description: String,
953    /// Bot-defined invoice payload (1–128 bytes).
954    pub payload: String,
955    /// Payment provider token; not required for Telegram Stars.
956    #[serde(skip_serializing_if = "Option::is_none")]
957    pub provider_token: Option<String>,
958    /// Three-letter ISO 4217 currency code.
959    pub currency: String,
960    /// Price breakdown as a list of labeled portions.
961    pub prices: Vec<crate::payments::LabeledPrice>,
962    /// Maximum accepted tip amount in the smallest currency unit.
963    #[serde(skip_serializing_if = "Option::is_none")]
964    pub max_tip_amount: Option<u64>,
965    /// Suggested tip amounts in the smallest currency unit.
966    #[serde(skip_serializing_if = "Option::is_none")]
967    pub suggested_tip_amounts: Option<Vec<u64>>,
968    /// JSON-encoded data about the invoice for the payment provider.
969    #[serde(skip_serializing_if = "Option::is_none")]
970    pub provider_data: Option<String>,
971    /// URL of the product photo.
972    #[serde(skip_serializing_if = "Option::is_none")]
973    pub photo_url: Option<String>,
974    /// Photo size in bytes.
975    #[serde(skip_serializing_if = "Option::is_none")]
976    pub photo_size: Option<u64>,
977    /// Photo width in pixels.
978    #[serde(skip_serializing_if = "Option::is_none")]
979    pub photo_width: Option<u32>,
980    /// Photo height in pixels.
981    #[serde(skip_serializing_if = "Option::is_none")]
982    pub photo_height: Option<u32>,
983    /// Requests the buyer's full name.
984    #[serde(skip_serializing_if = "Option::is_none")]
985    pub need_name: Option<bool>,
986    /// Requests the buyer's phone number.
987    #[serde(skip_serializing_if = "Option::is_none")]
988    pub need_phone_number: Option<bool>,
989    /// Requests the buyer's email address.
990    #[serde(skip_serializing_if = "Option::is_none")]
991    pub need_email: Option<bool>,
992    /// Requests the buyer's shipping address.
993    #[serde(skip_serializing_if = "Option::is_none")]
994    pub need_shipping_address: Option<bool>,
995    /// Passes the buyer's phone number to the payment provider.
996    #[serde(skip_serializing_if = "Option::is_none")]
997    pub send_phone_number_to_provider: Option<bool>,
998    /// Passes the buyer's email address to the payment provider.
999    #[serde(skip_serializing_if = "Option::is_none")]
1000    pub send_email_to_provider: Option<bool>,
1001    /// `true` if the final price depends on the shipping method.
1002    #[serde(skip_serializing_if = "Option::is_none")]
1003    pub is_flexible: Option<bool>,
1004}
1005
1006// ─── Misc ─────────────────────────────────────────────────────────────────────
1007
1008/// A message sent from a Web App on behalf of the user.
1009#[derive(Debug, Clone, Serialize, Deserialize)]
1010pub struct SentWebAppMessage {
1011    /// Identifier of the sent inline message, if one was sent.
1012    #[serde(skip_serializing_if = "Option::is_none")]
1013    pub inline_message_id: Option<String>,
1014}
1015
1016/// An inline message sent by a guest bot.
1017///
1018/// Returned by [`answerGuestQuery`](https://core.telegram.org/bots/api#answerguestquery).
1019#[derive(Debug, Clone, Serialize, Deserialize)]
1020pub struct SentGuestMessage {
1021    /// Identifier of the sent inline message.
1022    pub inline_message_id: String,
1023}
1024
1025/// A button shown above inline query results.
1026///
1027/// Exactly one of `web_app` or `start_parameter` should be set.
1028#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1029#[non_exhaustive]
1030pub struct InlineQueryResultsButton {
1031    /// Label text on the button.
1032    pub text: String,
1033    /// Description of the Web App launched when the button is pressed.
1034    #[serde(skip_serializing_if = "Option::is_none")]
1035    pub web_app: Option<crate::message::WebAppInfo>,
1036    /// Deep-linking parameter for the /start message sent when the button is pressed.
1037    #[serde(skip_serializing_if = "Option::is_none")]
1038    pub start_parameter: Option<String>,
1039}
1040
1041/// An inline message prepared for sending by a Mini App.
1042#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1043#[non_exhaustive]
1044pub struct PreparedInlineMessage {
1045    /// Unique identifier of the prepared message.
1046    pub id: String,
1047    /// Point in time when the prepared message can no longer be used, as a Unix timestamp.
1048    pub expiration_date: i64,
1049}