1use std::future::Future;
2use std::sync::Arc;
3use std::time::Duration;
4
5use botkit_core::{
6 Bot, BotBuilder, BotError, Context, ContextData, Event, IntoHandler, Response, Shutdown,
7};
8use executor_core::spawn;
9use http_kit::{Body, Endpoint, HttpError, Request, Response as HttpResponse, StatusCode};
10use tracing::{error, warn};
11
12use crate::client::TelegramClient;
13use crate::event::TelegramContextData;
14use crate::types::{
15 BotCommand, InlineKeyboardButton, InlineKeyboardMarkup, ReplyMarkup, Update, UpdateKind,
16};
17
18const POLL_TIMEOUT_SECS: u32 = 30;
20const INITIAL_POLL_BACKOFF: Duration = Duration::from_secs(1);
22const MAX_POLL_BACKOFF: Duration = Duration::from_secs(60);
24
25#[derive(Debug)]
27pub struct WebhookError(BotError);
28
29impl WebhookError {
30 pub fn into_inner(self) -> BotError {
32 self.0
33 }
34}
35
36impl std::fmt::Display for WebhookError {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 write!(f, "{}", self.0)
39 }
40}
41
42impl std::error::Error for WebhookError {}
43
44impl HttpError for WebhookError {
45 fn status(&self) -> StatusCode {
46 StatusCode::INTERNAL_SERVER_ERROR
47 }
48}
49
50pub struct TelegramBot {
80 token: String,
81 builder: BotBuilder,
82 register_commands: bool,
83}
84
85impl TelegramBot {
86 pub fn new(token: impl Into<String>) -> Self {
88 Self {
89 token: token.into(),
90 builder: BotBuilder::new(),
91 register_commands: true,
92 }
93 }
94
95 pub fn command<H, Args>(mut self, name: impl Into<String>, handler: H) -> Self
97 where
98 H: IntoHandler<Args>,
99 {
100 self.builder = self.builder.command(name, handler);
101 self
102 }
103
104 pub fn command_with_description<H, Args>(
108 mut self,
109 name: impl Into<String>,
110 description: impl Into<String>,
111 handler: H,
112 ) -> Self
113 where
114 H: IntoHandler<Args>,
115 {
116 self.builder = self
117 .builder
118 .command_with_description(name, description, handler);
119 self
120 }
121
122 pub fn button<H, Args>(mut self, pattern: impl Into<String>, handler: H) -> Self
126 where
127 H: IntoHandler<Args>,
128 {
129 self.builder = self.builder.button(pattern, handler);
130 self
131 }
132
133 pub fn message<H, Args>(mut self, handler: H) -> Self
135 where
136 H: IntoHandler<Args>,
137 {
138 self.builder = self.builder.message(handler);
139 self
140 }
141
142 pub fn fallback<H, Args>(mut self, handler: H) -> Self
147 where
148 H: IntoHandler<Args>,
149 {
150 self.builder = self.builder.fallback(handler);
151 self
152 }
153
154 pub fn skip_command_registration(mut self) -> Self {
159 self.register_commands = false;
160 self
161 }
162
163 pub fn build(self) -> TelegramWebhook {
169 TelegramWebhook {
170 dispatcher: Arc::new(Dispatcher {
171 client: TelegramClient::new(&self.token),
172 builder: self.builder,
173 }),
174 }
175 }
176
177 pub fn commands(&self) -> Vec<BotCommand> {
179 self.builder
180 .commands()
181 .map(|command| {
182 let description = if command.description.is_empty() {
184 command.name
185 } else {
186 command.description
187 };
188 BotCommand::new(command.name, description)
189 })
190 .collect()
191 }
192}
193
194impl Bot for TelegramBot {
195 async fn run_until(self, shutdown: Shutdown) -> Result<(), BotError> {
196 let client = TelegramClient::new(&self.token);
197
198 if self.register_commands {
199 let commands = self.commands();
200 if !commands.is_empty()
201 && let Err(e) = client.set_my_commands(&commands).await
202 {
203 warn!("Failed to register commands: {e}");
204 }
205 }
206
207 client.delete_webhook().await?;
209
210 let dispatcher = Arc::new(Dispatcher {
211 client,
212 builder: self.builder,
213 });
214
215 poll_updates(dispatcher, shutdown).await
216 }
217}
218
219async fn poll_updates(dispatcher: Arc<Dispatcher>, shutdown: Shutdown) -> Result<(), BotError> {
220 let mut offset: Option<i64> = None;
221 let mut backoff = INITIAL_POLL_BACKOFF;
222
223 loop {
224 if shutdown.is_shutdown() {
225 return Ok(());
226 }
227
228 let poll = dispatcher
229 .client
230 .get_updates(offset, Some(POLL_TIMEOUT_SECS));
231
232 let updates = match race_shutdown(poll, &shutdown).await {
233 None => return Ok(()),
234 Some(Ok(updates)) => {
235 backoff = INITIAL_POLL_BACKOFF;
236 updates
237 }
238 Some(Err(e)) => {
239 error!("Error fetching updates: {e}");
242 if race_shutdown(sleep(backoff), &shutdown).await.is_none() {
243 return Ok(());
244 }
245 backoff = (backoff * 2).min(MAX_POLL_BACKOFF);
246 continue;
247 }
248 };
249
250 for update in updates {
251 offset = Some(update.update_id + 1);
254
255 if let Err(e) = dispatcher.dispatch(update).await {
256 error!("Error handling update: {e}");
257 }
258 }
259 }
260}
261
262struct Dispatcher {
266 client: TelegramClient,
267 builder: BotBuilder,
268}
269
270impl Dispatcher {
271 async fn dispatch(&self, update: Update) -> Result<(), BotError> {
273 let Some((data, handler)) = self.prepare(update).await? else {
274 return Ok(());
275 };
276
277 let chat_id = data.chat_id();
278 let thread_id = data.thread_id();
279 let response = handler.call(Context::new(data)).await;
280 send_response(&self.client, chat_id, thread_id, response).await
281 }
282
283 async fn dispatch_detached(self: &Arc<Self>, update: Update) -> Result<(), BotError> {
285 let Some((data, handler)) = self.prepare(update).await? else {
286 return Ok(());
287 };
288
289 let this = Arc::clone(self);
290 spawn(async move {
291 let chat_id = data.chat_id();
292 let thread_id = data.thread_id();
293 let response = handler.call(Context::new(data)).await;
294 if let Err(e) = send_response(&this.client, chat_id, thread_id, response).await {
295 error!("Telegram response error: {e}");
296 }
297 })
298 .detach();
299
300 Ok(())
301 }
302
303 async fn prepare(
306 &self,
307 update: Update,
308 ) -> Result<Option<(TelegramContextData, botkit_core::AnyHandler)>, BotError> {
309 if let UpdateKind::CallbackQuery(callback_query) = &update.kind
313 && let Err(e) = self
314 .client
315 .answer_callback_query(&callback_query.id, None, false)
316 .await
317 {
318 warn!("Failed to answer callback query: {e}");
319 }
320
321 if !matches!(
326 update.kind,
327 UpdateKind::Message(_)
328 | UpdateKind::EditedMessage(_)
329 | UpdateKind::CallbackQuery(_)
330 | UpdateKind::MessageReaction(_)
331 ) {
332 return Ok(None);
333 }
334
335 let is_callback = matches!(update.kind, UpdateKind::CallbackQuery(_));
336 let data = TelegramContextData::new(update, self.client.clone());
337
338 let event = match (is_callback, data.command_name(), data.button_id()) {
339 (true, _, Some(button)) => Event::Button(button),
342 (true, _, None) => return Ok(None),
343 (false, Some(command), _) => Event::Command(command),
344 (false, None, _) => Event::Message,
345 };
346
347 Ok(self
348 .builder
349 .route(event)
350 .cloned()
351 .map(|handler| (data, handler)))
352 }
353}
354
355#[derive(Clone)]
359pub struct TelegramWebhook {
360 dispatcher: Arc<Dispatcher>,
361}
362
363impl TelegramWebhook {
364 pub fn client(&self) -> &TelegramClient {
366 &self.dispatcher.client
367 }
368
369 pub async fn handle(&self, update: Update) -> Result<(), BotError> {
374 self.dispatcher.dispatch_detached(update).await
375 }
376}
377
378impl Endpoint for TelegramWebhook {
379 type Error = WebhookError;
380
381 async fn respond(&mut self, request: &mut Request) -> Result<HttpResponse, Self::Error> {
382 let update: Update = request
383 .body_mut()
384 .into_json()
385 .await
386 .map_err(|e| WebhookError(BotError::Other(e.to_string())))?;
387
388 self.handle(update).await.map_err(WebhookError)?;
389
390 Ok(HttpResponse::new(Body::from_bytes("OK")))
391 }
392}
393
394async fn send_response(
395 client: &TelegramClient,
396 chat_id: Option<i64>,
397 thread_id: Option<i64>,
398 mut response: Response,
399) -> Result<(), BotError> {
400 if response.is_empty() || response.is_acknowledge() {
401 return Ok(());
402 }
403
404 let Some(chat_id) = chat_id else {
406 return Ok(());
407 };
408
409 if let Some(file) = response.take_file() {
410 let _ = client
411 .send_chat_action(chat_id, "upload_document", thread_id)
412 .await;
413
414 return client
415 .send_document(
416 chat_id,
417 file.file,
418 file.filename.as_deref(),
419 file.caption.as_deref(),
420 thread_id,
421 )
422 .await
423 .map(|_| ());
424 }
425
426 let content = response.content().unwrap_or("");
427 if content.is_empty() {
428 return Ok(());
429 }
430
431 client
432 .send_message(chat_id, content, thread_id, build_reply_markup(&response))
433 .await?;
434 Ok(())
435}
436
437fn build_reply_markup(response: &Response) -> Option<ReplyMarkup> {
442 use botkit_core::types::component::{Button, Component};
443
444 fn to_button(button: &Button) -> Option<InlineKeyboardButton> {
445 match (&button.url, &button.custom_id) {
446 (Some(url), _) => Some(InlineKeyboardButton::url(&button.label, url)),
447 (None, Some(custom_id)) => {
448 Some(InlineKeyboardButton::callback(&button.label, custom_id))
449 }
450 (None, None) => None,
451 }
452 }
453
454 let mut rows: Vec<Vec<InlineKeyboardButton>> = Vec::new();
455
456 for component in response.components() {
457 match component {
458 Component::ActionRow(action_row) => {
459 let row: Vec<_> = action_row
460 .components
461 .iter()
462 .filter_map(|c| match c {
463 Component::Button(button) => to_button(button),
464 _ => None,
465 })
466 .collect();
467
468 if !row.is_empty() {
469 rows.push(row);
470 }
471 }
472 Component::Button(button) => rows.extend(to_button(button).map(|b| vec![b])),
473 Component::SelectMenu(_) => {}
474 }
475 }
476
477 (!rows.is_empty()).then_some(ReplyMarkup::InlineKeyboard(InlineKeyboardMarkup {
478 inline_keyboard: rows,
479 }))
480}
481
482async fn race_shutdown<F: Future>(future: F, shutdown: &Shutdown) -> Option<F::Output> {
484 futures_lite::future::or(async { Some(future.await) }, async {
485 shutdown.wait().await;
486 None
487 })
488 .await
489}
490
491async fn sleep(duration: Duration) {
492 async_io::Timer::after(duration).await;
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498 use botkit_core::types::component::{ActionRow, Button, Component, SelectMenu, SelectOption};
499
500 fn routed_event(update: &Update) -> Option<&'static str> {
502 if !matches!(
503 update.kind,
504 UpdateKind::Message(_)
505 | UpdateKind::EditedMessage(_)
506 | UpdateKind::CallbackQuery(_)
507 | UpdateKind::MessageReaction(_)
508 ) {
509 return None;
510 }
511
512 let is_callback = matches!(update.kind, UpdateKind::CallbackQuery(_));
513 let data = TelegramContextData::new(update.clone(), TelegramClient::new("t"));
514
515 match (is_callback, data.command_name(), data.button_id()) {
516 (true, _, Some(_)) => Some("button"),
517 (true, _, None) => None,
518 (false, Some(_), _) => Some("command"),
519 (false, None, _) => Some("message"),
520 }
521 }
522
523 fn update(json: serde_json::Value) -> Update {
524 serde_json::from_value(json).expect("update parses")
525 }
526
527 #[test]
528 fn commands_messages_and_buttons_route_to_distinct_events() {
529 let command = update(serde_json::json!({
530 "update_id": 1,
531 "message": {
532 "message_id": 1, "date": 0,
533 "chat": { "id": 1, "type": "private" },
534 "text": "/ping",
535 "entities": [{ "type": "bot_command", "offset": 0, "length": 5 }]
536 }
537 }));
538 assert_eq!(routed_event(&command), Some("command"));
539
540 let message = update(serde_json::json!({
541 "update_id": 2,
542 "message": {
543 "message_id": 1, "date": 0,
544 "chat": { "id": 1, "type": "private" },
545 "text": "just chatting"
546 }
547 }));
548 assert_eq!(routed_event(&message), Some("message"));
549
550 let button = update(serde_json::json!({
551 "update_id": 3,
552 "callback_query": {
553 "id": "cb", "chat_instance": "x", "data": "confirm",
554 "from": { "id": 1, "is_bot": false, "first_name": "Ada" }
555 }
556 }));
557 assert_eq!(routed_event(&button), Some("button"));
558 }
559
560 #[test]
561 fn reaction_updates_route_to_the_message_handler() {
562 let reaction = update(serde_json::json!({
563 "update_id": 7,
564 "message_reaction": {
565 "message_id": 9,
566 "chat": { "id": 42, "type": "private" },
567 "user": { "id": 1, "is_bot": false, "first_name": "Ada" },
568 "date": 0,
569 "old_reaction": [],
570 "new_reaction": [{ "type": "emoji", "emoji": "👍" }]
571 }
572 }));
573 assert_eq!(routed_event(&reaction), Some("message"));
574
575 let data = TelegramContextData::new(reaction, TelegramClient::new("t"));
576 assert_eq!(data.chat_id(), Some(42));
577 assert_eq!(data.user_name(), "Ada");
578 let UpdateKind::MessageReaction(r) = &data.update.kind else {
579 panic!("expected a reaction update");
580 };
581 assert_eq!(r.message_id, 9);
582 assert!(matches!(
583 r.new_reaction.as_slice(),
584 [crate::types::ReactionType::Emoji { .. }]
585 ));
586 }
587
588 #[test]
589 fn a_callback_without_data_does_not_fall_through_to_the_message_handler() {
590 let update = update(serde_json::json!({
591 "update_id": 4,
592 "callback_query": {
593 "id": "cb", "chat_instance": "x",
594 "from": { "id": 1, "is_bot": false, "first_name": "Ada" }
595 }
596 }));
597 assert_eq!(routed_event(&update), None);
598 }
599
600 #[test]
601 fn edits_route_to_the_message_handler_but_unmodelled_kinds_do_not() {
602 let edit = update(serde_json::json!({
603 "update_id": 5,
604 "edited_message": {
605 "message_id": 1, "date": 0,
606 "chat": { "id": 1, "type": "private" },
607 "text": "reworded"
608 }
609 }));
610 assert_eq!(routed_event(&edit), Some("message"));
611
612 let poll = update(serde_json::json!({ "update_id": 6, "poll": { "id": "p" } }));
613 assert_eq!(routed_event(&poll), None);
614 }
615
616 fn markup(response: &Response) -> Option<Vec<Vec<InlineKeyboardButton>>> {
617 build_reply_markup(response).map(|ReplyMarkup::InlineKeyboard(m)| m.inline_keyboard)
618 }
619
620 #[test]
621 fn no_components_means_no_markup() {
622 assert!(markup(&Response::text("hi")).is_none());
623 }
624
625 #[test]
626 fn action_rows_become_keyboard_rows() {
627 let response = Response::text("hi").with_components(vec![Component::ActionRow(
628 ActionRow::buttons(vec![
629 Button::primary("a", "A"),
630 Button::link("https://example.com", "Link"),
631 ]),
632 )]);
633
634 let rows = markup(&response).expect("one row");
635 assert_eq!(rows.len(), 1);
636 assert_eq!(rows[0][0].callback_data.as_deref(), Some("a"));
637 assert_eq!(rows[0][1].url.as_deref(), Some("https://example.com"));
638 assert!(rows[0][1].callback_data.is_none());
639 }
640
641 #[test]
642 fn bare_buttons_get_their_own_row() {
643 let response = Response::text("hi").with_components(vec![
644 Component::Button(Button::primary("a", "A")),
645 Component::Button(Button::secondary("b", "B")),
646 ]);
647
648 let rows = markup(&response).expect("two rows");
649 assert_eq!(rows.len(), 2);
650 assert_eq!(rows[0][0].text, "A");
651 assert_eq!(rows[1][0].text, "B");
652 }
653
654 #[test]
655 fn components_without_a_telegram_equivalent_are_skipped() {
656 let response = Response::text("hi").with_components(vec![
657 Component::SelectMenu(SelectMenu::new("menu", vec![SelectOption::new("l", "v")])),
658 Component::ActionRow(ActionRow::new(vec![])),
659 ]);
660 assert!(markup(&response).is_none());
661 }
662}