diff --git a/Cargo.toml b/Cargo.toml index 4c97752..3b7cf65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ categories = ["web-programming::http-client"] all-features = true [features] -client-reqwest = ["trait-async", "dep:reqwest", "dep:tokio", "dep:serde_json"] +client-reqwest = ["trait-async", "dep:reqwest", "dep:serde_json"] client-ureq = ["trait-sync", "dep:ureq", "dep:multipart", "dep:mime_guess", "dep:serde_json"] trait-async = ["dep:async-trait"] trait-sync = [] @@ -59,11 +59,6 @@ default-features = false features = ["multipart", "stream"] optional = true -[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio] -version = "1" -features = ["fs"] -optional = true - [dev-dependencies] isahc = "1" mockito = "1.0" @@ -86,6 +81,10 @@ required-features = ["client-ureq"] name = "inline_keyboard" required-features = ["client-ureq"] +[[example]] +name = "file_upload" +required-features = ["client-ureq"] + [[example]] name = "custom_client" required-features = ["client-ureq"] diff --git a/README.md b/README.md index efb3631..78d4089 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Without enabling any additional features this crate will only ship with Telegram - `client-ureq` - a blocking HTTP API client based on `ureq` - `trait-sync` - a blocking API trait, it's included in the `client-ureq` feature. It may be useful for people who want to create a custom blocking client (for example, replacing an HTTP client) - async - - `client-reqwest` - an async HTTP API client based on `reqwest`. This client partially supports wasm32, but file uploads are currently not supported there. + - `client-reqwest` - an async HTTP API client based on `reqwest`. This client supports wasm32 - `trait-async` - an async API trait, it's used in the `client-reqwest`. It may be useful for people who want to create a custom async client For example for the async client add the following line to your `Cargo.toml` file: @@ -114,26 +114,6 @@ Every function returns a `Result` with a successful response or failed response. See more examples in the [`examples`](https://github.com/ayrat555/frankenstein/tree/0.40.2/examples) directory. -### Uploading files - -Some methods in the API allow uploading files. In the Frankenstein for this `FileUpload` enum is used: - -```rust -pub enum FileUpload { - InputFile(InputFile), - String(String), -} - -pub struct InputFile { - path: std::path::PathBuf -} -``` - -It has two variants: - -- `FileUpload::String` is used to pass the ID of the already uploaded file -- `FileUpload::InputFile` is used to upload a new file using multipart upload. - ### Documentation Frankenstein implements all Telegram bot API methods. To see which parameters you should pass, check the [official Telegram Bot API documentation](https://core.telegram.org/bots/api#available-methods) or [docs.rs/frankenstein](https://docs.rs/frankenstein/0.40.0/frankenstein/trait.TelegramApi.html#provided-methods) diff --git a/examples/api_trait_implementation.rs b/examples/api_trait_implementation.rs index 55cfde6..ec94d8d 100644 --- a/examples/api_trait_implementation.rs +++ b/examples/api_trait_implementation.rs @@ -1,5 +1,4 @@ -use std::path::PathBuf; - +use frankenstein::input_file::InputFile; use frankenstein::methods::SendMessageParams; use frankenstein::response::ErrorResponse; use frankenstein::TelegramApi; @@ -91,7 +90,7 @@ impl TelegramApi for MyApiClient { &self, _method: &str, _params: Params, - _files: Vec<(&str, PathBuf)>, + _files: Vec<(std::borrow::Cow<'static, str>, InputFile)>, ) -> Result where Params: serde::ser::Serialize + std::fmt::Debug, diff --git a/examples/async_file_upload.rs b/examples/async_file_upload.rs index 13f1b6e..36ec304 100644 --- a/examples/async_file_upload.rs +++ b/examples/async_file_upload.rs @@ -18,7 +18,7 @@ async fn main() { .chat_id(chat_id) .photo(file) .build(); - match bot.send_photo(¶ms).await { + match bot.send_photo(params).await { Ok(response) => { println!("Photo was uploaded successfully"); dbg!(response); diff --git a/examples/file_upload.rs b/examples/file_upload.rs new file mode 100644 index 0000000..e9eaba0 --- /dev/null +++ b/examples/file_upload.rs @@ -0,0 +1,29 @@ +use frankenstein::client_ureq::Bot; +use frankenstein::methods::SendPhotoParams; +use frankenstein::TelegramApi; + +fn main() { + let token = std::env::var("BOT_TOKEN").expect("Should have BOT_TOKEN as environment variable"); + let chat_id = std::env::var("TARGET_CHAT") + .expect("Should have TARGET_CHAT as environment variable") + .parse::() + .expect("TARGET_CHAT should be i64"); + + let bot = Bot::new(&token); + + let file = std::path::PathBuf::from("./frankenstein_logo.png"); + + let params = SendPhotoParams::builder() + .chat_id(chat_id) + .photo(file) + .build(); + match bot.send_photo(params) { + Ok(response) => { + println!("Photo was uploaded successfully"); + dbg!(response); + } + Err(error) => { + eprintln!("Failed to upload photo: {error:?}"); + } + } +} diff --git a/src/client_reqwest.rs b/src/client_reqwest.rs index 4f525fa..9f5a489 100644 --- a/src/client_reqwest.rs +++ b/src/client_reqwest.rs @@ -1,8 +1,8 @@ -use std::path::PathBuf; - use async_trait::async_trait; use bon::Builder; +use serde_json::Value; +use crate::input_file::InputFile; use crate::trait_async::AsyncTelegramApi; use crate::Error; @@ -93,51 +93,48 @@ impl AsyncTelegramApi for Bot { &self, method: &str, params: Params, - files: Vec<(&str, PathBuf)>, + files: Vec<(std::borrow::Cow<'static, str>, InputFile)>, ) -> Result where Params: serde::ser::Serialize + std::fmt::Debug + std::marker::Send, Output: serde::de::DeserializeOwned, { - #[cfg(not(target_arch = "wasm32"))] - { - use reqwest::multipart; - use serde_json::Value; - - let json_string = crate::json::encode(¶ms)?; - let json_struct: Value = serde_json::from_str(&json_string).unwrap(); - - let file_keys: Vec<&str> = files.iter().map(|(key, _)| *key).collect(); - - let mut form = multipart::Form::new(); - for (key, val) in json_struct.as_object().unwrap() { - if !file_keys.contains(&key.as_str()) { - let val = match val { - Value::String(val) => val.to_string(), - other => other.to_string(), - }; + let json_string = crate::json::encode(¶ms)?; + let json_struct: serde_json::Map = + serde_json::from_str(&json_string).unwrap(); + let file_keys: Vec<&str> = files.iter().map(|(key, _)| key.as_ref()).collect(); + + let mut form = reqwest::multipart::Form::new(); + for (key, val) in json_struct { + if !file_keys.contains(&key.as_str()) { + let val = match val { + Value::String(val) => val, + other => other.to_string(), + }; + form = form.text(key, val); + } + } - form = form.text(key.clone(), val); + for (parameter_name, input_file) in files { + let part = match input_file { + InputFile::Bytes { bytes, file_name } => { + // The reqwest::multipart stuff requires 'static which we can not grant here. + // So we provide owned data by cloning it. + reqwest::multipart::Part::bytes(bytes).file_name(file_name) } - } - for (parameter_name, file_path) in files { - let file = tokio::fs::File::open(&file_path) + #[cfg(not(target_arch = "wasm32"))] + InputFile::Path(path) => reqwest::multipart::Part::file(path) .await - .map_err(Error::ReadFile)?; - let file_name = file_path.file_name().unwrap().to_string_lossy().to_string(); - let part = multipart::Part::stream(file).file_name(file_name); - form = form.part(parameter_name.to_owned(), part); - } - - let url = format!("{}/{method}", self.api_url); - - let response = self.client.post(url).multipart(form).send().await?; - Self::decode_response(response).await + .map_err(crate::Error::ReadFile)?, + }; + form = form.part(parameter_name, part); } - #[cfg(target_arch = "wasm32")] - Err(Error::WasmHasNoFileSupportYet) + let url = format!("{}/{method}", self.api_url); + + let response = self.client.post(url).multipart(form).send().await?; + Self::decode_response(response).await } } diff --git a/src/client_ureq.rs b/src/client_ureq.rs index a125667..fc1788e 100644 --- a/src/client_ureq.rs +++ b/src/client_ureq.rs @@ -1,10 +1,10 @@ -use std::path::PathBuf; use std::time::Duration; use bon::Builder; use multipart::client::lazy::Multipart; use serde_json::Value; +use crate::input_file::InputFile; use crate::trait_sync::TelegramApi; use crate::Error; @@ -85,41 +85,50 @@ impl TelegramApi for Bot { &self, method: &str, params: Params, - files: Vec<(&str, PathBuf)>, + files: Vec<(std::borrow::Cow<'static, str>, InputFile)>, ) -> Result where Params: serde::ser::Serialize + std::fmt::Debug, Output: serde::de::DeserializeOwned, { let json_string = crate::json::encode(¶ms)?; - let json_struct: Value = serde_json::from_str(&json_string).unwrap(); - let file_keys: Vec<&str> = files.iter().map(|(key, _)| *key).collect(); + let json_struct: serde_json::Map = + serde_json::from_str(&json_string).unwrap(); + let file_keys: Vec<&str> = files.iter().map(|(key, _)| key.as_ref()).collect(); let mut form = Multipart::new(); - for (key, val) in json_struct.as_object().unwrap() { + for (key, val) in json_struct { if !file_keys.contains(&key.as_str()) { let val = match val { - Value::String(val) => val.to_string(), + Value::String(val) => val, other => other.to_string(), }; - form.add_text(key, val); } } - for (parameter_name, file_path) in &files { - let file = std::fs::File::open(file_path).map_err(Error::ReadFile)?; - let file_name = file_path.file_name().unwrap().to_string_lossy(); - let file_extension = file_path - .extension() - .and_then(std::ffi::OsStr::to_str) - .unwrap_or(""); - let mime = mime_guess::from_ext(file_extension).first_or_octet_stream(); - form.add_stream(*parameter_name, file, Some(file_name), Some(mime)); + for (parameter_name, input_file) in &files { + match input_file { + InputFile::Bytes { bytes, file_name } => { + let mime = mime_guess::from_path(std::path::Path::new(&file_name)) + .first_or_octet_stream(); + form.add_stream( + parameter_name.as_ref(), + &**bytes, + Some(file_name), + Some(mime), + ); + } + InputFile::Path(path) => { + form.add_file::<&str, &std::path::Path>(parameter_name.as_ref(), path.as_ref()); + } + } } let url = format!("{}/{method}", self.api_url); - let mut form_data = form.prepare().unwrap(); + let mut form_data = form + .prepare() + .map_err(|error| crate::Error::ReadFile(error.error))?; let response = self .request_agent .post(&url) @@ -179,6 +188,25 @@ mod tests { }}; } + /// Test case for methods using files. They move the params in instead of taking a reference. + macro_rules! case_f { + ($method:ident, $status:literal, $body:ident $(, $params:ident )? ) => {{ + paste::paste! { + let mut server = mockito::Server::new(); + let mock = server + .mock("POST", concat!("/", stringify!($method))) + .with_status($status) + .with_body($body) + .create(); + let api = Bot::new_url(server.url()); + let response = dbg!(api.[<$method:snake>]($( $params )?)); + mock.assert(); + drop(server); + response + } + }}; + } + #[test] fn new_sets_correct_url() { let api = Bot::new("hey"); @@ -232,7 +260,7 @@ mod tests { let response_string = "{\"ok\":true,\"description\":\"Webhook is already deleted\",\"result\":true}"; let params = SetWebhookParams::builder().url("").build(); - let response = case!(setWebhook, 200, response_string, params).unwrap(); + let response = case_f!(setWebhook, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); } @@ -561,7 +589,7 @@ mod tests { .chat_id(275808073) .photo(file) .build(); - let response = case!(sendPhoto, 200, response_string, params).unwrap(); + let response = case_f!(sendPhoto, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); } @@ -573,7 +601,7 @@ mod tests { .chat_id(275808073) .audio(file) .build(); - let response = case!(sendAudio, 200, response_string, params).unwrap(); + let response = case_f!(sendAudio, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); } @@ -586,7 +614,7 @@ mod tests { .audio(file.clone()) .thumbnail(file) .build(); - let response = case!(sendAudio, 200, response_string, params).unwrap(); + let response = case_f!(sendAudio, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); } @@ -599,7 +627,7 @@ mod tests { .chat_id(275808073) .audio(file) .build(); - let response = case!(sendAudio, 200, response_string, params).unwrap(); + let response = case_f!(sendAudio, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); } @@ -611,7 +639,7 @@ mod tests { .chat_id(275808073) .document(file) .build(); - let response = case!(sendDocument, 200, response_string, params).unwrap(); + let response = case_f!(sendDocument, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); } @@ -623,7 +651,7 @@ mod tests { .chat_id(275808073) .video(file) .build(); - let response = case!(sendVideo, 200, response_string, params).unwrap(); + let response = case_f!(sendVideo, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); } @@ -635,7 +663,7 @@ mod tests { .chat_id(275808073) .animation(file) .build(); - let response = case!(sendAnimation, 200, response_string, params).unwrap(); + let response = case_f!(sendAnimation, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); } @@ -647,7 +675,7 @@ mod tests { .chat_id(275808073) .voice(file) .build(); - let response = case!(sendVoice, 200, response_string, params).unwrap(); + let response = case_f!(sendVoice, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); } @@ -659,7 +687,7 @@ mod tests { .chat_id(275808073) .video_note(file) .build(); - let response = case!(sendVideoNote, 200, response_string, params).unwrap(); + let response = case_f!(sendVideoNote, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); } @@ -671,7 +699,7 @@ mod tests { .chat_id(275808073) .photo(file) .build(); - let response = case!(setChatPhoto, 200, response_string, params).unwrap(); + let response = case_f!(setChatPhoto, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); } @@ -941,7 +969,7 @@ mod tests { .chat_id(275808073) .sticker(file) .build(); - let response = case!(sendSticker, 200, response_string, params).unwrap(); + let response = case_f!(sendSticker, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); } @@ -966,7 +994,7 @@ mod tests { .chat_id(-1001368460856) .media(medias) .build(); - let response = case!(sendMediaGroup, 200, response_string, params).unwrap(); + let response = case_f!(sendMediaGroup, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); } @@ -979,7 +1007,7 @@ mod tests { .chat_id(-1001368460856) .message_id(513) .build(); - let response = case!(editMessageMedia, 200, response_string, params).unwrap(); + let response = case_f!(editMessageMedia, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); } diff --git a/src/error.rs b/src/error.rs index 6c5bdd8..b2452de 100644 --- a/src/error.rs +++ b/src/error.rs @@ -22,10 +22,6 @@ pub enum Error { #[error("Read File Error: {0}")] ReadFile(#[source] std::io::Error), - #[cfg(all(feature = "client-reqwest", target_arch = "wasm32"))] - #[error("Handling files is not yet supported in Wasm due to missing form_data / attachment support. Pull Request welcome!")] - WasmHasNoFileSupportYet, - #[cfg(feature = "client-reqwest")] #[error("HTTP error: {0}")] HttpReqwest(#[source] reqwest::Error), diff --git a/src/input_file.rs b/src/input_file.rs index 2cf9a26..ee90c7c 100644 --- a/src/input_file.rs +++ b/src/input_file.rs @@ -1,20 +1,24 @@ //! Structs for handling and uploading files -use std::path::PathBuf; - use serde::{Deserialize, Serialize}; /// Represents a new file to be uploaded via `multipart/form-data`. /// /// See . #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct InputFile { - pub path: PathBuf, +pub enum InputFile { + Bytes { + bytes: Vec, + file_name: String, + }, + #[cfg(not(target_arch = "wasm32"))] + Path(std::path::PathBuf), } -impl From for InputFile { - fn from(path: PathBuf) -> Self { - Self { path } +#[cfg(not(target_arch = "wasm32"))] +impl From for InputFile { + fn from(value: std::path::PathBuf) -> Self { + Self::Path(value) } } @@ -36,9 +40,10 @@ impl From for FileUpload { } } -impl From for FileUpload { - fn from(path: PathBuf) -> Self { - Self::InputFile(InputFile { path }) +#[cfg(not(target_arch = "wasm32"))] +impl From for FileUpload { + fn from(path: std::path::PathBuf) -> Self { + Self::InputFile(InputFile::Path(path)) } } @@ -48,83 +53,59 @@ impl From for FileUpload { } } +#[cfg(any(feature = "trait-sync", feature = "trait-async"))] +type Filelist = Vec<(std::borrow::Cow<'static, str>, InputFile)>; + #[cfg(any(feature = "trait-sync", feature = "trait-async"))] pub(crate) trait HasInputFile { - fn clone_path(&self) -> Option; - fn replace_attach(&mut self, name: &str) -> Option; - fn replace_attach_dyn(&mut self, index: impl FnOnce() -> usize) -> Option<(String, PathBuf)>; + fn move_named_to_filelist(&mut self, name: &'static str, files: &mut Filelist); + fn move_to_filelist(&mut self, files: &mut Filelist); } #[cfg(any(feature = "trait-sync", feature = "trait-async"))] impl HasInputFile for FileUpload { - fn clone_path(&self) -> Option { - match self { - Self::InputFile(input_file) => Some(input_file.path.clone()), - Self::String(_) => None, + fn move_named_to_filelist(&mut self, name: &'static str, files: &mut Filelist) { + if let Self::InputFile(_) = self { + let attach = Self::String(format!("attach://{name}")); + let Self::InputFile(file) = std::mem::replace(self, attach) else { + unreachable!("the match already ensures it being an input file"); + }; + files.push((std::borrow::Cow::Borrowed(name), file)); } } - fn replace_attach(&mut self, name: &str) -> Option { - match self { - Self::InputFile(_) => { - let attach = Self::String(format!("attach://{name}")); - let Self::InputFile(file) = std::mem::replace(self, attach) else { - unreachable!("the match already ensures it being an input file"); - }; - Some(file.path) - } - Self::String(_) => None, - } - } - - fn replace_attach_dyn(&mut self, index: impl FnOnce() -> usize) -> Option<(String, PathBuf)> { - match self { - Self::InputFile(_) => { - let name = format!("file{}", index()); - let attach = Self::String(format!("attach://{name}")); - let Self::InputFile(file) = std::mem::replace(self, attach) else { - unreachable!("the match already ensures it being an input file"); - }; - Some((name, file.path)) - } - Self::String(_) => None, + fn move_to_filelist(&mut self, files: &mut Filelist) { + if let Self::InputFile(_) = self { + let name = format!("file{}", files.len()); + let attach = Self::String(format!("attach://{name}")); + let Self::InputFile(file) = std::mem::replace(self, attach) else { + unreachable!("the match already ensures it being an input file"); + }; + files.push((std::borrow::Cow::Owned(name), file)); } } } #[cfg(any(feature = "trait-sync", feature = "trait-async"))] impl HasInputFile for Option { - fn clone_path(&self) -> Option { - match self { - Some(FileUpload::InputFile(input_file)) => Some(input_file.path.clone()), - _ => None, - } - } - - fn replace_attach(&mut self, name: &str) -> Option { - match self { - Some(FileUpload::InputFile(_)) => { - let attach = Some(FileUpload::String(format!("attach://{name}"))); - let Some(FileUpload::InputFile(file)) = std::mem::replace(self, attach) else { - unreachable!("the match already ensures it being an input file"); - }; - Some(file.path) - } - _ => None, + fn move_named_to_filelist(&mut self, name: &'static str, files: &mut Filelist) { + if let Some(FileUpload::InputFile(_)) = self { + let attach = Some(FileUpload::String(format!("attach://{name}"))); + let Some(FileUpload::InputFile(file)) = std::mem::replace(self, attach) else { + unreachable!("the match already ensures it being an input file"); + }; + files.push((std::borrow::Cow::Borrowed(name), file)); } } - fn replace_attach_dyn(&mut self, index: impl FnOnce() -> usize) -> Option<(String, PathBuf)> { - match self { - Some(FileUpload::InputFile(_)) => { - let name = format!("file{}", index()); - let attach = Some(FileUpload::String(format!("attach://{name}"))); - let Some(FileUpload::InputFile(file)) = std::mem::replace(self, attach) else { - unreachable!("the match already ensures it being an input file"); - }; - Some((name, file.path)) - } - _ => None, + fn move_to_filelist(&mut self, files: &mut Filelist) { + if let Some(FileUpload::InputFile(_)) = self { + let name = format!("file{}", files.len()); + let attach = Some(FileUpload::String(format!("attach://{name}"))); + let Some(FileUpload::InputFile(file)) = std::mem::replace(self, attach) else { + unreachable!("the match already ensures it being an input file"); + }; + files.push((std::borrow::Cow::Owned(name), file)); } } } diff --git a/src/methods.rs b/src/methods.rs index 1b41137..7b6bb40 100644 --- a/src/methods.rs +++ b/src/methods.rs @@ -1,7 +1,7 @@ //! Parameters of [Bot API methods](https://core.telegram.org/bots/api#available-methods). use crate::inline_mode::{InlineQueryResult, InlineQueryResultsButton}; -use crate::input_file::{FileUpload, InputFile}; +use crate::input_file::FileUpload; use crate::input_media::{InputMedia, InputPaidMedia, MediaGroupInputMedia}; use crate::macros::{apistruct, apply}; use crate::passport::PassportElementError; @@ -27,7 +27,7 @@ pub struct GetUpdatesParams { #[derive(Eq)] pub struct SetWebhookParams { pub url: String, - pub certificate: Option, + pub certificate: Option, pub ip_address: Option, pub max_connections: Option, pub allowed_updates: Option>, @@ -611,7 +611,7 @@ pub struct DeclineChatJoinRequestParams { #[derive(Eq)] pub struct SetChatPhotoParams { pub chat_id: ChatId, - pub photo: InputFile, + pub photo: FileUpload, } #[apply(apistruct!)] @@ -960,7 +960,7 @@ pub struct GetStickerSetParams { #[derive(Eq)] pub struct UploadStickerFileParams { pub user_id: u64, - pub sticker: InputFile, + pub sticker: FileUpload, pub sticker_format: StickerFormat, } diff --git a/src/trait_async.rs b/src/trait_async.rs index c985482..129e950 100644 --- a/src/trait_async.rs +++ b/src/trait_async.rs @@ -1,8 +1,6 @@ -use std::path::PathBuf; - use crate::games::GameHighScore; use crate::inline_mode::{PreparedInlineMessage, SentWebAppMessage}; -use crate::input_file::HasInputFile; +use crate::input_file::{HasInputFile, InputFile}; use crate::input_media::{InputMedia, MediaGroupInputMedia}; use crate::payments::StarTransactions; use crate::response::{MessageOrBool, MethodResponse}; @@ -55,13 +53,11 @@ macro_rules! request_f { #[doc = "Call the `" $name "` method.\n\nSee ."] async fn [<$name:snake>] ( &self, - params: &crate::methods::[<$name:camel Params>], + mut params: crate::methods::[<$name:camel Params>], ) -> Result, Self::Error> { let mut files = Vec::new(); $( - if let Some(path) = params.$fileproperty.clone_path() { - files.push((stringify!($fileproperty), path)); - } + params.$fileproperty.move_named_to_filelist(stringify!($fileproperty), &mut files); )+ self.request_with_possible_form_data(stringify!($name), params, files).await } @@ -69,6 +65,18 @@ macro_rules! request_f { } } +macro_rules! docs_file { + ($name:ident, $url:ident) => { + concat!( + "Call the `", + stringify!($name), + "` method.\n\nSee ." + ) + }; +} + // Wasm target need not be `Send` because it is single-threaded #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] @@ -80,7 +88,7 @@ where request!(getUpdates, Vec); request!(sendMessage, Message); - request!(setWebhook, bool); + request_f!(setWebhook, bool, certificate); request!(deleteWebhook, bool); request_nb!(getWebhookInfo, WebhookInfo); request_nb!(getMe, User); @@ -93,47 +101,32 @@ where request_f!(sendPhoto, Message, photo); request_f!(sendAudio, Message, audio, thumbnail); + #[doc = docs_file!(sendMediaGroup, send_media_group)] async fn send_media_group( &self, - params: &crate::methods::SendMediaGroupParams, + mut params: crate::methods::SendMediaGroupParams, ) -> Result>, Self::Error> { let mut files = Vec::new(); - - macro_rules! replace_attach { - ($base:ident. $property:ident) => { - if let Some(file) = $base.$property.replace_attach_dyn(|| files.len()) { - files.push(file); - } - }; - } - - let mut params = params.clone(); for media in &mut params.media { match media { MediaGroupInputMedia::Audio(audio) => { - replace_attach!(audio.media); - replace_attach!(audio.thumbnail); + audio.media.move_to_filelist(&mut files); + audio.thumbnail.move_to_filelist(&mut files); } MediaGroupInputMedia::Document(document) => { - replace_attach!(document.media); + document.media.move_to_filelist(&mut files); } MediaGroupInputMedia::Photo(photo) => { - replace_attach!(photo.media); + photo.media.move_to_filelist(&mut files); } MediaGroupInputMedia::Video(video) => { - replace_attach!(video.media); - replace_attach!(video.cover); - replace_attach!(video.thumbnail); + video.media.move_to_filelist(&mut files); + video.cover.move_to_filelist(&mut files); + video.thumbnail.move_to_filelist(&mut files); } } } - - let files_with_str_names = files - .iter() - .map(|(key, path)| (key.as_str(), path.clone())) - .collect(); - - self.request_with_possible_form_data("sendMediaGroup", ¶ms, files_with_str_names) + self.request_with_possible_form_data("sendMediaGroup", ¶ms, files) .await } @@ -171,16 +164,7 @@ where request!(revokeChatInviteLink, ChatInviteLink); request!(approveChatJoinRequest, bool); request!(declineChatJoinRequest, bool); - - async fn set_chat_photo( - &self, - params: &crate::methods::SetChatPhotoParams, - ) -> Result, Self::Error> { - let photo = ¶ms.photo; - self.request_with_form_data("setChatPhoto", params, vec![("photo", photo.path.clone())]) - .await - } - + request_f!(setChatPhoto, bool, photo); request!(deleteChatPhoto, bool); request!(setChatTitle, bool); request!(setChatDescription, bool); @@ -222,22 +206,22 @@ where request!(editMessageText, MessageOrBool); request!(editMessageCaption, MessageOrBool); + #[doc = docs_file!(editMessageMedia, edit_message_media)] async fn edit_message_media( &self, - params: &crate::methods::EditMessageMediaParams, + mut params: crate::methods::EditMessageMediaParams, ) -> Result, Self::Error> { let mut files = Vec::new(); macro_rules! replace_attach { - ($base:ident. $property:ident) => {{ - const NAME: &str = concat!(stringify!($base), "_", stringify!($property)); - if let Some(file) = $base.$property.replace_attach(NAME) { - files.push((NAME, file)); - } - }}; + ($base:ident. $property:ident) => { + $base.$property.move_named_to_filelist( + concat!(stringify!($base), "_", stringify!($property)), + &mut files, + ); + }; } - let mut params = params.clone(); match &mut params.media { InputMedia::Animation(animation) => { replace_attach!(animation.media); @@ -271,52 +255,35 @@ where request!(deleteMessages, bool); request_f!(sendSticker, Message, sticker); request!(getStickerSet, StickerSet); + request_f!(uploadStickerFile, File, sticker); - async fn upload_sticker_file( - &self, - params: &crate::methods::UploadStickerFileParams, - ) -> Result, Self::Error> { - let sticker = ¶ms.sticker; - self.request_with_form_data( - "uploadStickerFile", - params, - vec![("sticker", sticker.path.clone())], - ) - .await - } - + #[doc = docs_file!(createNewStickerSet, create_new_sticker_set)] async fn create_new_sticker_set( &self, - params: &crate::methods::CreateNewStickerSetParams, + mut params: crate::methods::CreateNewStickerSetParams, ) -> Result, Self::Error> { let mut files = Vec::new(); - let mut params = params.clone(); - for (index, sticker) in params.stickers.iter_mut().enumerate() { - if let Some(file) = sticker.sticker.replace_attach_dyn(|| index) { - files.push(file); - } + for sticker in &mut params.stickers { + sticker.sticker.move_to_filelist(&mut files); } - let files_with_str_names = files - .iter() - .map(|(key, path)| (key.as_str(), path.clone())) - .collect(); - - self.request_with_possible_form_data("createNewStickerSet", ¶ms, files_with_str_names) + self.request_with_possible_form_data("createNewStickerSet", ¶ms, files) .await } request!(getCustomEmojiStickers, Vec); + #[doc = docs_file!(addStickerToSet, add_sticker_to_set)] async fn add_sticker_to_set( &self, - params: &crate::methods::AddStickerToSetParams, + mut params: crate::methods::AddStickerToSetParams, ) -> Result, Self::Error> { let mut files = Vec::new(); - if let Some(file) = params.sticker.sticker.clone_path() { - files.push(("sticker", file)); - } + params + .sticker + .sticker + .move_named_to_filelist("sticker", &mut files); self.request_with_possible_form_data("addStickerToSet", params, files) .await } @@ -356,11 +323,12 @@ where request!(unpinAllGeneralForumTopicMessages, bool); request!(setPassportDataErrors, bool); + #[doc(hidden)] async fn request_with_possible_form_data( &self, method_name: &str, params: Params, - files: Vec<(&str, PathBuf)>, + files: Vec<(std::borrow::Cow<'static, str>, InputFile)>, ) -> Result where Params: serde::ser::Serialize + std::fmt::Debug + std::marker::Send, @@ -387,7 +355,7 @@ where &self, method: &str, params: Params, - files: Vec<(&str, PathBuf)>, + files: Vec<(std::borrow::Cow<'static, str>, InputFile)>, ) -> Result where Params: serde::ser::Serialize + std::fmt::Debug + std::marker::Send, diff --git a/src/trait_sync.rs b/src/trait_sync.rs index 4b85e1d..acd21fe 100644 --- a/src/trait_sync.rs +++ b/src/trait_sync.rs @@ -1,8 +1,6 @@ -use std::path::PathBuf; - use crate::games::GameHighScore; use crate::inline_mode::{PreparedInlineMessage, SentWebAppMessage}; -use crate::input_file::HasInputFile; +use crate::input_file::{HasInputFile, InputFile}; use crate::input_media::{InputMedia, MediaGroupInputMedia}; use crate::payments::StarTransactions; use crate::response::{MessageOrBool, MethodResponse}; @@ -52,13 +50,11 @@ macro_rules! request_f { #[doc = "Call the `" $name "` method.\n\nSee ."] fn [<$name:snake>] ( &self, - params: &crate::methods::[<$name:camel Params>], + mut params: crate::methods::[<$name:camel Params>], ) -> Result, Self::Error> { let mut files = Vec::new(); $( - if let Some(path) = params.$fileproperty.clone_path() { - files.push((stringify!($fileproperty), path)); - } + params.$fileproperty.move_named_to_filelist(stringify!($fileproperty), &mut files); )+ self.request_with_possible_form_data(stringify!($name), params, files) } @@ -66,12 +62,24 @@ macro_rules! request_f { } } +macro_rules! docs_file { + ($name:ident, $url:ident) => { + concat!( + "Call the `", + stringify!($name), + "` method.\n\nSee ." + ) + }; +} + pub trait TelegramApi { type Error; request!(getUpdates, Vec); request!(sendMessage, Message); - request!(setWebhook, bool); + request_f!(setWebhook, bool, certificate); request!(deleteWebhook, bool); request_nb!(getWebhookInfo, WebhookInfo); request_nb!(getMe, User); @@ -84,47 +92,32 @@ pub trait TelegramApi { request_f!(sendPhoto, Message, photo); request_f!(sendAudio, Message, audio, thumbnail); + #[doc = docs_file!(sendMediaGroup, send_media_group)] fn send_media_group( &self, - params: &crate::methods::SendMediaGroupParams, + mut params: crate::methods::SendMediaGroupParams, ) -> Result>, Self::Error> { let mut files = Vec::new(); - - macro_rules! replace_attach { - ($base:ident. $property:ident) => { - if let Some(file) = $base.$property.replace_attach_dyn(|| files.len()) { - files.push(file); - } - }; - } - - let mut params = params.clone(); for media in &mut params.media { match media { MediaGroupInputMedia::Audio(audio) => { - replace_attach!(audio.media); - replace_attach!(audio.thumbnail); + audio.media.move_to_filelist(&mut files); + audio.thumbnail.move_to_filelist(&mut files); } MediaGroupInputMedia::Document(document) => { - replace_attach!(document.media); + document.media.move_to_filelist(&mut files); } MediaGroupInputMedia::Photo(photo) => { - replace_attach!(photo.media); + photo.media.move_to_filelist(&mut files); } MediaGroupInputMedia::Video(video) => { - replace_attach!(video.media); - replace_attach!(video.cover); - replace_attach!(video.thumbnail); + video.media.move_to_filelist(&mut files); + video.cover.move_to_filelist(&mut files); + video.thumbnail.move_to_filelist(&mut files); } } } - - let files_with_str_names = files - .iter() - .map(|(key, path)| (key.as_str(), path.clone())) - .collect(); - - self.request_with_possible_form_data("sendMediaGroup", ¶ms, files_with_str_names) + self.request_with_possible_form_data("sendMediaGroup", ¶ms, files) } request_f!(sendDocument, Message, document, thumbnail); @@ -161,15 +154,7 @@ pub trait TelegramApi { request!(revokeChatInviteLink, ChatInviteLink); request!(approveChatJoinRequest, bool); request!(declineChatJoinRequest, bool); - - fn set_chat_photo( - &self, - params: &crate::methods::SetChatPhotoParams, - ) -> Result, Self::Error> { - let photo = ¶ms.photo; - self.request_with_form_data("setChatPhoto", params, vec![("photo", photo.path.clone())]) - } - + request_f!(setChatPhoto, bool, photo); request!(deleteChatPhoto, bool); request!(setChatTitle, bool); request!(setChatDescription, bool); @@ -211,22 +196,22 @@ pub trait TelegramApi { request!(editMessageText, MessageOrBool); request!(editMessageCaption, MessageOrBool); + #[doc = docs_file!(editMessageMedia, edit_message_media)] fn edit_message_media( &self, - params: &crate::methods::EditMessageMediaParams, + mut params: crate::methods::EditMessageMediaParams, ) -> Result, Self::Error> { let mut files = Vec::new(); macro_rules! replace_attach { - ($base:ident. $property:ident) => {{ - const NAME: &str = concat!(stringify!($base), "_", stringify!($property)); - if let Some(file) = $base.$property.replace_attach(NAME) { - files.push((NAME, file)); - } - }}; + ($base:ident. $property:ident) => { + $base.$property.move_named_to_filelist( + concat!(stringify!($base), "_", stringify!($property)), + &mut files, + ); + }; } - let mut params = params.clone(); match &mut params.media { InputMedia::Animation(animation) => { replace_attach!(animation.media); @@ -259,50 +244,32 @@ pub trait TelegramApi { request!(deleteMessages, bool); request_f!(sendSticker, Message, sticker); request!(getStickerSet, StickerSet); + request_f!(uploadStickerFile, File, sticker); - fn upload_sticker_file( - &self, - params: &crate::methods::UploadStickerFileParams, - ) -> Result, Self::Error> { - let sticker = ¶ms.sticker; - self.request_with_form_data( - "uploadStickerFile", - params, - vec![("sticker", sticker.path.clone())], - ) - } - + #[doc = docs_file!(createNewStickerSet, create_new_sticker_set)] fn create_new_sticker_set( &self, - params: &crate::methods::CreateNewStickerSetParams, + mut params: crate::methods::CreateNewStickerSetParams, ) -> Result, Self::Error> { let mut files = Vec::new(); - - let mut params = params.clone(); - for (index, sticker) in params.stickers.iter_mut().enumerate() { - if let Some(file) = sticker.sticker.replace_attach_dyn(|| index) { - files.push(file); - } + for sticker in &mut params.stickers { + sticker.sticker.move_to_filelist(&mut files); } - - let files_with_str_names = files - .iter() - .map(|(key, path)| (key.as_str(), path.clone())) - .collect(); - - self.request_with_possible_form_data("createNewStickerSet", ¶ms, files_with_str_names) + self.request_with_possible_form_data("createNewStickerSet", ¶ms, files) } request!(getCustomEmojiStickers, Vec); + #[doc = docs_file!(addStickerToSet, add_sticker_to_set)] fn add_sticker_to_set( &self, - params: &crate::methods::AddStickerToSetParams, + mut params: crate::methods::AddStickerToSetParams, ) -> Result, Self::Error> { let mut files = Vec::new(); - if let Some(file) = params.sticker.sticker.clone_path() { - files.push(("sticker", file)); - } + params + .sticker + .sticker + .move_named_to_filelist("sticker", &mut files); self.request_with_possible_form_data("addStickerToSet", params, files) } @@ -341,11 +308,12 @@ pub trait TelegramApi { request!(unpinAllGeneralForumTopicMessages, bool); request!(setPassportDataErrors, bool); + #[doc(hidden)] fn request_with_possible_form_data( &self, method_name: &str, params: Params, - files: Vec<(&str, PathBuf)>, + files: Vec<(std::borrow::Cow<'static, str>, InputFile)>, ) -> Result where Params: serde::ser::Serialize + std::fmt::Debug, @@ -371,7 +339,7 @@ pub trait TelegramApi { &self, method: &str, params: Params, - files: Vec<(&str, PathBuf)>, + files: Vec<(std::borrow::Cow<'static, str>, InputFile)>, ) -> Result where Params: serde::ser::Serialize + std::fmt::Debug,