tgbot/api/client/
mod.rs

1use std::{error::Error, fmt, time::Duration};
2
3use bytes::Bytes;
4use futures_util::stream::Stream;
5use log::debug;
6use reqwest::{
7    Client as HttpClient,
8    ClientBuilder as HttpClientBuilder,
9    Error as HttpError,
10    RequestBuilder as HttpRequestBuilder,
11};
12use serde::de::DeserializeOwned;
13use tokio::time::sleep;
14
15use super::payload::{Payload, PayloadError};
16use crate::types::{Response, ResponseError};
17
18#[cfg(test)]
19mod tests;
20
21const DEFAULT_HOST: &str = "https://api.telegram.org";
22const DEFAULT_MAX_RETRIES: u8 = 2;
23
24/// A client for interacting with the Telegram Bot API.
25#[derive(Clone)]
26pub struct Client {
27    host: String,
28    http_client: HttpClient,
29    token: String,
30    max_retries: u8,
31}
32
33impl Client {
34    /// Creates a new Telegram Bot API client with the provided bot token.
35    ///
36    /// # Arguments
37    ///
38    /// * `token` - A token associated with your bot.
39    pub fn new<T>(token: T) -> Result<Self, ClientError>
40    where
41        T: Into<String>,
42    {
43        let client = HttpClientBuilder::new()
44            .use_rustls_tls()
45            .build()
46            .map_err(ClientError::BuildClient)?;
47        Ok(Self::with_http_client(client, token))
48    }
49
50    /// Creates a new Telegram Bot API client with a custom HTTP client and bot token.
51    ///
52    /// # Arguments
53    ///
54    /// * `client` - An HTTP client.
55    /// * `token` - A token associated with your bot.
56    ///
57    pub fn with_http_client<T>(http_client: HttpClient, token: T) -> Self
58    where
59        T: Into<String>,
60    {
61        Self {
62            http_client,
63            host: String::from(DEFAULT_HOST),
64            token: token.into(),
65            max_retries: DEFAULT_MAX_RETRIES,
66        }
67    }
68
69    /// Overrides the default API host with a custom one.
70    ///
71    /// # Arguments
72    ///
73    /// * `host` - The new API host to use.
74    pub fn with_host<T>(mut self, host: T) -> Self
75    where
76        T: Into<String>,
77    {
78        self.host = host.into();
79        self
80    }
81
82    /// Overrides the default number of max retries.
83    ///
84    /// # Arguments
85    ///
86    /// * `value` - The new number of max retries
87    pub fn with_max_retries(mut self, value: u8) -> Self {
88        self.max_retries = value;
89        self
90    }
91
92    /// Downloads a file.
93    ///
94    /// Use [`crate::types::GetFile`] method to get a value for the `file_path` argument.
95    ///
96    /// # Arguments
97    ///
98    /// * `file_path` - The path to the file to be downloaded.
99    ///
100    /// # Example
101    ///
102    /// ```
103    /// # async fn download_file() {
104    /// use tgbot::api::Client;
105    /// use futures_util::stream::StreamExt;
106    /// let api = Client::new("token").unwrap();
107    /// let mut stream = api.download_file("path").await.unwrap();
108    /// while let Some(chunk) = stream.next().await {
109    ///     let chunk = chunk.unwrap();
110    ///     // write chunk to something...
111    /// }
112    /// # }
113    /// ```
114    pub async fn download_file<P>(
115        &self,
116        file_path: P,
117    ) -> Result<impl Stream<Item = Result<Bytes, HttpError>> + use<P>, DownloadFileError>
118    where
119        P: AsRef<str>,
120    {
121        let payload = Payload::empty(file_path.as_ref());
122        let url = payload.build_url(&format!("{}/file", &self.host), &self.token);
123        debug!("Downloading file from {url}");
124        let rep = self.http_client.get(&url).send().await?;
125        let status = rep.status();
126        if !status.is_success() {
127            Err(DownloadFileError::Response {
128                status: status.as_u16(),
129                text: rep.text().await?,
130            })
131        } else {
132            Ok(rep.bytes_stream())
133        }
134    }
135
136    /// Executes a method.
137    ///
138    /// # Arguments
139    ///
140    /// * `method` - The method to execute.
141    ///
142    /// # Notes
143    ///
144    /// The client will not retry a request on a timeout error if the request is not cloneable
145    /// (e.g. contains a stream).
146    pub async fn execute<M>(&self, method: M) -> Result<M::Response, ExecuteError>
147    where
148        M: Method,
149        M::Response: DeserializeOwned + Send + 'static,
150    {
151        let request = method
152            .into_payload()
153            .into_http_request_builder(&self.http_client, &self.host, &self.token)?;
154        let response = match send_request_retry(Box::new(request)).await? {
155            RetryResponse::Ok(response) => response,
156            RetryResponse::Retry {
157                mut request,
158                mut response,
159                mut retry_after,
160            } => {
161                for i in 0..self.max_retries {
162                    debug!("Retry attempt {i}, sleeping for {retry_after} second(s)");
163                    sleep(Duration::from_secs(retry_after)).await;
164                    match send_request_retry(request).await? {
165                        RetryResponse::Ok(new_response) => {
166                            response = new_response;
167                            break;
168                        }
169                        RetryResponse::Retry {
170                            request: new_request,
171                            response: new_response,
172                            retry_after: new_retry_after,
173                        } => {
174                            request = new_request;
175                            response = new_response;
176                            retry_after = new_retry_after;
177                        }
178                    }
179                }
180                response
181            }
182        };
183        Ok(response.into_result()?)
184    }
185}
186
187enum RetryResponse<T> {
188    Ok(Response<T>),
189    Retry {
190        request: Box<HttpRequestBuilder>,
191        response: Response<T>,
192        retry_after: u64,
193    },
194}
195
196async fn send_request_retry<T>(request: Box<HttpRequestBuilder>) -> Result<RetryResponse<T>, ExecuteError>
197where
198    T: DeserializeOwned,
199{
200    Ok(match request.try_clone() {
201        Some(try_request) => {
202            let response = send_request(try_request).await?;
203            match response.retry_after() {
204                Some(retry_after) => RetryResponse::Retry {
205                    request,
206                    response,
207                    retry_after,
208                },
209                None => RetryResponse::Ok(response),
210            }
211        }
212        None => {
213            debug!("Could not clone builder, sending request without retry");
214            RetryResponse::Ok(send_request(*request).await?)
215        }
216    })
217}
218
219async fn send_request<T>(request: HttpRequestBuilder) -> Result<Response<T>, ExecuteError>
220where
221    T: DeserializeOwned,
222{
223    let response = request.send().await?;
224    Ok(response.json::<Response<T>>().await?)
225}
226
227impl fmt::Debug for Client {
228    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
229        f.debug_struct("Client")
230            .field("http_client", &self.http_client)
231            .field("host", &self.host)
232            .field("token", &format_args!("..."))
233            .finish()
234    }
235}
236
237/// Represents an API method that can be executed by the Telegram Bot API client.
238pub trait Method {
239    /// The type representing a successful result in an API response.
240    type Response;
241
242    /// Converts the method into a payload for an HTTP request.
243    fn into_payload(self) -> Payload;
244}
245
246/// Represents general errors that can occur while working with the Telegram Bot API client.
247#[derive(Debug)]
248pub enum ClientError {
249    /// An error indicating a failure to build an HTTP client.
250    BuildClient(HttpError),
251}
252
253impl Error for ClientError {
254    fn source(&self) -> Option<&(dyn Error + 'static)> {
255        Some(match self {
256            ClientError::BuildClient(err) => err,
257        })
258    }
259}
260
261impl fmt::Display for ClientError {
262    fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
263        match self {
264            ClientError::BuildClient(err) => write!(out, "can not build HTTP client: {err}"),
265        }
266    }
267}
268
269/// Represents errors that can occur while attempting
270/// to download a file using the Telegram Bot API client.
271#[derive(Debug)]
272pub enum DownloadFileError {
273    /// An error indicating a failure to send an HTTP request.
274    Http(HttpError),
275    /// An error received from the server in response to the download request.
276    Response {
277        /// The HTTP status code received in the response.
278        status: u16,
279        /// The body of the response as a string.
280        text: String,
281    },
282}
283
284impl From<HttpError> for DownloadFileError {
285    fn from(err: HttpError) -> Self {
286        Self::Http(err)
287    }
288}
289
290impl Error for DownloadFileError {
291    fn source(&self) -> Option<&(dyn Error + 'static)> {
292        match self {
293            DownloadFileError::Http(err) => Some(err),
294            _ => None,
295        }
296    }
297}
298
299impl fmt::Display for DownloadFileError {
300    fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
301        match self {
302            DownloadFileError::Http(err) => write!(out, "failed to download file: {err}"),
303            DownloadFileError::Response { status, text } => {
304                write!(out, "failed to download file: status={status} text={text}")
305            }
306        }
307    }
308}
309
310/// Represents errors that can occur during the execution
311/// of a method using the Telegram Bot API client.
312#[derive(Debug, derive_more::From)]
313pub enum ExecuteError {
314    /// An error indicating a failure to send an HTTP request.
315    Http(HttpError),
316    /// An error indicating a failure to build an HTTP request payload.
317    Payload(PayloadError),
318    /// An error received from the Telegram server in response to the execution request.
319    Response(ResponseError),
320}
321
322impl Error for ExecuteError {
323    fn source(&self) -> Option<&(dyn Error + 'static)> {
324        use self::ExecuteError::*;
325        Some(match self {
326            Http(err) => err,
327            Payload(err) => err,
328            Response(err) => err,
329        })
330    }
331}
332
333impl fmt::Display for ExecuteError {
334    fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
335        use self::ExecuteError::*;
336        write!(
337            out,
338            "failed to execute method: {}",
339            match self {
340                Http(err) => err.to_string(),
341                Payload(err) => err.to_string(),
342                Response(err) => err.to_string(),
343            }
344        )
345    }
346}