Skip to main content

grammers_client/update/
callback_query.rs

1// Copyright 2020 - developers of the `grammers` project.
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9use std::convert::TryInto;
10use std::fmt;
11use std::time::Duration;
12
13use grammers_mtsender::InvocationError;
14use grammers_session::types::{PeerId, PeerRef};
15use grammers_session::updates::State;
16use grammers_tl_types as tl;
17
18use crate::Client;
19use crate::message::{InputMessage, Message};
20use crate::peer::{Peer, PeerMap};
21
22/// Update that bots receive when a user presses one of the bot's inline callback buttons.
23///
24/// You should always [`CallbackQuery::answer`] these queries, even if you have no data to display
25/// to the user, because otherwise they will think the bot is non-responsive (the button spinner
26/// will timeout).
27#[derive(Clone)]
28pub struct CallbackQuery {
29    pub raw: tl::enums::Update,
30    pub state: State,
31    pub(crate) client: Client,
32    pub(crate) peers: PeerMap,
33}
34
35/// A callback query answer builder.
36///
37/// It will be executed once `.await`-ed. Modifying it after polling it once will have no effect.
38pub struct Answer<'a> {
39    query: &'a CallbackQuery,
40    request: tl::functions::messages::SetBotCallbackAnswer,
41}
42
43impl CallbackQuery {
44    /// The [`Self::peer`]'s identifier.
45    pub fn peer_id(&self) -> PeerId {
46        match &self.raw {
47            tl::enums::Update::BotCallbackQuery(update) => update.peer.clone().into(),
48            tl::enums::Update::InlineBotCallbackQuery(update) => {
49                PeerId::user_unchecked(update.user_id)
50            }
51            _ => unreachable!(),
52        }
53    }
54
55    /// Cached reference to the [`Self::peer`], if it is in cache.
56    pub async fn peer_ref(
57        &self,
58    ) -> Result<Option<PeerRef>, Box<dyn std::error::Error + Send + Sync>> {
59        self.peers.get_ref(self.peer_id()).await
60    }
61
62    /// The peer where the callback query occured, if it is in cache.
63    pub fn peer(&self) -> Option<&Peer> {
64        self.peers.get(self.peer_id())
65    }
66
67    /// The [`Self::sender`]'s identifier.
68    pub fn sender_id(&self) -> PeerId {
69        PeerId::user_unchecked(match &self.raw {
70            tl::enums::Update::BotCallbackQuery(update) => update.user_id,
71            tl::enums::Update::InlineBotCallbackQuery(update) => update.user_id,
72            _ => unreachable!(),
73        })
74    }
75
76    /// Cached reference to the [`Self::sender`], if it is in cache.
77    pub async fn sender_ref(
78        &self,
79    ) -> Result<Option<PeerRef>, Box<dyn std::error::Error + Send + Sync>> {
80        self.peers.get_ref(self.sender_id()).await
81    }
82
83    /// The user who sent this callback query, if it is in cache.
84    pub fn sender(&self) -> Option<&Peer> {
85        self.peers.get(self.sender_id())
86    }
87
88    /// They binary payload data contained by the inline button which was pressed.
89    ///
90    /// This data cannot be faked by the client, since Telegram will only accept "button presses"
91    /// on data that actually existed in the buttons of the message, so you do not need to perform
92    /// any sanity checks.
93    ///
94    /// *Trivia*: it used to be possible to fake the callback data, but a server-side check was
95    /// added circa 2018 to prevent malicious clients from doing so.
96    pub fn data(&self) -> &[u8] {
97        match &self.raw {
98            tl::enums::Update::BotCallbackQuery(update) => update.data.as_deref().unwrap_or(&[]),
99            tl::enums::Update::InlineBotCallbackQuery(update) => {
100                update.data.as_deref().unwrap_or(&[])
101            }
102            _ => unreachable!(),
103        }
104    }
105
106    /// Whether the callback query was generated from an inline message.
107    pub fn is_from_inline(&self) -> bool {
108        matches!(self.raw, tl::enums::Update::InlineBotCallbackQuery(_))
109    }
110
111    /// Load the `Message` that contains the pressed inline button.
112    pub async fn load_message(&self) -> Result<Message, InvocationError> {
113        let msg_id = match &self.raw {
114            tl::enums::Update::BotCallbackQuery(update) => update.msg_id,
115            _ => return Err(InvocationError::Dropped),
116        };
117        Ok(self
118            .client
119            .get_messages_by_id(
120                self.peer_ref().await?.ok_or(InvocationError::Dropped)?,
121                &[msg_id],
122            )
123            .await?
124            .pop()
125            .unwrap()
126            .unwrap())
127    }
128
129    /// Answer the callback query.
130    pub fn answer(&self) -> Answer<'_> {
131        let query_id = match &self.raw {
132            tl::enums::Update::BotCallbackQuery(update) => update.query_id,
133            tl::enums::Update::InlineBotCallbackQuery(update) => update.query_id,
134            _ => unreachable!(),
135        };
136        Answer {
137            request: tl::functions::messages::SetBotCallbackAnswer {
138                alert: false,
139                query_id,
140                message: None,
141                url: None,
142                cache_time: 0,
143            },
144            query: self,
145        }
146    }
147}
148
149impl<'a> Answer<'a> {
150    /// Configure the answer's text.
151    ///
152    /// The text will be displayed as a toast message (small popup which does not interrupt the
153    /// user and fades on its own after a short period of time).
154    pub fn text<T: Into<String>>(mut self, text: T) -> Self {
155        self.request.message = Some(text.into());
156        self.request.alert = false;
157        self
158    }
159
160    /// For how long should the answer be considered valid. It will be cached by the client for
161    /// the given duration, so subsequent callback queries with the same data will not reach the
162    /// bot.
163    pub fn cache_time(mut self, time: Duration) -> Self {
164        self.request.cache_time = time.as_secs().try_into().unwrap_or(i32::MAX);
165        self
166    }
167
168    /// Configure the answer's text.
169    ///
170    /// The text will be displayed as an alert (popup modal window with the text, which the user
171    /// needs to close before performing other actions).
172    pub fn alert<T: Into<String>>(mut self, text: T) -> Self {
173        self.request.message = Some(text.into());
174        self.request.alert = true;
175        self
176    }
177
178    /// Send the answer back to Telegram, and then relayed to the user who pressed the inline
179    /// button.
180    pub async fn send(self) -> Result<(), InvocationError> {
181        self.query.client.invoke(&self.request).await?;
182        Ok(())
183    }
184
185    /// [`Self::send`] the answer, and also edit the message that contained the button.
186    pub async fn edit<M: Into<InputMessage>>(self, new_message: M) -> Result<(), InvocationError> {
187        self.query.client.invoke(&self.request).await?;
188        let peer = self
189            .query
190            .peer_ref()
191            .await?
192            .ok_or(InvocationError::Dropped)?;
193        match &self.query.raw {
194            tl::enums::Update::BotCallbackQuery(update) => {
195                self.query
196                    .client
197                    .edit_message(peer, update.msg_id, new_message)
198                    .await
199            }
200            tl::enums::Update::InlineBotCallbackQuery(update) => self
201                .query
202                .client
203                .edit_inline_message(update.msg_id.clone(), new_message.into())
204                .await
205                .map(drop),
206            _ => unreachable!(),
207        }
208    }
209
210    /// [`Self::send`] the answer, and also respond in the peer where the button was clicked.
211    pub async fn respond<M: Into<InputMessage>>(
212        self,
213        message: M,
214    ) -> Result<Message, InvocationError> {
215        self.query.client.invoke(&self.request).await?;
216        let peer = self
217            .query
218            .peer_ref()
219            .await?
220            .ok_or(InvocationError::Dropped)?;
221        self.query.client.send_message(peer, message).await
222    }
223
224    /// [`Self::send`] the answer, and also reply to the message that contained the button.
225    pub async fn reply<M: Into<InputMessage>>(
226        self,
227        message: M,
228    ) -> Result<Message, InvocationError> {
229        let msg_id = match &self.query.raw {
230            tl::enums::Update::BotCallbackQuery(update) => update.msg_id,
231            _ => return Err(InvocationError::Dropped),
232        };
233        self.query.client.invoke(&self.request).await?;
234        let peer = self
235            .query
236            .peer_ref()
237            .await?
238            .ok_or(InvocationError::Dropped)?;
239        let message = message.into();
240        self.query
241            .client
242            .send_message(peer, message.reply_to(Some(msg_id)))
243            .await
244    }
245}
246
247impl fmt::Debug for CallbackQuery {
248    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249        f.debug_struct("CallbackQuery")
250            .field("data", &self.data())
251            .field("sender", &self.sender())
252            .field("peer", &self.peer())
253            .finish()
254    }
255}