Skip to main content

layer_client/
keyboard.rs

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