Skip to main content

botkit_telegram/
client.rs

1use botkit_core::BotError;
2use futures_lite::io::AsyncReadExt;
3use serde::de::DeserializeOwned;
4use zenwave::Client;
5
6use crate::types::{BotCommand, ReplyMarkup};
7
8const API_BASE: &str = "https://api.telegram.org";
9
10/// Telegram REST API client
11#[derive(Clone)]
12pub struct TelegramClient {
13    token: String,
14}
15
16impl TelegramClient {
17    /// Create a new Telegram client
18    pub fn new(token: impl Into<String>) -> Self {
19        Self {
20            token: token.into(),
21        }
22    }
23
24    /// Get the bot token
25    pub fn token(&self) -> &str {
26        &self.token
27    }
28
29    fn api_url(&self, method: &str) -> String {
30        format!("{}/bot{}/{}", API_BASE, self.token, method)
31    }
32
33    async fn post_json<T>(&self, method: &str, body: &serde_json::Value) -> Result<T, BotError>
34    where
35        T: DeserializeOwned,
36    {
37        let mut client = zenwave::client();
38        let response = client
39            .post(self.api_url(method))
40            .json_body(body)
41            .await
42            .map_err(|e| BotError::Api(e.to_string()))?;
43
44        self.decode_response(method, response).await
45    }
46
47    async fn post_multipart<T>(
48        &self,
49        method: &str,
50        content_type: String,
51        body: Vec<u8>,
52    ) -> Result<T, BotError>
53    where
54        T: DeserializeOwned,
55    {
56        let mut client = zenwave::client();
57        let response = client
58            .post(self.api_url(method))
59            .header("Content-Type", content_type)
60            .bytes_body(body)
61            .await
62            .map_err(|e| BotError::Api(e.to_string()))?;
63
64        self.decode_response(method, response).await
65    }
66
67    async fn decode_response<T>(
68        &self,
69        method: &str,
70        response: http_kit::Response,
71    ) -> Result<T, BotError>
72    where
73        T: DeserializeOwned,
74    {
75        if !response.status().is_success() {
76            return Err(BotError::Api(format!(
77                "Telegram {method} failed with HTTP {}",
78                response.status()
79            )));
80        }
81
82        let body = response
83            .into_body()
84            .into_string()
85            .await
86            .map_err(|e| BotError::Api(e.to_string()))?;
87
88        parse_api_response(method, &body)
89    }
90
91    /// Send a text message
92    pub async fn send_message(
93        &self,
94        chat_id: i64,
95        text: &str,
96        reply_markup: Option<ReplyMarkup>,
97    ) -> Result<(), BotError> {
98        let mut body = serde_json::json!({
99            "chat_id": chat_id,
100            "text": text,
101        });
102
103        if let Some(markup) = reply_markup {
104            body["reply_markup"] = serde_json::to_value(markup)
105                .map_err(|e| BotError::Other(format!("failed to serialize reply markup: {e}")))?;
106        }
107
108        let _: serde_json::Value = self.post_json("sendMessage", &body).await?;
109        Ok(())
110    }
111
112    /// Edit a message
113    pub async fn edit_message_text(
114        &self,
115        chat_id: i64,
116        message_id: i64,
117        text: &str,
118        reply_markup: Option<ReplyMarkup>,
119    ) -> Result<(), BotError> {
120        let mut body = serde_json::json!({
121            "chat_id": chat_id,
122            "message_id": message_id,
123            "text": text,
124        });
125
126        if let Some(markup) = reply_markup {
127            body["reply_markup"] = serde_json::to_value(markup)
128                .map_err(|e| BotError::Other(format!("failed to serialize reply markup: {e}")))?;
129        }
130
131        let _: serde_json::Value = self.post_json("editMessageText", &body).await?;
132        Ok(())
133    }
134
135    /// Answer a callback query
136    pub async fn answer_callback_query(
137        &self,
138        callback_query_id: &str,
139        text: Option<&str>,
140        show_alert: bool,
141    ) -> Result<(), BotError> {
142        let mut body = serde_json::json!({
143            "callback_query_id": callback_query_id,
144            "show_alert": show_alert,
145        });
146
147        if let Some(text) = text {
148            body["text"] = serde_json::json!(text);
149        }
150
151        let _: serde_json::Value = self.post_json("answerCallbackQuery", &body).await?;
152        Ok(())
153    }
154
155    /// Set webhook URL
156    pub async fn set_webhook(&self, url: &str) -> Result<(), BotError> {
157        let body = serde_json::json!({
158            "url": url,
159        });
160
161        let _: serde_json::Value = self.post_json("setWebhook", &body).await?;
162        Ok(())
163    }
164
165    /// Delete webhook
166    pub async fn delete_webhook(&self) -> Result<(), BotError> {
167        let body = serde_json::json!({});
168
169        let _: serde_json::Value = self.post_json("deleteWebhook", &body).await?;
170        Ok(())
171    }
172
173    /// Get updates using long polling
174    pub async fn get_updates(
175        &self,
176        offset: Option<i64>,
177        timeout: Option<u32>,
178    ) -> Result<Vec<crate::types::Update>, BotError> {
179        let mut body = serde_json::json!({});
180
181        if let Some(offset) = offset {
182            body["offset"] = serde_json::json!(offset);
183        }
184        if let Some(timeout) = timeout {
185            body["timeout"] = serde_json::json!(timeout);
186        }
187
188        self.post_json("getUpdates", &body).await
189    }
190
191    /// Send a chat action (typing, uploading, etc.)
192    pub async fn send_chat_action(&self, chat_id: i64, action: &str) -> Result<(), BotError> {
193        let body = serde_json::json!({
194            "chat_id": chat_id,
195            "action": action,
196        });
197
198        let _: serde_json::Value = self.post_json("sendChatAction", &body).await?;
199        Ok(())
200    }
201
202    /// Set bot commands for the menu
203    ///
204    /// Registers commands with Telegram so they appear in the command menu.
205    pub async fn set_my_commands(&self, commands: &[BotCommand]) -> Result<(), BotError> {
206        let body = serde_json::json!({
207            "commands": commands,
208        });
209
210        let _: serde_json::Value = self.post_json("setMyCommands", &body).await?;
211        Ok(())
212    }
213
214    /// Send a document/file
215    pub async fn send_document(
216        &self,
217        chat_id: i64,
218        mut file: async_fs::File,
219        filename: Option<&str>,
220        caption: Option<&str>,
221    ) -> Result<(), BotError> {
222        use zenwave::multipart::{Multipart, MultipartPart};
223
224        let mut contents = Vec::new();
225        file.read_to_end(&mut contents)
226            .await
227            .map_err(|e| BotError::Other(e.to_string()))?;
228
229        let filename = filename.unwrap_or("file");
230
231        let mut multipart = Multipart::new();
232        multipart.push(MultipartPart::text("chat_id", chat_id.to_string()));
233
234        if let Some(caption) = caption {
235            multipart.push(MultipartPart::text("caption", caption));
236        }
237
238        multipart.push(MultipartPart::binary(
239            "document",
240            filename.to_owned(),
241            "application/octet-stream",
242            contents,
243        ));
244
245        let (boundary, body) = multipart.encode();
246        let content_type = format!("multipart/form-data; boundary={}", boundary);
247
248        let _: serde_json::Value = self
249            .post_multipart("sendDocument", content_type, body)
250            .await?;
251        Ok(())
252    }
253}
254
255#[derive(Debug, serde::Deserialize)]
256struct TelegramApiResponse<T> {
257    ok: bool,
258    result: Option<T>,
259    description: Option<String>,
260}
261
262fn parse_api_response<T>(method: &str, body: &str) -> Result<T, BotError>
263where
264    T: DeserializeOwned,
265{
266    let response: TelegramApiResponse<T> =
267        serde_json::from_str(body).map_err(|e| BotError::Api(e.to_string()))?;
268
269    if !response.ok {
270        let description = response
271            .description
272            .unwrap_or_else(|| format!("Telegram {method} failed without description"));
273        return Err(BotError::Api(description));
274    }
275
276    response
277        .result
278        .ok_or_else(|| BotError::Api(format!("Telegram {method} succeeded without result")))
279}
280
281#[cfg(test)]
282mod tests {
283    use super::parse_api_response;
284
285    #[test]
286    fn parses_successful_api_response() {
287        let updates: Vec<serde_json::Value> =
288            parse_api_response("getUpdates", r#"{"ok":true,"result":[{"update_id":1}]}"#).unwrap();
289        assert_eq!(updates.len(), 1);
290    }
291
292    #[test]
293    fn rejects_api_error_response() {
294        let err = parse_api_response::<serde_json::Value>(
295            "sendMessage",
296            r#"{"ok":false,"description":"chat not found"}"#,
297        )
298        .unwrap_err();
299        assert_eq!(err.to_string(), "API request failed: chat not found");
300    }
301}