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#[derive(Clone)]
12pub struct TelegramClient {
13 token: String,
14}
15
16impl TelegramClient {
17 pub fn new(token: impl Into<String>) -> Self {
19 Self {
20 token: token.into(),
21 }
22 }
23
24 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 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 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 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 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 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 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 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 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 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}