1use botkit_core::{BotError, FileSource};
2use serde::de::DeserializeOwned;
3use zenwave::{Client, ResponseExt};
4
5use crate::types::{BotCommand, InlineKeyboardMarkup, ReplyMarkup, StickerSet};
6
7const API_BASE: &str = "https://api.telegram.org";
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum MediaKind {
13 Photo,
15 Animation,
17 Video,
19 Audio,
21 Voice,
23 Document,
25 Sticker,
27}
28
29impl MediaKind {
30 fn spec(self) -> (&'static str, &'static str) {
32 match self {
33 Self::Photo => ("sendPhoto", "photo"),
34 Self::Animation => ("sendAnimation", "animation"),
35 Self::Video => ("sendVideo", "video"),
36 Self::Audio => ("sendAudio", "audio"),
37 Self::Voice => ("sendVoice", "voice"),
38 Self::Document => ("sendDocument", "document"),
39 Self::Sticker => ("sendSticker", "sticker"),
40 }
41 }
42}
43
44struct Upload<'a> {
47 field: &'a str,
49 file: FileSource,
51 filename: &'a str,
53 caption: Option<&'a str>,
55 thread_id: Option<i64>,
57}
58
59pub struct NewSticker {
62 pub file: FileSource,
65 pub filename: String,
67 pub format: &'static str,
69 pub emoji: String,
71 old_file_id: Option<String>,
73}
74
75impl NewSticker {
76 pub fn new(
78 file: FileSource,
79 filename: impl Into<String>,
80 format: &'static str,
81 emoji: impl Into<String>,
82 ) -> Self {
83 Self {
84 file,
85 filename: filename.into(),
86 format,
87 emoji: emoji.into(),
88 old_file_id: None,
89 }
90 }
91
92 fn with_old_file_id(mut self, file_id: &str) -> Self {
94 self.old_file_id = Some(file_id.to_string());
95 self
96 }
97}
98
99#[derive(Clone)]
101pub struct TelegramClient {
102 token: String,
103}
104
105impl TelegramClient {
106 pub fn new(token: impl Into<String>) -> Self {
108 install_crypto_provider();
109
110 Self {
111 token: token.into(),
112 }
113 }
114
115 pub fn token(&self) -> &str {
117 &self.token
118 }
119
120 fn api_url(&self, method: &str) -> String {
121 format!("{}/bot{}/{}", API_BASE, self.token, method)
122 }
123
124 fn api_error(&self, error: impl std::fmt::Display) -> BotError {
129 BotError::Api(error.to_string().replace(&self.token, "<token>"))
130 }
131
132 async fn post_json<T>(&self, method: &str, body: &serde_json::Value) -> Result<T, BotError>
133 where
134 T: DeserializeOwned,
135 {
136 let mut client = zenwave::client();
137 let response = client
138 .post(self.api_url(method))
139 .map_err(|e| self.api_error(e))?
140 .json_body(body)
141 .map_err(|e| self.api_error(e))?
142 .await
143 .map_err(|e| self.api_error(e))?;
144
145 self.decode_response(method, response).await
146 }
147
148 async fn post_multipart<T>(
149 &self,
150 method: &str,
151 content_type: String,
152 body: Vec<u8>,
153 ) -> Result<T, BotError>
154 where
155 T: DeserializeOwned,
156 {
157 let mut client = zenwave::client();
158 let response = client
159 .post(self.api_url(method))
160 .map_err(|e| self.api_error(e))?
161 .header("Content-Type", content_type)
162 .map_err(|e| self.api_error(e))?
163 .bytes_body(body)
164 .await
165 .map_err(|e| self.api_error(e))?;
166
167 self.decode_response(method, response).await
168 }
169
170 async fn decode_response<T>(
171 &self,
172 method: &str,
173 response: http_kit::Response,
174 ) -> Result<T, BotError>
175 where
176 T: DeserializeOwned,
177 {
178 let status = response.status();
181 let body = response
182 .into_body()
183 .into_string()
184 .await
185 .map_err(|e| self.api_error(e))?;
186
187 parse_api_response(method, &body).map_err(|e| {
188 if status.is_success() {
189 e
190 } else {
191 BotError::Api(format!("Telegram {method} failed with HTTP {status}: {e}"))
192 }
193 })
194 }
195
196 pub async fn send_message(
201 &self,
202 chat_id: i64,
203 text: &str,
204 thread_id: Option<i64>,
205 reply_markup: Option<ReplyMarkup>,
206 ) -> Result<i64, BotError> {
207 self.send_message_inner(chat_id, text, None, thread_id, reply_markup)
208 .await
209 }
210
211 pub async fn send_reply(
218 &self,
219 chat_id: i64,
220 reply_to: i64,
221 text: &str,
222 ) -> Result<i64, BotError> {
223 self.send_reply_markup(chat_id, reply_to, text, None).await
224 }
225
226 pub async fn send_reply_markup(
228 &self,
229 chat_id: i64,
230 reply_to: i64,
231 text: &str,
232 markup: Option<InlineKeyboardMarkup>,
233 ) -> Result<i64, BotError> {
234 self.send_message_inner(
235 chat_id,
236 text,
237 Some(reply_to),
238 None,
239 markup.map(ReplyMarkup::InlineKeyboard),
240 )
241 .await
242 }
243
244 async fn send_message_inner(
245 &self,
246 chat_id: i64,
247 text: &str,
248 reply_to: Option<i64>,
249 thread_id: Option<i64>,
250 reply_markup: Option<ReplyMarkup>,
251 ) -> Result<i64, BotError> {
252 let mut body = serde_json::json!({
253 "chat_id": chat_id,
254 "text": text,
255 });
256
257 if let Some(thread) = thread_id {
258 body["message_thread_id"] = serde_json::json!(thread);
259 }
260
261 if let Some(message_id) = reply_to {
262 body["reply_parameters"] = serde_json::json!({
263 "message_id": message_id,
264 "allow_sending_without_reply": true,
265 });
266 }
267
268 if let Some(markup) = reply_markup {
269 body["reply_markup"] = serde_json::to_value(markup)
270 .map_err(|e| BotError::Other(format!("failed to serialize reply markup: {e}")))?;
271 }
272
273 let message: crate::types::Message = self.post_json("sendMessage", &body).await?;
274 Ok(message.message_id)
275 }
276
277 pub async fn edit_message_text(
279 &self,
280 chat_id: i64,
281 message_id: i64,
282 text: &str,
283 reply_markup: Option<ReplyMarkup>,
284 ) -> Result<(), BotError> {
285 let mut body = serde_json::json!({
286 "chat_id": chat_id,
287 "message_id": message_id,
288 "text": text,
289 });
290
291 if let Some(markup) = reply_markup {
292 body["reply_markup"] = serde_json::to_value(markup)
293 .map_err(|e| BotError::Other(format!("failed to serialize reply markup: {e}")))?;
294 }
295
296 let _: serde_json::Value = self.post_json("editMessageText", &body).await?;
297 Ok(())
298 }
299
300 pub async fn edit_message_reply_markup(
304 &self,
305 chat_id: i64,
306 message_id: i64,
307 reply_markup: Option<InlineKeyboardMarkup>,
308 ) -> Result<(), BotError> {
309 let mut body = serde_json::json!({
310 "chat_id": chat_id,
311 "message_id": message_id,
312 });
313 if let Some(markup) = reply_markup {
314 body["reply_markup"] = serde_json::to_value(markup)
315 .map_err(|e| BotError::Other(format!("failed to serialize reply markup: {e}")))?;
316 }
317 let _: serde_json::Value = self.post_json("editMessageReplyMarkup", &body).await?;
318 Ok(())
319 }
320
321 pub async fn answer_callback_query(
323 &self,
324 callback_query_id: &str,
325 text: Option<&str>,
326 show_alert: bool,
327 ) -> Result<(), BotError> {
328 let mut body = serde_json::json!({
329 "callback_query_id": callback_query_id,
330 "show_alert": show_alert,
331 });
332
333 if let Some(text) = text {
334 body["text"] = serde_json::json!(text);
335 }
336
337 let _: serde_json::Value = self.post_json("answerCallbackQuery", &body).await?;
338 Ok(())
339 }
340
341 pub async fn set_webhook(&self, url: &str) -> Result<(), BotError> {
343 let body = serde_json::json!({
344 "url": url,
345 "allowed_updates": [
346 "message",
347 "edited_message",
348 "callback_query",
349 "message_reaction"
350 ],
351 });
352
353 let _: serde_json::Value = self.post_json("setWebhook", &body).await?;
354 Ok(())
355 }
356
357 pub async fn delete_webhook(&self) -> Result<(), BotError> {
359 let body = serde_json::json!({});
360
361 let _: serde_json::Value = self.post_json("deleteWebhook", &body).await?;
362 Ok(())
363 }
364
365 pub async fn get_updates(
367 &self,
368 offset: Option<i64>,
369 timeout: Option<u32>,
370 ) -> Result<Vec<crate::types::Update>, BotError> {
371 let mut body = serde_json::json!({});
372
373 if let Some(offset) = offset {
374 body["offset"] = serde_json::json!(offset);
375 }
376 if let Some(timeout) = timeout {
377 body["timeout"] = serde_json::json!(timeout);
378 }
379 body["allowed_updates"] = serde_json::json!([
382 "message",
383 "edited_message",
384 "callback_query",
385 "message_reaction"
386 ]);
387
388 self.post_json("getUpdates", &body).await
389 }
390
391 pub async fn send_chat_action(
394 &self,
395 chat_id: i64,
396 action: &str,
397 thread_id: Option<i64>,
398 ) -> Result<(), BotError> {
399 let mut body = serde_json::json!({
400 "chat_id": chat_id,
401 "action": action,
402 });
403 if let Some(thread) = thread_id {
404 body["message_thread_id"] = serde_json::json!(thread);
405 }
406
407 let _: serde_json::Value = self.post_json("sendChatAction", &body).await?;
408 Ok(())
409 }
410
411 pub async fn set_message_reaction(
414 &self,
415 chat_id: i64,
416 message_id: i64,
417 emoji: Option<&str>,
418 is_big: bool,
419 ) -> Result<(), BotError> {
420 let reactions: Vec<_> = emoji
421 .into_iter()
422 .map(|emoji| serde_json::json!({"type": "emoji", "emoji": emoji}))
423 .collect();
424 let body = serde_json::json!({
425 "chat_id": chat_id,
426 "message_id": message_id,
427 "reaction": reactions,
428 "is_big": is_big,
429 });
430 let _: serde_json::Value = self.post_json("setMessageReaction", &body).await?;
431 Ok(())
432 }
433
434 pub async fn delete_message(&self, chat_id: i64, message_id: i64) -> Result<(), BotError> {
437 let _: serde_json::Value = self
438 .post_json(
439 "deleteMessage",
440 &serde_json::json!({"chat_id": chat_id, "message_id": message_id}),
441 )
442 .await?;
443 Ok(())
444 }
445
446 pub async fn pin_message(
449 &self,
450 chat_id: i64,
451 message_id: i64,
452 notify: bool,
453 ) -> Result<(), BotError> {
454 let _: serde_json::Value = self
455 .post_json(
456 "pinChatMessage",
457 &serde_json::json!({
458 "chat_id": chat_id,
459 "message_id": message_id,
460 "disable_notification": !notify,
461 }),
462 )
463 .await?;
464 Ok(())
465 }
466
467 pub async fn unpin_message(&self, chat_id: i64, message_id: i64) -> Result<(), BotError> {
469 let _: serde_json::Value = self
470 .post_json(
471 "unpinChatMessage",
472 &serde_json::json!({"chat_id": chat_id, "message_id": message_id}),
473 )
474 .await?;
475 Ok(())
476 }
477
478 pub async fn set_my_commands(&self, commands: &[BotCommand]) -> Result<(), BotError> {
482 let body = serde_json::json!({
483 "commands": commands,
484 });
485
486 let _: serde_json::Value = self.post_json("setMyCommands", &body).await?;
487 Ok(())
488 }
489
490 pub async fn get_me(&self) -> Result<crate::types::User, BotError> {
494 self.post_json("getMe", &serde_json::json!({})).await
495 }
496
497 pub async fn get_file(&self, file_id: &str) -> Result<crate::types::File, BotError> {
499 self.post_json("getFile", &serde_json::json!({"file_id": file_id}))
500 .await
501 }
502
503 pub async fn download_file(&self, file_path: &str, limit: usize) -> Result<Vec<u8>, BotError> {
510 let url = format!("{}/file/bot{}/{}", API_BASE, self.token, file_path);
511 let response = zenwave::get(&url).await.map_err(|e| self.api_error(e))?;
512 let bytes = response
513 .error_for_status()
514 .await
515 .map_err(|e| self.api_error(e))?
516 .into_bytes_with_limit(limit)
517 .await
518 .map_err(|e| self.api_error(e))?;
519 Ok(bytes.to_vec())
520 }
521
522 pub async fn send_document(
526 &self,
527 chat_id: i64,
528 file: FileSource,
529 filename: Option<&str>,
530 caption: Option<&str>,
531 thread_id: Option<i64>,
532 ) -> Result<i64, BotError> {
533 self.send_media(
534 chat_id,
535 MediaKind::Document,
536 file,
537 filename.unwrap_or("file"),
538 caption,
539 thread_id,
540 )
541 .await
542 }
543
544 pub async fn send_photo(
548 &self,
549 chat_id: i64,
550 file: FileSource,
551 filename: &str,
552 caption: Option<&str>,
553 thread_id: Option<i64>,
554 ) -> Result<i64, BotError> {
555 self.send_media(
556 chat_id,
557 MediaKind::Photo,
558 file,
559 filename,
560 caption,
561 thread_id,
562 )
563 .await
564 }
565
566 pub async fn send_sticker(
570 &self,
571 chat_id: i64,
572 file: FileSource,
573 filename: &str,
574 thread_id: Option<i64>,
575 ) -> Result<i64, BotError> {
576 self.send_media(chat_id, MediaKind::Sticker, file, filename, None, thread_id)
577 .await
578 }
579
580 pub async fn send_media(
583 &self,
584 chat_id: i64,
585 kind: MediaKind,
586 file: FileSource,
587 filename: &str,
588 caption: Option<&str>,
589 thread_id: Option<i64>,
590 ) -> Result<i64, BotError> {
591 let (method, field) = kind.spec();
592 self.send_upload(
593 method,
594 chat_id,
595 Upload {
596 field,
597 file,
598 filename,
599 caption,
600 thread_id,
601 },
602 )
603 .await
604 }
605
606 pub async fn send_media_id(
610 &self,
611 chat_id: i64,
612 kind: MediaKind,
613 file_id: &str,
614 caption: Option<&str>,
615 thread_id: Option<i64>,
616 ) -> Result<i64, BotError> {
617 let (method, field) = kind.spec();
618 let mut body = serde_json::json!({
619 "chat_id": chat_id,
620 field: file_id,
621 });
622 if let Some(caption) = caption {
623 body["caption"] = serde_json::json!(caption);
624 }
625 if let Some(thread) = thread_id {
626 body["message_thread_id"] = serde_json::json!(thread);
627 }
628 let message: crate::types::Message = self.post_json(method, &body).await?;
629 Ok(message.message_id)
630 }
631
632 pub async fn get_sticker_set(&self, name: &str) -> Result<StickerSet, BotError> {
637 self.post_json("getStickerSet", &serde_json::json!({"name": name}))
638 .await
639 }
640
641 pub async fn create_sticker_set(
644 &self,
645 user_id: i64,
646 name: &str,
647 title: &str,
648 sticker: NewSticker,
649 ) -> Result<(), BotError> {
650 self.sticker_set_edit("createNewStickerSet", user_id, name, Some(title), sticker)
651 .await
652 }
653
654 pub async fn add_sticker_to_set(
656 &self,
657 user_id: i64,
658 name: &str,
659 sticker: NewSticker,
660 ) -> Result<(), BotError> {
661 self.sticker_set_edit("addStickerToSet", user_id, name, None, sticker)
662 .await
663 }
664
665 pub async fn replace_sticker_in_set(
668 &self,
669 user_id: i64,
670 name: &str,
671 old_file_id: &str,
672 sticker: NewSticker,
673 ) -> Result<(), BotError> {
674 self.sticker_set_edit(
675 "replaceStickerInSet",
676 user_id,
677 name,
678 None,
679 sticker.with_old_file_id(old_file_id),
680 )
681 .await
682 }
683
684 async fn sticker_set_edit(
688 &self,
689 method: &str,
690 user_id: i64,
691 name: &str,
692 title: Option<&str>,
693 sticker: NewSticker,
694 ) -> Result<(), BotError> {
695 use zenwave::multipart::{Multipart, MultipartPart};
696
697 let contents = sticker
698 .file
699 .read()
700 .await
701 .map_err(|e| BotError::Other(format!("failed to read sticker file: {e}")))?;
702
703 let input = serde_json::json!({
704 "sticker": "attach://s0",
705 "format": sticker.format,
706 "emoji_list": [sticker.emoji],
707 });
708
709 let mut multipart = Multipart::new();
710 multipart.push(MultipartPart::text("user_id", user_id.to_string()));
711 multipart.push(MultipartPart::text("name", name));
712 if let Some(title) = title {
713 multipart.push(MultipartPart::text("title", title));
714 multipart.push(MultipartPart::text("sticker_type", "regular"));
718 multipart.push(MultipartPart::text(
719 "stickers",
720 serde_json::json!([input]).to_string(),
721 ));
722 } else {
723 multipart.push(MultipartPart::text("sticker", input.to_string()));
724 if let Some(old) = sticker.old_file_id {
725 multipart.push(MultipartPart::text("old_sticker", old));
726 }
727 }
728 let mime = mime_guess::from_path(&sticker.filename)
729 .first_or_octet_stream()
730 .to_string();
731 multipart.push(MultipartPart::binary(
732 "s0".to_owned(),
733 sticker.filename,
734 mime,
735 contents,
736 ));
737
738 let (boundary, body) = multipart.encode();
739 let content_type = format!("multipart/form-data; boundary={}", boundary);
740 let _: serde_json::Value = self.post_multipart(method, content_type, body).await?;
741 Ok(())
742 }
743
744 async fn send_upload(
747 &self,
748 method: &str,
749 chat_id: i64,
750 upload: Upload<'_>,
751 ) -> Result<i64, BotError> {
752 use zenwave::multipart::{Multipart, MultipartPart};
753
754 let contents = upload
755 .file
756 .read()
757 .await
758 .map_err(|e| BotError::Other(format!("failed to read attachment: {e}")))?;
759
760 let mut multipart = Multipart::new();
761 multipart.push(MultipartPart::text("chat_id", chat_id.to_string()));
762
763 if let Some(thread) = upload.thread_id {
764 multipart.push(MultipartPart::text("message_thread_id", thread.to_string()));
765 }
766
767 if let Some(caption) = upload.caption {
768 multipart.push(MultipartPart::text("caption", caption));
769 }
770
771 multipart.push(MultipartPart::binary(
772 upload.field.to_owned(),
773 upload.filename.to_owned(),
774 mime_guess::from_path(upload.filename)
775 .first_or_octet_stream()
776 .to_string(),
777 contents,
778 ));
779
780 let (boundary, body) = multipart.encode();
781 let content_type = format!("multipart/form-data; boundary={}", boundary);
782
783 let message: crate::types::Message =
784 self.post_multipart(method, content_type, body).await?;
785 Ok(message.message_id)
786 }
787}
788
789fn install_crypto_provider() {
797 if rustls::crypto::CryptoProvider::get_default().is_none() {
798 let _ = rustls::crypto::ring::default_provider().install_default();
800 }
801}
802
803#[derive(Debug, serde::Deserialize)]
804struct TelegramApiResponse<T> {
805 ok: bool,
806 result: Option<T>,
807 description: Option<String>,
808}
809
810fn parse_api_response<T>(method: &str, body: &str) -> Result<T, BotError>
811where
812 T: DeserializeOwned,
813{
814 let response: TelegramApiResponse<T> =
815 serde_json::from_str(body).map_err(|e| BotError::Api(e.to_string()))?;
816
817 if !response.ok {
818 let description = response
819 .description
820 .unwrap_or_else(|| format!("Telegram {method} failed without description"));
821 return Err(BotError::Api(description));
822 }
823
824 response
825 .result
826 .ok_or_else(|| BotError::Api(format!("Telegram {method} succeeded without result")))
827}
828
829#[cfg(test)]
830mod tests {
831 use super::{TelegramClient, parse_api_response};
832
833 #[test]
834 fn building_a_client_installs_a_crypto_provider() {
835 let _client = TelegramClient::new("token");
839 assert!(rustls::crypto::CryptoProvider::get_default().is_some());
840 }
841
842 #[test]
843 fn redacts_the_token_from_error_messages() {
844 let client = TelegramClient::new("123456:SECRET");
846 let error = client.api_error("connect to https://api.telegram.org/bot123456:SECRET/x");
847 assert!(!error.to_string().contains("SECRET"), "{error}");
848 assert!(error.to_string().contains("<token>"), "{error}");
849 }
850
851 #[test]
852 fn parses_successful_api_response() {
853 let updates: Vec<serde_json::Value> =
854 parse_api_response("getUpdates", r#"{"ok":true,"result":[{"update_id":1}]}"#).unwrap();
855 assert_eq!(updates.len(), 1);
856 }
857
858 #[test]
859 fn rejects_api_error_response() {
860 let err = parse_api_response::<serde_json::Value>(
861 "sendMessage",
862 r#"{"ok":false,"description":"chat not found"}"#,
863 )
864 .unwrap_err();
865 assert_eq!(err.to_string(), "API request failed: chat not found");
866 }
867}