From 431d67d535ba076acc187e5703589b746d064ec8 Mon Sep 17 00:00:00 2001 From: LasterAlex Date: Fri, 30 Aug 2024 05:03:41 +0300 Subject: [PATCH 01/20] Added bot message sugar --- CHANGELOG.md | 1 + crates/teloxide-core/src/types/message.rs | 10 + crates/teloxide/src/lib.rs | 1 + crates/teloxide/src/sugar.rs | 3 + crates/teloxide/src/sugar/bot.rs | 244 ++++++++++++++++++++++ 5 files changed, 259 insertions(+) create mode 100644 crates/teloxide/src/sugar.rs create mode 100644 crates/teloxide/src/sugar/bot.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index ac09cb91..b38ea06b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `filter_boost_added` and `filter_reply_to_story` filters to `MessageFilterExt` trait - Add `filter_mention_command` filter to `HandlerExt` trait ([issue #494](https://github.com/teloxide/teloxide/issues/494)) - Add `filter_business_connection`, `filter_business_message`, `filter_edited_business_message`, and `filter_deleted_business_messages` filters to update filters ([PR 1146](https://github.com/teloxide/teloxide/pull/1146)) +- Add `bot.forward`, `bot.copy` and `bot.delete` to new `crate::sugar::BotMessagesExt` trait ([issue #1143](https://github.com/teloxide/teloxide/issues/1143)) ### Changed diff --git a/crates/teloxide-core/src/types/message.rs b/crates/teloxide-core/src/types/message.rs index 2b2ce6e2..93411078 100644 --- a/crates/teloxide-core/src/types/message.rs +++ b/crates/teloxide-core/src/types/message.rs @@ -1816,6 +1816,16 @@ impl Message { } } +/// Implemented for syntax sugar, see issue +impl IntoIterator for Message { + type Item = Message; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + vec![self].into_iter() + } +} + #[cfg(test)] mod tests { use chrono::DateTime; diff --git a/crates/teloxide/src/lib.rs b/crates/teloxide/src/lib.rs index bcb901f1..edab596d 100644 --- a/crates/teloxide/src/lib.rs +++ b/crates/teloxide/src/lib.rs @@ -140,6 +140,7 @@ pub mod prelude; #[cfg(feature = "ctrlc_handler")] pub mod repls; pub mod stop; +pub mod sugar; pub mod update_listeners; pub mod utils; diff --git a/crates/teloxide/src/sugar.rs b/crates/teloxide/src/sugar.rs new file mode 100644 index 00000000..6bb10ad8 --- /dev/null +++ b/crates/teloxide/src/sugar.rs @@ -0,0 +1,3 @@ +//! Some non-detrimental, but nice additions + +pub mod bot; diff --git a/crates/teloxide/src/sugar/bot.rs b/crates/teloxide/src/sugar/bot.rs new file mode 100644 index 00000000..1d69380d --- /dev/null +++ b/crates/teloxide/src/sugar/bot.rs @@ -0,0 +1,244 @@ +//! Additions to [`Bot`]. +//! +//! [`Bot`]: crate::Bot +use crate::{prelude::*, types::*}; +use std::collections::HashSet; + +pub trait BotMessagesExt { + /// This function is the same as [`Bot::forward_messages`], + /// but can take in [`Message`], including just one. + /// + /// [`Bot::forward_messages`]: crate::Bot::forward_messages + /// [`Message`]: crate::types::Message + fn forward(&self, to_chat_id: C, messages: M) -> ::ForwardMessages + where + C: Into, + M: IntoIterator; + + /// This function is the same as [`Bot::copy_messages`], + /// but can take in [`Message`], including just one. + /// + /// [`Bot::copy_messages`]: crate::Bot::copy_messages + /// [`Message`]: crate::types::Message + fn copy(&self, to_chat_id: C, messages: M) -> ::CopyMessages + where + C: Into, + M: IntoIterator; + + /// This function is the same as [`Bot::delete_messages`], + /// but can take in [`Message`], including just one. + /// + /// [`Bot::delete_messages`]: crate::Bot::delete_messages + /// [`Message`]: crate::types::Message + fn delete(&self, messages: M) -> ::DeleteMessages + where + M: IntoIterator; +} + +fn compress_chat_messages(messages: M) -> (ChatId, Vec) +where + M: IntoIterator, +{ + let (message_ids, unique_chat_ids): (Vec, HashSet) = + messages.into_iter().map(|m| (m.id, m.chat.id)).unzip(); + + if unique_chat_ids.is_empty() { + panic!("There needs to be at least one message!"); + } else if unique_chat_ids.len() > 1 { + panic!( + "Messages shouldn't come from different chats! Current chat ids: {:?}", + unique_chat_ids.into_iter().map(|c| c.0).collect::>() + ); + } + + // Unwrap: length is checked to be non-zero before + let chat_id = unique_chat_ids.into_iter().next().unwrap(); + + (chat_id, message_ids) +} + +impl BotMessagesExt for Bot { + fn forward(&self, to_chat_id: C, messages: M) -> ::ForwardMessages + where + C: Into, + M: IntoIterator, + { + let (from_chat_id, message_ids) = compress_chat_messages(messages); + self.forward_messages(to_chat_id, from_chat_id, message_ids) + } + + fn copy(&self, to_chat_id: C, messages: M) -> ::CopyMessages + where + C: Into, + M: IntoIterator, + { + let (from_chat_id, message_ids) = compress_chat_messages(messages); + self.copy_messages(to_chat_id, from_chat_id, message_ids) + } + + fn delete(&self, messages: M) -> ::DeleteMessages + where + M: IntoIterator, + { + let (chat_id, message_ids) = compress_chat_messages(messages); + self.delete_messages(chat_id, message_ids) + } +} + +#[cfg(test)] +mod tests { + use std::ops::Deref; + + use chrono::DateTime; + + use super::*; + + fn make_message(chat_id: ChatId, message_id: MessageId) -> Message { + let timestamp = 1_569_518_829; + let date = DateTime::from_timestamp(timestamp, 0).unwrap(); + Message { + via_bot: None, + id: message_id, + thread_id: None, + from: Some(User { + id: UserId(109_998_024), + is_bot: false, + first_name: String::from("Laster"), + last_name: None, + username: Some(String::from("laster_alex")), + language_code: Some(String::from("en")), + is_premium: false, + added_to_attachment_menu: false, + }), + sender_chat: None, + is_topic_message: false, + sender_business_bot: None, + date, + chat: Chat { + id: chat_id, + kind: ChatKind::Private(ChatPrivate { + username: Some(String::from("Laster")), + first_name: Some(String::from("laster_alex")), + last_name: None, + bio: None, + has_private_forwards: None, + has_restricted_voice_and_video_messages: None, + business_intro: None, + business_location: None, + business_opening_hours: None, + birthdate: None, + personal_chat: None, + }), + photo: None, + available_reactions: None, + pinned_message: None, + message_auto_delete_time: None, + has_hidden_members: false, + has_aggressive_anti_spam_enabled: false, + chat_full_info: ChatFullInfo::default(), + }, + kind: MessageKind::Common(MessageCommon { + reply_to_message: None, + forward_origin: None, + external_reply: None, + quote: None, + edit_date: None, + media_kind: MediaKind::Text(MediaText { + text: "text".to_owned(), + entities: vec![], + link_preview_options: None, + }), + reply_markup: None, + author_signature: None, + is_automatic_forward: false, + has_protected_content: false, + reply_to_story: None, + sender_boost_count: None, + is_from_offline: false, + business_connection_id: None, + }), + } + } + + #[test] + fn test_forward() { + let bot = Bot::new("TOKEN"); + + let to_chat_id = ChatId(12345); + let from_chat_id = ChatId(6789); + let message_ids = vec![MessageId(100), MessageId(101), MessageId(102)]; + + let sugar_forward_req = bot.forward( + to_chat_id, + vec![ + make_message(from_chat_id, message_ids[0]), + make_message(from_chat_id, message_ids[1]), + make_message(from_chat_id, message_ids[2]), + ], + ); + let real_forward_req = bot.forward_messages(to_chat_id, from_chat_id, message_ids); + + assert_eq!(sugar_forward_req.deref(), real_forward_req.deref()) + } + + #[test] + fn test_copy() { + let bot = Bot::new("TOKEN"); + + let to_chat_id = ChatId(12345); + let from_chat_id = ChatId(6789); + let message_ids = vec![MessageId(100), MessageId(101), MessageId(102)]; + + let sugar_copy_req = bot.copy( + to_chat_id, + vec![ + make_message(from_chat_id, message_ids[0]), + make_message(from_chat_id, message_ids[1]), + make_message(from_chat_id, message_ids[2]), + ], + ); + let real_copy_req = bot.copy_messages(to_chat_id, from_chat_id, message_ids); + + assert_eq!(sugar_copy_req.deref(), real_copy_req.deref()) + } + + #[test] + fn test_delete() { + let bot = Bot::new("TOKEN"); + + let chat_id = ChatId(6789); + let message_ids = vec![MessageId(100), MessageId(101), MessageId(102)]; + + let sugar_delete_req = bot.delete(vec![ + make_message(chat_id, message_ids[0]), + make_message(chat_id, message_ids[1]), + make_message(chat_id, message_ids[2]), + ]); + let real_delete_req = bot.delete_messages(chat_id, message_ids); + + assert_eq!(sugar_delete_req.deref(), real_delete_req.deref()) + } + + #[test] + #[should_panic] + fn test_forward_many_chats() { + // They all use the same validation, only one check is enough + let bot = Bot::new("TOKEN"); + + let _ = bot.forward( + ChatId(12345), + vec![ + make_message(ChatId(6789), MessageId(100)), + make_message(ChatId(6789), MessageId(101)), + make_message(ChatId(9012), MessageId(102)), + ], + ); + } + + #[test] + fn message_to_iterator() { + // Just to make sure one message still can be passed in + let message = make_message(ChatId(1), MessageId(1)); + assert_eq!(message.clone().into_iter().next(), Some(message)); + } +} From 19ae8dd7a9b616d3d19e7abada5148be0f6ff36a Mon Sep 17 00:00:00 2001 From: LasterAlex Date: Fri, 30 Aug 2024 06:17:44 +0300 Subject: [PATCH 02/20] Added simple json request sugar --- crates/teloxide/src/sugar.rs | 1 + crates/teloxide/src/sugar/request.rs | 51 ++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 crates/teloxide/src/sugar/request.rs diff --git a/crates/teloxide/src/sugar.rs b/crates/teloxide/src/sugar.rs index 6bb10ad8..b6f23cc0 100644 --- a/crates/teloxide/src/sugar.rs +++ b/crates/teloxide/src/sugar.rs @@ -1,3 +1,4 @@ //! Some non-detrimental, but nice additions pub mod bot; +pub mod request; diff --git a/crates/teloxide/src/sugar/request.rs b/crates/teloxide/src/sugar/request.rs new file mode 100644 index 00000000..a3b82091 --- /dev/null +++ b/crates/teloxide/src/sugar/request.rs @@ -0,0 +1,51 @@ +//! Additions to [`JsonRequest`]. +//! +//! [`JsonRequest`]: teloxide_core::requests::JsonRequest + +use teloxide_core::{payloads::*, prelude::Requester, types::*, Bot}; + +macro_rules! impl_request_reply_ext { + ($($t:ty),*) => { + $( + impl RequestReplyExt for $t { + fn reply_to(self, message_id: M) -> Self + where + M: Into, + Self: teloxide_core::requests::HasPayload + Sized, + { + self.reply_parameters(ReplyParameters::new(message_id.into())) + } + } + )* + }; +} + +pub trait RequestReplyExt { + fn reply_to(self, message_id: M) -> Self + where + M: Into, + Self: Sized; +} + +impl_request_reply_ext! { + ::SendMessage +} + +#[cfg(test)] +mod tests { + use std::ops::Deref; + + use super::*; + use teloxide_core::{prelude::Requester, Bot}; + + #[test] + fn test_reply_to() { + let bot = Bot::new("TOKEN"); + let real_reply_req = bot + .send_message(ChatId(1234), "test") + .reply_parameters(ReplyParameters::new(MessageId(1))); + let sugar_reply_req = bot.send_message(ChatId(1234), "test").reply_to(MessageId(1)); + + assert_eq!(real_reply_req.deref(), sugar_reply_req.deref()) + } +} From cc0f2a73a8e412ac22084cd74ab57860e4e61689 Mon Sep 17 00:00:00 2001 From: LasterAlex Date: Fri, 30 Aug 2024 06:28:39 +0300 Subject: [PATCH 03/20] Added link preview sugar --- crates/teloxide/src/sugar/request.rs | 51 +++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/crates/teloxide/src/sugar/request.rs b/crates/teloxide/src/sugar/request.rs index a3b82091..dc31ae50 100644 --- a/crates/teloxide/src/sugar/request.rs +++ b/crates/teloxide/src/sugar/request.rs @@ -11,7 +11,7 @@ macro_rules! impl_request_reply_ext { fn reply_to(self, message_id: M) -> Self where M: Into, - Self: teloxide_core::requests::HasPayload + Sized, + Self: Sized, { self.reply_parameters(ReplyParameters::new(message_id.into())) } @@ -20,6 +20,28 @@ macro_rules! impl_request_reply_ext { }; } +macro_rules! impl_request_link_preview_ext { + ($($t:ty),*) => { + $( + impl RequestLinkPreviewExt for $t { + fn disable_link_preview(self) -> Self + where + Self: Sized + { + let link_preview_options = LinkPreviewOptions { + is_disabled: true, + url: None, + prefer_small_media: false, + prefer_large_media: false, + show_above_text: false, + }; + self.link_preview_options(link_preview_options) + } + } + )* + }; +} + pub trait RequestReplyExt { fn reply_to(self, message_id: M) -> Self where @@ -27,10 +49,20 @@ pub trait RequestReplyExt { Self: Sized; } +pub trait RequestLinkPreviewExt { + fn disable_link_preview(self) -> Self + where + Self: Sized; +} + impl_request_reply_ext! { ::SendMessage } +impl_request_link_preview_ext! { + ::SendMessage +} + #[cfg(test)] mod tests { use std::ops::Deref; @@ -48,4 +80,21 @@ mod tests { assert_eq!(real_reply_req.deref(), sugar_reply_req.deref()) } + + #[test] + fn test_disable_link_preview() { + let link_preview_options = LinkPreviewOptions { + is_disabled: true, + url: None, + prefer_small_media: false, + prefer_large_media: false, + show_above_text: false, + }; + let bot = Bot::new("TOKEN"); + let real_link_req = + bot.send_message(ChatId(1234), "test").link_preview_options(link_preview_options); + let sugar_link_req = bot.send_message(ChatId(1234), "test").disable_link_preview(); + + assert_eq!(real_link_req.deref(), sugar_link_req.deref()) + } } From fc7abb70d58b60f55215a64a9b6ed2a98f4c4d22 Mon Sep 17 00:00:00 2001 From: LasterAlex Date: Fri, 30 Aug 2024 09:05:49 +0300 Subject: [PATCH 04/20] More types + some fixes --- crates/teloxide-core/src/types/message.rs | 8 +++ crates/teloxide/src/sugar/bot.rs | 28 ++++++--- crates/teloxide/src/sugar/request.rs | 71 ++++++++++++++++++++--- 3 files changed, 91 insertions(+), 16 deletions(-) diff --git a/crates/teloxide-core/src/types/message.rs b/crates/teloxide-core/src/types/message.rs index 93411078..36fec591 100644 --- a/crates/teloxide-core/src/types/message.rs +++ b/crates/teloxide-core/src/types/message.rs @@ -1826,6 +1826,14 @@ impl IntoIterator for Message { } } +/// Implemented for syntax sugar, see issue +#[allow(clippy::from_over_into)] +impl Into for Message { + fn into(self) -> MessageId { + self.id + } +} + #[cfg(test)] mod tests { use chrono::DateTime; diff --git a/crates/teloxide/src/sugar/bot.rs b/crates/teloxide/src/sugar/bot.rs index 1d69380d..4c8781a2 100644 --- a/crates/teloxide/src/sugar/bot.rs +++ b/crates/teloxide/src/sugar/bot.rs @@ -2,15 +2,19 @@ //! //! [`Bot`]: crate::Bot use crate::{prelude::*, types::*}; +use teloxide_core::{payloads::*, requests::JsonRequest}; use std::collections::HashSet; +/// Adds useful manipulations with [`Message`] structs +/// +/// [`Message`]: crate::types::Message pub trait BotMessagesExt { /// This function is the same as [`Bot::forward_messages`], /// but can take in [`Message`], including just one. /// /// [`Bot::forward_messages`]: crate::Bot::forward_messages /// [`Message`]: crate::types::Message - fn forward(&self, to_chat_id: C, messages: M) -> ::ForwardMessages + fn forward(&self, to_chat_id: C, messages: M) -> JsonRequest where C: Into, M: IntoIterator; @@ -20,7 +24,7 @@ pub trait BotMessagesExt { /// /// [`Bot::copy_messages`]: crate::Bot::copy_messages /// [`Message`]: crate::types::Message - fn copy(&self, to_chat_id: C, messages: M) -> ::CopyMessages + fn copy(&self, to_chat_id: C, messages: M) -> JsonRequest where C: Into, M: IntoIterator; @@ -30,7 +34,7 @@ pub trait BotMessagesExt { /// /// [`Bot::delete_messages`]: crate::Bot::delete_messages /// [`Message`]: crate::types::Message - fn delete(&self, messages: M) -> ::DeleteMessages + fn delete(&self, messages: M) -> JsonRequest where M: IntoIterator; } @@ -58,7 +62,7 @@ where } impl BotMessagesExt for Bot { - fn forward(&self, to_chat_id: C, messages: M) -> ::ForwardMessages + fn forward(&self, to_chat_id: C, messages: M) -> JsonRequest where C: Into, M: IntoIterator, @@ -67,7 +71,7 @@ impl BotMessagesExt for Bot { self.forward_messages(to_chat_id, from_chat_id, message_ids) } - fn copy(&self, to_chat_id: C, messages: M) -> ::CopyMessages + fn copy(&self, to_chat_id: C, messages: M) -> JsonRequest where C: Into, M: IntoIterator, @@ -76,7 +80,7 @@ impl BotMessagesExt for Bot { self.copy_messages(to_chat_id, from_chat_id, message_ids) } - fn delete(&self, messages: M) -> ::DeleteMessages + fn delete(&self, messages: M) -> JsonRequest where M: IntoIterator, { @@ -86,14 +90,14 @@ impl BotMessagesExt for Bot { } #[cfg(test)] -mod tests { +pub(crate) mod tests { use std::ops::Deref; use chrono::DateTime; use super::*; - fn make_message(chat_id: ChatId, message_id: MessageId) -> Message { + pub(crate) fn make_message(chat_id: ChatId, message_id: MessageId) -> Message { let timestamp = 1_569_518_829; let date = DateTime::from_timestamp(timestamp, 0).unwrap(); Message { @@ -241,4 +245,12 @@ mod tests { let message = make_message(ChatId(1), MessageId(1)); assert_eq!(message.clone().into_iter().next(), Some(message)); } + + #[test] + fn message_to_message_id() { + // Just to make sure message still can be in Into + let message = make_message(ChatId(1), MessageId(1)); + let message_id: MessageId = message.into(); + assert_eq!(message_id, MessageId(1)); + } } diff --git a/crates/teloxide/src/sugar/request.rs b/crates/teloxide/src/sugar/request.rs index dc31ae50..6c2d4f3d 100644 --- a/crates/teloxide/src/sugar/request.rs +++ b/crates/teloxide/src/sugar/request.rs @@ -1,8 +1,13 @@ -//! Additions to [`JsonRequest`]. +//! Additions to [`JsonRequest`] and [`MultipartRequest`]. //! //! [`JsonRequest`]: teloxide_core::requests::JsonRequest +//! [`MultipartRequest`]: teloxide_core::requests::MultipartRequest -use teloxide_core::{payloads::*, prelude::Requester, types::*, Bot}; +use teloxide_core::{ + payloads::*, + requests::{JsonRequest, MultipartRequest}, + types::*, +}; macro_rules! impl_request_reply_ext { ($($t:ty),*) => { @@ -24,12 +29,12 @@ macro_rules! impl_request_link_preview_ext { ($($t:ty),*) => { $( impl RequestLinkPreviewExt for $t { - fn disable_link_preview(self) -> Self + fn disable_link_preview(self, is_disabled: bool) -> Self where Self: Sized { let link_preview_options = LinkPreviewOptions { - is_disabled: true, + is_disabled, url: None, prefer_small_media: false, prefer_large_media: false, @@ -42,25 +47,57 @@ macro_rules! impl_request_link_preview_ext { }; } +/// Adds `.reply_to(message_id)` to requests pub trait RequestReplyExt { + /// Replaces `.reply_parameters(ReplyParameters::new(msg.id))` + /// with `.reply_to(msg.id)` or `.reply_to(msg)` fn reply_to(self, message_id: M) -> Self where M: Into, Self: Sized; } +/// Adds `.disable_link_preview(is_disabled)` to requests pub trait RequestLinkPreviewExt { - fn disable_link_preview(self) -> Self + /// Replaces + /// `.link_preview_options(LinkPreviewOptions { + /// is_disabled: true, + /// url: None, + /// prefer_small_media: false, + /// prefer_large_media: false, + /// show_above_text: false + /// };)` + /// + /// With `.disable_link_preview(true)` + fn disable_link_preview(self, is_disabled: bool) -> Self where Self: Sized; } impl_request_reply_ext! { - ::SendMessage + JsonRequest, + JsonRequest, + JsonRequest, + JsonRequest, + JsonRequest, + JsonRequest, + JsonRequest, + JsonRequest, + JsonRequest, + MultipartRequest, + MultipartRequest, + MultipartRequest, + MultipartRequest, + MultipartRequest, + MultipartRequest, + MultipartRequest, + MultipartRequest, + MultipartRequest } impl_request_link_preview_ext! { - ::SendMessage + JsonRequest, + JsonRequest } #[cfg(test)] @@ -73,6 +110,7 @@ mod tests { #[test] fn test_reply_to() { let bot = Bot::new("TOKEN"); + let real_reply_req = bot .send_message(ChatId(1234), "test") .reply_parameters(ReplyParameters::new(MessageId(1))); @@ -81,6 +119,22 @@ mod tests { assert_eq!(real_reply_req.deref(), sugar_reply_req.deref()) } + #[test] + fn test_reply_to_multipart() { + let bot = Bot::new("TOKEN"); + let document = InputFile::memory("hello world!"); + + let real_reply_req = bot + .send_document(ChatId(1234), document.clone()) + .reply_parameters(ReplyParameters::new(MessageId(1))); + let sugar_reply_req = bot.send_document(ChatId(1234), document).reply_to(MessageId(1)); + + assert_eq!( + real_reply_req.deref().reply_parameters, + sugar_reply_req.deref().reply_parameters + ) + } + #[test] fn test_disable_link_preview() { let link_preview_options = LinkPreviewOptions { @@ -91,9 +145,10 @@ mod tests { show_above_text: false, }; let bot = Bot::new("TOKEN"); + let real_link_req = bot.send_message(ChatId(1234), "test").link_preview_options(link_preview_options); - let sugar_link_req = bot.send_message(ChatId(1234), "test").disable_link_preview(); + let sugar_link_req = bot.send_message(ChatId(1234), "test").disable_link_preview(true); assert_eq!(real_link_req.deref(), sugar_link_req.deref()) } From 578b6655417f8932766d0ea8bd4fa1d94af3cf07 Mon Sep 17 00:00:00 2001 From: LasterAlex Date: Fri, 30 Aug 2024 05:03:41 +0300 Subject: [PATCH 05/20] Added bot message sugar --- CHANGELOG.md | 1 + crates/teloxide-core/src/types/message.rs | 10 + crates/teloxide/src/lib.rs | 1 + crates/teloxide/src/sugar.rs | 3 + crates/teloxide/src/sugar/bot.rs | 244 ++++++++++++++++++++++ 5 files changed, 259 insertions(+) create mode 100644 crates/teloxide/src/sugar.rs create mode 100644 crates/teloxide/src/sugar/bot.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index e9a314e6..916126ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `filter_boost_added` and `filter_reply_to_story` filters to `MessageFilterExt` trait - Add `filter_mention_command` filter to `HandlerExt` trait ([issue #494](https://github.com/teloxide/teloxide/issues/494)) - Add `filter_business_connection`, `filter_business_message`, `filter_edited_business_message`, and `filter_deleted_business_messages` filters to update filters ([PR 1146](https://github.com/teloxide/teloxide/pull/1146)) +- Add `bot.forward`, `bot.copy` and `bot.delete` to new `crate::sugar::BotMessagesExt` trait ([issue #1143](https://github.com/teloxide/teloxide/issues/1143)) ### Changed diff --git a/crates/teloxide-core/src/types/message.rs b/crates/teloxide-core/src/types/message.rs index 2b2ce6e2..93411078 100644 --- a/crates/teloxide-core/src/types/message.rs +++ b/crates/teloxide-core/src/types/message.rs @@ -1816,6 +1816,16 @@ impl Message { } } +/// Implemented for syntax sugar, see issue +impl IntoIterator for Message { + type Item = Message; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + vec![self].into_iter() + } +} + #[cfg(test)] mod tests { use chrono::DateTime; diff --git a/crates/teloxide/src/lib.rs b/crates/teloxide/src/lib.rs index bcb901f1..edab596d 100644 --- a/crates/teloxide/src/lib.rs +++ b/crates/teloxide/src/lib.rs @@ -140,6 +140,7 @@ pub mod prelude; #[cfg(feature = "ctrlc_handler")] pub mod repls; pub mod stop; +pub mod sugar; pub mod update_listeners; pub mod utils; diff --git a/crates/teloxide/src/sugar.rs b/crates/teloxide/src/sugar.rs new file mode 100644 index 00000000..6bb10ad8 --- /dev/null +++ b/crates/teloxide/src/sugar.rs @@ -0,0 +1,3 @@ +//! Some non-detrimental, but nice additions + +pub mod bot; diff --git a/crates/teloxide/src/sugar/bot.rs b/crates/teloxide/src/sugar/bot.rs new file mode 100644 index 00000000..1d69380d --- /dev/null +++ b/crates/teloxide/src/sugar/bot.rs @@ -0,0 +1,244 @@ +//! Additions to [`Bot`]. +//! +//! [`Bot`]: crate::Bot +use crate::{prelude::*, types::*}; +use std::collections::HashSet; + +pub trait BotMessagesExt { + /// This function is the same as [`Bot::forward_messages`], + /// but can take in [`Message`], including just one. + /// + /// [`Bot::forward_messages`]: crate::Bot::forward_messages + /// [`Message`]: crate::types::Message + fn forward(&self, to_chat_id: C, messages: M) -> ::ForwardMessages + where + C: Into, + M: IntoIterator; + + /// This function is the same as [`Bot::copy_messages`], + /// but can take in [`Message`], including just one. + /// + /// [`Bot::copy_messages`]: crate::Bot::copy_messages + /// [`Message`]: crate::types::Message + fn copy(&self, to_chat_id: C, messages: M) -> ::CopyMessages + where + C: Into, + M: IntoIterator; + + /// This function is the same as [`Bot::delete_messages`], + /// but can take in [`Message`], including just one. + /// + /// [`Bot::delete_messages`]: crate::Bot::delete_messages + /// [`Message`]: crate::types::Message + fn delete(&self, messages: M) -> ::DeleteMessages + where + M: IntoIterator; +} + +fn compress_chat_messages(messages: M) -> (ChatId, Vec) +where + M: IntoIterator, +{ + let (message_ids, unique_chat_ids): (Vec, HashSet) = + messages.into_iter().map(|m| (m.id, m.chat.id)).unzip(); + + if unique_chat_ids.is_empty() { + panic!("There needs to be at least one message!"); + } else if unique_chat_ids.len() > 1 { + panic!( + "Messages shouldn't come from different chats! Current chat ids: {:?}", + unique_chat_ids.into_iter().map(|c| c.0).collect::>() + ); + } + + // Unwrap: length is checked to be non-zero before + let chat_id = unique_chat_ids.into_iter().next().unwrap(); + + (chat_id, message_ids) +} + +impl BotMessagesExt for Bot { + fn forward(&self, to_chat_id: C, messages: M) -> ::ForwardMessages + where + C: Into, + M: IntoIterator, + { + let (from_chat_id, message_ids) = compress_chat_messages(messages); + self.forward_messages(to_chat_id, from_chat_id, message_ids) + } + + fn copy(&self, to_chat_id: C, messages: M) -> ::CopyMessages + where + C: Into, + M: IntoIterator, + { + let (from_chat_id, message_ids) = compress_chat_messages(messages); + self.copy_messages(to_chat_id, from_chat_id, message_ids) + } + + fn delete(&self, messages: M) -> ::DeleteMessages + where + M: IntoIterator, + { + let (chat_id, message_ids) = compress_chat_messages(messages); + self.delete_messages(chat_id, message_ids) + } +} + +#[cfg(test)] +mod tests { + use std::ops::Deref; + + use chrono::DateTime; + + use super::*; + + fn make_message(chat_id: ChatId, message_id: MessageId) -> Message { + let timestamp = 1_569_518_829; + let date = DateTime::from_timestamp(timestamp, 0).unwrap(); + Message { + via_bot: None, + id: message_id, + thread_id: None, + from: Some(User { + id: UserId(109_998_024), + is_bot: false, + first_name: String::from("Laster"), + last_name: None, + username: Some(String::from("laster_alex")), + language_code: Some(String::from("en")), + is_premium: false, + added_to_attachment_menu: false, + }), + sender_chat: None, + is_topic_message: false, + sender_business_bot: None, + date, + chat: Chat { + id: chat_id, + kind: ChatKind::Private(ChatPrivate { + username: Some(String::from("Laster")), + first_name: Some(String::from("laster_alex")), + last_name: None, + bio: None, + has_private_forwards: None, + has_restricted_voice_and_video_messages: None, + business_intro: None, + business_location: None, + business_opening_hours: None, + birthdate: None, + personal_chat: None, + }), + photo: None, + available_reactions: None, + pinned_message: None, + message_auto_delete_time: None, + has_hidden_members: false, + has_aggressive_anti_spam_enabled: false, + chat_full_info: ChatFullInfo::default(), + }, + kind: MessageKind::Common(MessageCommon { + reply_to_message: None, + forward_origin: None, + external_reply: None, + quote: None, + edit_date: None, + media_kind: MediaKind::Text(MediaText { + text: "text".to_owned(), + entities: vec![], + link_preview_options: None, + }), + reply_markup: None, + author_signature: None, + is_automatic_forward: false, + has_protected_content: false, + reply_to_story: None, + sender_boost_count: None, + is_from_offline: false, + business_connection_id: None, + }), + } + } + + #[test] + fn test_forward() { + let bot = Bot::new("TOKEN"); + + let to_chat_id = ChatId(12345); + let from_chat_id = ChatId(6789); + let message_ids = vec![MessageId(100), MessageId(101), MessageId(102)]; + + let sugar_forward_req = bot.forward( + to_chat_id, + vec![ + make_message(from_chat_id, message_ids[0]), + make_message(from_chat_id, message_ids[1]), + make_message(from_chat_id, message_ids[2]), + ], + ); + let real_forward_req = bot.forward_messages(to_chat_id, from_chat_id, message_ids); + + assert_eq!(sugar_forward_req.deref(), real_forward_req.deref()) + } + + #[test] + fn test_copy() { + let bot = Bot::new("TOKEN"); + + let to_chat_id = ChatId(12345); + let from_chat_id = ChatId(6789); + let message_ids = vec![MessageId(100), MessageId(101), MessageId(102)]; + + let sugar_copy_req = bot.copy( + to_chat_id, + vec![ + make_message(from_chat_id, message_ids[0]), + make_message(from_chat_id, message_ids[1]), + make_message(from_chat_id, message_ids[2]), + ], + ); + let real_copy_req = bot.copy_messages(to_chat_id, from_chat_id, message_ids); + + assert_eq!(sugar_copy_req.deref(), real_copy_req.deref()) + } + + #[test] + fn test_delete() { + let bot = Bot::new("TOKEN"); + + let chat_id = ChatId(6789); + let message_ids = vec![MessageId(100), MessageId(101), MessageId(102)]; + + let sugar_delete_req = bot.delete(vec![ + make_message(chat_id, message_ids[0]), + make_message(chat_id, message_ids[1]), + make_message(chat_id, message_ids[2]), + ]); + let real_delete_req = bot.delete_messages(chat_id, message_ids); + + assert_eq!(sugar_delete_req.deref(), real_delete_req.deref()) + } + + #[test] + #[should_panic] + fn test_forward_many_chats() { + // They all use the same validation, only one check is enough + let bot = Bot::new("TOKEN"); + + let _ = bot.forward( + ChatId(12345), + vec![ + make_message(ChatId(6789), MessageId(100)), + make_message(ChatId(6789), MessageId(101)), + make_message(ChatId(9012), MessageId(102)), + ], + ); + } + + #[test] + fn message_to_iterator() { + // Just to make sure one message still can be passed in + let message = make_message(ChatId(1), MessageId(1)); + assert_eq!(message.clone().into_iter().next(), Some(message)); + } +} From c6eb43138dffe63dcd266c464840ec4462a55f9c Mon Sep 17 00:00:00 2001 From: LasterAlex Date: Fri, 30 Aug 2024 06:17:44 +0300 Subject: [PATCH 06/20] Added simple json request sugar --- crates/teloxide/src/sugar.rs | 1 + crates/teloxide/src/sugar/request.rs | 51 ++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 crates/teloxide/src/sugar/request.rs diff --git a/crates/teloxide/src/sugar.rs b/crates/teloxide/src/sugar.rs index 6bb10ad8..b6f23cc0 100644 --- a/crates/teloxide/src/sugar.rs +++ b/crates/teloxide/src/sugar.rs @@ -1,3 +1,4 @@ //! Some non-detrimental, but nice additions pub mod bot; +pub mod request; diff --git a/crates/teloxide/src/sugar/request.rs b/crates/teloxide/src/sugar/request.rs new file mode 100644 index 00000000..a3b82091 --- /dev/null +++ b/crates/teloxide/src/sugar/request.rs @@ -0,0 +1,51 @@ +//! Additions to [`JsonRequest`]. +//! +//! [`JsonRequest`]: teloxide_core::requests::JsonRequest + +use teloxide_core::{payloads::*, prelude::Requester, types::*, Bot}; + +macro_rules! impl_request_reply_ext { + ($($t:ty),*) => { + $( + impl RequestReplyExt for $t { + fn reply_to(self, message_id: M) -> Self + where + M: Into, + Self: teloxide_core::requests::HasPayload + Sized, + { + self.reply_parameters(ReplyParameters::new(message_id.into())) + } + } + )* + }; +} + +pub trait RequestReplyExt { + fn reply_to(self, message_id: M) -> Self + where + M: Into, + Self: Sized; +} + +impl_request_reply_ext! { + ::SendMessage +} + +#[cfg(test)] +mod tests { + use std::ops::Deref; + + use super::*; + use teloxide_core::{prelude::Requester, Bot}; + + #[test] + fn test_reply_to() { + let bot = Bot::new("TOKEN"); + let real_reply_req = bot + .send_message(ChatId(1234), "test") + .reply_parameters(ReplyParameters::new(MessageId(1))); + let sugar_reply_req = bot.send_message(ChatId(1234), "test").reply_to(MessageId(1)); + + assert_eq!(real_reply_req.deref(), sugar_reply_req.deref()) + } +} From 2ea545a15cfa09cf0204a0c2e3ea3220789dc6f7 Mon Sep 17 00:00:00 2001 From: LasterAlex Date: Fri, 30 Aug 2024 06:28:39 +0300 Subject: [PATCH 07/20] Added link preview sugar --- crates/teloxide/src/sugar/request.rs | 51 +++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/crates/teloxide/src/sugar/request.rs b/crates/teloxide/src/sugar/request.rs index a3b82091..dc31ae50 100644 --- a/crates/teloxide/src/sugar/request.rs +++ b/crates/teloxide/src/sugar/request.rs @@ -11,7 +11,7 @@ macro_rules! impl_request_reply_ext { fn reply_to(self, message_id: M) -> Self where M: Into, - Self: teloxide_core::requests::HasPayload + Sized, + Self: Sized, { self.reply_parameters(ReplyParameters::new(message_id.into())) } @@ -20,6 +20,28 @@ macro_rules! impl_request_reply_ext { }; } +macro_rules! impl_request_link_preview_ext { + ($($t:ty),*) => { + $( + impl RequestLinkPreviewExt for $t { + fn disable_link_preview(self) -> Self + where + Self: Sized + { + let link_preview_options = LinkPreviewOptions { + is_disabled: true, + url: None, + prefer_small_media: false, + prefer_large_media: false, + show_above_text: false, + }; + self.link_preview_options(link_preview_options) + } + } + )* + }; +} + pub trait RequestReplyExt { fn reply_to(self, message_id: M) -> Self where @@ -27,10 +49,20 @@ pub trait RequestReplyExt { Self: Sized; } +pub trait RequestLinkPreviewExt { + fn disable_link_preview(self) -> Self + where + Self: Sized; +} + impl_request_reply_ext! { ::SendMessage } +impl_request_link_preview_ext! { + ::SendMessage +} + #[cfg(test)] mod tests { use std::ops::Deref; @@ -48,4 +80,21 @@ mod tests { assert_eq!(real_reply_req.deref(), sugar_reply_req.deref()) } + + #[test] + fn test_disable_link_preview() { + let link_preview_options = LinkPreviewOptions { + is_disabled: true, + url: None, + prefer_small_media: false, + prefer_large_media: false, + show_above_text: false, + }; + let bot = Bot::new("TOKEN"); + let real_link_req = + bot.send_message(ChatId(1234), "test").link_preview_options(link_preview_options); + let sugar_link_req = bot.send_message(ChatId(1234), "test").disable_link_preview(); + + assert_eq!(real_link_req.deref(), sugar_link_req.deref()) + } } From 83b178c8ca4262f0e74611e87fdb7a9e987da4cd Mon Sep 17 00:00:00 2001 From: LasterAlex Date: Fri, 30 Aug 2024 09:05:49 +0300 Subject: [PATCH 08/20] More types + some fixes --- crates/teloxide-core/src/types/message.rs | 8 +++ crates/teloxide/src/sugar/bot.rs | 28 ++++++--- crates/teloxide/src/sugar/request.rs | 71 ++++++++++++++++++++--- 3 files changed, 91 insertions(+), 16 deletions(-) diff --git a/crates/teloxide-core/src/types/message.rs b/crates/teloxide-core/src/types/message.rs index 93411078..36fec591 100644 --- a/crates/teloxide-core/src/types/message.rs +++ b/crates/teloxide-core/src/types/message.rs @@ -1826,6 +1826,14 @@ impl IntoIterator for Message { } } +/// Implemented for syntax sugar, see issue +#[allow(clippy::from_over_into)] +impl Into for Message { + fn into(self) -> MessageId { + self.id + } +} + #[cfg(test)] mod tests { use chrono::DateTime; diff --git a/crates/teloxide/src/sugar/bot.rs b/crates/teloxide/src/sugar/bot.rs index 1d69380d..4c8781a2 100644 --- a/crates/teloxide/src/sugar/bot.rs +++ b/crates/teloxide/src/sugar/bot.rs @@ -2,15 +2,19 @@ //! //! [`Bot`]: crate::Bot use crate::{prelude::*, types::*}; +use teloxide_core::{payloads::*, requests::JsonRequest}; use std::collections::HashSet; +/// Adds useful manipulations with [`Message`] structs +/// +/// [`Message`]: crate::types::Message pub trait BotMessagesExt { /// This function is the same as [`Bot::forward_messages`], /// but can take in [`Message`], including just one. /// /// [`Bot::forward_messages`]: crate::Bot::forward_messages /// [`Message`]: crate::types::Message - fn forward(&self, to_chat_id: C, messages: M) -> ::ForwardMessages + fn forward(&self, to_chat_id: C, messages: M) -> JsonRequest where C: Into, M: IntoIterator; @@ -20,7 +24,7 @@ pub trait BotMessagesExt { /// /// [`Bot::copy_messages`]: crate::Bot::copy_messages /// [`Message`]: crate::types::Message - fn copy(&self, to_chat_id: C, messages: M) -> ::CopyMessages + fn copy(&self, to_chat_id: C, messages: M) -> JsonRequest where C: Into, M: IntoIterator; @@ -30,7 +34,7 @@ pub trait BotMessagesExt { /// /// [`Bot::delete_messages`]: crate::Bot::delete_messages /// [`Message`]: crate::types::Message - fn delete(&self, messages: M) -> ::DeleteMessages + fn delete(&self, messages: M) -> JsonRequest where M: IntoIterator; } @@ -58,7 +62,7 @@ where } impl BotMessagesExt for Bot { - fn forward(&self, to_chat_id: C, messages: M) -> ::ForwardMessages + fn forward(&self, to_chat_id: C, messages: M) -> JsonRequest where C: Into, M: IntoIterator, @@ -67,7 +71,7 @@ impl BotMessagesExt for Bot { self.forward_messages(to_chat_id, from_chat_id, message_ids) } - fn copy(&self, to_chat_id: C, messages: M) -> ::CopyMessages + fn copy(&self, to_chat_id: C, messages: M) -> JsonRequest where C: Into, M: IntoIterator, @@ -76,7 +80,7 @@ impl BotMessagesExt for Bot { self.copy_messages(to_chat_id, from_chat_id, message_ids) } - fn delete(&self, messages: M) -> ::DeleteMessages + fn delete(&self, messages: M) -> JsonRequest where M: IntoIterator, { @@ -86,14 +90,14 @@ impl BotMessagesExt for Bot { } #[cfg(test)] -mod tests { +pub(crate) mod tests { use std::ops::Deref; use chrono::DateTime; use super::*; - fn make_message(chat_id: ChatId, message_id: MessageId) -> Message { + pub(crate) fn make_message(chat_id: ChatId, message_id: MessageId) -> Message { let timestamp = 1_569_518_829; let date = DateTime::from_timestamp(timestamp, 0).unwrap(); Message { @@ -241,4 +245,12 @@ mod tests { let message = make_message(ChatId(1), MessageId(1)); assert_eq!(message.clone().into_iter().next(), Some(message)); } + + #[test] + fn message_to_message_id() { + // Just to make sure message still can be in Into + let message = make_message(ChatId(1), MessageId(1)); + let message_id: MessageId = message.into(); + assert_eq!(message_id, MessageId(1)); + } } diff --git a/crates/teloxide/src/sugar/request.rs b/crates/teloxide/src/sugar/request.rs index dc31ae50..6c2d4f3d 100644 --- a/crates/teloxide/src/sugar/request.rs +++ b/crates/teloxide/src/sugar/request.rs @@ -1,8 +1,13 @@ -//! Additions to [`JsonRequest`]. +//! Additions to [`JsonRequest`] and [`MultipartRequest`]. //! //! [`JsonRequest`]: teloxide_core::requests::JsonRequest +//! [`MultipartRequest`]: teloxide_core::requests::MultipartRequest -use teloxide_core::{payloads::*, prelude::Requester, types::*, Bot}; +use teloxide_core::{ + payloads::*, + requests::{JsonRequest, MultipartRequest}, + types::*, +}; macro_rules! impl_request_reply_ext { ($($t:ty),*) => { @@ -24,12 +29,12 @@ macro_rules! impl_request_link_preview_ext { ($($t:ty),*) => { $( impl RequestLinkPreviewExt for $t { - fn disable_link_preview(self) -> Self + fn disable_link_preview(self, is_disabled: bool) -> Self where Self: Sized { let link_preview_options = LinkPreviewOptions { - is_disabled: true, + is_disabled, url: None, prefer_small_media: false, prefer_large_media: false, @@ -42,25 +47,57 @@ macro_rules! impl_request_link_preview_ext { }; } +/// Adds `.reply_to(message_id)` to requests pub trait RequestReplyExt { + /// Replaces `.reply_parameters(ReplyParameters::new(msg.id))` + /// with `.reply_to(msg.id)` or `.reply_to(msg)` fn reply_to(self, message_id: M) -> Self where M: Into, Self: Sized; } +/// Adds `.disable_link_preview(is_disabled)` to requests pub trait RequestLinkPreviewExt { - fn disable_link_preview(self) -> Self + /// Replaces + /// `.link_preview_options(LinkPreviewOptions { + /// is_disabled: true, + /// url: None, + /// prefer_small_media: false, + /// prefer_large_media: false, + /// show_above_text: false + /// };)` + /// + /// With `.disable_link_preview(true)` + fn disable_link_preview(self, is_disabled: bool) -> Self where Self: Sized; } impl_request_reply_ext! { - ::SendMessage + JsonRequest, + JsonRequest, + JsonRequest, + JsonRequest, + JsonRequest, + JsonRequest, + JsonRequest, + JsonRequest, + JsonRequest, + MultipartRequest, + MultipartRequest, + MultipartRequest, + MultipartRequest, + MultipartRequest, + MultipartRequest, + MultipartRequest, + MultipartRequest, + MultipartRequest } impl_request_link_preview_ext! { - ::SendMessage + JsonRequest, + JsonRequest } #[cfg(test)] @@ -73,6 +110,7 @@ mod tests { #[test] fn test_reply_to() { let bot = Bot::new("TOKEN"); + let real_reply_req = bot .send_message(ChatId(1234), "test") .reply_parameters(ReplyParameters::new(MessageId(1))); @@ -81,6 +119,22 @@ mod tests { assert_eq!(real_reply_req.deref(), sugar_reply_req.deref()) } + #[test] + fn test_reply_to_multipart() { + let bot = Bot::new("TOKEN"); + let document = InputFile::memory("hello world!"); + + let real_reply_req = bot + .send_document(ChatId(1234), document.clone()) + .reply_parameters(ReplyParameters::new(MessageId(1))); + let sugar_reply_req = bot.send_document(ChatId(1234), document).reply_to(MessageId(1)); + + assert_eq!( + real_reply_req.deref().reply_parameters, + sugar_reply_req.deref().reply_parameters + ) + } + #[test] fn test_disable_link_preview() { let link_preview_options = LinkPreviewOptions { @@ -91,9 +145,10 @@ mod tests { show_above_text: false, }; let bot = Bot::new("TOKEN"); + let real_link_req = bot.send_message(ChatId(1234), "test").link_preview_options(link_preview_options); - let sugar_link_req = bot.send_message(ChatId(1234), "test").disable_link_preview(); + let sugar_link_req = bot.send_message(ChatId(1234), "test").disable_link_preview(true); assert_eq!(real_link_req.deref(), sugar_link_req.deref()) } From 968fd5fbc3f7b2f685a40adc30af83ae9b5a626f Mon Sep 17 00:00:00 2001 From: LasterAlex Date: Fri, 30 Aug 2024 09:47:15 +0300 Subject: [PATCH 09/20] Cargo fmt --- crates/teloxide/src/sugar/bot.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/teloxide/src/sugar/bot.rs b/crates/teloxide/src/sugar/bot.rs index 4c8781a2..e4a0a934 100644 --- a/crates/teloxide/src/sugar/bot.rs +++ b/crates/teloxide/src/sugar/bot.rs @@ -2,8 +2,8 @@ //! //! [`Bot`]: crate::Bot use crate::{prelude::*, types::*}; -use teloxide_core::{payloads::*, requests::JsonRequest}; use std::collections::HashSet; +use teloxide_core::{payloads::*, requests::JsonRequest}; /// Adds useful manipulations with [`Message`] structs /// From 4ac9226f83b6397259011dbd44a6ca34dcfaf42e Mon Sep 17 00:00:00 2001 From: LasterAlex Date: Fri, 30 Aug 2024 09:50:52 +0300 Subject: [PATCH 10/20] Changelog update --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 916126ce..cb2bab28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `filter_boost_added` and `filter_reply_to_story` filters to `MessageFilterExt` trait - Add `filter_mention_command` filter to `HandlerExt` trait ([issue #494](https://github.com/teloxide/teloxide/issues/494)) - Add `filter_business_connection`, `filter_business_message`, `filter_edited_business_message`, and `filter_deleted_business_messages` filters to update filters ([PR 1146](https://github.com/teloxide/teloxide/pull/1146)) -- Add `bot.forward`, `bot.copy` and `bot.delete` to new `crate::sugar::BotMessagesExt` trait ([issue #1143](https://github.com/teloxide/teloxide/issues/1143)) +- Add `bot.forward`, `bot.copy` and `bot.delete` to new `crate::sugar::bot::BotMessagesExt` trait ([issue #1143](https://github.com/teloxide/teloxide/issues/1143)) +- Add `.reply_to(message_id)` to new `crate::sugar::request::RequestReplyExt` trait, and `.disable_link_preview(is_disabled)` to new `crate::sugar::request::RequestLinkPreviewExt` trait ([issue #1143](https://github.com/teloxide/teloxide/issues/1143)) ### Changed From 9f7d980a702a6246c6cf6316ae62ebd4be7c7da5 Mon Sep 17 00:00:00 2001 From: LasterAlex Date: Tue, 3 Sep 2024 13:43:50 +0300 Subject: [PATCH 11/20] Removed multiple messages sugar functionality --- crates/teloxide-core/src/types/message.rs | 10 -- crates/teloxide/src/sugar.rs | 2 +- crates/teloxide/src/sugar/bot.rs | 134 +++++----------------- 3 files changed, 30 insertions(+), 116 deletions(-) diff --git a/crates/teloxide-core/src/types/message.rs b/crates/teloxide-core/src/types/message.rs index 36fec591..e1634315 100644 --- a/crates/teloxide-core/src/types/message.rs +++ b/crates/teloxide-core/src/types/message.rs @@ -1816,16 +1816,6 @@ impl Message { } } -/// Implemented for syntax sugar, see issue -impl IntoIterator for Message { - type Item = Message; - type IntoIter = std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - vec![self].into_iter() - } -} - /// Implemented for syntax sugar, see issue #[allow(clippy::from_over_into)] impl Into for Message { diff --git a/crates/teloxide/src/sugar.rs b/crates/teloxide/src/sugar.rs index b6f23cc0..ae5bb627 100644 --- a/crates/teloxide/src/sugar.rs +++ b/crates/teloxide/src/sugar.rs @@ -1,4 +1,4 @@ -//! Some non-detrimental, but nice additions +//! Some syntax sugar support for TBA functionality pub mod bot; pub mod request; diff --git a/crates/teloxide/src/sugar/bot.rs b/crates/teloxide/src/sugar/bot.rs index e4a0a934..c4efed27 100644 --- a/crates/teloxide/src/sugar/bot.rs +++ b/crates/teloxide/src/sugar/bot.rs @@ -2,90 +2,55 @@ //! //! [`Bot`]: crate::Bot use crate::{prelude::*, types::*}; -use std::collections::HashSet; use teloxide_core::{payloads::*, requests::JsonRequest}; /// Adds useful manipulations with [`Message`] structs /// /// [`Message`]: crate::types::Message pub trait BotMessagesExt { - /// This function is the same as [`Bot::forward_messages`], - /// but can take in [`Message`], including just one. + /// This function is the same as [`Bot::forward_message`], + /// but can take in [`Message`] to forward it. /// - /// [`Bot::forward_messages`]: crate::Bot::forward_messages + /// [`Bot::forward_message`]: crate::Bot::forward_message /// [`Message`]: crate::types::Message - fn forward(&self, to_chat_id: C, messages: M) -> JsonRequest + fn forward(&self, to_chat_id: C, message: Message) -> JsonRequest where - C: Into, - M: IntoIterator; + C: Into; - /// This function is the same as [`Bot::copy_messages`], - /// but can take in [`Message`], including just one. + /// This function is the same as [`Bot::copy_message`], + /// but can take in [`Message`] to copy it. /// - /// [`Bot::copy_messages`]: crate::Bot::copy_messages + /// [`Bot::copy_messages`]: crate::Bot::copy_message /// [`Message`]: crate::types::Message - fn copy(&self, to_chat_id: C, messages: M) -> JsonRequest + fn copy(&self, to_chat_id: C, message: Message) -> JsonRequest where - C: Into, - M: IntoIterator; + C: Into; - /// This function is the same as [`Bot::delete_messages`], - /// but can take in [`Message`], including just one. + /// This function is the same as [`Bot::delete_message`], + /// but can take in [`Message`] to delete it. /// - /// [`Bot::delete_messages`]: crate::Bot::delete_messages + /// [`Bot::delete_message`]: crate::Bot::delete_message /// [`Message`]: crate::types::Message - fn delete(&self, messages: M) -> JsonRequest - where - M: IntoIterator; -} - -fn compress_chat_messages(messages: M) -> (ChatId, Vec) -where - M: IntoIterator, -{ - let (message_ids, unique_chat_ids): (Vec, HashSet) = - messages.into_iter().map(|m| (m.id, m.chat.id)).unzip(); - - if unique_chat_ids.is_empty() { - panic!("There needs to be at least one message!"); - } else if unique_chat_ids.len() > 1 { - panic!( - "Messages shouldn't come from different chats! Current chat ids: {:?}", - unique_chat_ids.into_iter().map(|c| c.0).collect::>() - ); - } - - // Unwrap: length is checked to be non-zero before - let chat_id = unique_chat_ids.into_iter().next().unwrap(); - - (chat_id, message_ids) + fn delete(&self, message: Message) -> JsonRequest; } impl BotMessagesExt for Bot { - fn forward(&self, to_chat_id: C, messages: M) -> JsonRequest + fn forward(&self, to_chat_id: C, message: Message) -> JsonRequest where C: Into, - M: IntoIterator, { - let (from_chat_id, message_ids) = compress_chat_messages(messages); - self.forward_messages(to_chat_id, from_chat_id, message_ids) + self.forward_message(to_chat_id, message.chat.id, message.id) } - fn copy(&self, to_chat_id: C, messages: M) -> JsonRequest + fn copy(&self, to_chat_id: C, message: Message) -> JsonRequest where C: Into, - M: IntoIterator, { - let (from_chat_id, message_ids) = compress_chat_messages(messages); - self.copy_messages(to_chat_id, from_chat_id, message_ids) + self.copy_message(to_chat_id, message.chat.id, message.id) } - fn delete(&self, messages: M) -> JsonRequest - where - M: IntoIterator, - { - let (chat_id, message_ids) = compress_chat_messages(messages); - self.delete_messages(chat_id, message_ids) + fn delete(&self, message: Message) -> JsonRequest { + self.delete_message(message.chat.id, message.id) } } @@ -170,17 +135,10 @@ pub(crate) mod tests { let to_chat_id = ChatId(12345); let from_chat_id = ChatId(6789); - let message_ids = vec![MessageId(100), MessageId(101), MessageId(102)]; + let message_id = MessageId(100); - let sugar_forward_req = bot.forward( - to_chat_id, - vec![ - make_message(from_chat_id, message_ids[0]), - make_message(from_chat_id, message_ids[1]), - make_message(from_chat_id, message_ids[2]), - ], - ); - let real_forward_req = bot.forward_messages(to_chat_id, from_chat_id, message_ids); + let sugar_forward_req = bot.forward(to_chat_id, make_message(from_chat_id, message_id)); + let real_forward_req = bot.forward_message(to_chat_id, from_chat_id, message_id); assert_eq!(sugar_forward_req.deref(), real_forward_req.deref()) } @@ -191,17 +149,10 @@ pub(crate) mod tests { let to_chat_id = ChatId(12345); let from_chat_id = ChatId(6789); - let message_ids = vec![MessageId(100), MessageId(101), MessageId(102)]; + let message_id = MessageId(100); - let sugar_copy_req = bot.copy( - to_chat_id, - vec![ - make_message(from_chat_id, message_ids[0]), - make_message(from_chat_id, message_ids[1]), - make_message(from_chat_id, message_ids[2]), - ], - ); - let real_copy_req = bot.copy_messages(to_chat_id, from_chat_id, message_ids); + let sugar_copy_req = bot.copy(to_chat_id, make_message(from_chat_id, message_id)); + let real_copy_req = bot.copy_message(to_chat_id, from_chat_id, message_id); assert_eq!(sugar_copy_req.deref(), real_copy_req.deref()) } @@ -211,41 +162,14 @@ pub(crate) mod tests { let bot = Bot::new("TOKEN"); let chat_id = ChatId(6789); - let message_ids = vec![MessageId(100), MessageId(101), MessageId(102)]; + let message_id = MessageId(100); - let sugar_delete_req = bot.delete(vec![ - make_message(chat_id, message_ids[0]), - make_message(chat_id, message_ids[1]), - make_message(chat_id, message_ids[2]), - ]); - let real_delete_req = bot.delete_messages(chat_id, message_ids); + let sugar_delete_req = bot.delete(make_message(chat_id, message_id)); + let real_delete_req = bot.delete_message(chat_id, message_id); assert_eq!(sugar_delete_req.deref(), real_delete_req.deref()) } - #[test] - #[should_panic] - fn test_forward_many_chats() { - // They all use the same validation, only one check is enough - let bot = Bot::new("TOKEN"); - - let _ = bot.forward( - ChatId(12345), - vec![ - make_message(ChatId(6789), MessageId(100)), - make_message(ChatId(6789), MessageId(101)), - make_message(ChatId(9012), MessageId(102)), - ], - ); - } - - #[test] - fn message_to_iterator() { - // Just to make sure one message still can be passed in - let message = make_message(ChatId(1), MessageId(1)); - assert_eq!(message.clone().into_iter().next(), Some(message)); - } - #[test] fn message_to_message_id() { // Just to make sure message still can be in Into From c9a0fe452de858dff609a421513e8cde159a428d Mon Sep 17 00:00:00 2001 From: LasterAlex Date: Tue, 3 Sep 2024 14:20:44 +0300 Subject: [PATCH 12/20] Added sugar to examples --- crates/teloxide/examples/dispatching_features.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/teloxide/examples/dispatching_features.rs b/crates/teloxide/examples/dispatching_features.rs index bcc65a2e..23f127ff 100644 --- a/crates/teloxide/examples/dispatching_features.rs +++ b/crates/teloxide/examples/dispatching_features.rs @@ -6,8 +6,9 @@ use rand::Rng; use teloxide::{ dispatching::HandlerExt, prelude::*, - types::{Dice, ReplyParameters}, + types::Dice, utils::command::BotCommands, + sugar::request::RequestReplyExt, }; #[tokio::main] @@ -84,7 +85,7 @@ async fn main() { // filter only messages with dices. Message::filter_dice().endpoint(|bot: Bot, msg: Message, dice: Dice| async move { bot.send_message(msg.chat.id, format!("Dice value: {}", dice.value)) - .reply_parameters(ReplyParameters::new(msg.id)) + .reply_to(msg) .await?; Ok(()) }), From b533a85a33ee9ce9439e01fdb81fc75f23e55d01 Mon Sep 17 00:00:00 2001 From: LasterAlex Date: Tue, 3 Sep 2024 14:21:57 +0300 Subject: [PATCH 13/20] Cargo fmt --- crates/teloxide/examples/dispatching_features.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/teloxide/examples/dispatching_features.rs b/crates/teloxide/examples/dispatching_features.rs index 23f127ff..8b327b55 100644 --- a/crates/teloxide/examples/dispatching_features.rs +++ b/crates/teloxide/examples/dispatching_features.rs @@ -4,11 +4,8 @@ use rand::Rng; use teloxide::{ - dispatching::HandlerExt, - prelude::*, - types::Dice, + dispatching::HandlerExt, prelude::*, sugar::request::RequestReplyExt, types::Dice, utils::command::BotCommands, - sugar::request::RequestReplyExt, }; #[tokio::main] From 2c21479aec356712a7a1a789d17d53d84858ae1c Mon Sep 17 00:00:00 2001 From: LasterAlex Date: Wed, 4 Sep 2024 03:19:12 +0300 Subject: [PATCH 14/20] Changed ownership to ref --- crates/teloxide/src/sugar/bot.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/teloxide/src/sugar/bot.rs b/crates/teloxide/src/sugar/bot.rs index c4efed27..7755cc58 100644 --- a/crates/teloxide/src/sugar/bot.rs +++ b/crates/teloxide/src/sugar/bot.rs @@ -13,7 +13,7 @@ pub trait BotMessagesExt { /// /// [`Bot::forward_message`]: crate::Bot::forward_message /// [`Message`]: crate::types::Message - fn forward(&self, to_chat_id: C, message: Message) -> JsonRequest + fn forward(&self, to_chat_id: C, message: &Message) -> JsonRequest where C: Into; @@ -22,7 +22,7 @@ pub trait BotMessagesExt { /// /// [`Bot::copy_messages`]: crate::Bot::copy_message /// [`Message`]: crate::types::Message - fn copy(&self, to_chat_id: C, message: Message) -> JsonRequest + fn copy(&self, to_chat_id: C, message: &Message) -> JsonRequest where C: Into; @@ -31,25 +31,25 @@ pub trait BotMessagesExt { /// /// [`Bot::delete_message`]: crate::Bot::delete_message /// [`Message`]: crate::types::Message - fn delete(&self, message: Message) -> JsonRequest; + fn delete(&self, message: &Message) -> JsonRequest; } impl BotMessagesExt for Bot { - fn forward(&self, to_chat_id: C, message: Message) -> JsonRequest + fn forward(&self, to_chat_id: C, message: &Message) -> JsonRequest where C: Into, { self.forward_message(to_chat_id, message.chat.id, message.id) } - fn copy(&self, to_chat_id: C, message: Message) -> JsonRequest + fn copy(&self, to_chat_id: C, message: &Message) -> JsonRequest where C: Into, { self.copy_message(to_chat_id, message.chat.id, message.id) } - fn delete(&self, message: Message) -> JsonRequest { + fn delete(&self, message: &Message) -> JsonRequest { self.delete_message(message.chat.id, message.id) } } @@ -137,7 +137,7 @@ pub(crate) mod tests { let from_chat_id = ChatId(6789); let message_id = MessageId(100); - let sugar_forward_req = bot.forward(to_chat_id, make_message(from_chat_id, message_id)); + let sugar_forward_req = bot.forward(to_chat_id, &make_message(from_chat_id, message_id)); let real_forward_req = bot.forward_message(to_chat_id, from_chat_id, message_id); assert_eq!(sugar_forward_req.deref(), real_forward_req.deref()) @@ -151,7 +151,7 @@ pub(crate) mod tests { let from_chat_id = ChatId(6789); let message_id = MessageId(100); - let sugar_copy_req = bot.copy(to_chat_id, make_message(from_chat_id, message_id)); + let sugar_copy_req = bot.copy(to_chat_id, &make_message(from_chat_id, message_id)); let real_copy_req = bot.copy_message(to_chat_id, from_chat_id, message_id); assert_eq!(sugar_copy_req.deref(), real_copy_req.deref()) @@ -164,7 +164,7 @@ pub(crate) mod tests { let chat_id = ChatId(6789); let message_id = MessageId(100); - let sugar_delete_req = bot.delete(make_message(chat_id, message_id)); + let sugar_delete_req = bot.delete(&make_message(chat_id, message_id)); let real_delete_req = bot.delete_message(chat_id, message_id); assert_eq!(sugar_delete_req.deref(), real_delete_req.deref()) From af3305cae87c5449d81ffbd1000bf44d6b8ea821 Mon Sep 17 00:00:00 2001 From: LasterAlex Date: Mon, 23 Sep 2024 01:30:28 +0300 Subject: [PATCH 15/20] Updated code with suggestions --- crates/teloxide/src/sugar.rs | 2 +- crates/teloxide/src/sugar/bot.rs | 2 +- crates/teloxide/src/sugar/request.rs | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/teloxide/src/sugar.rs b/crates/teloxide/src/sugar.rs index ae5bb627..0e8bb856 100644 --- a/crates/teloxide/src/sugar.rs +++ b/crates/teloxide/src/sugar.rs @@ -1,4 +1,4 @@ -//! Some syntax sugar support for TBA functionality +//! Some syntax sugar support for TBA functionality. pub mod bot; pub mod request; diff --git a/crates/teloxide/src/sugar/bot.rs b/crates/teloxide/src/sugar/bot.rs index 7755cc58..7b62ba66 100644 --- a/crates/teloxide/src/sugar/bot.rs +++ b/crates/teloxide/src/sugar/bot.rs @@ -4,7 +4,7 @@ use crate::{prelude::*, types::*}; use teloxide_core::{payloads::*, requests::JsonRequest}; -/// Adds useful manipulations with [`Message`] structs +/// Syntax sugar for [`Message`] manipulations. /// /// [`Message`]: crate::types::Message pub trait BotMessagesExt { diff --git a/crates/teloxide/src/sugar/request.rs b/crates/teloxide/src/sugar/request.rs index 6c2d4f3d..59ca41b0 100644 --- a/crates/teloxide/src/sugar/request.rs +++ b/crates/teloxide/src/sugar/request.rs @@ -47,7 +47,7 @@ macro_rules! impl_request_link_preview_ext { }; } -/// Adds `.reply_to(message_id)` to requests +/// `.reply_to(msg)` syntax sugar for requests. pub trait RequestReplyExt { /// Replaces `.reply_parameters(ReplyParameters::new(msg.id))` /// with `.reply_to(msg.id)` or `.reply_to(msg)` @@ -57,7 +57,7 @@ pub trait RequestReplyExt { Self: Sized; } -/// Adds `.disable_link_preview(is_disabled)` to requests +/// `.disable_link_preview(is_disabled)` syntax sugar for requests. pub trait RequestLinkPreviewExt { /// Replaces /// `.link_preview_options(LinkPreviewOptions { @@ -68,7 +68,7 @@ pub trait RequestLinkPreviewExt { /// show_above_text: false /// };)` /// - /// With `.disable_link_preview(true)` + /// with `.disable_link_preview(true)`. fn disable_link_preview(self, is_disabled: bool) -> Self where Self: Sized; From 1f23983bfb5fda1eec39213ab14cd4b019ee2f2a Mon Sep 17 00:00:00 2001 From: LasterAlex Date: Mon, 23 Sep 2024 01:36:07 +0300 Subject: [PATCH 16/20] Changed Into to From --- crates/teloxide-core/src/types/message.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/teloxide-core/src/types/message.rs b/crates/teloxide-core/src/types/message.rs index e1634315..2da52dcf 100644 --- a/crates/teloxide-core/src/types/message.rs +++ b/crates/teloxide-core/src/types/message.rs @@ -1817,10 +1817,9 @@ impl Message { } /// Implemented for syntax sugar, see issue -#[allow(clippy::from_over_into)] -impl Into for Message { - fn into(self) -> MessageId { - self.id +impl From for MessageId { + fn from(message: Message) -> MessageId { + message.id } } From 3b618a7b5a2bc83a07f037d1a64673fc7a132148 Mon Sep 17 00:00:00 2001 From: LasterAlex Date: Mon, 23 Sep 2024 02:29:28 +0300 Subject: [PATCH 17/20] Added syntactic sugar to all methods with message_id and chat_id --- CHANGELOG.md | 2 +- crates/teloxide/src/sugar/bot.rs | 291 +++++++++++++++++++++++++++---- 2 files changed, 261 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb2bab28..dac415f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `filter_boost_added` and `filter_reply_to_story` filters to `MessageFilterExt` trait - Add `filter_mention_command` filter to `HandlerExt` trait ([issue #494](https://github.com/teloxide/teloxide/issues/494)) - Add `filter_business_connection`, `filter_business_message`, `filter_edited_business_message`, and `filter_deleted_business_messages` filters to update filters ([PR 1146](https://github.com/teloxide/teloxide/pull/1146)) -- Add `bot.forward`, `bot.copy` and `bot.delete` to new `crate::sugar::bot::BotMessagesExt` trait ([issue #1143](https://github.com/teloxide/teloxide/issues/1143)) +- Add `bot.forward`, `bot.edit_live_location`, `bot.stop_live_location`, `bot.set_reaction`, `bot.pin`, `bot.unpin`, `bot.edit_text`, `bot.edit_caption`, `bot.edit_media`, `bot.edit_reply_markup`, `bot.stop_poll_message`, `bot.delete` and `bot.copy` to new `crate::sugar::bot::BotMessagesExt` trait ([issue #1143](https://github.com/teloxide/teloxide/issues/1143)) - Add `.reply_to(message_id)` to new `crate::sugar::request::RequestReplyExt` trait, and `.disable_link_preview(is_disabled)` to new `crate::sugar::request::RequestLinkPreviewExt` trait ([issue #1143](https://github.com/teloxide/teloxide/issues/1143)) ### Changed diff --git a/crates/teloxide/src/sugar/bot.rs b/crates/teloxide/src/sugar/bot.rs index 7b62ba66..bd0242f0 100644 --- a/crates/teloxide/src/sugar/bot.rs +++ b/crates/teloxide/src/sugar/bot.rs @@ -2,7 +2,10 @@ //! //! [`Bot`]: crate::Bot use crate::{prelude::*, types::*}; -use teloxide_core::{payloads::*, requests::JsonRequest}; +use teloxide_core::{ + payloads::*, + requests::{JsonRequest, MultipartRequest}, +}; /// Syntax sugar for [`Message`] manipulations. /// @@ -17,6 +20,94 @@ pub trait BotMessagesExt { where C: Into; + /// This function is the same as [`Bot::edit_message_live_location`], + /// but can take in [`Message`] to edit it. + /// + /// [`Bot::edit_message_live_location`]: crate::Bot::edit_message_live_location + /// [`Message`]: crate::types::Message + fn edit_live_location( + &self, + message: &Message, + latitude: f64, + longitude: f64, + ) -> JsonRequest; + + /// This function is the same as [`Bot::stop_message_live_location`], + /// but can take in [`Message`] to stop the live location in it. + /// + /// [`Bot::stop_message_live_location`]: crate::Bot::stop_message_live_location + /// [`Message`]: crate::types::Message + fn stop_live_location(&self, message: &Message) -> JsonRequest; + + /// This function is the same as [`Bot::set_message_reaction`], + /// but can take in [`Message`] to set a reaction on it. + /// + /// [`Bot::set_message_reaction`]: crate::Bot::set_message_reaction + /// [`Message`]: crate::types::Message + fn set_reaction(&self, message: &Message) -> JsonRequest; + + /// This function is the same as [`Bot::pin_chat_message`], + /// but can take in [`Message`] to pin it. + /// + /// [`Bot::pin_chat_message`]: crate::Bot::pin_chat_message + /// [`Message`]: crate::types::Message + fn pin(&self, message: &Message) -> JsonRequest; + + /// This function is the same as [`Bot::unpin_chat_message`], + /// but can take in [`Message`] to unpin it. + /// + /// [`Bot::unpin_chat_message`]: crate::Bot::unpin_chat_message + /// [`Message`]: crate::types::Message + fn unpin(&self, message: &Message) -> JsonRequest; + + /// This function is the same as [`Bot::edit_message_text`], + /// but can take in [`Message`] to edit it. + /// + /// [`Bot::edit_message_text`]: crate::Bot::edit_message_text + /// [`Message`]: crate::types::Message + fn edit_text(&self, message: &Message, text: T) -> JsonRequest + where + T: Into; + + /// This function is the same as [`Bot::edit_message_caption`], + /// but can take in [`Message`] to edit it. + /// + /// [`Bot::edit_message_caption`]: crate::Bot::edit_message_caption + /// [`Message`]: crate::types::Message + fn edit_caption(&self, message: &Message) -> JsonRequest; + + /// This function is the same as [`Bot::edit_message_media`], + /// but can take in [`Message`] to edit it. + /// + /// [`Bot::edit_message_media`]: crate::Bot::edit_message_media + /// [`Message`]: crate::types::Message + fn edit_media( + &self, + message: &Message, + media: InputMedia, + ) -> MultipartRequest; + + /// This function is the same as [`Bot::edit_message_reply_markup`], + /// but can take in [`Message`] to edit it. + /// + /// [`Bot::edit_message_reply_markup`]: crate::Bot::edit_message_reply_markup + /// [`Message`]: crate::types::Message + fn edit_reply_markup(&self, message: &Message) -> JsonRequest; + + /// This function is the same as [`Bot::stop_poll`], + /// but can take in [`Message`] to stop the poll in it. + /// + /// [`Bot::stop_poll`]: crate::Bot::stop_poll + /// [`Message`]: crate::types::Message + fn stop_poll_message(&self, message: &Message) -> JsonRequest; + + /// This function is the same as [`Bot::delete_message`], + /// but can take in [`Message`] to delete it. + /// + /// [`Bot::delete_message`]: crate::Bot::delete_message + /// [`Message`]: crate::types::Message + fn delete(&self, message: &Message) -> JsonRequest; + /// This function is the same as [`Bot::copy_message`], /// but can take in [`Message`] to copy it. /// @@ -25,13 +116,6 @@ pub trait BotMessagesExt { fn copy(&self, to_chat_id: C, message: &Message) -> JsonRequest where C: Into; - - /// This function is the same as [`Bot::delete_message`], - /// but can take in [`Message`] to delete it. - /// - /// [`Bot::delete_message`]: crate::Bot::delete_message - /// [`Message`]: crate::types::Message - fn delete(&self, message: &Message) -> JsonRequest; } impl BotMessagesExt for Bot { @@ -41,6 +125,61 @@ impl BotMessagesExt for Bot { { self.forward_message(to_chat_id, message.chat.id, message.id) } + fn edit_live_location( + &self, + message: &Message, + latitude: f64, + longitude: f64, + ) -> JsonRequest { + self.edit_message_live_location(message.chat.id, message.id, latitude, longitude) + } + + fn stop_live_location(&self, message: &Message) -> JsonRequest { + self.stop_message_live_location(message.chat.id, message.id) + } + + fn set_reaction(&self, message: &Message) -> JsonRequest { + self.set_message_reaction(message.chat.id, message.id) + } + + fn pin(&self, message: &Message) -> JsonRequest { + self.pin_chat_message(message.chat.id, message.id) + } + + fn unpin(&self, message: &Message) -> JsonRequest { + self.unpin_chat_message(message.chat.id).message_id(message.id) + } + + fn edit_text(&self, message: &Message, text: T) -> JsonRequest + where + T: Into, + { + self.edit_message_text(message.chat.id, message.id, text) + } + + fn edit_caption(&self, message: &Message) -> JsonRequest { + self.edit_message_caption(message.chat.id, message.id) + } + + fn edit_media( + &self, + message: &Message, + media: InputMedia, + ) -> MultipartRequest { + self.edit_message_media(message.chat.id, message.id, media) + } + + fn edit_reply_markup(&self, message: &Message) -> JsonRequest { + self.edit_message_reply_markup(message.chat.id, message.id) + } + + fn stop_poll_message(&self, message: &Message) -> JsonRequest { + self.stop_poll(message.chat.id, message.id) + } + + fn delete(&self, message: &Message) -> JsonRequest { + self.delete_message(message.chat.id, message.id) + } fn copy(&self, to_chat_id: C, message: &Message) -> JsonRequest where @@ -48,10 +187,6 @@ impl BotMessagesExt for Bot { { self.copy_message(to_chat_id, message.chat.id, message.id) } - - fn delete(&self, message: &Message) -> JsonRequest { - self.delete_message(message.chat.id, message.id) - } } #[cfg(test)] @@ -129,45 +264,139 @@ pub(crate) mod tests { } } + const TO_CHAT_ID: ChatId = ChatId(12345); + const FROM_CHAT_ID: ChatId = ChatId(6789); + const CHAT_ID: ChatId = ChatId(12345); + const MESSAGE_ID: MessageId = MessageId(100); + #[test] fn test_forward() { let bot = Bot::new("TOKEN"); - let to_chat_id = ChatId(12345); - let from_chat_id = ChatId(6789); - let message_id = MessageId(100); + let sugar_req = bot.forward(TO_CHAT_ID, &make_message(FROM_CHAT_ID, MESSAGE_ID)); + let real_req = bot.forward_message(TO_CHAT_ID, FROM_CHAT_ID, MESSAGE_ID); - let sugar_forward_req = bot.forward(to_chat_id, &make_message(from_chat_id, message_id)); - let real_forward_req = bot.forward_message(to_chat_id, from_chat_id, message_id); - - assert_eq!(sugar_forward_req.deref(), real_forward_req.deref()) + assert_eq!(sugar_req.deref(), real_req.deref()) } #[test] - fn test_copy() { + fn test_edit_live_location() { let bot = Bot::new("TOKEN"); - let to_chat_id = ChatId(12345); - let from_chat_id = ChatId(6789); - let message_id = MessageId(100); + let longitude = 1.0; + let latitude = 1.0; - let sugar_copy_req = bot.copy(to_chat_id, &make_message(from_chat_id, message_id)); - let real_copy_req = bot.copy_message(to_chat_id, from_chat_id, message_id); + let sugar_req = + bot.edit_live_location(&make_message(CHAT_ID, MESSAGE_ID), latitude, longitude); + let real_req = bot.edit_message_live_location(CHAT_ID, MESSAGE_ID, latitude, longitude); + assert_eq!(sugar_req.deref(), real_req.deref()) + } - assert_eq!(sugar_copy_req.deref(), real_copy_req.deref()) + #[test] + fn test_stop_live_location() { + let bot = Bot::new("TOKEN"); + + let sugar_req = bot.stop_live_location(&make_message(CHAT_ID, MESSAGE_ID)); + let real_req = bot.stop_message_live_location(CHAT_ID, MESSAGE_ID); + assert_eq!(sugar_req.deref(), real_req.deref()) + } + + #[test] + fn test_set_reaction() { + let bot = Bot::new("TOKEN"); + + let sugar_req = bot.set_reaction(&make_message(CHAT_ID, MESSAGE_ID)); + let real_req = bot.set_message_reaction(CHAT_ID, MESSAGE_ID); + assert_eq!(sugar_req.deref(), real_req.deref()) + } + + #[test] + fn test_pin() { + let bot = Bot::new("TOKEN"); + + let sugar_req = bot.pin(&make_message(CHAT_ID, MESSAGE_ID)); + let real_req = bot.pin_chat_message(CHAT_ID, MESSAGE_ID); + assert_eq!(sugar_req.deref(), real_req.deref()) + } + + #[test] + fn test_unpin() { + let bot = Bot::new("TOKEN"); + + let sugar_req = bot.unpin(&make_message(CHAT_ID, MESSAGE_ID)); + let real_req = bot.unpin_chat_message(CHAT_ID).message_id(MESSAGE_ID); + assert_eq!(sugar_req.deref(), real_req.deref()) + } + + #[test] + fn test_edit_text() { + let bot = Bot::new("TOKEN"); + + let text = "text"; + + let sugar_req = bot.edit_text(&make_message(CHAT_ID, MESSAGE_ID), text); + let real_req = bot.edit_message_text(CHAT_ID, MESSAGE_ID, text); + assert_eq!(sugar_req.deref(), real_req.deref()) + } + + #[test] + fn test_edit_caption() { + let bot = Bot::new("TOKEN"); + + let sugar_req = bot.edit_caption(&make_message(CHAT_ID, MESSAGE_ID)); + let real_req = bot.edit_message_caption(CHAT_ID, MESSAGE_ID); + assert_eq!(sugar_req.deref(), real_req.deref()) + } + + #[test] + fn test_edit_media() { + let bot = Bot::new("TOKEN"); + + let media = + InputMedia::Document(InputMediaDocument::new(InputFile::memory("Hello World!"))); + + let sugar_req = bot.edit_media(&make_message(CHAT_ID, MESSAGE_ID), media.clone()); + let real_req = bot.edit_message_media(CHAT_ID, MESSAGE_ID, media); + assert_eq!(sugar_req.deref().chat_id, real_req.deref().chat_id); + assert_eq!(sugar_req.deref().message_id, real_req.deref().message_id); + } + + #[test] + fn test_edit_reply_markup() { + let bot = Bot::new("TOKEN"); + + let sugar_req = bot.edit_reply_markup(&make_message(CHAT_ID, MESSAGE_ID)); + let real_req = bot.edit_message_reply_markup(CHAT_ID, MESSAGE_ID); + assert_eq!(sugar_req.deref(), real_req.deref()) + } + + #[test] + fn test_stop_poll_message() { + let bot = Bot::new("TOKEN"); + + let sugar_req = bot.stop_poll_message(&make_message(CHAT_ID, MESSAGE_ID)); + let real_req = bot.stop_poll(CHAT_ID, MESSAGE_ID); + assert_eq!(sugar_req.deref(), real_req.deref()) } #[test] fn test_delete() { let bot = Bot::new("TOKEN"); - let chat_id = ChatId(6789); - let message_id = MessageId(100); + let sugar_req = bot.delete(&make_message(CHAT_ID, MESSAGE_ID)); + let real_req = bot.delete_message(CHAT_ID, MESSAGE_ID); - let sugar_delete_req = bot.delete(&make_message(chat_id, message_id)); - let real_delete_req = bot.delete_message(chat_id, message_id); + assert_eq!(sugar_req.deref(), real_req.deref()) + } - assert_eq!(sugar_delete_req.deref(), real_delete_req.deref()) + #[test] + fn test_copy() { + let bot = Bot::new("TOKEN"); + + let sugar_req = bot.copy(TO_CHAT_ID, &make_message(FROM_CHAT_ID, MESSAGE_ID)); + let real_req = bot.copy_message(TO_CHAT_ID, FROM_CHAT_ID, MESSAGE_ID); + + assert_eq!(sugar_req.deref(), real_req.deref()) } #[test] From 6e484382a9f422d600807f2231d631c85288cf99 Mon Sep 17 00:00:00 2001 From: LasterAlex Date: Mon, 23 Sep 2024 02:46:09 +0300 Subject: [PATCH 18/20] Added sugar to buttons.rs example --- crates/teloxide/examples/buttons.rs | 9 +++++---- crates/teloxide/src/sugar/bot.rs | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/teloxide/examples/buttons.rs b/crates/teloxide/examples/buttons.rs index daa538c2..9f188915 100644 --- a/crates/teloxide/examples/buttons.rs +++ b/crates/teloxide/examples/buttons.rs @@ -2,6 +2,7 @@ use std::error::Error; use teloxide::{ payloads::SendMessageSetters, prelude::*, + sugar::bot::BotMessagesExt, types::{ InlineKeyboardButton, InlineKeyboardMarkup, InlineQueryResultArticle, InputMessageContent, InputMessageContentText, Me, @@ -107,17 +108,17 @@ async fn inline_query_handler( /// **IMPORTANT**: do not send privacy-sensitive data this way!!! /// Anyone can read data stored in the callback button. async fn callback_handler(bot: Bot, q: CallbackQuery) -> Result<(), Box> { - if let Some(version) = q.data { + if let Some(ref version) = q.data { let text = format!("You chose: {version}"); // Tell telegram that we've seen this query, to remove 🕑 icons from the // clients. You could also use `answer_callback_query`'s optional // parameters to tweak what happens on the client side. - bot.answer_callback_query(q.id).await?; + bot.answer_callback_query(&q.id).await?; // Edit text of the message to which the buttons were attached - if let Some(message) = q.message { - bot.edit_message_text(message.chat().id, message.id(), text).await?; + if let Some(message) = q.regular_message() { + bot.edit_text(message, text).await?; } else if let Some(id) = q.inline_message_id { bot.edit_message_text_inline(id, text).await?; } diff --git a/crates/teloxide/src/sugar/bot.rs b/crates/teloxide/src/sugar/bot.rs index bd0242f0..801cabe6 100644 --- a/crates/teloxide/src/sugar/bot.rs +++ b/crates/teloxide/src/sugar/bot.rs @@ -125,6 +125,7 @@ impl BotMessagesExt for Bot { { self.forward_message(to_chat_id, message.chat.id, message.id) } + fn edit_live_location( &self, message: &Message, From 2e7cd550575a378a4aa41a6e3d4f96f34d635d46 Mon Sep 17 00:00:00 2001 From: LasterAlex Date: Mon, 23 Sep 2024 11:41:07 +0300 Subject: [PATCH 19/20] Deleted unnecessary tests --- crates/teloxide/src/sugar/bot.rs | 219 ------------------------------- 1 file changed, 219 deletions(-) diff --git a/crates/teloxide/src/sugar/bot.rs b/crates/teloxide/src/sugar/bot.rs index 801cabe6..c1e4f0cd 100644 --- a/crates/teloxide/src/sugar/bot.rs +++ b/crates/teloxide/src/sugar/bot.rs @@ -189,222 +189,3 @@ impl BotMessagesExt for Bot { self.copy_message(to_chat_id, message.chat.id, message.id) } } - -#[cfg(test)] -pub(crate) mod tests { - use std::ops::Deref; - - use chrono::DateTime; - - use super::*; - - pub(crate) fn make_message(chat_id: ChatId, message_id: MessageId) -> Message { - let timestamp = 1_569_518_829; - let date = DateTime::from_timestamp(timestamp, 0).unwrap(); - Message { - via_bot: None, - id: message_id, - thread_id: None, - from: Some(User { - id: UserId(109_998_024), - is_bot: false, - first_name: String::from("Laster"), - last_name: None, - username: Some(String::from("laster_alex")), - language_code: Some(String::from("en")), - is_premium: false, - added_to_attachment_menu: false, - }), - sender_chat: None, - is_topic_message: false, - sender_business_bot: None, - date, - chat: Chat { - id: chat_id, - kind: ChatKind::Private(ChatPrivate { - username: Some(String::from("Laster")), - first_name: Some(String::from("laster_alex")), - last_name: None, - bio: None, - has_private_forwards: None, - has_restricted_voice_and_video_messages: None, - business_intro: None, - business_location: None, - business_opening_hours: None, - birthdate: None, - personal_chat: None, - }), - photo: None, - available_reactions: None, - pinned_message: None, - message_auto_delete_time: None, - has_hidden_members: false, - has_aggressive_anti_spam_enabled: false, - chat_full_info: ChatFullInfo::default(), - }, - kind: MessageKind::Common(MessageCommon { - reply_to_message: None, - forward_origin: None, - external_reply: None, - quote: None, - edit_date: None, - media_kind: MediaKind::Text(MediaText { - text: "text".to_owned(), - entities: vec![], - link_preview_options: None, - }), - reply_markup: None, - author_signature: None, - is_automatic_forward: false, - has_protected_content: false, - reply_to_story: None, - sender_boost_count: None, - is_from_offline: false, - business_connection_id: None, - }), - } - } - - const TO_CHAT_ID: ChatId = ChatId(12345); - const FROM_CHAT_ID: ChatId = ChatId(6789); - const CHAT_ID: ChatId = ChatId(12345); - const MESSAGE_ID: MessageId = MessageId(100); - - #[test] - fn test_forward() { - let bot = Bot::new("TOKEN"); - - let sugar_req = bot.forward(TO_CHAT_ID, &make_message(FROM_CHAT_ID, MESSAGE_ID)); - let real_req = bot.forward_message(TO_CHAT_ID, FROM_CHAT_ID, MESSAGE_ID); - - assert_eq!(sugar_req.deref(), real_req.deref()) - } - - #[test] - fn test_edit_live_location() { - let bot = Bot::new("TOKEN"); - - let longitude = 1.0; - let latitude = 1.0; - - let sugar_req = - bot.edit_live_location(&make_message(CHAT_ID, MESSAGE_ID), latitude, longitude); - let real_req = bot.edit_message_live_location(CHAT_ID, MESSAGE_ID, latitude, longitude); - assert_eq!(sugar_req.deref(), real_req.deref()) - } - - #[test] - fn test_stop_live_location() { - let bot = Bot::new("TOKEN"); - - let sugar_req = bot.stop_live_location(&make_message(CHAT_ID, MESSAGE_ID)); - let real_req = bot.stop_message_live_location(CHAT_ID, MESSAGE_ID); - assert_eq!(sugar_req.deref(), real_req.deref()) - } - - #[test] - fn test_set_reaction() { - let bot = Bot::new("TOKEN"); - - let sugar_req = bot.set_reaction(&make_message(CHAT_ID, MESSAGE_ID)); - let real_req = bot.set_message_reaction(CHAT_ID, MESSAGE_ID); - assert_eq!(sugar_req.deref(), real_req.deref()) - } - - #[test] - fn test_pin() { - let bot = Bot::new("TOKEN"); - - let sugar_req = bot.pin(&make_message(CHAT_ID, MESSAGE_ID)); - let real_req = bot.pin_chat_message(CHAT_ID, MESSAGE_ID); - assert_eq!(sugar_req.deref(), real_req.deref()) - } - - #[test] - fn test_unpin() { - let bot = Bot::new("TOKEN"); - - let sugar_req = bot.unpin(&make_message(CHAT_ID, MESSAGE_ID)); - let real_req = bot.unpin_chat_message(CHAT_ID).message_id(MESSAGE_ID); - assert_eq!(sugar_req.deref(), real_req.deref()) - } - - #[test] - fn test_edit_text() { - let bot = Bot::new("TOKEN"); - - let text = "text"; - - let sugar_req = bot.edit_text(&make_message(CHAT_ID, MESSAGE_ID), text); - let real_req = bot.edit_message_text(CHAT_ID, MESSAGE_ID, text); - assert_eq!(sugar_req.deref(), real_req.deref()) - } - - #[test] - fn test_edit_caption() { - let bot = Bot::new("TOKEN"); - - let sugar_req = bot.edit_caption(&make_message(CHAT_ID, MESSAGE_ID)); - let real_req = bot.edit_message_caption(CHAT_ID, MESSAGE_ID); - assert_eq!(sugar_req.deref(), real_req.deref()) - } - - #[test] - fn test_edit_media() { - let bot = Bot::new("TOKEN"); - - let media = - InputMedia::Document(InputMediaDocument::new(InputFile::memory("Hello World!"))); - - let sugar_req = bot.edit_media(&make_message(CHAT_ID, MESSAGE_ID), media.clone()); - let real_req = bot.edit_message_media(CHAT_ID, MESSAGE_ID, media); - assert_eq!(sugar_req.deref().chat_id, real_req.deref().chat_id); - assert_eq!(sugar_req.deref().message_id, real_req.deref().message_id); - } - - #[test] - fn test_edit_reply_markup() { - let bot = Bot::new("TOKEN"); - - let sugar_req = bot.edit_reply_markup(&make_message(CHAT_ID, MESSAGE_ID)); - let real_req = bot.edit_message_reply_markup(CHAT_ID, MESSAGE_ID); - assert_eq!(sugar_req.deref(), real_req.deref()) - } - - #[test] - fn test_stop_poll_message() { - let bot = Bot::new("TOKEN"); - - let sugar_req = bot.stop_poll_message(&make_message(CHAT_ID, MESSAGE_ID)); - let real_req = bot.stop_poll(CHAT_ID, MESSAGE_ID); - assert_eq!(sugar_req.deref(), real_req.deref()) - } - - #[test] - fn test_delete() { - let bot = Bot::new("TOKEN"); - - let sugar_req = bot.delete(&make_message(CHAT_ID, MESSAGE_ID)); - let real_req = bot.delete_message(CHAT_ID, MESSAGE_ID); - - assert_eq!(sugar_req.deref(), real_req.deref()) - } - - #[test] - fn test_copy() { - let bot = Bot::new("TOKEN"); - - let sugar_req = bot.copy(TO_CHAT_ID, &make_message(FROM_CHAT_ID, MESSAGE_ID)); - let real_req = bot.copy_message(TO_CHAT_ID, FROM_CHAT_ID, MESSAGE_ID); - - assert_eq!(sugar_req.deref(), real_req.deref()) - } - - #[test] - fn message_to_message_id() { - // Just to make sure message still can be in Into - let message = make_message(ChatId(1), MessageId(1)); - let message_id: MessageId = message.into(); - assert_eq!(message_id, MessageId(1)); - } -} From 67cba5a1d6f3ad2f8a4f195014f3f16e0425264b Mon Sep 17 00:00:00 2001 From: Tima Kinsart Date: Mon, 23 Sep 2024 16:48:27 -0400 Subject: [PATCH 20/20] Refactor `CHANGELOG.md` --- CHANGELOG.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dac415f1..ab2c37d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,15 +8,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `filter_boost_added` and `filter_reply_to_story` filters to `MessageFilterExt` trait -- Add `filter_mention_command` filter to `HandlerExt` trait ([issue #494](https://github.com/teloxide/teloxide/issues/494)) -- Add `filter_business_connection`, `filter_business_message`, `filter_edited_business_message`, and `filter_deleted_business_messages` filters to update filters ([PR 1146](https://github.com/teloxide/teloxide/pull/1146)) -- Add `bot.forward`, `bot.edit_live_location`, `bot.stop_live_location`, `bot.set_reaction`, `bot.pin`, `bot.unpin`, `bot.edit_text`, `bot.edit_caption`, `bot.edit_media`, `bot.edit_reply_markup`, `bot.stop_poll_message`, `bot.delete` and `bot.copy` to new `crate::sugar::bot::BotMessagesExt` trait ([issue #1143](https://github.com/teloxide/teloxide/issues/1143)) -- Add `.reply_to(message_id)` to new `crate::sugar::request::RequestReplyExt` trait, and `.disable_link_preview(is_disabled)` to new `crate::sugar::request::RequestLinkPreviewExt` trait ([issue #1143](https://github.com/teloxide/teloxide/issues/1143)) +- `filter_boost_added` and `filter_reply_to_story` filters to the `MessageFilterExt` trait ([PR 1131](https://github.com/teloxide/teloxide/pull/1131)) +- `filter_mention_command` filter to the `HandlerExt` trait ([issue 494](https://github.com/teloxide/teloxide/issues/494)) +- `filter_business_connection`, `filter_business_message`, `filter_edited_business_message`, and `filter_deleted_business_messages` filters to the `UpdateFilterExt` trait ([PR 1146](https://github.com/teloxide/teloxide/pull/1146)) +- Syntax sugar for making requests ([issue 1143](https://github.com/teloxide/teloxide/issues/1143)): + - `bot.forward`, `bot.edit_live_location`, `bot.stop_live_location`, `bot.set_reaction`, `bot.pin`, `bot.unpin`, `bot.edit_text`, `bot.edit_caption`, `bot.edit_media`, `bot.edit_reply_markup`, `bot.stop_poll_message`, `bot.delete` and `bot.copy` methods to the new `crate::sugar::bot::BotMessagesExt` trait + - `req.reply_to` method to the new `crate::sugar::request::RequestReplyExt` trait + - `req.disable_link_preview` method to the new `crate::sugar::request::RequestLinkPreviewExt` trait ### Changed -- Environment bumps: ([#1147][pr1147]) +- Environment bumps: ([PR 1147](https://github.com/teloxide/teloxide/pull/1147)) - MSRV (Minimal Supported Rust Version) was bumped from `1.70.0` to `1.80.0` - Some dependencies was bumped: `sqlx` to `0.8.1`, `tower` to `0.5.0`, `reqwest` to `0.12.7` - `tokio` version was explicitly specified as `1.39` @@ -25,8 +27,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Now Vec in requests serializes into [number] instead of [ {message_id: number} ], `forward_messages`, `copy_messages` and `delete_messages` now work properly -[pr1147]: https://github.com/teloxide/teloxide/pull/1147 - ## 0.13.0 - 2024-08-16 ### Added