From a5bd40fbbddac80114e65571fbac1f3513903f8d Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Thu, 20 Feb 2025 17:38:32 +0100 Subject: [PATCH 01/14] feat: send files directly from bytes --- Cargo.lock | 1 - Cargo.toml | 18 ++-- README.md | 10 +- examples/api_trait_implementation.rs | 6 +- examples/async_file_upload.rs | 30 ++++-- examples/file_upload.rs | 35 +++++++ src/api_params.rs | 45 +++++++-- src/client_reqwest.rs | 74 ++++++--------- src/client_ureq.rs | 75 +++++++-------- src/error.rs | 8 +- src/trait_async.rs | 128 ++++++++++++------------- src/trait_sync.rs | 134 +++++++++++++-------------- 12 files changed, 290 insertions(+), 274 deletions(-) create mode 100644 examples/file_upload.rs diff --git a/Cargo.lock b/Cargo.lock index 4da58af..75d9f08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -344,7 +344,6 @@ dependencies = [ "bon", "isahc", "macro_rules_attribute", - "mime_guess", "mockito", "multipart", "paste", diff --git a/Cargo.toml b/Cargo.toml index dcb003b..cf7fd75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,10 +19,11 @@ categories = ["web-programming::http-client"] all-features = true [features] -client-reqwest = ["trait-async", "dep:reqwest", "dep:tokio", "dep:serde_json"] -client-ureq = ["trait-sync", "dep:ureq", "dep:multipart", "dep:mime_guess", "dep:serde_json"] +client-reqwest = ["trait-async", "dep:reqwest", "dep:serde_json"] +client-ureq = ["trait-sync", "dep:ureq", "dep:multipart", "dep:serde_json"] trait-async = ["dep:async-trait"] trait-sync = [] +inputfile-read-tokio = ["dep:tokio", "tokio/fs"] [lints.rust] unsafe_code = "forbid" @@ -39,13 +40,13 @@ unreadable_literal = "allow" async-trait = { version = "0.1", optional = true } bon = "3.0.0" macro_rules_attribute = "0.2.0" -mime_guess = { version = "2", optional = true } multipart = { version = "0.18", optional = true, default-features = false, features = ["client"] } paste = "1.0.2" serde = { version = "1.0.157", features = ["derive"] } serde_json = { version = "1.0.45", optional = true } serde_with = { version = "3.0.0", default-features = false, features = ["macros"] } thiserror = "2" +tokio = { version = "1", optional = true } ureq = { version = "3.0.0", optional = true, default-features = false, features = ["rustls"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies.reqwest] @@ -60,11 +61,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" @@ -87,6 +83,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"] @@ -101,7 +101,7 @@ required-features = ["client-reqwest"] [[example]] name = "async_file_upload" -required-features = ["client-reqwest"] +required-features = ["client-reqwest", "inputfile-read-tokio"] [[example]] name = "async_custom_client" diff --git a/README.md b/README.md index d75199e..ba7d90f 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,9 @@ 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 + - `inputfile-read-tokio` - helper function to read an `InputFile` from a file system with `tokio::fs::read` For example for the async client add the following line to your `Cargo.toml` file: @@ -116,7 +117,7 @@ See more examples in the [`examples`](https://github.com/ayrat555/frankenstein/t ### Uploading files -Some methods in the API allow uploading files. In the Frankenstein for this `FileUpload` enum is used: +Some methods in the API allow uploading files. In Frankenstein the `FileUpload` enum is used: ```rust pub enum FileUpload { @@ -125,7 +126,8 @@ pub enum FileUpload { } pub struct InputFile { - path: std::path::PathBuf + bytes: Vec, + file_name: String, } ``` @@ -134,6 +136,8 @@ 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. +You can use the helper functions `InputFile::read_std(Path)` or `InputFile::read_tokio(Path)` for reading from the file system. + ### 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.39.2/frankenstein/trait.TelegramApi.html#provided-methods) diff --git a/examples/api_trait_implementation.rs b/examples/api_trait_implementation.rs index 8f7be4d..2d8b952 100644 --- a/examples/api_trait_implementation.rs +++ b/examples/api_trait_implementation.rs @@ -1,6 +1,4 @@ -use std::path::PathBuf; - -use frankenstein::api_params::SendMessageParams; +use frankenstein::api_params::{InputFile, SendMessageParams}; use frankenstein::response::ErrorResponse; use frankenstein::TelegramApi; use isahc::prelude::*; @@ -93,7 +91,7 @@ impl TelegramApi for MyApiClient { &self, _method: &str, _params: Params, - _files: Vec<(&str, PathBuf)>, + _files: Vec<(&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 4a25df6..6f23028 100644 --- a/examples/async_file_upload.rs +++ b/examples/async_file_upload.rs @@ -1,27 +1,37 @@ -use frankenstein::api_params::SendPhotoParams; +use std::env; + +use frankenstein::api_params::{InputFile, SendPhotoParams}; use frankenstein::client_reqwest::Bot; use frankenstein::AsyncTelegramApi; -static TOKEN: &str = "TOKEN"; -static CHAT_ID: i64 = 1; - #[tokio::main] async fn main() { - let bot = Bot::new(TOKEN); + let token = env::var("BOT_TOKEN").expect("Should have BOT_TOKEN as environment variable"); + let chat_id = 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 input_file = InputFile::read_tokio_fs("./frankenstein_logo.png") + .await + .expect("Should be able to read file"); + println!("File size: {}", input_file.bytes.len()); let params = SendPhotoParams::builder() - .chat_id(CHAT_ID) - .photo(file) + .chat_id(chat_id) + .photo(input_file) .build(); match bot.send_photo(¶ms).await { Ok(response) => { - println!("Photo was uploaded {response:?}"); + println!("Photo was uploaded successfully"); + dbg!(response); } Err(error) => { - eprintln!("Failed to upload photo: {error:?}"); + eprintln!("Failed to upload photo: {error}"); + dbg!(error); } } } diff --git a/examples/file_upload.rs b/examples/file_upload.rs new file mode 100644 index 0000000..a36f243 --- /dev/null +++ b/examples/file_upload.rs @@ -0,0 +1,35 @@ +use std::env; + +use frankenstein::api_params::{InputFile, SendPhotoParams}; +use frankenstein::client_ureq::Bot; +use frankenstein::TelegramApi; + +fn main() { + let token = env::var("BOT_TOKEN").expect("Should have BOT_TOKEN as environment variable"); + let chat_id = 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 input_file = + InputFile::read_std("./frankenstein_logo.png").expect("Should be able to read file"); + println!("File size: {}", input_file.bytes.len()); + + let params = SendPhotoParams::builder() + .chat_id(chat_id) + .photo(input_file) + .build(); + + match bot.send_photo(¶ms) { + Ok(response) => { + println!("Photo was uploaded successfully"); + dbg!(response); + } + Err(error) => { + eprintln!("Failed to upload photo: {error}"); + dbg!(error); + } + } +} diff --git a/src/api_params.rs b/src/api_params.rs index 2826ed9..4d377b9 100644 --- a/src/api_params.rs +++ b/src/api_params.rs @@ -1,6 +1,6 @@ //! Parameters to Telegram API methods. -use std::path::PathBuf; +use std::path::Path; use serde::{Deserialize, Serialize}; @@ -31,14 +31,6 @@ pub enum FileUpload { String(String), } -impl From for FileUpload { - fn from(path: PathBuf) -> Self { - let input_file = InputFile { path }; - - Self::InputFile(input_file) - } -} - impl From for FileUpload { fn from(file: InputFile) -> Self { Self::InputFile(file) @@ -51,6 +43,38 @@ impl From for FileUpload { } } +impl InputFile { + /// This method is intended to be used after `fs` operations + fn file_name_from_path(path: &Path) -> std::io::Result { + let file_name = path + .file_name() + .ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::Other, + "A file that could be read should also have a name", + ) + })? + .to_string_lossy() + .to_string(); + Ok(file_name) + } + + pub fn read_std>(path: P) -> std::io::Result { + let path = path.as_ref(); + let bytes = std::fs::read(path)?; + let file_name = Self::file_name_from_path(path)?; + Ok(Self { bytes, file_name }) + } + + #[cfg(feature = "inputfile-read-tokio")] + pub async fn read_tokio_fs>(path: P) -> std::io::Result { + let path = path.as_ref(); + let bytes = tokio::fs::read(path).await?; + let file_name = Self::file_name_from_path(path)?; + Ok(Self { bytes, file_name }) + } +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(tag = "type", rename_all = "snake_case")] pub enum InlineQueryResult { @@ -188,7 +212,8 @@ pub struct BotCommandScopeChatMember { #[apply(apistruct!)] #[derive(Eq)] pub struct InputFile { - pub path: PathBuf, + pub bytes: Vec, + pub file_name: String, } #[apply(apistruct!)] diff --git a/src/client_reqwest.rs b/src/client_reqwest.rs index 0b1daa3..f809da7 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::api_params::InputFile; use crate::trait_async::AsyncTelegramApi; use crate::Error; @@ -93,60 +93,40 @@ impl AsyncTelegramApi for Bot { &self, method: &str, params: Params, - files: Vec<(&str, PathBuf)>, + files: Vec<(&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 files_with_paths: Vec<(String, &str, String)> = files - .iter() - .map(|(key, path)| { - ( - (*key).to_string(), - path.to_str().unwrap(), - path.file_name().unwrap().to_str().unwrap().to_string(), - ) - }) - .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(), - }; - - form = form.text(key.clone(), val); - } - } - - for (parameter_name, file_path, file_name) in files_with_paths { - let file = tokio::fs::File::open(file_path) - .await - .map_err(Error::ReadFile)?; - let part = multipart::Part::stream(file).file_name(file_name); - form = form.part(parameter_name, part); + let json_string = crate::json::encode(¶ms)?; + let json_struct: Value = serde_json::from_str(&json_string).unwrap(); + + let file_keys = files.iter().map(|(key, _)| *key).collect::>(); + + let mut form = reqwest::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(), + }; + form = form.text(key.clone(), val); } + } - let url = format!("{}/{method}", self.api_url); - - let response = self.client.post(url).multipart(form).send().await?; - Self::decode_response(response).await + for (parameter_name, input_file) in files { + // The reqwest::multipart stuff requires 'static which we can not grant here. + // So we provide owned data. + let part = reqwest::multipart::Part::bytes(input_file.bytes.clone()) + .file_name(input_file.file_name.clone()); + form = form.part(parameter_name.to_owned(), 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 10b5087..3cc059d 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::api_params::InputFile; use crate::trait_sync::TelegramApi; use crate::Error; @@ -85,7 +85,7 @@ impl TelegramApi for Bot { &self, method: &str, params: Params, - files: Vec<(&str, PathBuf)>, + files: Vec<(&str, &InputFile)>, ) -> Result where Params: serde::ser::Serialize + std::fmt::Debug, @@ -93,11 +93,7 @@ impl TelegramApi for Bot { { 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 files_with_names: Vec<(&str, Option<&str>, PathBuf)> = files - .iter() - .map(|(key, path)| (*key, path.file_name().unwrap().to_str(), path.clone())) - .collect(); + let file_keys = files.iter().map(|(key, _)| *key).collect::>(); let mut form = Multipart::new(); for (key, val) in json_struct.as_object().unwrap() { @@ -111,15 +107,13 @@ impl TelegramApi for Bot { } } - for (parameter_name, file_name, file_path) in files_with_names { - let file = std::fs::File::open(&file_path).map_err(Error::ReadFile)?; - 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, file_name, Some(mime)); + for (parameter_name, input_file) in files { + form.add_stream( + parameter_name, + &*input_file.bytes, + Some(&input_file.file_name), + None, + ); } let url = format!("{}/{method}", self.api_url); @@ -148,7 +142,7 @@ mod tests { EditMessageTextParams, ExportChatInviteLinkParams, FileUpload, ForwardMessageParams, GetChatAdministratorsParams, GetChatMemberCountParams, GetChatMemberParams, GetChatParams, GetFileParams, GetMyCommandsParams, GetStickerSetParams, GetUpdatesParams, - GetUserProfilePhotosParams, InlineQueryResult, InputFile, InputMedia, InputMediaPhoto, + GetUserProfilePhotosParams, InlineQueryResult, InputMedia, InputMediaPhoto, LeaveChatParams, Media, PinChatMessageParams, PromoteChatMemberParams, RestrictChatMemberParams, RevokeChatInviteLinkParams, SendAnimationParams, SendAudioParams, SendChatActionParams, SendContactParams, SendDiceParams, SendDocumentParams, @@ -182,6 +176,13 @@ mod tests { }}; } + fn dummy_file() -> InputFile { + InputFile { + bytes: vec![1, 2, 3], + file_name: "dummy.file".to_owned(), + } + } + #[test] fn new_sets_correct_url() { let api = Bot::new("hey"); @@ -559,10 +560,9 @@ mod tests { #[test] fn send_photo_success() { let response_string = "{\"ok\":true,\"result\":{\"message_id\":2763,\"from\":{\"id\":1276618370,\"is_bot\":true,\"first_name\":\"test_el_bot\",\"username\":\"el_mon_test_bot\"},\"date\":1618730180,\"chat\":{\"id\":275808073,\"type\":\"private\",\"username\":\"Ayrat555\",\"first_name\":\"Ayrat\",\"last_name\":\"Badykov\"},\"photo\":[{\"file_id\":\"AgACAgIAAxkDAAIKy2B73MQXIhoDDmLXjcUjgqGf-m8bAALjsDEbORLgS-s4BkBzcC5DYvIBny4AAwEAAwIAA20AA0U3AwABHwQ\",\"file_unique_id\":\"AQADYvIBny4AA0U3AwAB\",\"width\":320,\"height\":320,\"file_size\":19968},{\"file_id\":\"AgACAgIAAxkDAAIKy2B73MQXIhoDDmLXjcUjgqGf-m8bAALjsDEbORLgS-s4BkBzcC5DYvIBny4AAwEAAwIAA3gAA0Y3AwABHwQ\",\"file_unique_id\":\"AQADYvIBny4AA0Y3AwAB\",\"width\":799,\"height\":800,\"file_size\":63581},{\"file_id\":\"AgACAgIAAxkDAAIKy2B73MQXIhoDDmLXjcUjgqGf-m8bAALjsDEbORLgS-s4BkBzcC5DYvIBny4AAwEAAwIAA3kAA0M3AwABHwQ\",\"file_unique_id\":\"AQADYvIBny4AA0M3AwAB\",\"width\":847,\"height\":848,\"file_size\":63763}]}}"; - let file = std::path::PathBuf::from("./frankenstein_logo.png"); let params = SendPhotoParams::builder() .chat_id(275808073) - .photo(file) + .photo(dummy_file()) .build(); let response = case!(sendPhoto, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); @@ -571,10 +571,9 @@ mod tests { #[test] fn send_audio_file_success() { let response_string = "{\"ok\":true,\"result\":{\"message_id\":2766,\"from\":{\"id\":1276618370,\"is_bot\":true,\"first_name\":\"test_el_bot\",\"username\":\"el_mon_test_bot\"},\"date\":1618735176,\"chat\":{\"id\":275808073,\"type\":\"private\",\"username\":\"Ayrat555\",\"first_name\":\"Ayrat\",\"last_name\":\"Badykov\"},\"audio\":{\"file_id\":\"CQACAgIAAxkDAAIKzmB78EjK-iOHo-HKC-M6p4r0jGdmAALkDAACORLgS5co1z0uFAKgHwQ\",\"file_unique_id\":\"AgAD5AwAAjkS4Es\",\"duration\":123,\"title\":\"Way Back Home\",\"file_name\":\"audio.mp3\",\"mime_type\":\"audio/mpeg\",\"file_size\":2957092}}}"; - let file = std::path::PathBuf::from("./frankenstein_logo.png"); let params = SendAudioParams::builder() .chat_id(275808073) - .audio(file) + .audio(dummy_file()) .build(); let response = case!(sendAudio, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); @@ -583,11 +582,10 @@ mod tests { #[test] fn send_audio_file_with_thumb_success() { let response_string = "{\"ok\":true,\"result\":{\"message_id\":2766,\"from\":{\"id\":1276618370,\"is_bot\":true,\"first_name\":\"test_el_bot\",\"username\":\"el_mon_test_bot\"},\"date\":1618735176,\"chat\":{\"id\":275808073,\"type\":\"private\",\"username\":\"Ayrat555\",\"first_name\":\"Ayrat\",\"last_name\":\"Badykov\"},\"audio\":{\"file_id\":\"CQACAgIAAxkDAAIKzmB78EjK-iOHo-HKC-M6p4r0jGdmAALkDAACORLgS5co1z0uFAKgHwQ\",\"file_unique_id\":\"AgAD5AwAAjkS4Es\",\"duration\":123,\"title\":\"Way Back Home\",\"file_name\":\"audio.mp3\",\"mime_type\":\"audio/mpeg\",\"file_size\":2957092}}}"; - let file = std::path::PathBuf::from("./frankenstein_logo.png"); let params = SendAudioParams::builder() .chat_id(275808073) - .audio(file.clone()) - .thumbnail(file) + .audio(dummy_file()) + .thumbnail(dummy_file()) .build(); let response = case!(sendAudio, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); @@ -610,10 +608,9 @@ mod tests { #[test] fn send_document_success() { let response_string = "{\"ok\":true,\"result\":{\"message_id\":2770,\"from\":{\"id\":1276618370,\"is_bot\":true,\"first_name\":\"test_el_bot\",\"username\":\"el_mon_test_bot\"},\"date\":1618737593,\"chat\":{\"id\":275808073,\"type\":\"private\",\"username\":\"Ayrat555\",\"first_name\":\"Ayrat\",\"last_name\":\"Badykov\"},\"audio\":{\"file_id\":\"CQACAgIAAxkDAAIK0mB7-bnnewABfdaFKK4NzVLQ7BvgCwAC6gwAAjkS4Et_njaNR8IUMB8E\",\"file_unique_id\":\"AgAD6gwAAjkS4Es\",\"duration\":123,\"title\":\"Way Back Home\",\"file_name\":\"audio.mp3\",\"mime_type\":\"audio/mpeg\",\"file_size\":2957092}}}"; - let file = std::path::PathBuf::from("./frankenstein_logo.png"); let params = SendDocumentParams::builder() .chat_id(275808073) - .document(file) + .document(dummy_file()) .build(); let response = case!(sendDocument, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); @@ -622,10 +619,9 @@ mod tests { #[test] fn send_video_success() { let response_string = "{\"ok\":true,\"result\":{\"message_id\":2770,\"from\":{\"id\":1276618370,\"is_bot\":true,\"first_name\":\"test_el_bot\",\"username\":\"el_mon_test_bot\"},\"date\":1618737593,\"chat\":{\"id\":275808073,\"type\":\"private\",\"username\":\"Ayrat555\",\"first_name\":\"Ayrat\",\"last_name\":\"Badykov\"},\"audio\":{\"file_id\":\"CQACAgIAAxkDAAIK0mB7-bnnewABfdaFKK4NzVLQ7BvgCwAC6gwAAjkS4Et_njaNR8IUMB8E\",\"file_unique_id\":\"AgAD6gwAAjkS4Es\",\"duration\":123,\"title\":\"Way Back Home\",\"file_name\":\"audio.mp3\",\"mime_type\":\"audio/mpeg\",\"file_size\":2957092}}}"; - let file = std::path::PathBuf::from("./frankenstein_logo.png"); let params = SendVideoParams::builder() .chat_id(275808073) - .video(file) + .video(dummy_file()) .build(); let response = case!(sendVideo, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); @@ -634,10 +630,9 @@ mod tests { #[test] fn send_animation_success() { let response_string = "{\"ok\":true,\"result\":{\"message_id\":2770,\"from\":{\"id\":1276618370,\"is_bot\":true,\"first_name\":\"test_el_bot\",\"username\":\"el_mon_test_bot\"},\"date\":1618737593,\"chat\":{\"id\":275808073,\"type\":\"private\",\"username\":\"Ayrat555\",\"first_name\":\"Ayrat\",\"last_name\":\"Badykov\"},\"audio\":{\"file_id\":\"CQACAgIAAxkDAAIK0mB7-bnnewABfdaFKK4NzVLQ7BvgCwAC6gwAAjkS4Et_njaNR8IUMB8E\",\"file_unique_id\":\"AgAD6gwAAjkS4Es\",\"duration\":123,\"title\":\"Way Back Home\",\"file_name\":\"audio.mp3\",\"mime_type\":\"audio/mpeg\",\"file_size\":2957092}}}"; - let file = std::path::PathBuf::from("./frankenstein_logo.png"); let params = SendAnimationParams::builder() .chat_id(275808073) - .animation(file) + .animation(dummy_file()) .build(); let response = case!(sendAnimation, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); @@ -646,10 +641,9 @@ mod tests { #[test] fn send_voice_success() { let response_string = "{\"ok\":true,\"result\":{\"message_id\":2770,\"from\":{\"id\":1276618370,\"is_bot\":true,\"first_name\":\"test_el_bot\",\"username\":\"el_mon_test_bot\"},\"date\":1618737593,\"chat\":{\"id\":275808073,\"type\":\"private\",\"username\":\"Ayrat555\",\"first_name\":\"Ayrat\",\"last_name\":\"Badykov\"},\"audio\":{\"file_id\":\"CQACAgIAAxkDAAIK0mB7-bnnewABfdaFKK4NzVLQ7BvgCwAC6gwAAjkS4Et_njaNR8IUMB8E\",\"file_unique_id\":\"AgAD6gwAAjkS4Es\",\"duration\":123,\"title\":\"Way Back Home\",\"file_name\":\"audio.mp3\",\"mime_type\":\"audio/mpeg\",\"file_size\":2957092}}}"; - let file = std::path::PathBuf::from("./frankenstein_logo.png"); let params = SendVoiceParams::builder() .chat_id(275808073) - .voice(file) + .voice(dummy_file()) .build(); let response = case!(sendVoice, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); @@ -658,10 +652,9 @@ mod tests { #[test] fn send_video_note_success() { let response_string = "{\"ok\":true,\"result\":{\"message_id\":2770,\"from\":{\"id\":1276618370,\"is_bot\":true,\"first_name\":\"test_el_bot\",\"username\":\"el_mon_test_bot\"},\"date\":1618737593,\"chat\":{\"id\":275808073,\"type\":\"private\",\"username\":\"Ayrat555\",\"first_name\":\"Ayrat\",\"last_name\":\"Badykov\"},\"audio\":{\"file_id\":\"CQACAgIAAxkDAAIK0mB7-bnnewABfdaFKK4NzVLQ7BvgCwAC6gwAAjkS4Et_njaNR8IUMB8E\",\"file_unique_id\":\"AgAD6gwAAjkS4Es\",\"duration\":123,\"title\":\"Way Back Home\",\"file_name\":\"audio.mp3\",\"mime_type\":\"audio/mpeg\",\"file_size\":2957092}}}"; - let file = std::path::PathBuf::from("./frankenstein_logo.png"); let params = SendVideoNoteParams::builder() .chat_id(275808073) - .video_note(file) + .video_note(dummy_file()) .build(); let response = case!(sendVideoNote, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); @@ -670,12 +663,9 @@ mod tests { #[test] fn set_chat_photo_success() { let response_string = "{\"ok\":true,\"result\":true}"; - let file = InputFile { - path: std::path::PathBuf::from("./frankenstein_logo.png"), - }; let params = SetChatPhotoParams::builder() .chat_id(275808073) - .photo(file) + .photo(dummy_file()) .build(); let response = case!(setChatPhoto, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); @@ -942,10 +932,9 @@ mod tests { #[test] fn send_sticker_success() { let response_string = "{\"ok\":true,\"result\":{\"message_id\":2788,\"from\":{\"id\":1276618370,\"is_bot\":true,\"first_name\":\"test_el_bot\",\"username\":\"el_mon_test_bot\"},\"date\":1619245784,\"chat\":{\"id\":275808073,\"type\":\"private\",\"username\":\"Ayrat555\",\"first_name\":\"Ayrat\",\"last_name\":\"Badykov\"},\"sticker\":{\"file_id\":\"CAACAgIAAxkDAAIK5GCDutgNxc07rqqtjkGWrGskbHfQAAIMEAACRx8ZSKJ6Z5GkdVHcHwQ\",\"file_unique_id\":\"AgADDBAAAkcfGUg\",\"type\":\"regular\",\"width\":512,\"height\":512,\"is_animated\":false,\"is_video\":false,\"thumbnail\":{\"file_id\":\"AAMCAgADGQMAAgrkYIO62A3FzTuuqq2OQZasayRsd9AAAgwQAAJHHxlIonpnkaR1Udz29bujLgADAQAHbQADzR4AAh8E\",\"file_unique_id\":\"AQAD9vW7oy4AA80eAAI\",\"width\":320,\"height\":320,\"file_size\":19264},\"file_size\":36596}}}"; - let file = std::path::PathBuf::from("./frankenstein_logo.png"); let params = SendStickerParams::builder() .chat_id(275808073) - .sticker(file) + .sticker(dummy_file()) .build(); let response = case!(sendSticker, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); @@ -962,8 +951,7 @@ mod tests { #[test] fn send_media_group_success() { let response_string = "{\"ok\":true,\"result\":[{\"message_id\":510,\"from\":{\"id\":1276618370,\"is_bot\":true,\"first_name\":\"test_el_bot\",\"username\":\"el_mon_test_bot\"},\"date\":1619267462,\"chat\":{\"id\":-1001368460856,\"type\":\"supergroup\",\"title\":\"Frankenstein\"},\"media_group_id\":\"12954139699368426\",\"photo\":[{\"file_id\":\"AgACAgIAAx0EUZEOOAACAf5ghA-GtOaBIP2NOmtXdze-Un7PGgAC_q8xG0cfEUgpwpFo17XTfWTS5p8uAAMBAAMCAANtAANYQgACHwQ\",\"file_unique_id\":\"AQADZNLmny4AA1hCAAI\",\"width\":320,\"height\":320,\"file_size\":19162},{\"file_id\":\"AgACAgIAAx0EUZEOOAACAf5ghA-GtOaBIP2NOmtXdze-Un7PGgAC_q8xG0cfEUgpwpFo17XTfWTS5p8uAAMBAAMCAAN4AANZQgACHwQ\",\"file_unique_id\":\"AQADZNLmny4AA1lCAAI\",\"width\":800,\"height\":800,\"file_size\":65697},{\"file_id\":\"AgACAgIAAx0EUZEOOAACAf5ghA-GtOaBIP2NOmtXdze-Un7PGgAC_q8xG0cfEUgpwpFo17XTfWTS5p8uAAMBAAMCAAN5AANaQgACHwQ\",\"file_unique_id\":\"AQADZNLmny4AA1pCAAI\",\"width\":1146,\"height\":1146,\"file_size\":101324}]},{\"message_id\":511,\"from\":{\"id\":1276618370,\"is_bot\":true,\"first_name\":\"test_el_bot\",\"username\":\"el_mon_test_bot\"},\"date\":1619267462,\"chat\":{\"id\":-1001368460856,\"type\":\"supergroup\",\"title\":\"Frankenstein\"},\"media_group_id\":\"12954139699368426\",\"photo\":[{\"file_id\":\"AgACAgIAAx0EUZEOOAACAf9ghA-GeFo0B7v78UyXoOD9drjEGgAC_q8xG0cfEUgpwpFo17XTfWTS5p8uAAMBAAMCAANtAANYQgACHwQ\",\"file_unique_id\":\"AQADZNLmny4AA1hCAAI\",\"width\":320,\"height\":320,\"file_size\":19162},{\"file_id\":\"AgACAgIAAx0EUZEOOAACAf9ghA-GeFo0B7v78UyXoOD9drjEGgAC_q8xG0cfEUgpwpFo17XTfWTS5p8uAAMBAAMCAAN4AANZQgACHwQ\",\"file_unique_id\":\"AQADZNLmny4AA1lCAAI\",\"width\":800,\"height\":800,\"file_size\":65697},{\"file_id\":\"AgACAgIAAx0EUZEOOAACAf9ghA-GeFo0B7v78UyXoOD9drjEGgAC_q8xG0cfEUgpwpFo17XTfWTS5p8uAAMBAAMCAAN5AANaQgACHwQ\",\"file_unique_id\":\"AQADZNLmny4AA1pCAAI\",\"width\":1146,\"height\":1146,\"file_size\":101324}]}]}"; - let file = std::path::PathBuf::from("./frankenstein_logo.png"); - let photo = InputMediaPhoto::builder().media(file).build(); + let photo = InputMediaPhoto::builder().media(dummy_file()).build(); let medias = vec![Media::Photo(photo.clone()), Media::Photo(photo)]; let params = SendMediaGroupParams::builder() .chat_id(-1001368460856) @@ -976,10 +964,9 @@ mod tests { #[test] fn edit_message_media_success() { let response_string = "{\"ok\":true,\"result\":{\"message_id\":513,\"from\":{\"id\":1276618370,\"is_bot\":true,\"first_name\":\"test_el_bot\",\"username\":\"el_mon_test_bot\"},\"date\":1619336672,\"chat\":{\"id\":-1001368460856,\"type\":\"supergroup\",\"title\":\"Frankenstein\"},\"edit_date\":1619336788,\"photo\":[{\"file_id\":\"AgACAgIAAx0EUZEOOAACAgFghR5URaBN41jx7VNgLPi29xmfQgAC_q8xG0cfEUgpwpFo17XTfWTS5p8uAAMBAAMCAANtAANYQgACHwQ\",\"file_unique_id\":\"AQADZNLmny4AA1hCAAI\",\"width\":320,\"height\":320,\"file_size\":19162},{\"file_id\":\"AgACAgIAAx0EUZEOOAACAgFghR5URaBN41jx7VNgLPi29xmfQgAC_q8xG0cfEUgpwpFo17XTfWTS5p8uAAMBAAMCAAN4AANZQgACHwQ\",\"file_unique_id\":\"AQADZNLmny4AA1lCAAI\",\"width\":800,\"height\":800,\"file_size\":65697},{\"file_id\":\"AgACAgIAAx0EUZEOOAACAgFghR5URaBN41jx7VNgLPi29xmfQgAC_q8xG0cfEUgpwpFo17XTfWTS5p8uAAMBAAMCAAN5AANaQgACHwQ\",\"file_unique_id\":\"AQADZNLmny4AA1pCAAI\",\"width\":1146,\"height\":1146,\"file_size\":101324}]}}"; - let file = std::path::PathBuf::from("./frankenstein_logo.png"); let params = EditMessageMediaParams::builder() .media(InputMedia::Photo( - InputMediaPhoto::builder().media(file).build(), + InputMediaPhoto::builder().media(dummy_file()).build(), )) .chat_id(-1001368460856) .message_id(513) diff --git a/src/error.rs b/src/error.rs index 6c5bdd8..4fbd75f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -19,13 +19,6 @@ pub enum Error { input: String, }, - #[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), @@ -36,6 +29,7 @@ pub enum Error { } impl Error { + #[allow(irrefutable_let_patterns)] // See https://github.com/rust-lang/rust/issues/72469 #[cfg(test)] #[track_caller] pub(crate) fn unwrap_api(self) -> ErrorResponse { diff --git a/src/trait_async.rs b/src/trait_async.rs index 8058efc..7246861 100644 --- a/src/trait_async.rs +++ b/src/trait_async.rs @@ -1,15 +1,13 @@ -use std::path::PathBuf; - use crate::api_params::{ AddStickerToSetParams, CreateNewStickerSetParams, EditMessageMediaParams, FileUpload, - InputMedia, Media, SendAnimationParams, SendAudioParams, SendDocumentParams, + InputFile, InputMedia, Media, SendAnimationParams, SendAudioParams, SendDocumentParams, SendMediaGroupParams, SendPhotoParams, SendStickerParams, SendVideoNoteParams, SendVideoParams, SendVoiceParams, SetChatPhotoParams, SetStickerSetThumbnailParams, UploadStickerFileParams, }; use crate::objects::{ BotCommand, BotDescription, BotName, BotShortDescription, BusinessConnection, - ChatAdministratorRights, ChatFullInfo, ChatInviteLink, ChatMember, File as FileObject, - ForumTopic, GameHighScore, Gifts, InputSticker, MenuButton, Message, MessageId, Poll, + ChatAdministratorRights, ChatFullInfo, ChatInviteLink, ChatMember, File, ForumTopic, + GameHighScore, Gifts, InputSticker, MenuButton, Message, MessageId, Poll, PreparedInlineMessage, SentWebAppMessage, StarTransactions, Sticker, StickerSet, Update, User, UserChatBoosts, UserProfilePhotos, WebhookInfo, }; @@ -75,10 +73,10 @@ where params: &SendPhotoParams, ) -> Result, Self::Error> { let method_name = "sendPhoto"; - let mut files: Vec<(&str, PathBuf)> = vec![]; + let mut files = Vec::new(); if let FileUpload::InputFile(input_file) = ¶ms.photo { - files.push(("photo", input_file.path.clone())); + files.push(("photo", input_file)); } self.request_with_possible_form_data(method_name, params, files) @@ -90,14 +88,14 @@ where params: &SendAudioParams, ) -> Result, Self::Error> { let method_name = "sendAudio"; - let mut files: Vec<(&str, PathBuf)> = vec![]; + let mut files = Vec::new(); if let FileUpload::InputFile(input_file) = ¶ms.audio { - files.push(("audio", input_file.path.clone())); + files.push(("audio", input_file)); } if let Some(FileUpload::InputFile(input_file)) = ¶ms.thumbnail { - files.push(("thumbnail", input_file.path.clone())); + files.push(("thumbnail", input_file)); } self.request_with_possible_form_data(method_name, params, files) @@ -109,7 +107,7 @@ where params: &SendMediaGroupParams, ) -> Result>, Self::Error> { let method_name = "sendMediaGroup"; - let mut files: Vec<(String, PathBuf)> = vec![]; + let mut files = Vec::new(); let mut new_medias: Vec = vec![]; let mut file_idx = 0; @@ -125,7 +123,7 @@ where new_audio.media = FileUpload::String(attach_name); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } if let Some(FileUpload::InputFile(input_file)) = &audio.thumbnail { @@ -135,7 +133,7 @@ where new_audio.thumbnail = Some(FileUpload::String(attach_name)); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } new_medias.push(Media::Audio(new_audio)); @@ -151,7 +149,7 @@ where new_document.media = FileUpload::String(attach_name); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } new_medias.push(Media::Document(new_document)); @@ -166,7 +164,7 @@ where new_photo.media = FileUpload::String(attach_name); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } new_medias.push(Media::Photo(new_photo)); @@ -182,7 +180,7 @@ where new_video.media = FileUpload::String(attach_name); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } if let Some(FileUpload::InputFile(input_file)) = &video.cover { @@ -192,7 +190,7 @@ where new_video.cover = Some(FileUpload::String(attach_name)); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } if let Some(FileUpload::InputFile(input_file)) = &video.thumbnail { @@ -202,7 +200,7 @@ where new_video.thumbnail = Some(FileUpload::String(attach_name)); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } new_medias.push(Media::Video(new_video)); @@ -215,7 +213,7 @@ where let files_with_str_names = files .iter() - .map(|(key, path)| (key.as_str(), path.clone())) + .map(|(key, file)| (key.as_str(), *file)) .collect(); self.request_with_possible_form_data(method_name, &new_params, files_with_str_names) @@ -227,14 +225,14 @@ where params: &SendDocumentParams, ) -> Result, Self::Error> { let method_name = "sendDocument"; - let mut files: Vec<(&str, PathBuf)> = vec![]; + let mut files = Vec::new(); if let FileUpload::InputFile(input_file) = ¶ms.document { - files.push(("document", input_file.path.clone())); + files.push(("document", input_file)); } if let Some(FileUpload::InputFile(input_file)) = ¶ms.thumbnail { - files.push(("thumbnail", input_file.path.clone())); + files.push(("thumbnail", input_file)); } self.request_with_possible_form_data(method_name, params, files) @@ -246,18 +244,18 @@ where params: &SendVideoParams, ) -> Result, Self::Error> { let method_name = "sendVideo"; - let mut files: Vec<(&str, PathBuf)> = vec![]; + let mut files = Vec::new(); if let FileUpload::InputFile(input_file) = ¶ms.video { - files.push(("video", input_file.path.clone())); + files.push(("video", input_file)); } if let Some(FileUpload::InputFile(input_file)) = ¶ms.cover { - files.push(("cover", input_file.path.clone())); + files.push(("cover", input_file)); } if let Some(FileUpload::InputFile(input_file)) = ¶ms.thumbnail { - files.push(("thumbnail", input_file.path.clone())); + files.push(("thumbnail", input_file)); } self.request_with_possible_form_data(method_name, params, files) @@ -269,14 +267,14 @@ where params: &SendAnimationParams, ) -> Result, Self::Error> { let method_name = "sendAnimation"; - let mut files: Vec<(&str, PathBuf)> = vec![]; + let mut files = Vec::new(); if let FileUpload::InputFile(input_file) = ¶ms.animation { - files.push(("animation", input_file.path.clone())); + files.push(("animation", input_file)); } if let Some(FileUpload::InputFile(input_file)) = ¶ms.thumbnail { - files.push(("thumbnail", input_file.path.clone())); + files.push(("thumbnail", input_file)); } self.request_with_possible_form_data(method_name, params, files) @@ -288,10 +286,10 @@ where params: &SendVoiceParams, ) -> Result, Self::Error> { let method_name = "sendVoice"; - let mut files: Vec<(&str, PathBuf)> = vec![]; + let mut files = Vec::new(); if let FileUpload::InputFile(input_file) = ¶ms.voice { - files.push(("voice", input_file.path.clone())); + files.push(("voice", input_file)); } self.request_with_possible_form_data(method_name, params, files) @@ -303,14 +301,14 @@ where params: &SendVideoNoteParams, ) -> Result, Self::Error> { let method_name = "sendVideoNote"; - let mut files: Vec<(&str, PathBuf)> = vec![]; + let mut files = Vec::new(); if let FileUpload::InputFile(input_file) = ¶ms.video_note { - files.push(("video_note", input_file.path.clone())); + files.push(("video_note", input_file)); } if let Some(FileUpload::InputFile(input_file)) = ¶ms.thumbnail { - files.push(("thumbnail", input_file.path.clone())); + files.push(("thumbnail", input_file)); } self.request_with_possible_form_data(method_name, params, files) @@ -329,7 +327,7 @@ where request!(setMessageReaction, bool); request!(getUserProfilePhotos, UserProfilePhotos); request!(setUserEmojiStatus, bool); - request!(getFile, FileObject); + request!(getFile, File); request!(banChatMember, bool); request!(unbanChatMember, bool); request!(restrictChatMember, bool); @@ -352,8 +350,7 @@ where params: &SetChatPhotoParams, ) -> Result, Self::Error> { let photo = ¶ms.photo; - - self.request_with_form_data("setChatPhoto", params, vec![("photo", photo.path.clone())]) + self.request_with_form_data("setChatPhoto", params, vec![("photo", photo)]) .await } @@ -403,7 +400,7 @@ where params: &EditMessageMediaParams, ) -> Result, Self::Error> { let method_name = "editMessageMedia"; - let mut files: Vec<(String, PathBuf)> = vec![]; + let mut files = Vec::new(); let new_media = match ¶ms.media { InputMedia::Animation(animation) => { @@ -415,7 +412,7 @@ where new_animation.media = FileUpload::String(attach_name); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } if let Some(FileUpload::InputFile(input_file)) = &animation.thumbnail { @@ -424,7 +421,7 @@ where new_animation.thumbnail = Some(FileUpload::String(attach_name)); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } InputMedia::Animation(new_animation) @@ -438,7 +435,7 @@ where new_document.media = FileUpload::String(attach_name); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } if let Some(FileUpload::InputFile(input_file)) = &document.thumbnail { @@ -447,7 +444,7 @@ where new_document.thumbnail = Some(FileUpload::String(attach_name)); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } InputMedia::Document(new_document) @@ -461,7 +458,7 @@ where new_audio.media = FileUpload::String(attach_name); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } if let Some(FileUpload::InputFile(input_file)) = &audio.thumbnail { @@ -470,7 +467,7 @@ where new_audio.thumbnail = Some(FileUpload::String(attach_name)); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } InputMedia::Audio(new_audio) @@ -484,7 +481,7 @@ where new_photo.media = FileUpload::String(attach_name); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } InputMedia::Photo(new_photo) @@ -498,7 +495,7 @@ where new_video.media = FileUpload::String(attach_name); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } if let Some(FileUpload::InputFile(input_file)) = &video.cover { @@ -507,7 +504,7 @@ where new_video.cover = Some(FileUpload::String(attach_name)); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } if let Some(FileUpload::InputFile(input_file)) = &video.thumbnail { @@ -516,7 +513,7 @@ where new_video.thumbnail = Some(FileUpload::String(attach_name)); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } InputMedia::Video(new_video) @@ -528,7 +525,7 @@ where let files_with_str_names = files .iter() - .map(|(key, path)| (key.as_str(), path.clone())) + .map(|(key, file)| (key.as_str(), *file)) .collect(); self.request_with_possible_form_data(method_name, &new_params, files_with_str_names) @@ -545,10 +542,10 @@ where params: &SendStickerParams, ) -> Result, Self::Error> { let method_name = "sendSticker"; - let mut files: Vec<(&str, PathBuf)> = vec![]; + let mut files = Vec::new(); if let FileUpload::InputFile(input_file) = ¶ms.sticker { - files.push(("sticker", input_file.path.clone())); + files.push(("sticker", input_file)); } self.request_with_possible_form_data(method_name, params, files) @@ -560,15 +557,10 @@ where async fn upload_sticker_file( &self, params: &UploadStickerFileParams, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { let sticker = ¶ms.sticker; - - self.request_with_form_data( - "uploadStickerFile", - params, - vec![("sticker", sticker.path.clone())], - ) - .await + self.request_with_form_data("uploadStickerFile", params, vec![("sticker", sticker)]) + .await } async fn create_new_sticker_set( @@ -577,7 +569,7 @@ where ) -> Result, Self::Error> { let method_name = "createNewStickerSet"; let mut new_stickers: Vec = vec![]; - let mut files: Vec<(String, PathBuf)> = vec![]; + let mut files = Vec::new(); let mut file_idx = 0; for sticker in ¶ms.stickers { @@ -590,7 +582,7 @@ where new_sticker.sticker = FileUpload::String(attach_name); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } new_stickers.push(new_sticker); @@ -601,7 +593,7 @@ where let files_with_str_names = files .iter() - .map(|(key, path)| (key.as_str(), path.clone())) + .map(|(key, file)| (key.as_str(), *file)) .collect(); self.request_with_possible_form_data(method_name, &new_params, files_with_str_names) @@ -614,10 +606,10 @@ where params: &AddStickerToSetParams, ) -> Result, Self::Error> { let method_name = "addStickerToSet"; - let mut files: Vec<(&str, PathBuf)> = vec![]; + let mut files = Vec::new(); if let FileUpload::InputFile(input_file) = ¶ms.sticker.sticker { - files.push(("sticker", input_file.path.clone())); + files.push(("sticker", input_file)); } self.request_with_possible_form_data(method_name, params, files) @@ -637,10 +629,10 @@ where params: &SetStickerSetThumbnailParams, ) -> Result, Self::Error> { let method_name = "setStickerSetThumbnail"; - let mut files: Vec<(&str, PathBuf)> = vec![]; + let mut files = Vec::new(); if let Some(FileUpload::InputFile(input_file)) = ¶ms.thumbnail { - files.push(("thumbnail", input_file.path.clone())); + files.push(("thumbnail", input_file)); } self.request_with_possible_form_data(method_name, params, files) @@ -686,7 +678,7 @@ where &self, method_name: &str, params: Params, - files: Vec<(&str, PathBuf)>, + files: Vec<(&str, &InputFile)>, ) -> Result where Params: serde::ser::Serialize + std::fmt::Debug + std::marker::Send, @@ -704,7 +696,7 @@ where &self, method: &str, params: Params, - files: Vec<(&str, PathBuf)>, + files: Vec<(&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 73645b5..ae5b96b 100644 --- a/src/trait_sync.rs +++ b/src/trait_sync.rs @@ -1,17 +1,15 @@ -use std::path::PathBuf; - use crate::api_params::{ AddStickerToSetParams, CreateNewStickerSetParams, EditMessageMediaParams, FileUpload, - InputMedia, Media, SendAnimationParams, SendAudioParams, SendDocumentParams, + InputFile, InputMedia, Media, SendAnimationParams, SendAudioParams, SendDocumentParams, SendMediaGroupParams, SendPhotoParams, SendStickerParams, SendVideoNoteParams, SendVideoParams, SendVoiceParams, SetChatPhotoParams, SetStickerSetThumbnailParams, UploadStickerFileParams, }; use crate::objects::{ BotCommand, BotDescription, BotName, BotShortDescription, BusinessConnection, - ChatAdministratorRights, ChatFullInfo, ChatInviteLink, ChatMember, File as FileObject, - ForumTopic, GameHighScore, Gifts, InputSticker, MenuButton, Message, MessageId, Poll, - PreparedInlineMessage, SentWebAppMessage, StarTransactions, Sticker, StickerSet, Update, User, - UserChatBoosts, UserProfilePhotos, WebhookInfo, + ChatAdministratorRights, ChatFullInfo, ChatInviteLink, ChatMember, File, ForumTopic, + GameHighScore, Gifts, MenuButton, Message, MessageId, Poll, PreparedInlineMessage, + SentWebAppMessage, StarTransactions, Sticker, StickerSet, Update, User, UserChatBoosts, + UserProfilePhotos, WebhookInfo, }; use crate::response::{MessageOrBool, MethodResponse}; @@ -64,10 +62,10 @@ pub trait TelegramApi { fn send_photo(&self, params: &SendPhotoParams) -> Result, Self::Error> { let method_name = "sendPhoto"; - let mut files: Vec<(&str, PathBuf)> = vec![]; + let mut files = Vec::new(); if let FileUpload::InputFile(input_file) = ¶ms.photo { - files.push(("photo", input_file.path.clone())); + files.push(("photo", input_file)); } self.request_with_possible_form_data(method_name, params, files) @@ -75,14 +73,14 @@ pub trait TelegramApi { fn send_audio(&self, params: &SendAudioParams) -> Result, Self::Error> { let method_name = "sendAudio"; - let mut files: Vec<(&str, PathBuf)> = vec![]; + let mut files = Vec::new(); if let FileUpload::InputFile(input_file) = ¶ms.audio { - files.push(("audio", input_file.path.clone())); + files.push(("audio", input_file)); } if let Some(FileUpload::InputFile(input_file)) = ¶ms.thumbnail { - files.push(("thumbnail", input_file.path.clone())); + files.push(("thumbnail", input_file)); } self.request_with_possible_form_data(method_name, params, files) @@ -93,8 +91,8 @@ pub trait TelegramApi { params: &SendMediaGroupParams, ) -> Result>, Self::Error> { let method_name = "sendMediaGroup"; - let mut files: Vec<(String, PathBuf)> = vec![]; - let mut new_medias: Vec = vec![]; + let mut files = Vec::new(); + let mut new_medias = Vec::new(); let mut file_idx = 0; for media in ¶ms.media { @@ -109,7 +107,7 @@ pub trait TelegramApi { new_audio.media = FileUpload::String(attach_name); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } if let Some(FileUpload::InputFile(input_file)) = &audio.thumbnail { @@ -119,7 +117,7 @@ pub trait TelegramApi { new_audio.thumbnail = Some(FileUpload::String(attach_name)); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } new_medias.push(Media::Audio(new_audio)); @@ -135,7 +133,7 @@ pub trait TelegramApi { new_document.media = FileUpload::String(attach_name); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } new_medias.push(Media::Document(new_document)); @@ -150,7 +148,7 @@ pub trait TelegramApi { new_photo.media = FileUpload::String(attach_name); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } new_medias.push(Media::Photo(new_photo)); @@ -166,7 +164,7 @@ pub trait TelegramApi { new_video.media = FileUpload::String(attach_name); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } if let Some(FileUpload::InputFile(input_file)) = &video.cover { @@ -176,7 +174,7 @@ pub trait TelegramApi { new_video.cover = Some(FileUpload::String(attach_name)); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } if let Some(FileUpload::InputFile(input_file)) = &video.thumbnail { @@ -186,7 +184,7 @@ pub trait TelegramApi { new_video.thumbnail = Some(FileUpload::String(attach_name)); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } new_medias.push(Media::Video(new_video)); @@ -199,7 +197,7 @@ pub trait TelegramApi { let files_with_str_names = files .iter() - .map(|(key, path)| (key.as_str(), path.clone())) + .map(|(key, file)| (key.as_str(), *file)) .collect(); self.request_with_possible_form_data(method_name, &new_params, files_with_str_names) @@ -210,14 +208,14 @@ pub trait TelegramApi { params: &SendDocumentParams, ) -> Result, Self::Error> { let method_name = "sendDocument"; - let mut files: Vec<(&str, PathBuf)> = vec![]; + let mut files = Vec::new(); if let FileUpload::InputFile(input_file) = ¶ms.document { - files.push(("document", input_file.path.clone())); + files.push(("document", input_file)); } if let Some(FileUpload::InputFile(input_file)) = ¶ms.thumbnail { - files.push(("thumbnail", input_file.path.clone())); + files.push(("thumbnail", input_file)); } self.request_with_possible_form_data(method_name, params, files) @@ -225,18 +223,18 @@ pub trait TelegramApi { fn send_video(&self, params: &SendVideoParams) -> Result, Self::Error> { let method_name = "sendVideo"; - let mut files: Vec<(&str, PathBuf)> = vec![]; + let mut files = Vec::new(); if let FileUpload::InputFile(input_file) = ¶ms.video { - files.push(("video", input_file.path.clone())); + files.push(("video", input_file)); } if let Some(FileUpload::InputFile(input_file)) = ¶ms.cover { - files.push(("cover", input_file.path.clone())); + files.push(("cover", input_file)); } if let Some(FileUpload::InputFile(input_file)) = ¶ms.thumbnail { - files.push(("thumbnail", input_file.path.clone())); + files.push(("thumbnail", input_file)); } self.request_with_possible_form_data(method_name, params, files) @@ -247,14 +245,14 @@ pub trait TelegramApi { params: &SendAnimationParams, ) -> Result, Self::Error> { let method_name = "sendAnimation"; - let mut files: Vec<(&str, PathBuf)> = vec![]; + let mut files = Vec::new(); if let FileUpload::InputFile(input_file) = ¶ms.animation { - files.push(("animation", input_file.path.clone())); + files.push(("animation", input_file)); } if let Some(FileUpload::InputFile(input_file)) = ¶ms.thumbnail { - files.push(("thumbnail", input_file.path.clone())); + files.push(("thumbnail", input_file)); } self.request_with_possible_form_data(method_name, params, files) @@ -262,10 +260,10 @@ pub trait TelegramApi { fn send_voice(&self, params: &SendVoiceParams) -> Result, Self::Error> { let method_name = "sendVoice"; - let mut files: Vec<(&str, PathBuf)> = vec![]; + let mut files = Vec::new(); if let FileUpload::InputFile(input_file) = ¶ms.voice { - files.push(("voice", input_file.path.clone())); + files.push(("voice", input_file)); } self.request_with_possible_form_data(method_name, params, files) @@ -276,14 +274,14 @@ pub trait TelegramApi { params: &SendVideoNoteParams, ) -> Result, Self::Error> { let method_name = "sendVideoNote"; - let mut files: Vec<(&str, PathBuf)> = vec![]; + let mut files = Vec::new(); if let FileUpload::InputFile(input_file) = ¶ms.video_note { - files.push(("video_note", input_file.path.clone())); + files.push(("video_note", input_file)); } if let Some(FileUpload::InputFile(input_file)) = ¶ms.thumbnail { - files.push(("thumbnail", input_file.path.clone())); + files.push(("thumbnail", input_file)); } self.request_with_possible_form_data(method_name, params, files) @@ -301,7 +299,7 @@ pub trait TelegramApi { request!(setMessageReaction, bool); request!(getUserProfilePhotos, UserProfilePhotos); request!(setUserEmojiStatus, bool); - request!(getFile, FileObject); + request!(getFile, File); request!(banChatMember, bool); request!(unbanChatMember, bool); request!(restrictChatMember, bool); @@ -324,8 +322,7 @@ pub trait TelegramApi { params: &SetChatPhotoParams, ) -> Result, Self::Error> { let photo = ¶ms.photo; - - self.request_with_form_data("setChatPhoto", params, vec![("photo", photo.path.clone())]) + self.request_with_form_data("setChatPhoto", params, vec![("photo", photo)]) } request!(deleteChatPhoto, bool); @@ -374,7 +371,7 @@ pub trait TelegramApi { params: &EditMessageMediaParams, ) -> Result, Self::Error> { let method_name = "editMessageMedia"; - let mut files: Vec<(String, PathBuf)> = vec![]; + let mut files = Vec::new(); let new_media = match ¶ms.media { InputMedia::Animation(animation) => { @@ -386,7 +383,7 @@ pub trait TelegramApi { new_animation.media = FileUpload::String(attach_name); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } if let Some(FileUpload::InputFile(input_file)) = &animation.thumbnail { @@ -395,7 +392,7 @@ pub trait TelegramApi { new_animation.thumbnail = Some(FileUpload::String(attach_name)); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } InputMedia::Animation(new_animation) @@ -409,7 +406,7 @@ pub trait TelegramApi { new_document.media = FileUpload::String(attach_name); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } if let Some(FileUpload::InputFile(input_file)) = &document.thumbnail { @@ -418,7 +415,7 @@ pub trait TelegramApi { new_document.thumbnail = Some(FileUpload::String(attach_name)); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } InputMedia::Document(new_document) @@ -432,7 +429,7 @@ pub trait TelegramApi { new_audio.media = FileUpload::String(attach_name); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } if let Some(FileUpload::InputFile(input_file)) = &audio.thumbnail { @@ -441,7 +438,7 @@ pub trait TelegramApi { new_audio.thumbnail = Some(FileUpload::String(attach_name)); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } InputMedia::Audio(new_audio) @@ -455,7 +452,7 @@ pub trait TelegramApi { new_photo.media = FileUpload::String(attach_name); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } InputMedia::Photo(new_photo) @@ -469,7 +466,7 @@ pub trait TelegramApi { new_video.media = FileUpload::String(attach_name); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } if let Some(FileUpload::InputFile(input_file)) = &video.cover { @@ -478,7 +475,7 @@ pub trait TelegramApi { new_video.cover = Some(FileUpload::String(attach_name)); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } if let Some(FileUpload::InputFile(input_file)) = &video.thumbnail { @@ -487,7 +484,7 @@ pub trait TelegramApi { new_video.thumbnail = Some(FileUpload::String(attach_name)); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } InputMedia::Video(new_video) @@ -499,7 +496,7 @@ pub trait TelegramApi { let files_with_str_names = files .iter() - .map(|(key, path)| (key.as_str(), path.clone())) + .map(|(key, file)| (key.as_str(), *file)) .collect(); self.request_with_possible_form_data(method_name, &new_params, files_with_str_names) @@ -515,10 +512,10 @@ pub trait TelegramApi { params: &SendStickerParams, ) -> Result, Self::Error> { let method_name = "sendSticker"; - let mut files: Vec<(&str, PathBuf)> = vec![]; + let mut files = Vec::new(); if let FileUpload::InputFile(input_file) = ¶ms.sticker { - files.push(("sticker", input_file.path.clone())); + files.push(("sticker", input_file)); } self.request_with_possible_form_data(method_name, params, files) @@ -529,14 +526,9 @@ pub trait TelegramApi { fn upload_sticker_file( &self, params: &UploadStickerFileParams, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { let sticker = ¶ms.sticker; - - self.request_with_form_data( - "uploadStickerFile", - params, - vec![("sticker", sticker.path.clone())], - ) + self.request_with_form_data("uploadStickerFile", params, vec![("sticker", sticker)]) } fn create_new_sticker_set( @@ -544,8 +536,8 @@ pub trait TelegramApi { params: &CreateNewStickerSetParams, ) -> Result, Self::Error> { let method_name = "createNewStickerSet"; - let mut new_stickers: Vec = vec![]; - let mut files: Vec<(String, PathBuf)> = vec![]; + let mut new_stickers = Vec::new(); + let mut files = Vec::new(); let mut file_idx = 0; for sticker in ¶ms.stickers { @@ -558,7 +550,7 @@ pub trait TelegramApi { new_sticker.sticker = FileUpload::String(attach_name); - files.push((name, input_file.path.clone())); + files.push((name, input_file)); } new_stickers.push(new_sticker); @@ -569,7 +561,7 @@ pub trait TelegramApi { let files_with_str_names = files .iter() - .map(|(key, path)| (key.as_str(), path.clone())) + .map(|(key, file)| (key.as_str(), *file)) .collect(); self.request_with_possible_form_data(method_name, &new_params, files_with_str_names) @@ -582,10 +574,10 @@ pub trait TelegramApi { params: &AddStickerToSetParams, ) -> Result, Self::Error> { let method_name = "addStickerToSet"; - let mut files: Vec<(&str, PathBuf)> = vec![]; + let mut files = Vec::new(); if let FileUpload::InputFile(input_file) = ¶ms.sticker.sticker { - files.push(("sticker", input_file.path.clone())); + files.push(("sticker", input_file)); } self.request_with_possible_form_data(method_name, params, files) @@ -604,10 +596,10 @@ pub trait TelegramApi { params: &SetStickerSetThumbnailParams, ) -> Result, Self::Error> { let method_name = "setStickerSetThumbnail"; - let mut files: Vec<(&str, PathBuf)> = vec![]; + let mut files = Vec::new(); if let Some(FileUpload::InputFile(input_file)) = ¶ms.thumbnail { - files.push(("thumbnail", input_file.path.clone())); + files.push(("thumbnail", input_file)); } self.request_with_possible_form_data(method_name, params, files) @@ -643,7 +635,7 @@ pub trait TelegramApi { &self, method_name: &str, params: &Params, - files: Vec<(&str, PathBuf)>, + files: Vec<(&str, &InputFile)>, ) -> Result where Params: serde::ser::Serialize + std::fmt::Debug, @@ -660,7 +652,7 @@ pub trait TelegramApi { &self, method: &str, params: Params, - files: Vec<(&str, PathBuf)>, + files: Vec<(&str, &InputFile)>, ) -> Result where Params: serde::ser::Serialize + std::fmt::Debug, From bc10f7c54fe07a3247615d506797573ab6bc449f Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Fri, 21 Feb 2025 00:46:13 +0100 Subject: [PATCH 02/14] refactor: less error-prone new_params without InputFile --- src/trait_async.rs | 44 +++++++++++++++++++++----------------------- src/trait_sync.rs | 38 ++++++++++++++++++-------------------- 2 files changed, 39 insertions(+), 43 deletions(-) diff --git a/src/trait_async.rs b/src/trait_async.rs index 7246861..6106f03 100644 --- a/src/trait_async.rs +++ b/src/trait_async.rs @@ -7,9 +7,9 @@ use crate::api_params::{ use crate::objects::{ BotCommand, BotDescription, BotName, BotShortDescription, BusinessConnection, ChatAdministratorRights, ChatFullInfo, ChatInviteLink, ChatMember, File, ForumTopic, - GameHighScore, Gifts, InputSticker, MenuButton, Message, MessageId, Poll, - PreparedInlineMessage, SentWebAppMessage, StarTransactions, Sticker, StickerSet, Update, User, - UserChatBoosts, UserProfilePhotos, WebhookInfo, + GameHighScore, Gifts, MenuButton, Message, MessageId, Poll, PreparedInlineMessage, + SentWebAppMessage, StarTransactions, Sticker, StickerSet, Update, User, UserChatBoosts, + UserProfilePhotos, WebhookInfo, }; use crate::response::{MessageOrBool, MethodResponse}; @@ -568,28 +568,26 @@ where params: &CreateNewStickerSetParams, ) -> Result, Self::Error> { let method_name = "createNewStickerSet"; - let mut new_stickers: Vec = vec![]; let mut files = Vec::new(); - let mut file_idx = 0; - - for sticker in ¶ms.stickers { - let mut new_sticker = sticker.clone(); - - if let FileUpload::InputFile(input_file) = &sticker.sticker { - let name = format!("file{file_idx}"); - let attach_name = format!("attach://{name}"); - file_idx += 1; - - new_sticker.sticker = FileUpload::String(attach_name); - files.push((name, input_file)); - } - - new_stickers.push(new_sticker); - } - - let mut new_params = params.clone(); - new_params.stickers = new_stickers; + let new_params = CreateNewStickerSetParams { + stickers: params + .stickers + .iter() + .enumerate() + .map(|(index, sticker)| { + let mut new_sticker = sticker.clone(); + if let FileUpload::InputFile(input_file) = &sticker.sticker { + let name = format!("file{index}"); + let attach_name = format!("attach://{name}"); + new_sticker.sticker = FileUpload::String(attach_name); + files.push((name, input_file)); + } + new_sticker + }) + .collect(), + ..params.clone() + }; let files_with_str_names = files .iter() diff --git a/src/trait_sync.rs b/src/trait_sync.rs index ae5b96b..fd0894f 100644 --- a/src/trait_sync.rs +++ b/src/trait_sync.rs @@ -536,28 +536,26 @@ pub trait TelegramApi { params: &CreateNewStickerSetParams, ) -> Result, Self::Error> { let method_name = "createNewStickerSet"; - let mut new_stickers = Vec::new(); let mut files = Vec::new(); - let mut file_idx = 0; - - for sticker in ¶ms.stickers { - let mut new_sticker = sticker.clone(); - - if let FileUpload::InputFile(input_file) = &sticker.sticker { - let name = format!("file{file_idx}"); - let attach_name = format!("attach://{name}"); - file_idx += 1; - - new_sticker.sticker = FileUpload::String(attach_name); - files.push((name, input_file)); - } - - new_stickers.push(new_sticker); - } - - let mut new_params = params.clone(); - new_params.stickers = new_stickers; + let new_params = CreateNewStickerSetParams { + stickers: params + .stickers + .iter() + .enumerate() + .map(|(index, sticker)| { + let mut new_sticker = sticker.clone(); + if let FileUpload::InputFile(input_file) = &sticker.sticker { + let name = format!("file{index}"); + let attach_name = format!("attach://{name}"); + new_sticker.sticker = FileUpload::String(attach_name); + files.push((name, input_file)); + } + new_sticker + }) + .collect(), + ..params.clone() + }; let files_with_str_names = files .iter() From d56bf462466e550d331e3ebf5a5f6fd2c0d4954b Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Fri, 21 Feb 2025 01:13:16 +0100 Subject: [PATCH 03/14] refactor: move internal method to the end of the impl less confusing to read --- src/api_params.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/api_params.rs b/src/api_params.rs index 4d377b9..497b433 100644 --- a/src/api_params.rs +++ b/src/api_params.rs @@ -44,21 +44,6 @@ impl From for FileUpload { } impl InputFile { - /// This method is intended to be used after `fs` operations - fn file_name_from_path(path: &Path) -> std::io::Result { - let file_name = path - .file_name() - .ok_or_else(|| { - std::io::Error::new( - std::io::ErrorKind::Other, - "A file that could be read should also have a name", - ) - })? - .to_string_lossy() - .to_string(); - Ok(file_name) - } - pub fn read_std>(path: P) -> std::io::Result { let path = path.as_ref(); let bytes = std::fs::read(path)?; @@ -73,6 +58,21 @@ impl InputFile { let file_name = Self::file_name_from_path(path)?; Ok(Self { bytes, file_name }) } + + /// This method is intended to be used after `fs` operations + fn file_name_from_path(path: &Path) -> std::io::Result { + let file_name = path + .file_name() + .ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::Other, + "A file that could be read should also have a name", + ) + })? + .to_string_lossy() + .to_string(); + Ok(file_name) + } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] From 3feeaa666a147bfd94b1e461bb1bdb6e2d7b02cf Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Sun, 23 Feb 2025 19:05:17 +0100 Subject: [PATCH 04/14] fix(client-ureq): add back mime_guess removing that is part of another PR --- Cargo.lock | 1 + Cargo.toml | 3 ++- src/client_ureq.rs | 7 ++++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 75d9f08..4da58af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -344,6 +344,7 @@ dependencies = [ "bon", "isahc", "macro_rules_attribute", + "mime_guess", "mockito", "multipart", "paste", diff --git a/Cargo.toml b/Cargo.toml index cf7fd75..06b6a38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ all-features = true [features] client-reqwest = ["trait-async", "dep:reqwest", "dep:serde_json"] -client-ureq = ["trait-sync", "dep:ureq", "dep:multipart", "dep:serde_json"] +client-ureq = ["trait-sync", "dep:ureq", "dep:multipart", "dep:mime_guess", "dep:serde_json"] trait-async = ["dep:async-trait"] trait-sync = [] inputfile-read-tokio = ["dep:tokio", "tokio/fs"] @@ -40,6 +40,7 @@ unreadable_literal = "allow" async-trait = { version = "0.1", optional = true } bon = "3.0.0" macro_rules_attribute = "0.2.0" +mime_guess = { version = "2", optional = true } multipart = { version = "0.18", optional = true, default-features = false, features = ["client"] } paste = "1.0.2" serde = { version = "1.0.157", features = ["derive"] } diff --git a/src/client_ureq.rs b/src/client_ureq.rs index 6fadc6e..4e2408c 100644 --- a/src/client_ureq.rs +++ b/src/client_ureq.rs @@ -108,11 +108,16 @@ impl TelegramApi for Bot { } for (parameter_name, input_file) in files { + let file_extension = std::path::Path::new(&input_file.file_name) + .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, &*input_file.bytes, Some(&input_file.file_name), - None, + Some(mime), ); } From 64625c3bcc016e6610aaab488839f4c17bd5ef4e Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Wed, 26 Feb 2025 12:24:05 +0100 Subject: [PATCH 05/14] test(client-ureq): use include_bytes for more realistic dummyfile --- src/client_ureq.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client_ureq.rs b/src/client_ureq.rs index 3158ec7..7c5a4b5 100644 --- a/src/client_ureq.rs +++ b/src/client_ureq.rs @@ -183,8 +183,8 @@ mod tests { fn dummy_file() -> InputFile { InputFile { - bytes: vec![1, 2, 3], - file_name: "dummy.file".to_owned(), + bytes: include_bytes!("../frankenstein_logo.png").to_vec(), + file_name: "frankenstein_logo.png".to_owned(), } } From c94e76cb7cf3995775da52e41006ce66ebd0b953 Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Wed, 26 Feb 2025 12:26:21 +0100 Subject: [PATCH 06/14] fix(macros): InputFile no longer has impl From --- src/macros.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/macros.rs b/src/macros.rs index fe5c4a0..ed46382 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -9,7 +9,6 @@ macro_rules_attribute::attribute_alias! { on(Box<_>, into), on(ChatId, into), on(FileUpload, into), - on(InputFile, into), on(String, into), )]; } From c5629e073ee1afa43941d648b4f164be32e008e1 Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Mon, 10 Mar 2025 23:05:01 +0100 Subject: [PATCH 07/14] refactor: use simpler io::Error::other --- src/input_file.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/input_file.rs b/src/input_file.rs index faed8f0..ad9f787 100644 --- a/src/input_file.rs +++ b/src/input_file.rs @@ -34,10 +34,7 @@ impl InputFile { let file_name = path .file_name() .ok_or_else(|| { - std::io::Error::new( - std::io::ErrorKind::Other, - "A file that could be read should also have a name", - ) + std::io::Error::other("A file that could be read should also have a name") })? .to_string_lossy() .to_string(); From 1ff7f49dfbc19ff601f41c5c38bd2c3cb1d66e4e Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Tue, 11 Mar 2025 02:29:29 +0100 Subject: [PATCH 08/14] perf(files): keep PathBuf variant as its optimized by multistream --- Cargo.toml | 4 +- README.md | 1 - examples/async_file_upload.rs | 6 +-- examples/file_upload.rs | 4 +- src/client_reqwest.rs | 16 ++++++-- src/client_ureq.rs | 70 +++++++++++++++++++---------------- src/error.rs | 4 +- src/input_file.rs | 49 +++++++++--------------- src/trait_async.rs | 4 +- src/trait_sync.rs | 4 +- 10 files changed, 78 insertions(+), 84 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 06b6a38..563e5e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,6 @@ 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 = [] -inputfile-read-tokio = ["dep:tokio", "tokio/fs"] [lints.rust] unsafe_code = "forbid" @@ -47,7 +46,6 @@ serde = { version = "1.0.157", features = ["derive"] } serde_json = { version = "1.0.45", optional = true } serde_with = { version = "3.0.0", default-features = false, features = ["macros"] } thiserror = "2" -tokio = { version = "1", optional = true } ureq = { version = "3.0.0", optional = true, default-features = false, features = ["rustls"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies.reqwest] @@ -102,7 +100,7 @@ required-features = ["client-reqwest"] [[example]] name = "async_file_upload" -required-features = ["client-reqwest", "inputfile-read-tokio"] +required-features = ["client-reqwest"] [[example]] name = "async_custom_client" diff --git a/README.md b/README.md index ba7d90f..5aefdcd 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,6 @@ Without enabling any additional features this crate will only ship with Telegram - async - `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 - - `inputfile-read-tokio` - helper function to read an `InputFile` from a file system with `tokio::fs::read` For example for the async client add the following line to your `Cargo.toml` file: diff --git a/examples/async_file_upload.rs b/examples/async_file_upload.rs index 33e8420..5c08b9a 100644 --- a/examples/async_file_upload.rs +++ b/examples/async_file_upload.rs @@ -1,6 +1,5 @@ use frankenstein::api_params::SendPhotoParams; use frankenstein::client_reqwest::Bot; -use frankenstein::input_file::InputFile; use frankenstein::AsyncTelegramApi; #[tokio::main] @@ -13,10 +12,7 @@ async fn main() { let bot = Bot::new(&token); - let file = InputFile::read_tokio_fs("./frankenstein_logo.png") - .await - .expect("Should be able to read file"); - println!("File size: {}", file.bytes.len()); + let file = std::path::PathBuf::from("./frankenstein_logo.png"); let params = SendPhotoParams::builder() .chat_id(chat_id) diff --git a/examples/file_upload.rs b/examples/file_upload.rs index f33437b..5968115 100644 --- a/examples/file_upload.rs +++ b/examples/file_upload.rs @@ -1,6 +1,5 @@ use frankenstein::api_params::SendPhotoParams; use frankenstein::client_ureq::Bot; -use frankenstein::input_file::InputFile; use frankenstein::TelegramApi; fn main() { @@ -12,8 +11,7 @@ fn main() { let bot = Bot::new(&token); - let file = InputFile::read_std("./frankenstein_logo.png").expect("Should be able to read file"); - println!("File size: {}", file.bytes.len()); + let file = std::path::PathBuf::from("./frankenstein_logo.png"); let params = SendPhotoParams::builder() .chat_id(chat_id) diff --git a/src/client_reqwest.rs b/src/client_reqwest.rs index b26c1d3..2ee25ae 100644 --- a/src/client_reqwest.rs +++ b/src/client_reqwest.rs @@ -116,10 +116,18 @@ impl AsyncTelegramApi for Bot { } for (parameter_name, input_file) in files { - // The reqwest::multipart stuff requires 'static which we can not grant here. - // So we provide owned data. - let part = reqwest::multipart::Part::bytes(input_file.bytes.clone()) - .file_name(input_file.file_name.clone()); + 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.clone()).file_name(file_name.clone()) + } + + #[cfg(not(target_arch = "wasm32"))] + InputFile::Path(path) => reqwest::multipart::Part::file(path) + .await + .map_err(crate::Error::ReadFile)?, + }; form = form.part(parameter_name.to_owned(), part); } diff --git a/src/client_ureq.rs b/src/client_ureq.rs index 7201c96..3e226ab 100644 --- a/src/client_ureq.rs +++ b/src/client_ureq.rs @@ -108,21 +108,22 @@ impl TelegramApi for Bot { } for (parameter_name, input_file) in files { - let file_extension = std::path::Path::new(&input_file.file_name) - .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, - &*input_file.bytes, - Some(&input_file.file_name), - Some(mime), - ); + 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, &**bytes, Some(&**file_name), Some(mime)); + } + InputFile::Path(path) => { + form.add_file(parameter_name, &**path); + } + } } 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) @@ -182,13 +183,6 @@ mod tests { }}; } - fn dummy_file() -> InputFile { - InputFile { - bytes: include_bytes!("../frankenstein_logo.png").to_vec(), - file_name: "frankenstein_logo.png".to_owned(), - } - } - #[test] fn new_sets_correct_url() { let api = Bot::new("hey"); @@ -566,9 +560,10 @@ mod tests { #[test] fn send_photo_success() { let response_string = "{\"ok\":true,\"result\":{\"message_id\":2763,\"from\":{\"id\":1276618370,\"is_bot\":true,\"first_name\":\"test_el_bot\",\"username\":\"el_mon_test_bot\"},\"date\":1618730180,\"chat\":{\"id\":275808073,\"type\":\"private\",\"username\":\"Ayrat555\",\"first_name\":\"Ayrat\",\"last_name\":\"Badykov\"},\"photo\":[{\"file_id\":\"AgACAgIAAxkDAAIKy2B73MQXIhoDDmLXjcUjgqGf-m8bAALjsDEbORLgS-s4BkBzcC5DYvIBny4AAwEAAwIAA20AA0U3AwABHwQ\",\"file_unique_id\":\"AQADYvIBny4AA0U3AwAB\",\"width\":320,\"height\":320,\"file_size\":19968},{\"file_id\":\"AgACAgIAAxkDAAIKy2B73MQXIhoDDmLXjcUjgqGf-m8bAALjsDEbORLgS-s4BkBzcC5DYvIBny4AAwEAAwIAA3gAA0Y3AwABHwQ\",\"file_unique_id\":\"AQADYvIBny4AA0Y3AwAB\",\"width\":799,\"height\":800,\"file_size\":63581},{\"file_id\":\"AgACAgIAAxkDAAIKy2B73MQXIhoDDmLXjcUjgqGf-m8bAALjsDEbORLgS-s4BkBzcC5DYvIBny4AAwEAAwIAA3kAA0M3AwABHwQ\",\"file_unique_id\":\"AQADYvIBny4AA0M3AwAB\",\"width\":847,\"height\":848,\"file_size\":63763}]}}"; + let file = std::path::PathBuf::from("./frankenstein_logo.png"); let params = SendPhotoParams::builder() .chat_id(275808073) - .photo(dummy_file()) + .photo(file) .build(); let response = case!(sendPhoto, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); @@ -577,9 +572,10 @@ mod tests { #[test] fn send_audio_file_success() { let response_string = "{\"ok\":true,\"result\":{\"message_id\":2766,\"from\":{\"id\":1276618370,\"is_bot\":true,\"first_name\":\"test_el_bot\",\"username\":\"el_mon_test_bot\"},\"date\":1618735176,\"chat\":{\"id\":275808073,\"type\":\"private\",\"username\":\"Ayrat555\",\"first_name\":\"Ayrat\",\"last_name\":\"Badykov\"},\"audio\":{\"file_id\":\"CQACAgIAAxkDAAIKzmB78EjK-iOHo-HKC-M6p4r0jGdmAALkDAACORLgS5co1z0uFAKgHwQ\",\"file_unique_id\":\"AgAD5AwAAjkS4Es\",\"duration\":123,\"title\":\"Way Back Home\",\"file_name\":\"audio.mp3\",\"mime_type\":\"audio/mpeg\",\"file_size\":2957092}}}"; + let file = std::path::PathBuf::from("./frankenstein_logo.png"); let params = SendAudioParams::builder() .chat_id(275808073) - .audio(dummy_file()) + .audio(file) .build(); let response = case!(sendAudio, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); @@ -588,10 +584,11 @@ mod tests { #[test] fn send_audio_file_with_thumb_success() { let response_string = "{\"ok\":true,\"result\":{\"message_id\":2766,\"from\":{\"id\":1276618370,\"is_bot\":true,\"first_name\":\"test_el_bot\",\"username\":\"el_mon_test_bot\"},\"date\":1618735176,\"chat\":{\"id\":275808073,\"type\":\"private\",\"username\":\"Ayrat555\",\"first_name\":\"Ayrat\",\"last_name\":\"Badykov\"},\"audio\":{\"file_id\":\"CQACAgIAAxkDAAIKzmB78EjK-iOHo-HKC-M6p4r0jGdmAALkDAACORLgS5co1z0uFAKgHwQ\",\"file_unique_id\":\"AgAD5AwAAjkS4Es\",\"duration\":123,\"title\":\"Way Back Home\",\"file_name\":\"audio.mp3\",\"mime_type\":\"audio/mpeg\",\"file_size\":2957092}}}"; + let file = std::path::PathBuf::from("./frankenstein_logo.png"); let params = SendAudioParams::builder() .chat_id(275808073) - .audio(dummy_file()) - .thumbnail(dummy_file()) + .audio(file.clone()) + .thumbnail(file) .build(); let response = case!(sendAudio, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); @@ -613,9 +610,10 @@ mod tests { #[test] fn send_document_success() { let response_string = "{\"ok\":true,\"result\":{\"message_id\":2770,\"from\":{\"id\":1276618370,\"is_bot\":true,\"first_name\":\"test_el_bot\",\"username\":\"el_mon_test_bot\"},\"date\":1618737593,\"chat\":{\"id\":275808073,\"type\":\"private\",\"username\":\"Ayrat555\",\"first_name\":\"Ayrat\",\"last_name\":\"Badykov\"},\"audio\":{\"file_id\":\"CQACAgIAAxkDAAIK0mB7-bnnewABfdaFKK4NzVLQ7BvgCwAC6gwAAjkS4Et_njaNR8IUMB8E\",\"file_unique_id\":\"AgAD6gwAAjkS4Es\",\"duration\":123,\"title\":\"Way Back Home\",\"file_name\":\"audio.mp3\",\"mime_type\":\"audio/mpeg\",\"file_size\":2957092}}}"; + let file = std::path::PathBuf::from("./frankenstein_logo.png"); let params = SendDocumentParams::builder() .chat_id(275808073) - .document(dummy_file()) + .document(file) .build(); let response = case!(sendDocument, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); @@ -624,9 +622,10 @@ mod tests { #[test] fn send_video_success() { let response_string = "{\"ok\":true,\"result\":{\"message_id\":2770,\"from\":{\"id\":1276618370,\"is_bot\":true,\"first_name\":\"test_el_bot\",\"username\":\"el_mon_test_bot\"},\"date\":1618737593,\"chat\":{\"id\":275808073,\"type\":\"private\",\"username\":\"Ayrat555\",\"first_name\":\"Ayrat\",\"last_name\":\"Badykov\"},\"audio\":{\"file_id\":\"CQACAgIAAxkDAAIK0mB7-bnnewABfdaFKK4NzVLQ7BvgCwAC6gwAAjkS4Et_njaNR8IUMB8E\",\"file_unique_id\":\"AgAD6gwAAjkS4Es\",\"duration\":123,\"title\":\"Way Back Home\",\"file_name\":\"audio.mp3\",\"mime_type\":\"audio/mpeg\",\"file_size\":2957092}}}"; + let file = std::path::PathBuf::from("./frankenstein_logo.png"); let params = SendVideoParams::builder() .chat_id(275808073) - .video(dummy_file()) + .video(file) .build(); let response = case!(sendVideo, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); @@ -635,9 +634,10 @@ mod tests { #[test] fn send_animation_success() { let response_string = "{\"ok\":true,\"result\":{\"message_id\":2770,\"from\":{\"id\":1276618370,\"is_bot\":true,\"first_name\":\"test_el_bot\",\"username\":\"el_mon_test_bot\"},\"date\":1618737593,\"chat\":{\"id\":275808073,\"type\":\"private\",\"username\":\"Ayrat555\",\"first_name\":\"Ayrat\",\"last_name\":\"Badykov\"},\"audio\":{\"file_id\":\"CQACAgIAAxkDAAIK0mB7-bnnewABfdaFKK4NzVLQ7BvgCwAC6gwAAjkS4Et_njaNR8IUMB8E\",\"file_unique_id\":\"AgAD6gwAAjkS4Es\",\"duration\":123,\"title\":\"Way Back Home\",\"file_name\":\"audio.mp3\",\"mime_type\":\"audio/mpeg\",\"file_size\":2957092}}}"; + let file = std::path::PathBuf::from("./frankenstein_logo.png"); let params = SendAnimationParams::builder() .chat_id(275808073) - .animation(dummy_file()) + .animation(file) .build(); let response = case!(sendAnimation, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); @@ -646,9 +646,10 @@ mod tests { #[test] fn send_voice_success() { let response_string = "{\"ok\":true,\"result\":{\"message_id\":2770,\"from\":{\"id\":1276618370,\"is_bot\":true,\"first_name\":\"test_el_bot\",\"username\":\"el_mon_test_bot\"},\"date\":1618737593,\"chat\":{\"id\":275808073,\"type\":\"private\",\"username\":\"Ayrat555\",\"first_name\":\"Ayrat\",\"last_name\":\"Badykov\"},\"audio\":{\"file_id\":\"CQACAgIAAxkDAAIK0mB7-bnnewABfdaFKK4NzVLQ7BvgCwAC6gwAAjkS4Et_njaNR8IUMB8E\",\"file_unique_id\":\"AgAD6gwAAjkS4Es\",\"duration\":123,\"title\":\"Way Back Home\",\"file_name\":\"audio.mp3\",\"mime_type\":\"audio/mpeg\",\"file_size\":2957092}}}"; + let file = std::path::PathBuf::from("./frankenstein_logo.png"); let params = SendVoiceParams::builder() .chat_id(275808073) - .voice(dummy_file()) + .voice(file) .build(); let response = case!(sendVoice, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); @@ -657,9 +658,10 @@ mod tests { #[test] fn send_video_note_success() { let response_string = "{\"ok\":true,\"result\":{\"message_id\":2770,\"from\":{\"id\":1276618370,\"is_bot\":true,\"first_name\":\"test_el_bot\",\"username\":\"el_mon_test_bot\"},\"date\":1618737593,\"chat\":{\"id\":275808073,\"type\":\"private\",\"username\":\"Ayrat555\",\"first_name\":\"Ayrat\",\"last_name\":\"Badykov\"},\"audio\":{\"file_id\":\"CQACAgIAAxkDAAIK0mB7-bnnewABfdaFKK4NzVLQ7BvgCwAC6gwAAjkS4Et_njaNR8IUMB8E\",\"file_unique_id\":\"AgAD6gwAAjkS4Es\",\"duration\":123,\"title\":\"Way Back Home\",\"file_name\":\"audio.mp3\",\"mime_type\":\"audio/mpeg\",\"file_size\":2957092}}}"; + let file = std::path::PathBuf::from("./frankenstein_logo.png"); let params = SendVideoNoteParams::builder() .chat_id(275808073) - .video_note(dummy_file()) + .video_note(file) .build(); let response = case!(sendVideoNote, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); @@ -668,9 +670,10 @@ mod tests { #[test] fn set_chat_photo_success() { let response_string = "{\"ok\":true,\"result\":true}"; + let file = std::path::PathBuf::from("./frankenstein_logo.png"); let params = SetChatPhotoParams::builder() .chat_id(275808073) - .photo(dummy_file()) + .photo(file) .build(); let response = case!(setChatPhoto, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); @@ -937,9 +940,10 @@ mod tests { #[test] fn send_sticker_success() { let response_string = "{\"ok\":true,\"result\":{\"message_id\":2788,\"from\":{\"id\":1276618370,\"is_bot\":true,\"first_name\":\"test_el_bot\",\"username\":\"el_mon_test_bot\"},\"date\":1619245784,\"chat\":{\"id\":275808073,\"type\":\"private\",\"username\":\"Ayrat555\",\"first_name\":\"Ayrat\",\"last_name\":\"Badykov\"},\"sticker\":{\"file_id\":\"CAACAgIAAxkDAAIK5GCDutgNxc07rqqtjkGWrGskbHfQAAIMEAACRx8ZSKJ6Z5GkdVHcHwQ\",\"file_unique_id\":\"AgADDBAAAkcfGUg\",\"type\":\"regular\",\"width\":512,\"height\":512,\"is_animated\":false,\"is_video\":false,\"thumbnail\":{\"file_id\":\"AAMCAgADGQMAAgrkYIO62A3FzTuuqq2OQZasayRsd9AAAgwQAAJHHxlIonpnkaR1Udz29bujLgADAQAHbQADzR4AAh8E\",\"file_unique_id\":\"AQAD9vW7oy4AA80eAAI\",\"width\":320,\"height\":320,\"file_size\":19264},\"file_size\":36596}}}"; + let file = std::path::PathBuf::from("./frankenstein_logo.png"); let params = SendStickerParams::builder() .chat_id(275808073) - .sticker(dummy_file()) + .sticker(file) .build(); let response = case!(sendSticker, 200, response_string, params).unwrap(); assert_json_str(&response, response_string); @@ -956,7 +960,8 @@ mod tests { #[test] fn send_media_group_success() { let response_string = "{\"ok\":true,\"result\":[{\"message_id\":510,\"from\":{\"id\":1276618370,\"is_bot\":true,\"first_name\":\"test_el_bot\",\"username\":\"el_mon_test_bot\"},\"date\":1619267462,\"chat\":{\"id\":-1001368460856,\"type\":\"supergroup\",\"title\":\"Frankenstein\"},\"media_group_id\":\"12954139699368426\",\"photo\":[{\"file_id\":\"AgACAgIAAx0EUZEOOAACAf5ghA-GtOaBIP2NOmtXdze-Un7PGgAC_q8xG0cfEUgpwpFo17XTfWTS5p8uAAMBAAMCAANtAANYQgACHwQ\",\"file_unique_id\":\"AQADZNLmny4AA1hCAAI\",\"width\":320,\"height\":320,\"file_size\":19162},{\"file_id\":\"AgACAgIAAx0EUZEOOAACAf5ghA-GtOaBIP2NOmtXdze-Un7PGgAC_q8xG0cfEUgpwpFo17XTfWTS5p8uAAMBAAMCAAN4AANZQgACHwQ\",\"file_unique_id\":\"AQADZNLmny4AA1lCAAI\",\"width\":800,\"height\":800,\"file_size\":65697},{\"file_id\":\"AgACAgIAAx0EUZEOOAACAf5ghA-GtOaBIP2NOmtXdze-Un7PGgAC_q8xG0cfEUgpwpFo17XTfWTS5p8uAAMBAAMCAAN5AANaQgACHwQ\",\"file_unique_id\":\"AQADZNLmny4AA1pCAAI\",\"width\":1146,\"height\":1146,\"file_size\":101324}]},{\"message_id\":511,\"from\":{\"id\":1276618370,\"is_bot\":true,\"first_name\":\"test_el_bot\",\"username\":\"el_mon_test_bot\"},\"date\":1619267462,\"chat\":{\"id\":-1001368460856,\"type\":\"supergroup\",\"title\":\"Frankenstein\"},\"media_group_id\":\"12954139699368426\",\"photo\":[{\"file_id\":\"AgACAgIAAx0EUZEOOAACAf9ghA-GeFo0B7v78UyXoOD9drjEGgAC_q8xG0cfEUgpwpFo17XTfWTS5p8uAAMBAAMCAANtAANYQgACHwQ\",\"file_unique_id\":\"AQADZNLmny4AA1hCAAI\",\"width\":320,\"height\":320,\"file_size\":19162},{\"file_id\":\"AgACAgIAAx0EUZEOOAACAf9ghA-GeFo0B7v78UyXoOD9drjEGgAC_q8xG0cfEUgpwpFo17XTfWTS5p8uAAMBAAMCAAN4AANZQgACHwQ\",\"file_unique_id\":\"AQADZNLmny4AA1lCAAI\",\"width\":800,\"height\":800,\"file_size\":65697},{\"file_id\":\"AgACAgIAAx0EUZEOOAACAf9ghA-GeFo0B7v78UyXoOD9drjEGgAC_q8xG0cfEUgpwpFo17XTfWTS5p8uAAMBAAMCAAN5AANaQgACHwQ\",\"file_unique_id\":\"AQADZNLmny4AA1pCAAI\",\"width\":1146,\"height\":1146,\"file_size\":101324}]}]}"; - let photo = InputMediaPhoto::builder().media(dummy_file()).build(); + let file = std::path::PathBuf::from("./frankenstein_logo.png"); + let photo = InputMediaPhoto::builder().media(file).build(); let medias = vec![ MediaGroupInputMedia::Photo(photo.clone()), MediaGroupInputMedia::Photo(photo), @@ -972,8 +977,9 @@ mod tests { #[test] fn edit_message_media_success() { let response_string = "{\"ok\":true,\"result\":{\"message_id\":513,\"from\":{\"id\":1276618370,\"is_bot\":true,\"first_name\":\"test_el_bot\",\"username\":\"el_mon_test_bot\"},\"date\":1619336672,\"chat\":{\"id\":-1001368460856,\"type\":\"supergroup\",\"title\":\"Frankenstein\"},\"edit_date\":1619336788,\"photo\":[{\"file_id\":\"AgACAgIAAx0EUZEOOAACAgFghR5URaBN41jx7VNgLPi29xmfQgAC_q8xG0cfEUgpwpFo17XTfWTS5p8uAAMBAAMCAANtAANYQgACHwQ\",\"file_unique_id\":\"AQADZNLmny4AA1hCAAI\",\"width\":320,\"height\":320,\"file_size\":19162},{\"file_id\":\"AgACAgIAAx0EUZEOOAACAgFghR5URaBN41jx7VNgLPi29xmfQgAC_q8xG0cfEUgpwpFo17XTfWTS5p8uAAMBAAMCAAN4AANZQgACHwQ\",\"file_unique_id\":\"AQADZNLmny4AA1lCAAI\",\"width\":800,\"height\":800,\"file_size\":65697},{\"file_id\":\"AgACAgIAAx0EUZEOOAACAgFghR5URaBN41jx7VNgLPi29xmfQgAC_q8xG0cfEUgpwpFo17XTfWTS5p8uAAMBAAMCAAN5AANaQgACHwQ\",\"file_unique_id\":\"AQADZNLmny4AA1pCAAI\",\"width\":1146,\"height\":1146,\"file_size\":101324}]}}"; + let file = std::path::PathBuf::from("./frankenstein_logo.png"); let params = EditMessageMediaParams::builder() - .media(InputMediaPhoto::builder().media(dummy_file()).build()) + .media(InputMediaPhoto::builder().media(file).build()) .chat_id(-1001368460856) .message_id(513) .build(); diff --git a/src/error.rs b/src/error.rs index 4fbd75f..b2452de 100644 --- a/src/error.rs +++ b/src/error.rs @@ -19,6 +19,9 @@ pub enum Error { input: String, }, + #[error("Read File Error: {0}")] + ReadFile(#[source] std::io::Error), + #[cfg(feature = "client-reqwest")] #[error("HTTP error: {0}")] HttpReqwest(#[source] reqwest::Error), @@ -29,7 +32,6 @@ pub enum Error { } impl Error { - #[allow(irrefutable_let_patterns)] // See https://github.com/rust-lang/rust/issues/72469 #[cfg(test)] #[track_caller] pub(crate) fn unwrap_api(self) -> ErrorResponse { diff --git a/src/input_file.rs b/src/input_file.rs index ad9f787..661f857 100644 --- a/src/input_file.rs +++ b/src/input_file.rs @@ -1,44 +1,24 @@ //! Structs for handling and uploading files -use std::path::Path; - 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 bytes: Vec, - pub file_name: String, +pub enum InputFile { + Bytes { + bytes: Vec, + file_name: String, + }, + #[cfg(not(target_arch = "wasm32"))] + Path(std::path::PathBuf), } -impl InputFile { - pub fn read_std>(path: P) -> std::io::Result { - let path = path.as_ref(); - let bytes = std::fs::read(path)?; - let file_name = Self::file_name_from_path(path)?; - Ok(Self { bytes, file_name }) - } - - #[cfg(feature = "inputfile-read-tokio")] - pub async fn read_tokio_fs>(path: P) -> std::io::Result { - let path = path.as_ref(); - let bytes = tokio::fs::read(path).await?; - let file_name = Self::file_name_from_path(path)?; - Ok(Self { bytes, file_name }) - } - - /// This method is intended to be used after `fs` operations - fn file_name_from_path(path: &Path) -> std::io::Result { - let file_name = path - .file_name() - .ok_or_else(|| { - std::io::Error::other("A file that could be read should also have a name") - })? - .to_string_lossy() - .to_string(); - Ok(file_name) +#[cfg(not(target_arch = "wasm32"))] +impl From for InputFile { + fn from(value: std::path::PathBuf) -> Self { + Self::Path(value) } } @@ -60,6 +40,13 @@ impl From for FileUpload { } } +#[cfg(not(target_arch = "wasm32"))] +impl From for FileUpload { + fn from(path: std::path::PathBuf) -> Self { + Self::InputFile(InputFile::Path(path)) + } +} + impl From for FileUpload { fn from(file: InputFile) -> Self { Self::InputFile(file) diff --git a/src/trait_async.rs b/src/trait_async.rs index 7806851..6c35341 100644 --- a/src/trait_async.rs +++ b/src/trait_async.rs @@ -57,8 +57,8 @@ macro_rules! request_f { ) -> Result, Self::Error> { let mut files = Vec::new(); $( - if let Some(path) = params.$fileproperty.get_input_file_ref() { - files.push((stringify!($fileproperty), path)); + if let Some(file) = params.$fileproperty.get_input_file_ref() { + files.push((stringify!($fileproperty), file)); } )+ self.request_with_possible_form_data(stringify!($name), params, files).await diff --git a/src/trait_sync.rs b/src/trait_sync.rs index 62ed219..67f8de5 100644 --- a/src/trait_sync.rs +++ b/src/trait_sync.rs @@ -54,8 +54,8 @@ macro_rules! request_f { ) -> Result, Self::Error> { let mut files = Vec::new(); $( - if let Some(path) = params.$fileproperty.get_input_file_ref() { - files.push((stringify!($fileproperty), path)); + if let Some(file) = params.$fileproperty.get_input_file_ref() { + files.push((stringify!($fileproperty), file)); } )+ self.request_with_possible_form_data(stringify!($name), params, files) From a168c9bcfae75bd4ccd7c5910346774f747f1f18 Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Tue, 11 Mar 2025 02:47:36 +0100 Subject: [PATCH 09/14] docs(readme): remove relatively clear upload docs When someone wants to upload a file the FileUpload type is required. They can read about it in the autogenerated docs themselves. No need to manually add that to the README again. --- README.md | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/README.md b/README.md index 5aefdcd..74d1002 100644 --- a/README.md +++ b/README.md @@ -114,29 +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.39.2/examples) directory. -### Uploading files - -Some methods in the API allow uploading files. In Frankenstein the `FileUpload` enum is used: - -```rust -pub enum FileUpload { - InputFile(InputFile), - String(String), -} - -pub struct InputFile { - bytes: Vec, - file_name: String, -} -``` - -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. - -You can use the helper functions `InputFile::read_std(Path)` or `InputFile::read_tokio(Path)` for reading from the file system. - ### 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.39.2/frankenstein/trait.TelegramApi.html#provided-methods) From ac2aaf22e21a911679fefb602f74e8b034d0c8da Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Tue, 8 Apr 2025 13:54:50 +0200 Subject: [PATCH 10/14] refactor: generalize methods with file uploads They might not be perfectly accurate now (might not accept URLs or file_id) but they are far easier to handle with macros. Fixes setWebhook which never correctly uploaded certificate. --- src/methods.rs | 8 ++++---- src/trait_async.rs | 23 +++-------------------- src/trait_sync.rs | 21 +++------------------ 3 files changed, 10 insertions(+), 42 deletions(-) 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 79bb326..5f840db 100644 --- a/src/trait_async.rs +++ b/src/trait_async.rs @@ -78,7 +78,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); @@ -169,16 +169,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)]) - .await - } - + request_f!(setChatPhoto, bool, photo); request!(deleteChatPhoto, bool); request!(setChatTitle, bool); request!(setChatDescription, bool); @@ -271,15 +262,7 @@ where request!(deleteMessages, bool); request_f!(sendSticker, Message, sticker); request!(getStickerSet, StickerSet); - - 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)]) - .await - } + request_f!(uploadStickerFile, File, sticker); async fn create_new_sticker_set( &self, diff --git a/src/trait_sync.rs b/src/trait_sync.rs index 2e89e96..4b01976 100644 --- a/src/trait_sync.rs +++ b/src/trait_sync.rs @@ -69,7 +69,7 @@ pub trait TelegramApi { 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); @@ -159,15 +159,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)]) - } - + request_f!(setChatPhoto, bool, photo); request!(deleteChatPhoto, bool); request!(setChatTitle, bool); request!(setChatDescription, bool); @@ -259,14 +251,7 @@ pub trait TelegramApi { request!(deleteMessages, bool); request_f!(sendSticker, Message, sticker); request!(getStickerSet, StickerSet); - - 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)]) - } + request_f!(uploadStickerFile, File, sticker); fn create_new_sticker_set( &self, From 414c5732baf1ba04ca0b989bab512c69f07493c8 Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Tue, 8 Apr 2025 17:30:07 +0200 Subject: [PATCH 11/14] docs(traits): add docs for the manually implemented methods --- src/trait_async.rs | 17 +++++++++++++++++ src/trait_sync.rs | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/trait_async.rs b/src/trait_async.rs index 5f840db..b15d7de 100644 --- a/src/trait_async.rs +++ b/src/trait_async.rs @@ -67,6 +67,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)] @@ -91,6 +103,7 @@ 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, @@ -211,6 +224,7 @@ where request!(editMessageText, MessageOrBool); request!(editMessageCaption, MessageOrBool); + #[doc = docs_file!(editMessageMedia, edit_message_media)] async fn edit_message_media( &self, params: &crate::methods::EditMessageMediaParams, @@ -264,6 +278,7 @@ where request!(getStickerSet, StickerSet); request_f!(uploadStickerFile, File, sticker); + #[doc = docs_file!(createNewStickerSet, create_new_sticker_set)] async fn create_new_sticker_set( &self, params: &crate::methods::CreateNewStickerSetParams, @@ -288,6 +303,7 @@ where request!(getCustomEmojiStickers, Vec); + #[doc = docs_file!(addStickerToSet, add_sticker_to_set)] async fn add_sticker_to_set( &self, params: &crate::methods::AddStickerToSetParams, @@ -335,6 +351,7 @@ where request!(unpinAllGeneralForumTopicMessages, bool); request!(setPassportDataErrors, bool); + #[doc(hidden)] async fn request_with_possible_form_data( &self, method_name: &str, diff --git a/src/trait_sync.rs b/src/trait_sync.rs index 4b01976..d28b716 100644 --- a/src/trait_sync.rs +++ b/src/trait_sync.rs @@ -64,6 +64,18 @@ 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; @@ -82,6 +94,7 @@ 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, @@ -201,6 +214,7 @@ 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, @@ -253,6 +267,7 @@ pub trait TelegramApi { request!(getStickerSet, StickerSet); request_f!(uploadStickerFile, File, sticker); + #[doc = docs_file!(createNewStickerSet, create_new_sticker_set)] fn create_new_sticker_set( &self, params: &crate::methods::CreateNewStickerSetParams, @@ -276,6 +291,7 @@ pub trait TelegramApi { request!(getCustomEmojiStickers, Vec); + #[doc = docs_file!(addStickerToSet, add_sticker_to_set)] fn add_sticker_to_set( &self, params: &crate::methods::AddStickerToSetParams, @@ -322,6 +338,7 @@ pub trait TelegramApi { request!(unpinAllGeneralForumTopicMessages, bool); request!(setPassportDataErrors, bool); + #[doc(hidden)] fn request_with_possible_form_data( &self, method_name: &str, From 80f83ec7dca7313df9396c635921a0a9c9f3d6fd Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Wed, 9 Apr 2025 00:33:16 +0200 Subject: [PATCH 12/14] feat(files): move instead of clone --- examples/api_trait_implementation.rs | 2 +- examples/async_file_upload.rs | 2 +- examples/file_upload.rs | 2 +- src/client_reqwest.rs | 6 +- src/client_ureq.rs | 62 ++++++++++++------ src/input_file.rs | 96 +++++++++++----------------- src/trait_async.rs | 88 +++++++++---------------- src/trait_sync.rs | 90 +++++++++----------------- 8 files changed, 145 insertions(+), 203 deletions(-) diff --git a/examples/api_trait_implementation.rs b/examples/api_trait_implementation.rs index 2711f55..ec94d8d 100644 --- a/examples/api_trait_implementation.rs +++ b/examples/api_trait_implementation.rs @@ -90,7 +90,7 @@ impl TelegramApi for MyApiClient { &self, _method: &str, _params: Params, - _files: Vec<(&str, &InputFile)>, + _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 index 4a2ab29..e9eaba0 100644 --- a/examples/file_upload.rs +++ b/examples/file_upload.rs @@ -17,7 +17,7 @@ fn main() { .chat_id(chat_id) .photo(file) .build(); - match bot.send_photo(¶ms) { + match bot.send_photo(params) { Ok(response) => { println!("Photo was uploaded successfully"); dbg!(response); diff --git a/src/client_reqwest.rs b/src/client_reqwest.rs index c412cab..42e36c7 100644 --- a/src/client_reqwest.rs +++ b/src/client_reqwest.rs @@ -93,7 +93,7 @@ impl AsyncTelegramApi for Bot { &self, method: &str, params: Params, - files: Vec<(&str, &InputFile)>, + files: Vec<(std::borrow::Cow<'static, str>, InputFile)>, ) -> Result where Params: serde::ser::Serialize + std::fmt::Debug + std::marker::Send, @@ -102,7 +102,7 @@ impl AsyncTelegramApi for Bot { 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 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.as_object().unwrap() { @@ -128,7 +128,7 @@ impl AsyncTelegramApi for Bot { .await .map_err(crate::Error::ReadFile)?, }; - form = form.part(parameter_name.to_owned(), part); + form = form.part(parameter_name, part); } let url = format!("{}/{method}", self.api_url); diff --git a/src/client_ureq.rs b/src/client_ureq.rs index ba39406..13d9182 100644 --- a/src/client_ureq.rs +++ b/src/client_ureq.rs @@ -85,7 +85,7 @@ impl TelegramApi for Bot { &self, method: &str, params: Params, - files: Vec<(&str, &InputFile)>, + files: Vec<(std::borrow::Cow<'static, str>, InputFile)>, ) -> Result where Params: serde::ser::Serialize + std::fmt::Debug, @@ -93,7 +93,7 @@ impl TelegramApi for Bot { { 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 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() { @@ -107,15 +107,20 @@ impl TelegramApi for Bot { } } - for (parameter_name, input_file) in files { + 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, &**bytes, Some(&**file_name), Some(mime)); + form.add_stream( + parameter_name.as_ref(), + &**bytes, + Some(file_name), + Some(mime), + ); } InputFile::Path(path) => { - form.add_file(parameter_name, &**path); + form.add_file::<&str, &std::path::Path>(parameter_name.as_ref(), path.as_ref()); } } } @@ -183,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"); @@ -236,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); } @@ -565,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); } @@ -577,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); } @@ -590,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); } @@ -603,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); } @@ -615,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); } @@ -627,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); } @@ -639,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); } @@ -651,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); } @@ -663,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); } @@ -675,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); } @@ -945,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); } @@ -970,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); } @@ -983,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/input_file.rs b/src/input_file.rs index 661f857..8e87ad2 100644 --- a/src/input_file.rs +++ b/src/input_file.rs @@ -1,5 +1,7 @@ //! Structs for handling and uploading files +use std::borrow::Cow; + use serde::{Deserialize, Serialize}; /// Represents a new file to be uploaded via `multipart/form-data`. @@ -53,84 +55,58 @@ impl From for FileUpload { } } +type Filelist = Vec<(Cow<'static, str>, InputFile)>; + #[cfg(any(feature = "trait-sync", feature = "trait-async"))] pub(crate) trait HasInputFile { - // TODO: replace with `replace_attach` when possible - fn get_input_file_ref(&self) -> Option<&InputFile>; - fn replace_attach(&mut self, name: &str) -> Option; - fn replace_attach_dyn(&mut self, index: impl FnOnce() -> usize) -> Option<(String, InputFile)>; + 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 get_input_file_ref(&self) -> Option<&InputFile> { - match self { - Self::InputFile(input_file) => Some(input_file), - Self::String(_) => None, - } - } - - 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) - } - 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((Cow::Borrowed(name), file)); } } - fn replace_attach_dyn(&mut self, index: impl FnOnce() -> usize) -> Option<(String, InputFile)> { - 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)) - } - 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((Cow::Owned(name), file)); } } } #[cfg(any(feature = "trait-sync", feature = "trait-async"))] impl HasInputFile for Option { - fn get_input_file_ref(&self) -> Option<&InputFile> { - match self { - Some(FileUpload::InputFile(input_file)) => Some(input_file), - _ => 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) - } - _ => 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((Cow::Borrowed(name), file)); } } - fn replace_attach_dyn(&mut self, index: impl FnOnce() -> usize) -> Option<(String, InputFile)> { - 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)) - } - _ => 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((Cow::Owned(name), file)); } } } diff --git a/src/trait_async.rs b/src/trait_async.rs index b15d7de..129e950 100644 --- a/src/trait_async.rs +++ b/src/trait_async.rs @@ -53,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(file) = params.$fileproperty.get_input_file_ref() { - files.push((stringify!($fileproperty), file)); - } + params.$fileproperty.move_named_to_filelist(stringify!($fileproperty), &mut files); )+ self.request_with_possible_form_data(stringify!($name), params, files).await } @@ -106,45 +104,29 @@ where #[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, file)| (key.as_str(), file)) - .collect(); - - self.request_with_possible_form_data("sendMediaGroup", ¶ms, files_with_str_names) + self.request_with_possible_form_data("sendMediaGroup", ¶ms, files) .await } @@ -227,20 +209,19 @@ where #[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); @@ -264,9 +245,7 @@ where } } - let files_ref = files.iter().map(|(key, file)| (*key, file)).collect(); - - self.request_with_possible_form_data("editMessageMedia", ¶ms, files_ref) + self.request_with_possible_form_data("editMessageMedia", ¶ms, files) .await } @@ -281,23 +260,15 @@ where #[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, file)| (key.as_str(), file)) - .collect(); - - self.request_with_possible_form_data("createNewStickerSet", ¶ms, files_with_str_names) + self.request_with_possible_form_data("createNewStickerSet", ¶ms, files) .await } @@ -306,12 +277,13 @@ where #[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.get_input_file_ref() { - 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,7 +328,7 @@ where &self, method_name: &str, params: Params, - files: Vec<(&str, &InputFile)>, + files: Vec<(std::borrow::Cow<'static, str>, InputFile)>, ) -> Result where Params: serde::ser::Serialize + std::fmt::Debug + std::marker::Send, @@ -383,7 +355,7 @@ where &self, method: &str, params: Params, - files: Vec<(&str, &InputFile)>, + 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 d28b716..acd21fe 100644 --- a/src/trait_sync.rs +++ b/src/trait_sync.rs @@ -50,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(file) = params.$fileproperty.get_input_file_ref() { - files.push((stringify!($fileproperty), file)); - } + params.$fileproperty.move_named_to_filelist(stringify!($fileproperty), &mut files); )+ self.request_with_possible_form_data(stringify!($name), params, files) } @@ -97,45 +95,29 @@ pub trait TelegramApi { #[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, file)| (key.as_str(), file)) - .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); @@ -217,20 +199,19 @@ pub trait TelegramApi { #[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); @@ -254,9 +235,7 @@ pub trait TelegramApi { } } - let files_ref = files.iter().map(|(key, file)| (*key, file)).collect(); - - self.request_with_possible_form_data("editMessageMedia", ¶ms, files_ref) + self.request_with_possible_form_data("editMessageMedia", ¶ms, files) } request!(editMessageReplyMarkup, MessageOrBool); @@ -270,23 +249,13 @@ pub trait TelegramApi { #[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, file)| (key.as_str(), file)) - .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); @@ -294,12 +263,13 @@ pub trait TelegramApi { #[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.get_input_file_ref() { - files.push(("sticker", file)); - } + params + .sticker + .sticker + .move_named_to_filelist("sticker", &mut files); self.request_with_possible_form_data("addStickerToSet", params, files) } @@ -343,7 +313,7 @@ pub trait TelegramApi { &self, method_name: &str, params: Params, - files: Vec<(&str, &InputFile)>, + files: Vec<(std::borrow::Cow<'static, str>, InputFile)>, ) -> Result where Params: serde::ser::Serialize + std::fmt::Debug, @@ -369,7 +339,7 @@ pub trait TelegramApi { &self, method: &str, params: Params, - files: Vec<(&str, &InputFile)>, + files: Vec<(std::borrow::Cow<'static, str>, InputFile)>, ) -> Result where Params: serde::ser::Serialize + std::fmt::Debug, From e616f3b7933b568ac78ed92ede7f180ca6817212 Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Wed, 9 Apr 2025 00:57:58 +0200 Subject: [PATCH 13/14] fixup! feat(files): move instead of clone --- src/client_reqwest.rs | 2 +- src/input_file.rs | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/client_reqwest.rs b/src/client_reqwest.rs index 42e36c7..ae150b8 100644 --- a/src/client_reqwest.rs +++ b/src/client_reqwest.rs @@ -120,7 +120,7 @@ impl AsyncTelegramApi for Bot { 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.clone()).file_name(file_name.clone()) + reqwest::multipart::Part::bytes(bytes).file_name(file_name) } #[cfg(not(target_arch = "wasm32"))] diff --git a/src/input_file.rs b/src/input_file.rs index 8e87ad2..ee90c7c 100644 --- a/src/input_file.rs +++ b/src/input_file.rs @@ -1,7 +1,5 @@ //! Structs for handling and uploading files -use std::borrow::Cow; - use serde::{Deserialize, Serialize}; /// Represents a new file to be uploaded via `multipart/form-data`. @@ -55,7 +53,8 @@ impl From for FileUpload { } } -type Filelist = Vec<(Cow<'static, str>, InputFile)>; +#[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 { @@ -71,7 +70,7 @@ impl HasInputFile for FileUpload { let Self::InputFile(file) = std::mem::replace(self, attach) else { unreachable!("the match already ensures it being an input file"); }; - files.push((Cow::Borrowed(name), file)); + files.push((std::borrow::Cow::Borrowed(name), file)); } } @@ -82,7 +81,7 @@ impl HasInputFile for FileUpload { let Self::InputFile(file) = std::mem::replace(self, attach) else { unreachable!("the match already ensures it being an input file"); }; - files.push((Cow::Owned(name), file)); + files.push((std::borrow::Cow::Owned(name), file)); } } } @@ -95,7 +94,7 @@ impl HasInputFile for Option { let Some(FileUpload::InputFile(file)) = std::mem::replace(self, attach) else { unreachable!("the match already ensures it being an input file"); }; - files.push((Cow::Borrowed(name), file)); + files.push((std::borrow::Cow::Borrowed(name), file)); } } @@ -106,7 +105,7 @@ impl HasInputFile for Option { let Some(FileUpload::InputFile(file)) = std::mem::replace(self, attach) else { unreachable!("the match already ensures it being an input file"); }; - files.push((Cow::Owned(name), file)); + files.push((std::borrow::Cow::Owned(name), file)); } } } From 6676ee3ed3cd407e1577fb34878e60c50e5b356b Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Wed, 9 Apr 2025 01:34:06 +0200 Subject: [PATCH 14/14] perf: parse directly into map --- src/client_reqwest.rs | 10 +++++----- src/client_ureq.rs | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/client_reqwest.rs b/src/client_reqwest.rs index ae150b8..9f5a489 100644 --- a/src/client_reqwest.rs +++ b/src/client_reqwest.rs @@ -100,18 +100,18 @@ impl AsyncTelegramApi for Bot { Output: serde::de::DeserializeOwned, { let json_string = crate::json::encode(¶ms)?; - let json_struct: Value = serde_json::from_str(&json_string).unwrap(); - + 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.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 = form.text(key.clone(), val); + form = form.text(key, val); } } diff --git a/src/client_ureq.rs b/src/client_ureq.rs index 13d9182..fc1788e 100644 --- a/src/client_ureq.rs +++ b/src/client_ureq.rs @@ -92,17 +92,17 @@ impl TelegramApi for Bot { Output: serde::de::DeserializeOwned, { let json_string = crate::json::encode(¶ms)?; - let json_struct: Value = serde_json::from_str(&json_string).unwrap(); + 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); } }