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