1use crate::{
9 client::{telegram::APIServer, Bot},
10 errors::{SessionErrorKind, TelegramErrorKind},
11 methods::{Response, TelegramMethod},
12};
13
14use serde::de::DeserializeOwned;
15use std::{
16 fmt::{self, Display, Formatter},
17 future::Future,
18 ops::RangeInclusive,
19};
20use tracing::{debug_span, event, instrument, Level, Span};
21
22pub const DEFAULT_TIMEOUT: f32 = 60.0;
23
24#[derive(Debug)]
25pub struct StatusCode(u16);
26
27impl StatusCode {
28 const SUCESS_STATUS_CODE_RANGE: RangeInclusive<u16> = 200..=226;
29
30 #[must_use]
31 pub fn new(status_code: u16) -> Self {
32 Self(status_code)
33 }
34
35 #[must_use]
36 pub fn is_success(&self) -> bool {
37 Self::SUCESS_STATUS_CODE_RANGE.contains(&self.0)
38 }
39
40 #[must_use]
41 pub fn is_error(&self) -> bool {
42 !self.is_success()
43 }
44
45 #[must_use]
46 pub const fn as_u16(&self) -> u16 {
47 self.0
48 }
49}
50
51impl Display for StatusCode {
52 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
53 write!(f, "{}", self.0)
54 }
55}
56
57impl PartialEq<u16> for StatusCode {
58 fn eq(&self, other: &u16) -> bool {
59 self.0 == *other
60 }
61}
62
63impl From<u16> for StatusCode {
64 fn from(status_code: u16) -> Self {
65 Self::new(status_code)
66 }
67}
68
69#[derive(Debug)]
70pub struct ClientResponse {
71 pub status_code: StatusCode,
72 pub content: Box<str>,
73}
74
75impl ClientResponse {
76 #[must_use]
77 pub fn new(status_code: impl Into<StatusCode>, content: impl Into<Box<str>>) -> Self {
78 Self {
79 status_code: status_code.into(),
80 content: content.into(),
81 }
82 }
83}
84
85pub trait Session: Send + Sync {
86 #[must_use]
88 fn api(&self) -> &APIServer;
89
90 #[must_use]
99 fn send_request<Client, T>(
100 &self,
101 bot: &Bot<Client>,
102 method: &T,
103 timeout: Option<f32>,
104 ) -> impl Future<Output = Result<ClientResponse, anyhow::Error>> + Send
105 where
106 Client: Session,
107 T: TelegramMethod + Send + Sync,
108 T::Method: Send + Sync;
109
110 #[allow(clippy::redundant_else)]
118 #[instrument(name = "check", skip_all, fields(ok, error_message))]
119 fn check_response(
120 &self,
121 response: &Response<impl DeserializeOwned>,
122 status_code: StatusCode,
123 ) -> Result<(), TelegramErrorKind> {
124 if status_code.is_success() && response.ok {
125 Span::current().record("ok", true);
126
127 if response.result.is_none() {
128 event!(
129 Level::ERROR,
130 "Contract violation: result is empty in success response"
131 );
132
133 return Err(anyhow::Error::msg(
134 "Contract violation: result is empty in success response",
135 )
136 .into());
137 }
138
139 return Ok(());
140 } else {
141 Span::current().record("ok", false);
142 }
143
144 let Some(message) = response.description.clone() else {
145 event!(
147 Level::ERROR,
148 error_code = ?response.error_code,
149 parameters = ?response.parameters,
150 "Contract violation: description is empty in error response",
151 );
152
153 return Err(anyhow::Error::msg(
154 "Contract violation: description is empty in error response",
155 )
156 .into());
157 };
158
159 Span::current().record("error_message", message.as_ref());
160
161 if let Some(ref parameters) = response.parameters {
162 if let Some(retry_after) = parameters.retry_after {
163 let err = TelegramErrorKind::RetryAfter {
164 url: "https://core.telegram.org/bots/faq#my-bot-is-hitting-limits-how-do-i-avoid-this",
165 message,
166 retry_after,
167 };
168
169 event!(Level::ERROR, error = %err);
170
171 return Err(err);
172 }
173 if let Some(migrate_to_chat_id) = parameters.migrate_to_chat_id {
174 let err = TelegramErrorKind::MigrateToChat {
175 url: "https://core.telegram.org/bots/api#responseparameters",
176 message,
177 migrate_to_chat_id,
178 };
179
180 event!(Level::ERROR, error = %err);
181
182 return Err(err);
183 }
184 }
185
186 let err = match status_code.as_u16() {
187 400 => TelegramErrorKind::BadRequest { message },
188 401 => TelegramErrorKind::Unauthorized { message },
189 403 => TelegramErrorKind::Forbidden { message },
190 404 => TelegramErrorKind::NotFound { message },
191 409 => TelegramErrorKind::ConflictError { message },
192 413 => TelegramErrorKind::EntityTooLarge {
193 url: "https://core.telegram.org/bots/api#sending-files",
194 message,
195 },
196 500 => {
197 if message.contains("restart") {
198 TelegramErrorKind::RestartingTelegram { message }
199 } else {
200 TelegramErrorKind::ServerError { message }
201 }
202 }
203 _ => {
204 event!(
205 Level::ERROR,
206 %status_code,
207 message,
208 "Error with unknown status code",
209 );
210
211 return Err(anyhow::Error::msg(message).into());
212 }
213 };
214
215 event!(Level::ERROR, error = %err);
216
217 Err(err)
218 }
219
220 fn make_request<Client, T>(
231 &self,
232 bot: &Bot<Client>,
233 method: &T,
234 timeout: Option<f32>,
235 ) -> impl Future<Output = Result<Response<T::Return>, SessionErrorKind>> + Send
236 where
237 Client: Session,
238 T: TelegramMethod + Send + Sync,
239 T::Method: Send + Sync,
240 {
241 async move {
242 let response = self.send_request(bot, method, timeout).await?;
243
244 debug_span!("response", status_code = response.status_code.as_u16()).in_scope(|| {
245 let resp = method.build_response(&response.content)?;
246 self.check_response(&resp, response.status_code)?;
247
248 Ok(resp)
249 })
250 }
251 }
252
253 fn make_request_and_get_result<Client, T>(
264 &self,
265 bot: &Bot<Client>,
266 method: &T,
267 timeout: Option<f32>,
268 ) -> impl Future<Output = Result<T::Return, SessionErrorKind>> + Send
269 where
270 Client: Session,
271 T: TelegramMethod + Send + Sync,
272 T::Method: Send + Sync,
273 {
274 async move {
275 let resp = self.make_request(bot, method, timeout).await?;
276
277 Ok(resp.result.unwrap())
279 }
280 }
281
282 fn close(&self) -> impl Future<Output = Result<(), anyhow::Error>> + Send {
284 async move { Ok(()) }
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use crate::methods::SendMessage;
292
293 use serde_json::json;
294
295 #[test]
296 fn build_response() {
297 let method = SendMessage::new(810646651, "Hello, abc!");
298
299 let content = json!(
300 {
301 "ok": true,
302 "result": {
303 "message_id": 423,
304 "from": {
305 "id": 1i64,
306 "is_bot": true,
307 "first_name": "test",
308 "username": "test"
309 },
310 "chat": {
311 "id": 1,
312 "first_name": "test",
313 "username": "test",
314 "type": "private",
315 },
316 "date": 1706267365,
317 "reply_to_message": {
318 "message_id": 422,
319 "from": {
320 "id": 1,
321 "is_bot": false,
322 "first_name": "test",
323 "username": "test",
324 "language_code": "ru",
325 "is_premium": true,
326 },
327 "chat":{
328 "id": 1,
329 "first_name": "test",
330 "username": "test",
331 "type": "private",
332 },
333 "date": 1,
334 "text": "/start",
335 "entities":[
336 {
337 "offset": 0,
338 "length": 6,
339 "type": "bot_command",
340 },
341 ],
342 },
343 "text": "test",
344 },
345 "statud_code": 200,
346 });
347
348 let result = method
349 .build_response(&content.to_string())
350 .unwrap()
351 .result
352 .unwrap();
353
354 assert_eq!(result.id(), 423);
355 }
356}