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.cover.map(|cover| add_file(format!("tgbot_im_cover_{idx}"), cover)),
79                    thumbnail,
80                    info,
81                },
82            };
83            info.push(data);
84        }
85
86        form.insert_field(
87            "media",
88            serde_json::to_string(&info).map_err(MediaGroupError::Serialize)?,
89        );
90
91        Ok(Self { form })
92    }
93}
94
95impl From<MediaGroup> for Form {
96    fn from(group: MediaGroup) -> Self {
97        group.form
98    }
99}
100
101/// Represents a media group item.
102#[derive(Debug)]
103pub struct MediaGroupItem {
104    file: InputFile,
105    item_type: MediaGroupItemType,
106    cover: Option<InputFile>,
107    thumbnail: Option<InputFile>,
108}
109
110impl MediaGroupItem {
111    /// Creates a `MediaGroupItem` for an audio.
112    ///
113    /// # Arguments
114    ///
115    /// * `file` - File to attach.
116    /// * `metadata` - Metadata.
117    pub fn for_audio<T>(file: T, metadata: InputMediaAudio) -> Self
118    where
119        T: Into<InputFile>,
120    {
121        Self::new(file, MediaGroupItemType::Audio(metadata))
122    }
123
124    /// Creates a `MediaGroupItem` for a document.
125    ///
126    /// # Arguments
127    ///
128    /// * `file` - File to attach.
129    /// * `metadata` - Metadata.
130    pub fn for_document<T>(file: T, metadata: InputMediaDocument) -> Self
131    where
132        T: Into<InputFile>,
133    {
134        Self::new(file, MediaGroupItemType::Document(metadata))
135    }
136
137    /// Creates a `MediaGroupItem` for a photo.
138    ///
139    /// # Arguments
140    ///
141    /// * `file` - File to attach.
142    /// * `metadata` - Metadata.
143    pub fn for_photo<T>(file: T, metadata: InputMediaPhoto) -> Self
144    where
145        T: Into<InputFile>,
146    {
147        Self::new(file, MediaGroupItemType::Photo(metadata))
148    }
149
150    /// Creates a `MediaGroupItem` for a video.
151    ///
152    /// # Arguments
153    ///
154    /// * `file` - File to attach.
155    /// * `metadata` - Metadata.
156    pub fn for_video<T>(file: T, metadata: InputMediaVideo) -> Self
157    where
158        T: Into<InputFile>,
159    {
160        Self::new(file, MediaGroupItemType::Video(metadata))
161    }
162
163    /// Sets a new cover.
164    ///
165    /// # Arguments
166    ///
167    /// * `value` - Cover.
168    ///
169    /// Note that the cover is ignored when the media type is not a video.
170    pub fn with_cover<T>(mut self, value: T) -> Self
171    where
172        T: Into<InputFile>,
173    {
174        self.cover = Some(value.into());
175        self
176    }
177
178    /// Sets a new thumbnail.
179    ///
180    /// # Arguments
181    ///
182    /// * `value` - Thumbnail.
183    ///
184    /// Note that photo can not have thumbnail and it will be ignored.
185    pub fn with_thumbnail<T>(mut self, value: T) -> Self
186    where
187        T: Into<InputFile>,
188    {
189        self.thumbnail = Some(value.into());
190        self
191    }
192
193    fn new<T>(file: T, item_type: MediaGroupItemType) -> Self
194    where
195        T: Into<InputFile>,
196    {
197        Self {
198            item_type,
199            file: file.into(),
200            cover: None,
201            thumbnail: None,
202        }
203    }
204}
205
206#[derive(Debug)]
207enum MediaGroupItemType {
208    Audio(InputMediaAudio),
209    Document(InputMediaDocument),
210    Photo(InputMediaPhoto),
211    Video(InputMediaVideo),
212}
213
214#[serde_with::skip_serializing_none]
215#[derive(Debug, Serialize)]
216#[serde(tag = "type")]
217#[serde(rename_all = "lowercase")]
218enum MediaGroupItemData {
219    Audio {
220        media: String,
221        thumbnail: Option<String>,
222        #[serde(flatten)]
223        info: InputMediaAudio,
224    },
225    Document {
226        media: String,
227        thumbnail: Option<String>,
228        #[serde(flatten)]
229        info: InputMediaDocument,
230    },
231    Photo {
232        media: String,
233        #[serde(flatten)]
234        info: InputMediaPhoto,
235    },
236    Video {
237        media: String,
238        cover: Option<String>,
239        thumbnail: Option<String>,
240        #[serde(flatten)]
241        info: InputMediaVideo,
242    },
243}
244
245/// Represents a media group error.
246#[derive(Debug)]
247pub enum MediaGroupError {
248    /// Media group contains not enough files.
249    NotEnoughAttachments(usize),
250    /// Media group contains too many files.
251    TooManyAttachments(usize),
252    /// Can not serialize items.
253    Serialize(JsonError),
254}
255
256impl Error for MediaGroupError {
257    fn source(&self) -> Option<&(dyn Error + 'static)> {
258        match self {
259            MediaGroupError::Serialize(err) => Some(err),
260            _ => None,
261        }
262    }
263}
264
265impl fmt::Display for MediaGroupError {
266    fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
267        match self {
268            MediaGroupError::NotEnoughAttachments(number) => {
269                write!(out, "media group must contain at least {number} attachments")
270            }
271            MediaGroupError::TooManyAttachments(number) => {
272                write!(out, "media group must contain no more than {number} attachments")
273            }
274            MediaGroupError::Serialize(err) => write!(out, "can not serialize media group items: {err}"),
275        }
276    }
277}
278
279/// Sends a group of photos or videos as an album.
280#[derive(Debug)]
281pub struct SendMediaGroup {
282    form: Form,
283}
284
285impl SendMediaGroup {
286    /// Creates a new `SendMediaGroup`.
287    ///
288    /// * `chat_id` - Unique identifier of the target chat.
289    /// * `media` - Photos and videos to be sent; 2–10 items.
290    pub fn new<T>(chat_id: T, media: MediaGroup) -> Self
291    where
292        T: Into<ChatId>,
293    {
294        let mut form: Form = media.into();
295        form.insert_field("chat_id", chat_id.into());
296        Self { form }
297    }
298
299    /// Sets a new value for the `allow_paid_broadcast` flag.
300    ///
301    /// # Arguments
302    ///
303    /// * `value` - Whether to allow up to 1000 messages per second, ignoring broadcasting limits
304    ///   for a fee of 0.1 Telegram Stars per message.
305    ///   The relevant Stars will be withdrawn from the bot's balance.
306    pub fn with_allow_paid_broadcast(mut self, value: bool) -> Self {
307        self.form.insert_field("allow_paid_broadcast", value);
308        self
309    }
310
311    /// Sets a new business connection ID.
312    ///
313    /// # Arguments
314    ///
315    /// * `value` - Unique identifier of the business connection.
316    pub fn with_business_connection_id<T>(mut self, value: T) -> Self
317    where
318        T: Into<String>,
319    {
320        self.form.insert_field("business_connection_id", value.into());
321        self
322    }
323
324    /// Sets a new value for the `disable_notification` flag.
325    ///
326    /// # Arguments
327    ///
328    /// * `value` - Indicates whether to send the message silently or not;
329    ///   a user will receive a notification without sound.
330    pub fn with_disable_notification(mut self, value: bool) -> Self {
331        self.form.insert_field("disable_notification", value);
332        self
333    }
334
335    /// Sets a new message effect ID.
336    ///
337    /// # Arguments
338    ///
339    /// * `value` - Unique identifier of the message effect to be added to the message; for private chats only.
340    pub fn with_message_effect_id<T>(mut self, value: T) -> Self
341    where
342        T: Into<String>,
343    {
344        self.form.insert_field("message_effect_id", value.into());
345        self
346    }
347
348    /// Sets a new message thread ID.
349    ///
350    /// # Arguments
351    ///
352    /// * `value` - Unique identifier of the target message thread;
353    ///   supergroups only.
354    pub fn with_message_thread_id(mut self, value: Integer) -> Self {
355        self.form.insert_field("message_thread_id", value);
356        self
357    }
358
359    /// Sets a new value for the `protect_content` flag.
360    ///
361    /// # Arguments
362    ///
363    /// * `value` - Indicates whether to protect the contents
364    ///   of the sent message from forwarding and saving.
365    pub fn with_protect_content(mut self, value: bool) -> Self {
366        self.form.insert_field("protect_content", value);
367        self
368    }
369
370    /// Sets new reply parameters.
371    ///
372    /// # Arguments
373    ///
374    /// * `value` - Description of the message to reply to.
375    pub fn with_reply_parameters(mut self, value: ReplyParameters) -> Result<Self, ReplyParametersError> {
376        self.form.insert_field("reply_parameters", value.serialize()?);
377        Ok(self)
378    }
379}
380
381impl Method for SendMediaGroup {
382    type Response = Vec<Message>;
383
384    fn into_payload(self) -> Payload {
385        Payload::form("sendMediaGroup", self.form)
386    }
387}