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)); + } +}