Skip to main content

rustigram_types/
file.rs

1use serde::{Deserialize, Serialize};
2
3/// One size of a photo or file thumbnail.
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct PhotoSize {
6    /// Telegram file identifier.
7    pub file_id: String,
8    /// Unique file identifier, stable across bots and time.
9    pub file_unique_id: String,
10    /// Photo width in pixels.
11    pub width: u32,
12    /// Photo height in pixels.
13    pub height: u32,
14    /// File size in bytes.
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub file_size: Option<u64>,
17}
18
19/// A file ready to be downloaded.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct File {
22    /// Telegram file identifier.
23    pub file_id: String,
24    /// Unique file identifier, stable across bots and time.
25    pub file_unique_id: String,
26    /// File size in bytes.
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub file_size: Option<u64>,
29    /// Relative file path for constructing the download URL.
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub file_path: Option<String>,
32}
33
34impl File {
35    /// Constructs the full download URL for this file.
36    #[must_use]
37    pub fn url(&self, token: &str) -> Option<String> {
38        self.file_path
39            .as_ref()
40            .map(|path| format!("https://api.telegram.org/file/bot{token}/{path}"))
41    }
42}
43
44/// Audio file to be treated as music.
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct Audio {
47    /// Telegram file identifier.
48    pub file_id: String,
49    /// Unique file identifier, stable across bots and time.
50    pub file_unique_id: String,
51    /// Duration of the audio in seconds.
52    pub duration: u32,
53    /// Performer of the audio as defined by the sender or audio tags.
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub performer: Option<String>,
56    /// Title of the audio as defined by the sender or audio tags.
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub title: Option<String>,
59    /// Original filename as defined by the sender.
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub file_name: Option<String>,
62    /// MIME type of the audio.
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub mime_type: Option<String>,
65    /// File size in bytes.
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub file_size: Option<u64>,
68    /// Thumbnail of the album cover.
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub thumbnail: Option<PhotoSize>,
71}
72
73/// General file (not photo, voice, audio, or video).
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct Document {
76    /// Telegram file identifier.
77    pub file_id: String,
78    /// Unique file identifier, stable across bots and time.
79    pub file_unique_id: String,
80    /// Document thumbnail.
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub thumbnail: Option<PhotoSize>,
83    /// Original filename as defined by the sender.
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub file_name: Option<String>,
86    /// MIME type of the document.
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub mime_type: Option<String>,
89    /// File size in bytes.
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub file_size: Option<u64>,
92}
93
94/// A video file of a specific quality.
95///
96/// Returned inside [`Video::qualities`] when multiple quality levels are available.
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct VideoQuality {
99    /// Telegram file identifier.
100    pub file_id: String,
101    /// Unique file identifier, stable across bots and time.
102    pub file_unique_id: String,
103    /// Video width in pixels.
104    pub width: u32,
105    /// Video height in pixels.
106    pub height: u32,
107    /// Codec used to encode the video (e.g. `"h264"`, `"h265"`, `"av01"`).
108    pub codec: String,
109    /// File size in bytes.
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub file_size: Option<u64>,
112}
113
114/// Video file.
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct Video {
117    /// Telegram file identifier.
118    pub file_id: String,
119    /// Unique file identifier, stable across bots and time.
120    pub file_unique_id: String,
121    /// Video width in pixels.
122    pub width: u32,
123    /// Video height in pixels.
124    pub height: u32,
125    /// Duration of the video in seconds.
126    pub duration: u32,
127    /// Video thumbnail.
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub thumbnail: Option<PhotoSize>,
130    /// Cover image for the video in the message.
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub cover: Option<Vec<PhotoSize>>,
133    /// Start timestamp for video chapters.
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub start_timestamp: Option<u32>,
136    /// Other available quality levels for this video (Bot API 9.4).
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub qualities: Option<Vec<VideoQuality>>,
139    /// Original filename.
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub file_name: Option<String>,
142    /// MIME type of the video.
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub mime_type: Option<String>,
145    /// File size in bytes.
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub file_size: Option<u64>,
148}
149
150/// Animation file (GIF or H.264/MPEG-4 AVC, no sound).
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct Animation {
153    /// Telegram file identifier.
154    pub file_id: String,
155    /// Unique file identifier, stable across bots and time.
156    pub file_unique_id: String,
157    /// Animation width in pixels.
158    pub width: u32,
159    /// Animation height in pixels.
160    pub height: u32,
161    /// Duration of the animation in seconds.
162    pub duration: u32,
163    /// Animation thumbnail.
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub thumbnail: Option<PhotoSize>,
166    /// Original filename as defined by the sender.
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub file_name: Option<String>,
169    /// MIME type of the animation.
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub mime_type: Option<String>,
172    /// File size in bytes.
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub file_size: Option<u64>,
175}
176
177/// Voice note (OGG/OPUS audio).
178#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct Voice {
180    /// Telegram file identifier.
181    pub file_id: String,
182    /// Unique file identifier, stable across bots and time.
183    pub file_unique_id: String,
184    /// Duration of the voice note in seconds.
185    pub duration: u32,
186    /// MIME type of the voice note.
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub mime_type: Option<String>,
189    /// File size in bytes.
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub file_size: Option<u64>,
192}
193
194/// Rounded-square MPEG4 video note.
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct VideoNote {
197    /// Telegram file identifier.
198    pub file_id: String,
199    /// Unique file identifier, stable across bots and time.
200    pub file_unique_id: String,
201    /// Video width and height (diameter of the circle).
202    pub length: u32,
203    /// Duration of the video in seconds.
204    pub duration: u32,
205    /// Video thumbnail.
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub thumbnail: Option<PhotoSize>,
208    /// File size in bytes.
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub file_size: Option<u64>,
211}
212
213/// A photo with a short video (live photo).
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct LivePhoto {
216    /// Available sizes of the corresponding static photo.
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub photo: Option<Vec<PhotoSize>>,
219    /// Identifier for the video file.
220    pub file_id: String,
221    /// Unique identifier for the video file, stable across bots and time.
222    pub file_unique_id: String,
223    /// Video width as defined by the sender.
224    pub width: u32,
225    /// Video height as defined by the sender.
226    pub height: u32,
227    /// Duration of the video in seconds.
228    pub duration: u32,
229    /// MIME type of the file as defined by the sender.
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub mime_type: Option<String>,
232    /// File size in bytes.
233    #[serde(skip_serializing_if = "Option::is_none")]
234    pub file_size: Option<u64>,
235}
236
237/// Represents a file to be sent.
238#[derive(Debug, Clone)]
239pub enum InputFile {
240    /// Send an existing file by its Telegram `file_id`.
241    FileId(String),
242    /// Send a file from a URL (photo ≤5 MB, others ≤20 MB).
243    Url(String),
244    /// Upload new file bytes.
245    Bytes {
246        /// Original filename sent in the Content-Disposition header.
247        filename: String,
248        /// Raw file bytes.
249        data: Vec<u8>,
250        /// MIME type of the file.
251        mime_type: String,
252    },
253    /// Reference an already-included multipart attachment by `attach://<name>`.
254    Attach(String),
255}
256
257impl InputFile {
258    /// Returns the string representation for JSON/query-string fields.
259    #[must_use]
260    pub fn as_str(&self) -> &str {
261        match self {
262            Self::FileId(id) => id,
263            Self::Url(url) => url,
264            Self::Attach(name) => name,
265            Self::Bytes { filename, .. } => filename,
266        }
267    }
268
269    /// Returns `true` if this variant needs multipart form upload.
270    #[must_use]
271    pub fn requires_multipart(&self) -> bool {
272        matches!(self, Self::Bytes { .. })
273    }
274}
275
276/// Encrypted passport file.
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct PassportFile {
279    /// Telegram file identifier.
280    pub file_id: String,
281    /// Unique file identifier, stable across bots and time.
282    pub file_unique_id: String,
283    /// File size in bytes.
284    pub file_size: u64,
285    /// Unix timestamp when the file was uploaded.
286    pub file_date: i64,
287}
288
289// ─── InputMedia ───────────────────────────────────────────────────────────────
290//
291// Used by `sendMediaGroup` and `editMessageMedia`. The `media` field accepts a
292// `file_id`, HTTP URL, or `"attach://<n>"` for a multipart attachment.
293
294/// A photo to include in a media group or replace an existing media message.
295#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct InputMediaPhoto {
297    /// File to send: `file_id`, HTTP URL, or `"attach://<n>"`.
298    pub media: String,
299    /// Caption (0–1024 characters after entities parsing).
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub caption: Option<String>,
302    /// Parse mode for the caption.
303    #[serde(skip_serializing_if = "Option::is_none")]
304    pub parse_mode: Option<crate::message::ParseMode>,
305    /// Special entities in the caption.
306    #[serde(skip_serializing_if = "Option::is_none")]
307    pub caption_entities: Option<Vec<crate::message::MessageEntity>>,
308    /// `true` if the caption must be shown above the media.
309    #[serde(skip_serializing_if = "Option::is_none")]
310    pub show_caption_above_media: Option<bool>,
311    /// `true` if the photo needs a spoiler animation.
312    #[serde(skip_serializing_if = "Option::is_none")]
313    pub has_spoiler: Option<bool>,
314}
315
316/// A video to include in a media group or replace an existing media message.
317#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct InputMediaVideo {
319    /// File to send: `file_id`, HTTP URL, or `"attach://<n>"`.
320    pub media: String,
321    /// Thumbnail: `file_id` or `"attach://<n>"`.
322    #[serde(skip_serializing_if = "Option::is_none")]
323    pub thumbnail: Option<String>,
324    /// Cover image: `file_id`, HTTP URL, or `"attach://<n>"`.
325    #[serde(skip_serializing_if = "Option::is_none")]
326    pub cover: Option<String>,
327    /// Start timestamp for the video in the message.
328    #[serde(skip_serializing_if = "Option::is_none")]
329    pub start_timestamp: Option<i64>,
330    /// Caption (0–1024 characters after entities parsing).
331    #[serde(skip_serializing_if = "Option::is_none")]
332    pub caption: Option<String>,
333    /// Parse mode for the caption.
334    #[serde(skip_serializing_if = "Option::is_none")]
335    pub parse_mode: Option<crate::message::ParseMode>,
336    /// Special entities in the caption.
337    #[serde(skip_serializing_if = "Option::is_none")]
338    pub caption_entities: Option<Vec<crate::message::MessageEntity>>,
339    /// `true` if the caption must be shown above the media.
340    #[serde(skip_serializing_if = "Option::is_none")]
341    pub show_caption_above_media: Option<bool>,
342    /// Video width in pixels.
343    #[serde(skip_serializing_if = "Option::is_none")]
344    pub width: Option<u32>,
345    /// Video height in pixels.
346    #[serde(skip_serializing_if = "Option::is_none")]
347    pub height: Option<u32>,
348    /// Video duration in seconds.
349    #[serde(skip_serializing_if = "Option::is_none")]
350    pub duration: Option<u32>,
351    /// `true` if the uploaded video is suitable for streaming.
352    #[serde(skip_serializing_if = "Option::is_none")]
353    pub supports_streaming: Option<bool>,
354    /// `true` if the video needs a spoiler animation.
355    #[serde(skip_serializing_if = "Option::is_none")]
356    pub has_spoiler: Option<bool>,
357}
358
359/// An animation (GIF or silent H.264) to include in a media group.
360#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct InputMediaAnimation {
362    /// File to send: `file_id`, HTTP URL, or `"attach://<n>"`.
363    pub media: String,
364    /// Thumbnail: `file_id` or `"attach://<n>"`.
365    #[serde(skip_serializing_if = "Option::is_none")]
366    pub thumbnail: Option<String>,
367    /// Caption (0–1024 characters after entities parsing).
368    #[serde(skip_serializing_if = "Option::is_none")]
369    pub caption: Option<String>,
370    /// Parse mode for the caption.
371    #[serde(skip_serializing_if = "Option::is_none")]
372    pub parse_mode: Option<crate::message::ParseMode>,
373    /// Special entities in the caption.
374    #[serde(skip_serializing_if = "Option::is_none")]
375    pub caption_entities: Option<Vec<crate::message::MessageEntity>>,
376    /// `true` if the caption must be shown above the media.
377    #[serde(skip_serializing_if = "Option::is_none")]
378    pub show_caption_above_media: Option<bool>,
379    /// Animation width in pixels.
380    #[serde(skip_serializing_if = "Option::is_none")]
381    pub width: Option<u32>,
382    /// Animation height in pixels.
383    #[serde(skip_serializing_if = "Option::is_none")]
384    pub height: Option<u32>,
385    /// Animation duration in seconds.
386    #[serde(skip_serializing_if = "Option::is_none")]
387    pub duration: Option<u32>,
388    /// `true` if the animation needs a spoiler animation.
389    #[serde(skip_serializing_if = "Option::is_none")]
390    pub has_spoiler: Option<bool>,
391}
392
393/// An audio file to include in a media group.
394#[derive(Debug, Clone, Serialize, Deserialize)]
395pub struct InputMediaAudio {
396    /// File to send: `file_id`, HTTP URL, or `"attach://<n>"`.
397    pub media: String,
398    /// Thumbnail: `file_id` or `"attach://<n>"`.
399    #[serde(skip_serializing_if = "Option::is_none")]
400    pub thumbnail: Option<String>,
401    /// Caption (0–1024 characters after entities parsing).
402    #[serde(skip_serializing_if = "Option::is_none")]
403    pub caption: Option<String>,
404    /// Parse mode for the caption.
405    #[serde(skip_serializing_if = "Option::is_none")]
406    pub parse_mode: Option<crate::message::ParseMode>,
407    /// Special entities in the caption.
408    #[serde(skip_serializing_if = "Option::is_none")]
409    pub caption_entities: Option<Vec<crate::message::MessageEntity>>,
410    /// Audio duration in seconds.
411    #[serde(skip_serializing_if = "Option::is_none")]
412    pub duration: Option<u32>,
413    /// Performer of the audio.
414    #[serde(skip_serializing_if = "Option::is_none")]
415    pub performer: Option<String>,
416    /// Title of the audio.
417    #[serde(skip_serializing_if = "Option::is_none")]
418    pub title: Option<String>,
419}
420
421/// A document (general file) to include in a media group.
422#[derive(Debug, Clone, Serialize, Deserialize)]
423pub struct InputMediaDocument {
424    /// File to send: `file_id`, HTTP URL, or `"attach://<n>"`.
425    pub media: String,
426    /// Thumbnail: `file_id` or `"attach://<n>"`.
427    #[serde(skip_serializing_if = "Option::is_none")]
428    pub thumbnail: Option<String>,
429    /// Caption (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.
436    #[serde(skip_serializing_if = "Option::is_none")]
437    pub caption_entities: Option<Vec<crate::message::MessageEntity>>,
438    /// `true` to disable automatic server-side content type detection.
439    #[serde(skip_serializing_if = "Option::is_none")]
440    pub disable_content_type_detection: Option<bool>,
441}
442
443/// A live photo to be sent as part of a media group or to replace an existing media message.
444///
445/// Sending live photos by URL is currently unsupported — use `file_id` or `attach://<n>`.
446#[derive(Debug, Clone, Serialize, Deserialize)]
447pub struct InputMediaLivePhoto {
448    /// Video of the live photo: `file_id` or `"attach://<n>"`.
449    pub media: String,
450    /// The static photo: `file_id` or `"attach://<n>"`.
451    pub photo: String,
452    /// Caption (0–1024 characters after entities parsing).
453    #[serde(skip_serializing_if = "Option::is_none")]
454    pub caption: Option<String>,
455    /// Parse mode for the caption.
456    #[serde(skip_serializing_if = "Option::is_none")]
457    pub parse_mode: Option<crate::message::ParseMode>,
458    /// Special entities in the caption.
459    #[serde(skip_serializing_if = "Option::is_none")]
460    pub caption_entities: Option<Vec<crate::message::MessageEntity>>,
461    /// `true` if the caption must be shown above the media.
462    #[serde(skip_serializing_if = "Option::is_none")]
463    pub show_caption_above_media: Option<bool>,
464    /// `true` if the live photo needs a spoiler animation.
465    #[serde(skip_serializing_if = "Option::is_none")]
466    pub has_spoiler: Option<bool>,
467}
468
469/// A location to be sent as poll media.
470#[derive(Debug, Clone, Serialize, Deserialize)]
471pub struct InputMediaLocation {
472    /// Latitude of the location.
473    pub latitude: f64,
474    /// Longitude of the location.
475    pub longitude: f64,
476    /// Radius of uncertainty in metres (0–1500).
477    #[serde(skip_serializing_if = "Option::is_none")]
478    pub horizontal_accuracy: Option<f64>,
479}
480
481/// A venue to be sent as poll media.
482#[derive(Debug, Clone, Serialize, Deserialize)]
483pub struct InputMediaVenue {
484    /// Latitude of the venue.
485    pub latitude: f64,
486    /// Longitude of the venue.
487    pub longitude: f64,
488    /// Name of the venue.
489    pub title: String,
490    /// Address of the venue.
491    pub address: String,
492    /// Foursquare identifier of the venue.
493    #[serde(skip_serializing_if = "Option::is_none")]
494    pub foursquare_id: Option<String>,
495    /// Foursquare type of the venue.
496    #[serde(skip_serializing_if = "Option::is_none")]
497    pub foursquare_type: Option<String>,
498    /// Google Places identifier of the venue.
499    #[serde(skip_serializing_if = "Option::is_none")]
500    pub google_place_id: Option<String>,
501    /// Google Places type of the venue.
502    #[serde(skip_serializing_if = "Option::is_none")]
503    pub google_place_type: Option<String>,
504}
505
506/// A sticker file to be sent as poll option media.
507#[derive(Debug, Clone, Serialize, Deserialize)]
508pub struct InputMediaSticker {
509    /// File to send: `file_id`, HTTP URL, or `"attach://<n>"`.
510    pub media: String,
511    /// Emoji associated with the sticker; only for just-uploaded stickers.
512    #[serde(skip_serializing_if = "Option::is_none")]
513    pub emoji: Option<String>,
514}
515
516/// The media content of a message to be sent or edited.
517///
518/// Serialise with `serde_json::to_value(&media)` to pass to `sendMediaGroup`
519/// or `editMessageMedia` until the API methods are updated to accept this type
520/// directly.
521#[derive(Debug, Clone, Serialize, Deserialize)]
522#[serde(tag = "type", rename_all = "snake_case")]
523pub enum InputMedia {
524    /// A photo.
525    Photo(InputMediaPhoto),
526    /// A video.
527    Video(InputMediaVideo),
528    /// An animation (GIF or silent H.264).
529    Animation(InputMediaAnimation),
530    /// An audio file treated as music.
531    Audio(InputMediaAudio),
532    /// A general document.
533    Document(InputMediaDocument),
534    /// A live photo.
535    LivePhoto(InputMediaLivePhoto),
536}
537
538// ─── InputPaidMedia ───────────────────────────────────────────────────────────
539
540/// A photo to send as paid media.
541#[derive(Debug, Clone, Serialize, Deserialize)]
542pub struct InputPaidMediaPhoto {
543    /// File to send: `file_id`, HTTP URL, or `"attach://<n>"`.
544    pub media: String,
545}
546
547/// A video to send as paid media.
548#[derive(Debug, Clone, Serialize, Deserialize)]
549pub struct InputPaidMediaVideo {
550    /// File to send: `file_id`, HTTP URL, or `"attach://<n>"`.
551    pub media: String,
552    /// Thumbnail: `file_id` or `"attach://<n>"`.
553    #[serde(skip_serializing_if = "Option::is_none")]
554    pub thumbnail: Option<String>,
555    /// Cover image: `file_id`, HTTP URL, or `"attach://<n>"`.
556    #[serde(skip_serializing_if = "Option::is_none")]
557    pub cover: Option<String>,
558    /// Start timestamp for the video in the message.
559    #[serde(skip_serializing_if = "Option::is_none")]
560    pub start_timestamp: Option<i64>,
561    /// Video width in pixels.
562    #[serde(skip_serializing_if = "Option::is_none")]
563    pub width: Option<u32>,
564    /// Video height in pixels.
565    #[serde(skip_serializing_if = "Option::is_none")]
566    pub height: Option<u32>,
567    /// Video duration in seconds.
568    #[serde(skip_serializing_if = "Option::is_none")]
569    pub duration: Option<u32>,
570    /// `true` if the uploaded video is suitable for streaming.
571    #[serde(skip_serializing_if = "Option::is_none")]
572    pub supports_streaming: Option<bool>,
573}
574
575/// A live photo to send as paid media.
576///
577/// Both video and static photo must be `file_id` or `attach://<n>`.
578/// Sending live photos by URL is currently unsupported.
579#[derive(Debug, Clone, Serialize, Deserialize)]
580pub struct InputPaidMediaLivePhoto {
581    /// Video of the live photo: `file_id` or `"attach://<n>"`.
582    pub media: String,
583    /// The static photo: `file_id` or `"attach://<n>"`.
584    pub photo: String,
585}
586
587/// Paid media to send via `sendPaidMedia`.
588///
589/// Serialise with `serde_json::to_value(&media)` to pass to `sendPaidMedia`
590/// until the method is updated to accept this type directly.
591#[derive(Debug, Clone, Serialize, Deserialize)]
592#[serde(tag = "type", rename_all = "snake_case")]
593pub enum InputPaidMedia {
594    /// A photo.
595    Photo(InputPaidMediaPhoto),
596    /// A video.
597    Video(InputPaidMediaVideo),
598    /// A live photo.
599    LivePhoto(InputPaidMediaLivePhoto),
600}
601
602// ─── InputProfilePhoto ────────────────────────────────────────────────────────
603
604/// A static profile photo in JPEG format.
605#[derive(Debug, Clone, Serialize, Deserialize)]
606pub struct InputProfilePhotoStatic {
607    /// The photo file.
608    ///
609    /// Profile photos cannot be reused — upload as a new file using
610    /// `"attach://<n>"` via multipart/form-data.
611    pub photo: String,
612}
613
614/// An animated profile photo in MPEG4 format.
615#[derive(Debug, Clone, Serialize, Deserialize)]
616pub struct InputProfilePhotoAnimated {
617    /// The animation file.
618    ///
619    /// Profile photos cannot be reused — upload as a new file using
620    /// `"attach://<n>"` via multipart/form-data.
621    pub animation: String,
622    /// Timestamp in seconds of the frame to use as the static profile photo.
623    /// Defaults to `0.0`.
624    #[serde(skip_serializing_if = "Option::is_none")]
625    pub main_frame_timestamp: Option<f64>,
626}
627
628/// A profile photo to set via `setMyProfilePhoto` or `setBusinessAccountProfilePhoto`.
629///
630/// Serialise with `serde_json::to_string(&photo)` to pass to the relevant method.
631#[derive(Debug, Clone, Serialize, Deserialize)]
632#[serde(tag = "type", rename_all = "snake_case")]
633pub enum InputProfilePhoto {
634    /// A static JPEG profile photo.
635    Static(InputProfilePhotoStatic),
636    /// An animated MPEG4 profile photo.
637    Animated(InputProfilePhotoAnimated),
638}