diff --git a/lib/grammers-client/src/client/chats.rs b/lib/grammers-client/src/client/chats.rs index d9be6760..82e764bf 100644 --- a/lib/grammers-client/src/client/chats.rs +++ b/lib/grammers-client/src/client/chats.rs @@ -848,6 +848,65 @@ impl Client { Some(_) => Ok(None), } } + + /// Send a message action (such as typing, uploading photo, or viewing an emoji interaction) + /// + /// # Examples + /// + /// **Do a one-shot pulse and let it fade away** + /// ``` + /// # async fn f(chat: grammers_client::types::Chat, client: grammers_client::Client) -> Result<(), Box> { + /// use grammers_tl_types::enums::SendMessageAction; + /// + /// client + /// .action(&chat) + /// .oneshot(SendMessageAction::SendMessageTypingAction) + /// .await?; + /// # Ok(()) + /// # } + /// ``` + /// + /// **Repeat request until the future is done** + /// ``` + /// # use std::time::Duration; + /// + /// # async fn f(chat: grammers_client::types::Chat, client: grammers_client::Client) -> Result<(), Box> { + /// use grammers_tl_types as tl; + /// + /// let heavy_task = async { + /// tokio::time::sleep(Duration::from_secs(10)).await; + /// + /// 42 + /// }; + /// + /// tokio::pin!(heavy_task); + /// + /// let (task_result, _) = client + /// .action(&chat) + /// .repeat( + /// // most clients doesn't actually show progress of an action + /// || tl::types::SendMessageUploadDocumentAction { progress: 0 }, + /// heavy_task + /// ) + /// .await; + /// + /// // Note: repeat function does not cancel actions automatically, they will just fade away + /// + /// assert_eq!(task_result, 42); + /// # Ok(()) + /// # } + /// ``` + /// + /// **Cancel any actions** + /// ``` + /// # async fn f(chat: grammers_client::types::Chat, client: grammers_client::Client) -> Result<(), Box> { + /// client.action(&chat).cancel().await?; + /// # Ok(()) + /// # } + /// ``` + pub fn action>(&self, chat: C) -> crate::types::ActionSender { + crate::types::ActionSender::new(self, chat) + } } #[derive(Debug, Clone)] diff --git a/lib/grammers-client/src/client/messages.rs b/lib/grammers-client/src/client/messages.rs index c99c2881..61fbac31 100644 --- a/lib/grammers-client/src/client/messages.rs +++ b/lib/grammers-client/src/client/messages.rs @@ -16,15 +16,6 @@ use grammers_session::PackedChat; use grammers_tl_types as tl; use std::collections::HashMap; use tl::enums::InputPeer; -use tl::functions::messages::SendReaction; - -fn get_message_id(message: &tl::enums::Message) -> i32 { - match message { - tl::enums::Message::Empty(m) => m.id, - tl::enums::Message::Message(m) => m.id, - tl::enums::Message::Service(m) => m.id, - } -} fn map_random_ids_to_messages( client: &Client, @@ -151,12 +142,12 @@ impl> IterBuffer { - self.last_chunk = m.messages.is_empty() || get_message_id(&m.messages[0]) <= limit; + self.last_chunk = m.messages.is_empty() || m.messages[0].id() <= limit; self.total = Some(m.count as usize); (m.messages, m.users, m.chats, None) } @@ -1036,7 +1027,7 @@ impl Client { /// # async fn f(chat: grammers_client::types::Chat, client: grammers_client::Client) -> Result<(), Box> { /// let message_id = 123; /// - /// client.send_reaction(&chat, message_id, "👍").await?; + /// client.send_reactions(&chat, message_id, "👍").await?; /// # Ok(()) /// # } /// ``` @@ -1050,11 +1041,24 @@ impl Client { /// let message_id = 123; /// let reactions = InputReactions::emoticon("🤯").big().add_to_recent(); /// - /// client.send_reaction(&chat, message_id, reactions).await?; + /// client.send_reactions(&chat, message_id, reactions).await?; + /// # Ok(()) + /// # } + /// ``` + /// + /// Remove reactions + /// + /// ``` + /// # async fn f(chat: grammers_client::types::Chat, client: grammers_client::Client) -> Result<(), Box> { + /// use grammers_client::types::InputReactions; + /// + /// let message_id = 123; + /// + /// client.send_reactions(&chat, message_id, InputReactions::remove()).await?; /// # Ok(()) /// # } /// ``` - pub async fn send_reaction, R: Into>( + pub async fn send_reactions, R: Into>( &self, chat: C, message_id: i32, @@ -1062,7 +1066,7 @@ impl Client { ) -> Result<(), InvocationError> { let reactions = reactions.into(); - self.invoke(&SendReaction { + self.invoke(&tl::functions::messages::SendReaction { big: reactions.big, add_to_recent: reactions.add_to_recent, peer: chat.into().to_input_peer(), diff --git a/lib/grammers-client/src/types/action.rs b/lib/grammers-client/src/types/action.rs new file mode 100644 index 00000000..8e6dc276 --- /dev/null +++ b/lib/grammers-client/src/types/action.rs @@ -0,0 +1,127 @@ +use futures_util::future::Either; +use grammers_mtsender::InvocationError; +use grammers_session::PackedChat; +use grammers_tl_types as tl; +use std::future::Future; +use std::time::Duration; +use tl::enums::SendMessageAction; + +use crate::Client; + +const DEFAULT_REPEAT_DELAY: Duration = Duration::from_secs(4); + +pub struct ActionSender { + client: Client, + chat: PackedChat, + topic_id: Option, + repeat_delay: Duration, +} + +impl ActionSender { + pub fn new>(client: &Client, chat: C) -> Self { + Self { + client: client.clone(), + chat: chat.into(), + topic_id: None, + repeat_delay: DEFAULT_REPEAT_DELAY, + } + } + + /// Set custom repeat delay + pub fn repeat_delay(mut self, repeat_delay: Duration) -> Self { + self.repeat_delay = repeat_delay; + self + } + + /// Set a topic id + pub fn topic_id(mut self, topic_id: i32) -> Self { + self.topic_id = Some(topic_id); + self + } + + /// Cancel any actions + pub async fn cancel(&self) -> Result<(), InvocationError> { + self.oneshot(SendMessageAction::SendMessageCancelAction) + .await?; + + Ok(()) + } + + /// Do a one-shot set action request + pub async fn oneshot>( + &self, + action: A, + ) -> Result<(), InvocationError> { + self.client + .invoke(&tl::functions::messages::SetTyping { + peer: self.chat.to_input_peer(), + top_msg_id: self.topic_id, + action: action.into(), + }) + .await?; + + Ok(()) + } + + /// Repeat set action request until the future is done + /// + /// # Example + /// + /// ``` + /// # use std::time::Duration; + /// + /// # async fn f(chat: grammers_client::types::Chat, client: grammers_client::Client) -> Result<(), Box> { + /// use grammers_tl_types as tl; + /// + /// let heavy_task = async { + /// tokio::time::sleep(Duration::from_secs(10)).await; + /// + /// 42 + /// }; + /// + /// tokio::pin!(heavy_task); + /// + /// let (task_result, _) = client + /// .action(&chat) + /// .repeat( + /// // most clients doesn't actually show progress of an action + /// || tl::types::SendMessageUploadDocumentAction { progress: 0 }, + /// heavy_task + /// ) + /// .await; + /// + /// // Note: repeat function does not cancel actions automatically, they will just fade away + /// + /// assert_eq!(task_result, 42); + /// # Ok(()) + /// # } + /// ``` + pub async fn repeat, T>( + &self, + action: impl Fn() -> A, + mut future: impl Future + Unpin, + ) -> (T, Result<(), InvocationError>) { + let mut request_result = Ok(()); + + let future_output = loop { + if request_result.is_err() { + // Don't try to make a request again + return (future.await, request_result); + } + + let action = async { + request_result = self.oneshot(action().into()).await; + tokio::time::sleep(self.repeat_delay).await; + }; + + tokio::pin!(action); + + match futures_util::future::select(action, &mut future).await { + Either::Left((_, _)) => continue, + Either::Right((output, _)) => break output, + } + }; + + (future_output, request_result) + } +} diff --git a/lib/grammers-client/src/types/message.rs b/lib/grammers-client/src/types/message.rs index 3d6bf55f..2bd63567 100644 --- a/lib/grammers-client/src/types/message.rs +++ b/lib/grammers-client/src/types/message.rs @@ -401,12 +401,23 @@ impl Message { /// # Ok(()) /// # } /// ``` + /// + /// Remove reactions + /// + /// ``` + /// # async fn f(message: grammers_client::types::Message, client: grammers_client::Client) -> Result<(), Box> { + /// use grammers_client::types::InputReactions; + /// + /// message.react(InputReactions::remove()).await?; + /// # Ok(()) + /// # } + /// ``` pub async fn react>( &self, reactions: R, ) -> Result<(), InvocationError> { self.client - .send_reaction(self.chat(), self.id(), reactions) + .send_reactions(self.chat(), self.id(), reactions) .await?; Ok(()) } diff --git a/lib/grammers-client/src/types/mod.rs b/lib/grammers-client/src/types/mod.rs index 6cf070c8..e71c3c7d 100644 --- a/lib/grammers-client/src/types/mod.rs +++ b/lib/grammers-client/src/types/mod.rs @@ -10,6 +10,7 @@ //! //! A lot of fields in the types exported from this module are currently public even though //! they directly uses `grammers-tl-types`. This will probably change before the 1.0 release. +pub mod action; pub mod attributes; pub mod button; pub mod callback_query; @@ -34,6 +35,7 @@ pub mod reply_markup; pub mod terms_of_service; pub mod update; +pub use action::ActionSender; pub use attributes::Attribute; pub use callback_query::CallbackQuery; pub use chat::{Channel, Chat, Group, PackedChat, Platform, RestrictionReason, User}; diff --git a/lib/grammers-client/src/types/reactions.rs b/lib/grammers-client/src/types/reactions.rs index ae285e7e..67bba2b6 100644 --- a/lib/grammers-client/src/types/reactions.rs +++ b/lib/grammers-client/src/types/reactions.rs @@ -51,6 +51,11 @@ impl InputReactions { ..Self::default() } } + + /// Create an empty InputReactions which will remove reactions + pub fn remove() -> Self { + Self::default() + } } impl Default for InputReactions { @@ -86,6 +91,6 @@ impl From> for InputReactions { impl Into> for InputReactions { fn into(self) -> Vec { - return self.reactions; + self.reactions } }