Skip to main content

layer_client/
keyboard.rs

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