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