diff --git a/CHANGELOG.md b/CHANGELOG.md index e9a314e6..ab2c37d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +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)) +- `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` @@ -23,8 +27,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Now Vec<MessageId> 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 diff --git a/crates/teloxide-core/src/types/message.rs b/crates/teloxide-core/src/types/message.rs index 2b2ce6e2..2da52dcf 100644 --- a/crates/teloxide-core/src/types/message.rs +++ b/crates/teloxide-core/src/types/message.rs @@ -1816,6 +1816,13 @@ impl Message { } } +/// Implemented for syntax sugar, see issue <https://github.com/teloxide/teloxide/issues/1143> +impl From<Message> for MessageId { + fn from(message: Message) -> MessageId { + message.id + } +} + #[cfg(test)] mod tests { use chrono::DateTime; 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<dyn Error + Send + Sync>> { - 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/examples/dispatching_features.rs b/crates/teloxide/examples/dispatching_features.rs index bcc65a2e..8b327b55 100644 --- a/crates/teloxide/examples/dispatching_features.rs +++ b/crates/teloxide/examples/dispatching_features.rs @@ -4,9 +4,7 @@ use rand::Rng; use teloxide::{ - dispatching::HandlerExt, - prelude::*, - types::{Dice, ReplyParameters}, + dispatching::HandlerExt, prelude::*, sugar::request::RequestReplyExt, types::Dice, utils::command::BotCommands, }; @@ -84,7 +82,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(()) }), 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..0e8bb856 --- /dev/null +++ b/crates/teloxide/src/sugar.rs @@ -0,0 +1,4 @@ +//! 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 new file mode 100644 index 00000000..c1e4f0cd --- /dev/null +++ b/crates/teloxide/src/sugar/bot.rs @@ -0,0 +1,191 @@ +//! Additions to [`Bot`]. +//! +//! [`Bot`]: crate::Bot +use crate::{prelude::*, types::*}; +use teloxide_core::{ + payloads::*, + requests::{JsonRequest, MultipartRequest}, +}; + +/// Syntax sugar for [`Message`] manipulations. +/// +/// [`Message`]: crate::types::Message +pub trait BotMessagesExt { + /// This function is the same as [`Bot::forward_message`], + /// but can take in [`Message`] to forward it. + /// + /// [`Bot::forward_message`]: crate::Bot::forward_message + /// [`Message`]: crate::types::Message + fn forward<C>(&self, to_chat_id: C, message: &Message) -> JsonRequest<ForwardMessage> + where + C: Into<Recipient>; + + /// 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<EditMessageLiveLocation>; + + /// 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<StopMessageLiveLocation>; + + /// 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<SetMessageReaction>; + + /// 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<PinChatMessage>; + + /// 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<UnpinChatMessage>; + + /// 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<T>(&self, message: &Message, text: T) -> JsonRequest<EditMessageText> + where + T: Into<String>; + + /// 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<EditMessageCaption>; + + /// 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<EditMessageMedia>; + + /// 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<EditMessageReplyMarkup>; + + /// 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<StopPoll>; + + /// 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<DeleteMessage>; + + /// This function is the same as [`Bot::copy_message`], + /// but can take in [`Message`] to copy it. + /// + /// [`Bot::copy_messages`]: crate::Bot::copy_message + /// [`Message`]: crate::types::Message + fn copy<C>(&self, to_chat_id: C, message: &Message) -> JsonRequest<CopyMessage> + where + C: Into<Recipient>; +} + +impl BotMessagesExt for Bot { + fn forward<C>(&self, to_chat_id: C, message: &Message) -> JsonRequest<ForwardMessage> + where + C: Into<Recipient>, + { + self.forward_message(to_chat_id, message.chat.id, message.id) + } + + fn edit_live_location( + &self, + message: &Message, + latitude: f64, + longitude: f64, + ) -> JsonRequest<EditMessageLiveLocation> { + self.edit_message_live_location(message.chat.id, message.id, latitude, longitude) + } + + fn stop_live_location(&self, message: &Message) -> JsonRequest<StopMessageLiveLocation> { + self.stop_message_live_location(message.chat.id, message.id) + } + + fn set_reaction(&self, message: &Message) -> JsonRequest<SetMessageReaction> { + self.set_message_reaction(message.chat.id, message.id) + } + + fn pin(&self, message: &Message) -> JsonRequest<PinChatMessage> { + self.pin_chat_message(message.chat.id, message.id) + } + + fn unpin(&self, message: &Message) -> JsonRequest<UnpinChatMessage> { + self.unpin_chat_message(message.chat.id).message_id(message.id) + } + + fn edit_text<T>(&self, message: &Message, text: T) -> JsonRequest<EditMessageText> + where + T: Into<String>, + { + self.edit_message_text(message.chat.id, message.id, text) + } + + fn edit_caption(&self, message: &Message) -> JsonRequest<EditMessageCaption> { + self.edit_message_caption(message.chat.id, message.id) + } + + fn edit_media( + &self, + message: &Message, + media: InputMedia, + ) -> MultipartRequest<EditMessageMedia> { + self.edit_message_media(message.chat.id, message.id, media) + } + + fn edit_reply_markup(&self, message: &Message) -> JsonRequest<EditMessageReplyMarkup> { + self.edit_message_reply_markup(message.chat.id, message.id) + } + + fn stop_poll_message(&self, message: &Message) -> JsonRequest<StopPoll> { + self.stop_poll(message.chat.id, message.id) + } + + fn delete(&self, message: &Message) -> JsonRequest<DeleteMessage> { + self.delete_message(message.chat.id, message.id) + } + + fn copy<C>(&self, to_chat_id: C, message: &Message) -> JsonRequest<CopyMessage> + where + C: Into<Recipient>, + { + self.copy_message(to_chat_id, message.chat.id, message.id) + } +} diff --git a/crates/teloxide/src/sugar/request.rs b/crates/teloxide/src/sugar/request.rs new file mode 100644 index 00000000..59ca41b0 --- /dev/null +++ b/crates/teloxide/src/sugar/request.rs @@ -0,0 +1,155 @@ +//! Additions to [`JsonRequest`] and [`MultipartRequest`]. +//! +//! [`JsonRequest`]: teloxide_core::requests::JsonRequest +//! [`MultipartRequest`]: teloxide_core::requests::MultipartRequest + +use teloxide_core::{ + payloads::*, + requests::{JsonRequest, MultipartRequest}, + types::*, +}; + +macro_rules! impl_request_reply_ext { + ($($t:ty),*) => { + $( + impl RequestReplyExt for $t { + fn reply_to<M>(self, message_id: M) -> Self + where + M: Into<MessageId>, + Self: Sized, + { + self.reply_parameters(ReplyParameters::new(message_id.into())) + } + } + )* + }; +} + +macro_rules! impl_request_link_preview_ext { + ($($t:ty),*) => { + $( + impl RequestLinkPreviewExt for $t { + fn disable_link_preview(self, is_disabled: bool) -> Self + where + Self: Sized + { + let link_preview_options = LinkPreviewOptions { + is_disabled, + url: None, + prefer_small_media: false, + prefer_large_media: false, + show_above_text: false, + }; + self.link_preview_options(link_preview_options) + } + } + )* + }; +} + +/// `.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)` + fn reply_to<M>(self, message_id: M) -> Self + where + M: Into<MessageId>, + Self: Sized; +} + +/// `.disable_link_preview(is_disabled)` syntax sugar for requests. +pub trait RequestLinkPreviewExt { + /// 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! { + JsonRequest<SendDice>, + JsonRequest<SendInvoice>, + JsonRequest<SendPoll>, + JsonRequest<SendContact>, + JsonRequest<SendGame>, + JsonRequest<SendVenue>, + JsonRequest<SendLocation>, + JsonRequest<CopyMessage>, + JsonRequest<SendMessage>, + MultipartRequest<SendSticker>, + MultipartRequest<SendMediaGroup>, + MultipartRequest<SendAnimation>, + MultipartRequest<SendVideoNote>, + MultipartRequest<SendVideo>, + MultipartRequest<SendDocument>, + MultipartRequest<SendAudio>, + MultipartRequest<SendVoice>, + MultipartRequest<SendPhoto> +} + +impl_request_link_preview_ext! { + JsonRequest<SendMessage>, + JsonRequest<EditMessageText> +} + +#[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()) + } + + #[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 { + 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(true); + + assert_eq!(real_link_req.deref(), sugar_link_req.deref()) + } +}