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