use crate::{
request::FormValue,
types::{InputFile, InputFileKind, InputMediaAudio, InputMediaDocument, InputMediaPhoto, InputMediaVideo},
};
use serde::Serialize;
use serde_json::Error as JsonError;
use std::{collections::HashMap, error::Error as StdError, fmt};
const MIN_GROUP_ATTACHMENTS: usize = 2;
const MAX_GROUP_ATTACHMENTS: usize = 10;
#[derive(Debug, Default)]
pub struct MediaGroup {
files: HashMap<String, InputFile>,
items: Vec<MediaGroupItem>,
}
impl MediaGroup {
pub fn add_item<I, F>(mut self, file: F, info: I) -> Self
where
MediaGroupItem: From<(String, I)>,
F: Into<InputFile>,
{
let file = self.add_file(file.into());
self.items.push(MediaGroupItem::from((file, info)));
self
}
pub fn add_item_with_thumb<F, T, I>(mut self, file: F, thumb: T, info: I) -> Self
where
F: Into<InputFile>,
T: Into<InputFile>,
MediaGroupItem: From<(String, String, I)>,
{
let file = self.add_file(file.into());
let thumb = self.add_file(thumb.into());
self.items.push(MediaGroupItem::from((file, thumb, info)));
self
}
fn add_file(&mut self, file: InputFile) -> String {
match &file.kind {
InputFileKind::Id(text) | InputFileKind::Url(text) => text.clone(),
_ => {
let idx = self.files.len();
let key = format!("tgbot_im_file_{}", idx);
self.files.insert(key.clone(), file);
format!("attach://{}", key)
}
}
}
pub(crate) fn into_form(self) -> Result<HashMap<String, FormValue>, MediaGroupError> {
let total_files = self.items.len();
if total_files < MIN_GROUP_ATTACHMENTS {
return Err(MediaGroupError::NotEnoughAttachments(MIN_GROUP_ATTACHMENTS));
}
if total_files > MAX_GROUP_ATTACHMENTS {
return Err(MediaGroupError::TooManyAttachments(MAX_GROUP_ATTACHMENTS));
}
let mut fields: HashMap<String, FormValue> = self.files.into_iter().map(|(k, v)| (k, v.into())).collect();
fields.insert(
String::from("media"),
serde_json::to_string(&self.items)
.map_err(MediaGroupError::Serialize)?
.into(),
);
Ok(fields)
}
}
#[derive(Debug, Serialize)]
#[doc(hidden)]
#[serde(tag = "type")]
#[serde(rename_all = "lowercase")]
pub enum MediaGroupItem {
Audio {
media: String,
#[serde(skip_serializing_if = "Option::is_none")]
thumb: Option<String>,
#[serde(flatten)]
info: InputMediaAudio,
},
Document {
media: String,
#[serde(skip_serializing_if = "Option::is_none")]
thumb: Option<String>,
#[serde(flatten)]
info: InputMediaDocument,
},
Photo {
media: String,
#[serde(flatten)]
info: InputMediaPhoto,
},
Video {
media: String,
#[serde(skip_serializing_if = "Option::is_none")]
thumb: Option<String>,
#[serde(flatten)]
info: InputMediaVideo,
},
}
impl From<(String, InputMediaAudio)> for MediaGroupItem {
fn from((media, info): (String, InputMediaAudio)) -> Self {
MediaGroupItem::Audio {
media,
info,
thumb: None,
}
}
}
impl From<(String, String, InputMediaAudio)> for MediaGroupItem {
fn from((media, thumb, info): (String, String, InputMediaAudio)) -> Self {
MediaGroupItem::Audio {
media,
info,
thumb: Some(thumb),
}
}
}
impl From<(String, InputMediaDocument)> for MediaGroupItem {
fn from((media, info): (String, InputMediaDocument)) -> Self {
MediaGroupItem::Document {
media,
info,
thumb: None,
}
}
}
impl From<(String, String, InputMediaDocument)> for MediaGroupItem {
fn from((media, thumb, info): (String, String, InputMediaDocument)) -> Self {
MediaGroupItem::Document {
media,
info,
thumb: Some(thumb),
}
}
}
impl From<(String, InputMediaPhoto)> for MediaGroupItem {
fn from((media, info): (String, InputMediaPhoto)) -> Self {
MediaGroupItem::Photo { media, info }
}
}
impl From<(String, InputMediaVideo)> for MediaGroupItem {
fn from((media, info): (String, InputMediaVideo)) -> Self {
MediaGroupItem::Video {
media,
info,
thumb: None,
}
}
}
impl From<(String, String, InputMediaVideo)> for MediaGroupItem {
fn from((media, thumb, info): (String, String, InputMediaVideo)) -> Self {
MediaGroupItem::Video {
media,
info,
thumb: Some(thumb),
}
}
}
#[derive(Debug)]
pub enum MediaGroupError {
NotEnoughAttachments(usize),
TooManyAttachments(usize),
Serialize(JsonError),
}
impl StdError for MediaGroupError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match self {
MediaGroupError::Serialize(err) => Some(err),
_ => None,
}
}
}
impl fmt::Display for MediaGroupError {
fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
match self {
MediaGroupError::NotEnoughAttachments(number) => {
write!(out, "media group must contain at least {} attachments", number)
}
MediaGroupError::TooManyAttachments(number) => {
write!(out, "media group must contain no more than {} attachments", number)
}
MediaGroupError::Serialize(err) => write!(out, "can not serialize media group items: {}", err),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::InputFileReader;
use std::io::Cursor;
#[test]
fn media_group() {
let group = MediaGroup::default()
.add_item(InputFileReader::from(Cursor::new("test")), InputMediaAudio::default())
.add_item(
InputFileReader::from(Cursor::new("test")),
InputMediaDocument::default(),
)
.add_item(InputFileReader::from(Cursor::new("test")), InputMediaPhoto::default())
.add_item(InputFileReader::from(Cursor::new("test")), InputMediaVideo::default())
.add_item_with_thumb(
InputFile::file_id("file-id"),
InputFile::url("thumb-url"),
InputMediaAudio::default(),
)
.add_item_with_thumb(
InputFile::file_id("file-id"),
InputFile::url("thumb-url"),
InputMediaDocument::default(),
)
.add_item_with_thumb(
InputFile::file_id("file-id"),
InputFile::url("thumb-url"),
InputMediaVideo::default(),
)
.into_form()
.unwrap();
let media: &str = group.get("media").unwrap().get_text().unwrap();
assert_eq!(
serde_json::from_str::<serde_json::Value>(media).unwrap(),
serde_json::json!([
{
"media": "attach://tgbot_im_file_0",
"type": "audio"
},
{
"media": "attach://tgbot_im_file_1",
"type": "document"
},
{
"media": "attach://tgbot_im_file_2",
"type": "photo"
},
{
"media": "attach://tgbot_im_file_3",
"type": "video"
},
{
"media": "file-id",
"thumb": "thumb-url",
"type": "audio"
},
{
"media": "file-id",
"thumb": "thumb-url",
"type": "document"
},
{
"media": "file-id",
"thumb": "thumb-url",
"type": "video"
}
])
);
assert!(group.get("tgbot_im_file_0").is_some());
assert!(group.get("tgbot_im_file_1").is_some());
assert!(group.get("tgbot_im_file_2").is_some());
assert!(group.get("tgbot_im_file_3").is_some());
let err = MediaGroup::default().into_form().unwrap_err();
assert_eq!(err.to_string(), "media group must contain at least 2 attachments");
let mut group = MediaGroup::default();
for _ in 0..11 {
group = group.add_item(InputFile::file_id("file-id"), InputMediaPhoto::default());
}
let err = group.into_form().unwrap_err();
assert_eq!(err.to_string(), "media group must contain no more than 10 attachments");
}
#[test]
fn media_group_item() {
assert_eq!(
serde_json::to_value(MediaGroupItem::from((
String::from("file-id"),
String::from("thumb-id"),
InputMediaVideo::default().caption("test"),
)))
.unwrap(),
serde_json::json!({
"type": "video",
"media": "file-id",
"thumb": "thumb-id",
"caption": "test"
})
);
assert_eq!(
serde_json::to_value(MediaGroupItem::from((
String::from("file-id"),
InputMediaVideo::default().caption("test")
)))
.unwrap(),
serde_json::json!({
"type": "video",
"media": "file-id",
"caption": "test"
})
);
assert_eq!(
serde_json::to_value(MediaGroupItem::from((
String::from("file-id"),
InputMediaPhoto::default().caption("test")
)))
.unwrap(),
serde_json::json!({
"type": "photo",
"media": "file-id",
"caption": "test"
})
);
}
}