Skip to main content

tgbot/types/definitions/media/
group.rs

1use std::{error::Error, fmt};
2
3use serde::Serialize;
4use serde_json::Error as JsonError;
5
6use crate::{
7    api::{Form, Method, Payload},
8    types::{
9        ChatId,
10        InputFile,
11        InputMediaAudio,
12        InputMediaDocument,
13        InputMediaLivePhoto,
14        InputMediaPhoto,
15        InputMediaVideo,
16        Integer,
17        Message,
18        ReplyParameters,
19        ReplyParametersError,
20    },
21};
22
23const MIN_GROUP_ATTACHMENTS: usize = 2;
24const MAX_GROUP_ATTACHMENTS: usize = 10;
25
26/// Represents a group of input media to be sent.
27#[derive(Debug)]
28pub struct MediaGroup {
29    form: Form,
30}
31
32impl MediaGroup {
33    /// Creates a new `MediaGroup`.
34    ///
35    /// # Arguments
36    ///
37    /// * `items` - Items of the group.
38    pub fn new<T>(items: T) -> Result<Self, MediaGroupError>
39    where
40        T: IntoIterator<Item = MediaGroupItem>,
41    {
42        let items: Vec<(usize, MediaGroupItem)> = items.into_iter().enumerate().collect();
43
44        let total_items = items.len();
45        if total_items < MIN_GROUP_ATTACHMENTS {
46            return Err(MediaGroupError::NotEnoughAttachments(MIN_GROUP_ATTACHMENTS));
47        }
48        if total_items > MAX_GROUP_ATTACHMENTS {
49            return Err(MediaGroupError::TooManyAttachments(MAX_GROUP_ATTACHMENTS));
50        }
51
52        let mut form = Form::default();
53
54        let mut add_file = |key: String, file: InputFile| -> String {
55            match &file {
56                InputFile::Id(text) | InputFile::Url(text) => text.clone(),
57                _ => {
58                    form.insert_field(&key, file);
59                    format!("attach://{key}")
60                }
61            }
62        };
63
64        let mut info = Vec::new();
65        for (idx, item) in items {
66            let media = add_file(format!("tgbot_im_file_{idx}"), item.file);
67            let thumbnail = item
68                .thumbnail
69                .map(|thumbnail| add_file(format!("tgbot_im_thumb_{idx}"), thumbnail));
70            let data = match item.item_type {
71                MediaGroupItemType::Audio(info) => MediaGroupItemData::Audio { media, thumbnail, info },
72                MediaGroupItemType::Document(info) => MediaGroupItemData::Document { media, thumbnail, info },
73                MediaGroupItemType::LivePhoto(photo, info) => {
74                    let photo = add_file(format!("tgbot_im_live_photo_{idx}"), photo);
75                    MediaGroupItemData::LivePhoto { media, photo, info }
76                }
77                MediaGroupItemType::Photo(info) => MediaGroupItemData::Photo { media, info },
78                MediaGroupItemType::Video(info) => MediaGroupItemData::Video {
79                    media,
80                    cover: item.cover.map(|cover| add_file(format!("tgbot_im_cover_{idx}"), cover)),
81                    thumbnail,
82                    info,
83                },
84            };
85            info.push(data);
86        }
87
88        form.insert_field(
89            "media",
90            serde_json::to_string(&info).map_err(MediaGroupError::Serialize)?,
91        );
92
93        Ok(Self { form })
94    }
95}
96
97impl From<MediaGroup> for Form {
98    fn from(group: MediaGroup) -> Self {
99        group.form
100    }
101}
102
103/// Represents a media group item.
104#[derive(Debug)]
105pub struct MediaGroupItem {
106    file: InputFile,
107    item_type: MediaGroupItemType,
108    cover: Option<InputFile>,
109    thumbnail: Option<InputFile>,
110}
111
112impl MediaGroupItem {
113    /// Creates a `MediaGroupItem` for an audio.
114    ///
115    /// # Arguments
116    ///
117    /// * `file` - File to attach.
118    /// * `metadata` - Metadata.
119    pub fn for_audio<T>(file: T, metadata: InputMediaAudio) -> Self
120    where
121        T: Into<InputFile>,
122    {
123        Self::new(file, MediaGroupItemType::Audio(metadata))
124    }
125
126    /// Creates a `MediaGroupItem` for a document.
127    ///
128    /// # Arguments
129    ///
130    /// * `file` - File to attach.
131    /// * `metadata` - Metadata.
132    pub fn for_document<T>(file: T, metadata: InputMediaDocument) -> Self
133    where
134        T: Into<InputFile>,
135    {
136        Self::new(file, MediaGroupItemType::Document(metadata))
137    }
138
139    /// Creates a `MediaGroupItem` for a live photo.
140    ///
141    /// # Arguments
142    ///
143    /// * `file` - File to attach.
144    /// * `photo` - Static photo.
145    pub fn for_live_photo<A, B>(file: A, photo: B, metadata: InputMediaLivePhoto) -> Self
146    where
147        A: Into<InputFile>,
148        B: Into<InputFile>,
149    {
150        Self::new(file, MediaGroupItemType::LivePhoto(photo.into(), metadata))
151    }
152
153    /// Creates a `MediaGroupItem` for a photo.
154    ///
155    /// # Arguments
156    ///
157    /// * `file` - File to attach.
158    /// * `metadata` - Metadata.
159    pub fn for_photo<T>(file: T, metadata: InputMediaPhoto) -> Self
160    where
161        T: Into<InputFile>,
162    {
163        Self::new(file, MediaGroupItemType::Photo(metadata))
164    }
165
166    /// Creates a `MediaGroupItem` for a video.
167    ///
168    /// # Arguments
169    ///
170    /// * `file` - File to attach.
171    /// * `metadata` - Metadata.
172    pub fn for_video<T>(file: T, metadata: InputMediaVideo) -> Self
173    where
174        T: Into<InputFile>,
175    {
176        Self::new(file, MediaGroupItemType::Video(metadata))
177    }
178
179    /// Sets a new cover.
180    ///
181    /// # Arguments
182    ///
183    /// * `value` - Cover.
184    ///
185    /// Note that the cover is ignored when the media type is not a video.
186    pub fn with_cover<T>(mut self, value: T) -> Self
187    where
188        T: Into<InputFile>,
189    {
190        self.cover = Some(value.into());
191        self
192    }
193
194    /// Sets a new thumbnail.
195    ///
196    /// # Arguments
197    ///
198    /// * `value` - Thumbnail.
199    ///
200    /// Note that photo can not have thumbnail and it will be ignored.
201    pub fn with_thumbnail<T>(mut self, value: T) -> Self
202    where
203        T: Into<InputFile>,
204    {
205        self.thumbnail = Some(value.into());
206        self
207    }
208
209    fn new<T>(file: T, item_type: MediaGroupItemType) -> Self
210    where
211        T: Into<InputFile>,
212    {
213        Self {
214            item_type,
215            file: file.into(),
216            cover: None,
217            thumbnail: None,
218        }
219    }
220}
221
222#[derive(Debug)]
223enum MediaGroupItemType {
224    Audio(InputMediaAudio),
225    Document(InputMediaDocument),
226    LivePhoto(InputFile, InputMediaLivePhoto),
227    Photo(InputMediaPhoto),
228    Video(InputMediaVideo),
229}
230
231#[serde_with::skip_serializing_none]
232#[derive(Debug, Serialize)]
233#[serde(tag = "type")]
234#[serde(rename_all = "lowercase")]
235enum MediaGroupItemData {
236    Audio {
237        media: String,
238        thumbnail: Option<String>,
239        #[serde(flatten)]
240        info: InputMediaAudio,
241    },
242    Document {
243        media: String,
244        thumbnail: Option<String>,
245        #[serde(flatten)]
246        info: InputMediaDocument,
247    },
248    LivePhoto {
249        media: String,
250        photo: String,
251        #[serde(flatten)]
252        info: InputMediaLivePhoto,
253    },
254    Photo {
255        media: String,
256        #[serde(flatten)]
257        info: InputMediaPhoto,
258    },
259    Video {
260        media: String,
261        cover: Option<String>,
262        thumbnail: Option<String>,
263        #[serde(flatten)]
264        info: InputMediaVideo,
265    },
266}
267
268/// Represents a media group error.
269#[derive(Debug)]
270pub enum MediaGroupError {
271    /// Media group contains not enough files.
272    NotEnoughAttachments(usize),
273    /// Media group contains too many files.
274    TooManyAttachments(usize),
275    /// Can not serialize items.
276    Serialize(JsonError),
277}
278
279impl Error for MediaGroupError {
280    fn source(&self) -> Option<&(dyn Error + 'static)> {
281        match self {
282            MediaGroupError::Serialize(err) => Some(err),
283            _ => None,
284        }
285    }
286}
287
288impl fmt::Display for MediaGroupError {
289    fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
290        match self {
291            MediaGroupError::NotEnoughAttachments(number) => {
292                write!(out, "media group must contain at least {number} attachments")
293            }
294            MediaGroupError::TooManyAttachments(number) => {
295                write!(out, "media group must contain no more than {number} attachments")
296            }
297            MediaGroupError::Serialize(err) => write!(out, "can not serialize media group items: {err}"),
298        }
299    }
300}
301
302/// Sends a group of photos or videos as an album.
303#[derive(Debug)]
304pub struct SendMediaGroup {
305    form: Form,
306}
307
308impl SendMediaGroup {
309    /// Creates a new `SendMediaGroup`.
310    ///
311    /// * `chat_id` - Unique identifier of the target chat.
312    /// * `media` - Photos and videos to be sent; 2–10 items.
313    pub fn new<T>(chat_id: T, media: MediaGroup) -> Self
314    where
315        T: Into<ChatId>,
316    {
317        let mut form: Form = media.into();
318        form.insert_field("chat_id", chat_id.into());
319        Self { form }
320    }
321
322    /// Sets a new value for the `allow_paid_broadcast` flag.
323    ///
324    /// # Arguments
325    ///
326    /// * `value` - Whether to allow up to 1000 messages per second, ignoring broadcasting limits
327    ///   for a fee of 0.1 Telegram Stars per message.
328    ///   The relevant Stars will be withdrawn from the bot's balance.
329    pub fn with_allow_paid_broadcast(mut self, value: bool) -> Self {
330        self.form.insert_field("allow_paid_broadcast", value);
331        self
332    }
333
334    /// Sets a new business connection ID.
335    ///
336    /// # Arguments
337    ///
338    /// * `value` - Unique identifier of the business connection.
339    pub fn with_business_connection_id<T>(mut self, value: T) -> Self
340    where
341        T: Into<String>,
342    {
343        self.form.insert_field("business_connection_id", value.into());
344        self
345    }
346
347    /// Sets a new direct messages topic ID
348    ///
349    /// * `value` - Identifier of the direct messages topic to which the message will be sent.
350    ///
351    /// Required if the message is sent to a direct messages chat.
352    pub fn with_direct_messages_topic_id(mut self, value: Integer) -> Self {
353        self.form.insert_field("direct_messages_topic_id", value);
354        self
355    }
356
357    /// Sets a new value for the `disable_notification` flag.
358    ///
359    /// # Arguments
360    ///
361    /// * `value` - Indicates whether to send the message silently or not;
362    ///   a user will receive a notification without sound.
363    pub fn with_disable_notification(mut self, value: bool) -> Self {
364        self.form.insert_field("disable_notification", value);
365        self
366    }
367
368    /// Sets a new message effect ID.
369    ///
370    /// # Arguments
371    ///
372    /// * `value` - Unique identifier of the message effect to be added to the message; for private chats only.
373    pub fn with_message_effect_id<T>(mut self, value: T) -> Self
374    where
375        T: Into<String>,
376    {
377        self.form.insert_field("message_effect_id", value.into());
378        self
379    }
380
381    /// Sets a new message thread ID.
382    ///
383    /// # Arguments
384    ///
385    /// * `value` - Unique identifier of the target message thread;
386    ///   for forum supergroups and private chats of bots with forum topic mode enabled only.
387    pub fn with_message_thread_id(mut self, value: Integer) -> Self {
388        self.form.insert_field("message_thread_id", value);
389        self
390    }
391
392    /// Sets a new value for the `protect_content` flag.
393    ///
394    /// # Arguments
395    ///
396    /// * `value` - Indicates whether to protect the contents
397    ///   of the sent message from forwarding and saving.
398    pub fn with_protect_content(mut self, value: bool) -> Self {
399        self.form.insert_field("protect_content", value);
400        self
401    }
402
403    /// Sets new reply parameters.
404    ///
405    /// # Arguments
406    ///
407    /// * `value` - Description of the message to reply to.
408    pub fn with_reply_parameters(mut self, value: ReplyParameters) -> Result<Self, ReplyParametersError> {
409        self.form.insert_field("reply_parameters", value.serialize()?);
410        Ok(self)
411    }
412}
413
414impl Method for SendMediaGroup {
415    type Response = Vec<Message>;
416
417    fn into_payload(self) -> Payload {
418        Payload::form("sendMediaGroup", self.form)
419    }
420}