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#[derive(Debug)]
30pub struct MediaGroup {
31 form: Form,
32}
33
34impl MediaGroup {
35 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#[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 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 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 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 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 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 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#[derive(Debug)]
247pub enum MediaGroupError {
248 NotEnoughAttachments(usize),
250 TooManyAttachments(usize),
252 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#[derive(Debug)]
281pub struct SendMediaGroup {
282 form: Form,
283}
284
285impl SendMediaGroup {
286 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 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 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 pub fn with_disable_notification(mut self, value: bool) -> Self {
331 self.form.insert_field("disable_notification", value);
332 self
333 }
334
335 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 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 pub fn with_protect_content(mut self, value: bool) -> Self {
366 self.form.insert_field("protect_content", value);
367 self
368 }
369
370 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}