Skip to main content

ferogram/
keyboard.rs

1// Copyright (c) Ankit Chaubey <ankitchaubey.dev@gmail.com>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3//
4// ferogram: async Telegram MTProto client in Rust
5// https://github.com/ankit-chaubey/ferogram
6//
7// Based on layer: https://github.com/ankit-chaubey/layer
8// Follows official Telegram client behaviour (tdesktop, TDLib).
9//
10// If you use or modify this code, keep this notice at the top of your file
11// and include the LICENSE-MIT or LICENSE-APACHE file from this repository:
12// https://github.com/ankit-chaubey/ferogram
13
14//! Inline keyboard builder: create reply markups without raw TL verbosity.
15//!
16//! # Example
17//! ```rust,no_run
18//! use ferogram::keyboard::{InlineKeyboard, Button};
19//!
20//! let kb = InlineKeyboard::new()
21//! .row([Button::callback("✅ Yes", b"yes"),
22//!       Button::callback("❌ No",  b"no")])
23//! .row([Button::url("📖 Docs", "https://docs.rs/ferogram")]);
24//!
25//! // Pass to InputMessage:
26//! // let msg = InputMessage::text("Choose:").keyboard(kb);
27//! ```
28
29use ferogram_tl_types as tl;
30
31// Button
32
33/// A single inline keyboard button.
34#[derive(Clone)]
35pub struct Button {
36    inner: tl::enums::KeyboardButton,
37}
38
39impl Button {
40    /// A button that sends a callback data payload when pressed.
41    pub fn callback(text: impl Into<String>, data: impl Into<Vec<u8>>) -> Self {
42        Self {
43            inner: tl::enums::KeyboardButton::Callback(tl::types::KeyboardButtonCallback {
44                requires_password: false,
45                text: text.into(),
46                data: data.into(),
47                style: None,
48            }),
49        }
50    }
51
52    /// A button that opens a URL in the browser.
53    pub fn url(text: impl Into<String>, url: impl Into<String>) -> Self {
54        Self {
55            inner: tl::enums::KeyboardButton::Url(tl::types::KeyboardButtonUrl {
56                text: text.into(),
57                url: url.into(),
58                style: None,
59            }),
60        }
61    }
62
63    /// A button that opens a user-profile or bot link in Telegram.
64    pub fn url_auth(
65        text: impl Into<String>,
66        url: impl Into<String>,
67        fwd_text: Option<String>,
68        bot: tl::enums::InputUser,
69    ) -> Self {
70        Self {
71            inner: tl::enums::KeyboardButton::InputKeyboardButtonUrlAuth(
72                tl::types::InputKeyboardButtonUrlAuth {
73                    request_write_access: false,
74                    text: text.into(),
75                    fwd_text,
76                    url: url.into(),
77                    bot,
78                    style: None,
79                },
80            ),
81        }
82    }
83
84    /// A button that switches to inline mode in the current chat.
85    pub fn switch_inline(text: impl Into<String>, query: impl Into<String>) -> Self {
86        Self {
87            inner: tl::enums::KeyboardButton::SwitchInline(tl::types::KeyboardButtonSwitchInline {
88                same_peer: true,
89                peer_types: None,
90                text: text.into(),
91                query: query.into(),
92                style: None,
93            }),
94        }
95    }
96
97    /// A plain text button (for reply keyboards, not inline).
98    pub fn text(label: impl Into<String>) -> Self {
99        Self {
100            inner: tl::enums::KeyboardButton::KeyboardButton(tl::types::KeyboardButton {
101                text: label.into(),
102                style: None,
103            }),
104        }
105    }
106
107    /// A button that switches to inline mode in a different (user-chosen) chat.
108    pub fn switch_elsewhere(text: impl Into<String>, query: impl Into<String>) -> Self {
109        Self {
110            inner: tl::enums::KeyboardButton::SwitchInline(tl::types::KeyboardButtonSwitchInline {
111                same_peer: false,
112                peer_types: None,
113                text: text.into(),
114                query: query.into(),
115                style: None,
116            }),
117        }
118    }
119
120    /// A button that opens a mini-app WebView.
121    pub fn webview(text: impl Into<String>, url: impl Into<String>) -> Self {
122        Self {
123            inner: tl::enums::KeyboardButton::WebView(tl::types::KeyboardButtonWebView {
124                text: text.into(),
125                url: url.into(),
126                style: None,
127            }),
128        }
129    }
130
131    /// A button that opens a simple WebView (no JS bridge).
132    pub fn simple_webview(text: impl Into<String>, url: impl Into<String>) -> Self {
133        Self {
134            inner: tl::enums::KeyboardButton::SimpleWebView(
135                tl::types::KeyboardButtonSimpleWebView {
136                    text: text.into(),
137                    url: url.into(),
138                    style: None,
139                },
140            ),
141        }
142    }
143
144    /// A button that requests the user's phone number (reply keyboards only).
145    pub fn request_phone(text: impl Into<String>) -> Self {
146        Self {
147            inner: tl::enums::KeyboardButton::RequestPhone(tl::types::KeyboardButtonRequestPhone {
148                text: text.into(),
149                style: None,
150            }),
151        }
152    }
153
154    /// A button that requests the user's location (reply keyboards only).
155    pub fn request_geo(text: impl Into<String>) -> Self {
156        Self {
157            inner: tl::enums::KeyboardButton::RequestGeoLocation(
158                tl::types::KeyboardButtonRequestGeoLocation {
159                    text: text.into(),
160                    style: None,
161                },
162            ),
163        }
164    }
165
166    /// A button that requests the user to create/share a poll.
167    pub fn request_poll(text: impl Into<String>) -> Self {
168        Self {
169            inner: tl::enums::KeyboardButton::RequestPoll(tl::types::KeyboardButtonRequestPoll {
170                quiz: None,
171                text: text.into(),
172                style: None,
173            }),
174        }
175    }
176
177    /// A button that requests the user to create/share a quiz.
178    pub fn request_quiz(text: impl Into<String>) -> Self {
179        Self {
180            inner: tl::enums::KeyboardButton::RequestPoll(tl::types::KeyboardButtonRequestPoll {
181                quiz: Some(true),
182                text: text.into(),
183                style: None,
184            }),
185        }
186    }
187
188    /// A button that launches a game (bots only).
189    pub fn game(text: impl Into<String>) -> Self {
190        Self {
191            inner: tl::enums::KeyboardButton::Game(tl::types::KeyboardButtonGame {
192                text: text.into(),
193                style: None,
194            }),
195        }
196    }
197
198    /// A buy button for payments (bots only).
199    pub fn buy(text: impl Into<String>) -> Self {
200        Self {
201            inner: tl::enums::KeyboardButton::Buy(tl::types::KeyboardButtonBuy {
202                text: text.into(),
203                style: None,
204            }),
205        }
206    }
207
208    /// A copy-to-clipboard button.
209    pub fn copy_text(text: impl Into<String>, copy_text: impl Into<String>) -> Self {
210        Self {
211            inner: tl::enums::KeyboardButton::Copy(tl::types::KeyboardButtonCopy {
212                text: text.into(),
213                copy_text: copy_text.into(),
214                style: None,
215            }),
216        }
217    }
218
219    /// Consume into the raw TL type.
220    pub fn into_raw(self) -> tl::enums::KeyboardButton {
221        self.inner
222    }
223}
224
225// InlineKeyboard
226
227/// Builder for an inline keyboard reply markup.
228///
229/// Each call to [`row`](InlineKeyboard::row) adds a new horizontal row of
230/// buttons. Rows are displayed top-to-bottom.
231///
232/// # Example
233/// ```rust,no_run
234/// use ferogram::keyboard::{InlineKeyboard, Button};
235///
236/// let kb = InlineKeyboard::new()
237/// .row([Button::callback("Option A", b"a"),
238///       Button::callback("Option B", b"b")])
239/// .row([Button::url("More info", "https://example.com")]);
240/// ```
241#[derive(Clone, Default)]
242pub struct InlineKeyboard {
243    rows: Vec<Vec<Button>>,
244}
245
246impl InlineKeyboard {
247    /// Create an empty keyboard. Add rows with [`row`](Self::row).
248    pub fn new() -> Self {
249        Self::default()
250    }
251
252    /// Append a row of buttons.
253    pub fn row(mut self, buttons: impl IntoIterator<Item = Button>) -> Self {
254        self.rows.push(buttons.into_iter().collect());
255        self
256    }
257
258    /// Convert to the `ReplyMarkup` TL type expected by message-sending functions.
259    pub fn into_markup(self) -> tl::enums::ReplyMarkup {
260        let rows = self
261            .rows
262            .into_iter()
263            .map(|row| {
264                tl::enums::KeyboardButtonRow::KeyboardButtonRow(tl::types::KeyboardButtonRow {
265                    buttons: row.into_iter().map(Button::into_raw).collect(),
266                })
267            })
268            .collect();
269
270        tl::enums::ReplyMarkup::ReplyInlineMarkup(tl::types::ReplyInlineMarkup { rows })
271    }
272}
273
274impl From<InlineKeyboard> for tl::enums::ReplyMarkup {
275    fn from(kb: InlineKeyboard) -> Self {
276        kb.into_markup()
277    }
278}
279
280// ReplyKeyboard
281
282/// Builder for a reply keyboard (shown below the message input box).
283#[derive(Clone, Default)]
284pub struct ReplyKeyboard {
285    rows: Vec<Vec<Button>>,
286    resize: bool,
287    single_use: bool,
288    selective: bool,
289}
290
291impl ReplyKeyboard {
292    /// Create a new empty reply keyboard.
293    pub fn new() -> Self {
294        Self::default()
295    }
296
297    /// Append a row of text buttons.
298    pub fn row(mut self, buttons: impl IntoIterator<Item = Button>) -> Self {
299        self.rows.push(buttons.into_iter().collect());
300        self
301    }
302
303    /// Resize keyboard to fit its content (recommended).
304    pub fn resize(mut self) -> Self {
305        self.resize = true;
306        self
307    }
308
309    /// Hide keyboard after a single press.
310    pub fn single_use(mut self) -> Self {
311        self.single_use = true;
312        self
313    }
314
315    /// Show keyboard only to mentioned/replied users.
316    pub fn selective(mut self) -> Self {
317        self.selective = true;
318        self
319    }
320
321    /// Convert to `ReplyMarkup`.
322    pub fn into_markup(self) -> tl::enums::ReplyMarkup {
323        let rows = self
324            .rows
325            .into_iter()
326            .map(|row| {
327                tl::enums::KeyboardButtonRow::KeyboardButtonRow(tl::types::KeyboardButtonRow {
328                    buttons: row.into_iter().map(Button::into_raw).collect(),
329                })
330            })
331            .collect();
332
333        tl::enums::ReplyMarkup::ReplyKeyboardMarkup(tl::types::ReplyKeyboardMarkup {
334            resize: self.resize,
335            single_use: self.single_use,
336            selective: self.selective,
337            persistent: false,
338            rows,
339            placeholder: None,
340        })
341    }
342}
343
344impl From<ReplyKeyboard> for tl::enums::ReplyMarkup {
345    fn from(kb: ReplyKeyboard) -> Self {
346        kb.into_markup()
347    }
348}