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(&self) -> Option<PeerRef> {
57        self.peers.get_ref(self.peer_id()).await
58    }
59
60    /// The peer where the callback query occured, if it is in cache.
61    pub fn peer(&self) -> Option<&Peer> {
62        self.peers.get(self.peer_id())
63    }
64
65    /// The [`Self::sender`]'s identifier.
66    pub fn sender_id(&self) -> PeerId {
67        PeerId::user_unchecked(match &self.raw {
68            tl::enums::Update::BotCallbackQuery(update) => update.user_id,
69            tl::enums::Update::InlineBotCallbackQuery(update) => update.user_id,
70            _ => unreachable!(),
71        })
72    }
73
74    /// Cached reference to the [`Self::sender`], if it is in cache.
75    pub async fn sender_ref(&self) -> Option<PeerRef> {
76        self.peers.get_ref(self.sender_id()).await
77    }
78
79    /// The user who sent this callback query, if it is in cache.
80    pub fn sender(&self) -> Option<&Peer> {
81        self.peers.get(self.sender_id())
82    }
83
84    /// They binary payload data contained by the inline button which was pressed.
85    ///
86    /// This data cannot be faked by the client, since Telegram will only accept "button presses"
87    /// on data that actually existed in the buttons of the message, so you do not need to perform
88    /// any sanity checks.
89    ///
90    /// *Trivia*: it used to be possible to fake the callback data, but a server-side check was
91    /// added circa 2018 to prevent malicious clients from doing so.
92    pub fn data(&self) -> &[u8] {
93        match &self.raw {
94            tl::enums::Update::BotCallbackQuery(update) => update.data.as_deref().unwrap_or(&[]),
95            tl::enums::Update::InlineBotCallbackQuery(update) => {
96                update.data.as_deref().unwrap_or(&[])
97            }
98            _ => unreachable!(),
99        }
100    }
101
102    /// Whether the callback query was generated from an inline message.
103    pub fn is_from_inline(&self) -> bool {
104        matches!(self.raw, tl::enums::Update::InlineBotCallbackQuery(_))
105    }
106
107    /// Load the `Message` that contains the pressed inline button.
108    pub async fn load_message(&self) -> Result<Message, InvocationError> {
109        let msg_id = match &self.raw {
110            tl::enums::Update::BotCallbackQuery(update) => update.msg_id,
111            _ => return Err(InvocationError::Dropped),
112        };
113        Ok(self
114            .client
115            .get_messages_by_id(
116                self.peer_ref().await.ok_or(InvocationError::Dropped)?,
117                &[msg_id],
118            )
119            .await?
120            .pop()
121            .unwrap()
122            .unwrap())
123    }
124
125    /// Answer the callback query.
126    pub fn answer(&self) -> Answer<'_> {
127        let query_id = match &self.raw {
128            tl::enums::Update::BotCallbackQuery(update) => update.query_id,
129            tl::enums::Update::InlineBotCallbackQuery(update) => update.query_id,
130            _ => unreachable!(),
131        };
132        Answer {
133            request: tl::functions::messages::SetBotCallbackAnswer {
134                alert: false,
135                query_id,
136                message: None,
137                url: None,
138                cache_time: 0,
139            },
140            query: self,
141        }
142    }
143}
144
145impl<'a> Answer<'a> {
146    /// Configure the answer's text.
147    ///
148    /// The text will be displayed as a toast message (small popup which does not interrupt the
149    /// user and fades on its own after a short period of time).
150    pub fn text<T: Into<String>>(mut self, text: T) -> Self {
151        self.request.message = Some(text.into());
152        self.request.alert = false;
153        self
154    }
155
156    /// For how long should the answer be considered valid. It will be cached by the client for
157    /// the given duration, so subsequent callback queries with the same data will not reach the
158    /// bot.
159    pub fn cache_time(mut self, time: Duration) -> Self {
160        self.request.cache_time = time.as_secs().try_into().unwrap_or(i32::MAX);
161        self
162    }
163
164    /// Configure the answer's text.
165    ///
166    /// The text will be displayed as an alert (popup modal window with the text, which the user
167    /// needs to close before performing other actions).
168    pub fn alert<T: Into<String>>(mut self, text: T) -> Self {
169        self.request.message = Some(text.into());
170        self.request.alert = true;
171        self
172    }
173
174    /// Send the answer back to Telegram, and then relayed to the user who pressed the inline
175    /// button.
176    pub async fn send(self) -> Result<(), InvocationError> {
177        self.query.client.invoke(&self.request).await?;
178        Ok(())
179    }
180
181    /// [`Self::send`] the answer, and also edit the message that contained the button.
182    pub async fn edit<M: Into<InputMessage>>(self, new_message: M) -> Result<(), InvocationError> {
183        self.query.client.invoke(&self.request).await?;
184        let peer = self
185            .query
186            .peer_ref()
187            .await
188            .ok_or(InvocationError::Dropped)?;
189        match &self.query.raw {
190            tl::enums::Update::BotCallbackQuery(update) => {
191                self.query
192                    .client
193                    .edit_message(peer, update.msg_id, new_message)
194                    .await
195            }
196            tl::enums::Update::InlineBotCallbackQuery(update) => self
197                .query
198                .client
199                .edit_inline_message(update.msg_id.clone(), new_message.into())
200                .await
201                .map(drop),
202            _ => unreachable!(),
203        }
204    }
205
206    /// [`Self::send`] the answer, and also respond in the peer where the button was clicked.
207    pub async fn respond<M: Into<InputMessage>>(
208        self,
209        message: M,
210    ) -> Result<Message, InvocationError> {
211        self.query.client.invoke(&self.request).await?;
212        let peer = self
213            .query
214            .peer_ref()
215            .await
216            .ok_or(InvocationError::Dropped)?;
217        self.query.client.send_message(peer, message).await
218    }
219
220    /// [`Self::send`] the answer, and also reply to the message that contained the button.
221    pub async fn reply<M: Into<InputMessage>>(
222        self,
223        message: M,
224    ) -> Result<Message, InvocationError> {
225        let msg_id = match &self.query.raw {
226            tl::enums::Update::BotCallbackQuery(update) => update.msg_id,
227            _ => return Err(InvocationError::Dropped),
228        };
229        self.query.client.invoke(&self.request).await?;
230        let peer = self
231            .query
232            .peer_ref()
233            .await
234            .ok_or(InvocationError::Dropped)?;
235        let message = message.into();
236        self.query
237            .client
238            .send_message(peer, message.reply_to(Some(msg_id)))
239            .await
240    }
241}
242
243impl fmt::Debug for CallbackQuery {
244    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245        f.debug_struct("CallbackQuery")
246            .field("data", &self.data())
247            .field("sender", &self.sender())
248            .field("peer", &self.peer())
249            .finish()
250    }
251}