use std::{error::Error, fmt};
use serde::{Deserialize, Serialize};
use serde_json::Error as JsonError;
use crate::{
api::{Form, Method, Payload},
types::{
ChatId,
InputFile,
Integer,
Message,
ParseMode,
PhotoSize,
ReplyMarkup,
ReplyMarkupError,
ReplyParameters,
ReplyParametersError,
TextEntities,
TextEntityError,
Video,
},
};
#[cfg(test)]
mod tests;
#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize)]
pub struct PaidMediaInfo {
pub star_count: Integer,
pub paid_media: Vec<PaidMedia>,
}
impl PaidMediaInfo {
pub fn new<A, B>(star_count: Integer, paid_media: A) -> Self
where
A: IntoIterator<Item = B>,
B: Into<PaidMedia>,
{
Self {
star_count,
paid_media: paid_media.into_iter().map(Into::into).collect(),
}
}
}
#[derive(Clone, Debug, derive_more::From, Deserialize, PartialEq, PartialOrd, Serialize)]
#[serde(from = "RawPaidMedia", into = "RawPaidMedia")]
pub enum PaidMedia {
Photo(Vec<PhotoSize>),
Preview(PaidMediaPreview),
Video(Video),
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, PartialOrd, Serialize)]
pub struct PaidMediaPreview {
#[serde(skip_serializing_if = "Option::is_none")]
pub duration: Option<Integer>,
#[serde(skip_serializing_if = "Option::is_none")]
pub height: Option<Integer>,
#[serde(skip_serializing_if = "Option::is_none")]
pub width: Option<Integer>,
}
impl PaidMediaPreview {
pub fn with_duration(mut self, value: Integer) -> Self {
self.duration = Some(value);
self
}
pub fn with_height(mut self, value: Integer) -> Self {
self.height = Some(value);
self
}
pub fn with_width(mut self, value: Integer) -> Self {
self.width = Some(value);
self
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case", tag = "type")]
enum RawPaidMedia {
Photo {
photo: Vec<PhotoSize>,
},
Preview {
#[serde(skip_serializing_if = "Option::is_none")]
duration: Option<Integer>,
#[serde(skip_serializing_if = "Option::is_none")]
height: Option<Integer>,
#[serde(skip_serializing_if = "Option::is_none")]
width: Option<Integer>,
},
Video {
video: Video,
},
}
#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize)]
struct RawPaidMediaPhoto {
photo: Vec<PhotoSize>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize)]
struct RawPaidMediaVideo {
video: Video,
}
impl From<RawPaidMedia> for PaidMedia {
fn from(value: RawPaidMedia) -> Self {
match value {
RawPaidMedia::Photo { photo } => Self::Photo(photo),
RawPaidMedia::Preview {
duration,
height,
width,
} => Self::Preview(PaidMediaPreview {
duration,
height,
width,
}),
RawPaidMedia::Video { video } => Self::Video(video),
}
}
}
impl From<PaidMedia> for RawPaidMedia {
fn from(value: PaidMedia) -> Self {
match value {
PaidMedia::Photo(photo) => Self::Photo { photo },
PaidMedia::Preview(PaidMediaPreview {
duration,
height,
width,
}) => Self::Preview {
duration,
height,
width,
},
PaidMedia::Video(video) => Self::Video { video },
}
}
}
#[derive(Debug)]
pub struct SendPaidMedia {
form: Form,
}
impl SendPaidMedia {
pub fn new<T>(chat_id: T, media: InputPaidMediaGroup, star_count: Integer) -> Self
where
T: Into<ChatId>,
{
let mut form: Form = media.into();
form.insert_field("chat_id", chat_id.into());
form.insert_field("star_count", star_count);
Self { form }
}
pub fn with_caption<T>(mut self, value: T) -> Self
where
T: Into<String>,
{
self.form.insert_field("caption", value.into());
self
}
pub fn with_caption_entities<T>(mut self, value: T) -> Result<Self, TextEntityError>
where
T: Into<TextEntities>,
{
let value = value.into().serialize()?;
self.form.insert_field("caption_entities", value);
self.form.remove_field("parse_mode");
Ok(self)
}
pub fn with_disable_notification(mut self, value: bool) -> Self {
self.form.insert_field("disable_notification", value);
self
}
pub fn with_parse_mode(mut self, value: ParseMode) -> Self {
self.form.insert_field("parse_mode", value);
self.form.remove_field("caption_entities");
self
}
pub fn with_protect_content(mut self, value: bool) -> Self {
self.form.insert_field("protect_content", value);
self
}
pub fn with_reply_parameters(mut self, value: ReplyParameters) -> Result<Self, ReplyParametersError> {
let value = value.serialize()?;
self.form.insert_field("reply_parameters", value);
Ok(self)
}
pub fn with_reply_markup<T>(mut self, value: T) -> Result<Self, ReplyMarkupError>
where
T: Into<ReplyMarkup>,
{
let value = value.into().serialize()?;
self.form.insert_field("reply_markup", value);
Ok(self)
}
pub fn with_show_caption_above_media(mut self, value: bool) -> Self {
self.form.insert_field("show_caption_above_media", value);
self
}
}
impl Method for SendPaidMedia {
type Response = Message;
fn into_payload(self) -> Payload {
Payload::form("sendPaidMedia", self.form)
}
}
const MIN_INPUT_GROUP_ITEMS: usize = 1;
const MAX_INPUT_GROUP_ITEMS: usize = 10;
#[derive(Debug)]
pub struct InputPaidMediaGroup {
form: Form,
}
impl InputPaidMediaGroup {
pub fn new<T>(items: T) -> Result<Self, InputPaidMediaGroupError>
where
T: IntoIterator<Item = InputPaidMediaGroupItem>,
{
let items: Vec<(usize, InputPaidMediaGroupItem)> = items.into_iter().enumerate().collect();
let total_items = items.len();
if total_items < MIN_INPUT_GROUP_ITEMS {
return Err(InputPaidMediaGroupError::NotEnoughItems(MIN_INPUT_GROUP_ITEMS));
}
if total_items > MAX_INPUT_GROUP_ITEMS {
return Err(InputPaidMediaGroupError::TooManyItems(MAX_INPUT_GROUP_ITEMS));
}
let mut form = Form::default();
let mut add_file = |key: String, file: InputFile| -> String {
match &file {
InputFile::Id(text) | InputFile::Url(text) => text.clone(),
_ => {
form.insert_field(&key, file);
format!("attach://{}", key)
}
}
};
let mut info = Vec::new();
for (idx, item) in items {
let media = add_file(format!("tgbot_ipm_file_{}", idx), item.file);
let thumbnail = item
.thumbnail
.map(|thumbnail| add_file(format!("tgbot_ipm_thumb_{}", idx), thumbnail));
let data = match item.item_type {
InputPaidMediaGroupItemType::Photo => InputPaidMediaGroupItemData::Photo { media },
InputPaidMediaGroupItemType::Video(info) => {
InputPaidMediaGroupItemData::Video { media, thumbnail, info }
}
};
info.push(data);
}
form.insert_field(
"media",
serde_json::to_string(&info).map_err(InputPaidMediaGroupError::Serialize)?,
);
Ok(Self { form })
}
}
impl From<InputPaidMediaGroup> for Form {
fn from(value: InputPaidMediaGroup) -> Self {
value.form
}
}
#[derive(Debug)]
pub enum InputPaidMediaGroupError {
NotEnoughItems(usize),
TooManyItems(usize),
Serialize(JsonError),
}
impl Error for InputPaidMediaGroupError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
InputPaidMediaGroupError::Serialize(err) => Some(err),
_ => None,
}
}
}
impl fmt::Display for InputPaidMediaGroupError {
fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
use self::InputPaidMediaGroupError::*;
match self {
NotEnoughItems(number) => write!(out, "group must contain at least {} items", number),
TooManyItems(number) => write!(out, "group must contain no more than {} items", number),
Serialize(err) => write!(out, "can not serialize group items: {}", err),
}
}
}
#[derive(Debug)]
pub struct InputPaidMediaGroupItem {
file: InputFile,
item_type: InputPaidMediaGroupItemType,
thumbnail: Option<InputFile>,
}
impl InputPaidMediaGroupItem {
pub fn for_photo<T>(file: T) -> Self
where
T: Into<InputFile>,
{
Self::new(file, InputPaidMediaGroupItemType::Photo)
}
pub fn for_video<T>(file: T, metadata: InputPaidMediaVideo) -> Self
where
T: Into<InputFile>,
{
Self::new(file, InputPaidMediaGroupItemType::Video(metadata))
}
pub fn with_thumbnail<T>(mut self, file: T) -> Self
where
T: Into<InputFile>,
{
self.thumbnail = Some(file.into());
self
}
fn new<T>(file: T, item_type: InputPaidMediaGroupItemType) -> Self
where
T: Into<InputFile>,
{
Self {
item_type,
file: file.into(),
thumbnail: None,
}
}
}
#[derive(Debug)]
enum InputPaidMediaGroupItemType {
Photo,
Video(InputPaidMediaVideo),
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "snake_case", tag = "type")]
enum InputPaidMediaGroupItemData {
Photo {
media: String,
},
Video {
media: String,
#[serde(skip_serializing_if = "Option::is_none")]
thumbnail: Option<String>,
#[serde(flatten)]
info: InputPaidMediaVideo,
},
}
#[derive(Debug, Default, Serialize)]
pub struct InputPaidMediaVideo {
#[serde(skip_serializing_if = "Option::is_none")]
duration: Option<Integer>,
#[serde(skip_serializing_if = "Option::is_none")]
height: Option<Integer>,
#[serde(skip_serializing_if = "Option::is_none")]
supports_streaming: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
width: Option<Integer>,
}
impl InputPaidMediaVideo {
pub fn with_duration(mut self, value: Integer) -> Self {
self.duration = Some(value);
self
}
pub fn with_height(mut self, value: Integer) -> Self {
self.height = Some(value);
self
}
pub fn with_supports_streaming(mut self, value: bool) -> Self {
self.supports_streaming = Some(value);
self
}
pub fn with_width(mut self, value: Integer) -> Self {
self.width = Some(value);
self
}
}