telexide/api/
api_client.rs

1use super::{api::API, endpoints::APIEndpoint, response::Response};
2use crate::utils::{
3    encode_multipart_form_data,
4    result::Result,
5    AsFormData,
6    FormDataFile,
7    BOUNDARY,
8};
9use async_trait::async_trait;
10use hyper::{body::HttpBody, client::HttpConnector, Body, Client, Request};
11use std::io::Write;
12
13static TELEGRAM_API: &str = "https://api.telegram.org/bot";
14
15#[cfg(feature = "native-tls")]
16pub type TlsClient = Client<hyper_tls::HttpsConnector<HttpConnector>>;
17#[cfg(all(feature = "rustls", not(feature = "native-tls")))]
18pub type TlsClient = Client<hyper_rustls::HttpsConnector<HttpConnector>>;
19
20/// A default implementation of the [`API`] trait.
21///
22/// It requires your bot token in order to interact with the telegram API and
23/// also allows you to configure your own [`hyper::Client`] for it to use.
24///
25/// Using the default `APIClient` is as easy as:
26/// ```no_run
27/// use telexide::api::{APIClient, API, types::SendMessage};
28///
29/// # #[tokio::main]
30/// # async fn main() {
31///     # let token = "test token";
32///     # let chat_id = telexide::model::IntegerOrString::Integer(3);
33///     let message = SendMessage::new(chat_id, "hi!");
34///
35///     let client = APIClient::new_default(token);
36///     client.send_message(message).await;
37/// # }
38/// ```
39///
40/// In most cases you would want to get updates though and the [`Client`] is
41/// best suited for that, as it allows for easier handling of those updates
42///
43/// [`Client`]: ../client/struct.Client.html
44pub struct APIClient {
45    hyper_client: TlsClient,
46    token: String,
47}
48
49impl APIClient {
50    /// Creates a new `APIClient` with the provided token and hyper client (if
51    /// it is Some).
52    #[allow(clippy::needless_pass_by_value)]
53    pub fn new(hyper_client: Option<TlsClient>, token: impl ToString) -> Self {
54        hyper_client.map_or_else(
55            || Self {
56                hyper_client: Self::make_default_client(),
57                token: token.to_string(),
58            },
59            |c| Self {
60                hyper_client: c,
61                token: token.to_string(),
62            },
63        )
64    }
65
66    #[cfg(feature = "native-tls")]
67    fn make_default_client() -> TlsClient {
68        hyper::Client::builder().build(hyper_tls::HttpsConnector::new())
69    }
70
71    #[cfg(all(feature = "rustls", not(feature = "native-tls")))]
72    fn make_default_client() -> TlsClient {
73        hyper::Client::builder().build(
74            hyper_rustls::HttpsConnectorBuilder::new()
75                .with_native_roots()
76                .https_or_http()
77                .enable_http1()
78                .build(),
79        )
80    }
81
82    /// Creates a new `APIClient` with the provided token and the default hyper
83    /// client.
84    #[allow(clippy::needless_pass_by_value)]
85    pub fn new_default(token: impl ToString) -> Self {
86        Self::new(None, token)
87    }
88
89    fn parse_endpoint(&self, endpoint: &APIEndpoint) -> String {
90        format!("{}{}/{}", TELEGRAM_API, self.token, endpoint)
91    }
92
93    /// Sends a request to the provided `APIEndpoint` with the data provided
94    /// (does not support files)
95    pub async fn request<D>(&self, endpoint: APIEndpoint, data: Option<&D>) -> Result<Response>
96    where
97        D: ?Sized + serde::Serialize,
98    {
99        let data: Option<serde_json::Value> = if let Some(d) = data {
100            Some(serde_json::to_value(d)?)
101        } else {
102            None
103        };
104
105        match endpoint {
106            e if e.as_str().starts_with("get") => self.get(e, data).await,
107            e => self.post(e, data).await,
108        }
109    }
110
111    /// gets a reference to the underlying hyper client, for example so you can
112    /// make custom api requests
113    pub fn get_hyper(&self) -> &TlsClient {
114        &self.hyper_client
115    }
116}
117
118#[async_trait]
119impl API for APIClient {
120    async fn get(
121        &self,
122        endpoint: APIEndpoint,
123        data: Option<serde_json::Value>,
124    ) -> Result<Response> {
125        let req_builder = Request::get(self.parse_endpoint(&endpoint))
126            .header("content-type", "application/json")
127            .header("accept", "application/json");
128
129        let request = if let Some(d) = data {
130            req_builder.body(Body::from(serde_json::to_string(&d)?))?
131        } else {
132            req_builder.body(Body::empty())?
133        };
134
135        log::debug!("GET request to {}", &endpoint);
136        let mut response = self.hyper_client.request(request).await?;
137
138        let mut res: Vec<u8> = Vec::new();
139        while let Some(chunk) = response.body_mut().data().await {
140            res.write_all(&chunk?)?;
141        }
142
143        Ok(serde_json::from_slice(&res)?)
144    }
145
146    async fn post(
147        &self,
148        endpoint: APIEndpoint,
149        data: Option<serde_json::Value>,
150    ) -> Result<Response> {
151        let req_builder = Request::post(self.parse_endpoint(&endpoint))
152            .header("content-type", "application/json")
153            .header("accept", "application/json");
154
155        let request = if let Some(d) = data {
156            req_builder.body(Body::from(serde_json::to_string(&d)?))?
157        } else {
158            req_builder.body(Body::empty())?
159        };
160
161        log::debug!("POST request to {}", &endpoint);
162        let mut response = self.hyper_client.request(request).await?;
163
164        let mut res: Vec<u8> = Vec::new();
165        while let Some(chunk) = response.body_mut().data().await {
166            res.write_all(&chunk?)?;
167        }
168
169        Ok(serde_json::from_slice(&res)?)
170    }
171
172    async fn post_file(
173        &self,
174        endpoint: APIEndpoint,
175        data: Option<serde_json::Value>,
176        files: Option<Vec<FormDataFile>>,
177    ) -> Result<Response> {
178        if files.is_none() {
179            return self.post(endpoint, data).await;
180        }
181
182        let mut files = files.expect("no files");
183        if files.is_empty() {
184            return self.post(endpoint, data).await;
185        }
186
187        let req_builder = Request::post(self.parse_endpoint(&endpoint))
188            .header(
189                "content-type",
190                format!("multipart/form-data; boundary={BOUNDARY}"),
191            )
192            .header("accept", "application/json");
193
194        if data.is_some() {
195            files.append(&mut data.expect("no data").as_form_data()?);
196        }
197
198        let bytes = encode_multipart_form_data(&files)?;
199        let request = req_builder.body(Body::from(bytes))?;
200
201        log::debug!("POST request with files to {}", &endpoint);
202        let mut response = self.hyper_client.request(request).await?;
203
204        let mut res: Vec<u8> = Vec::new();
205        while let Some(chunk) = response.body_mut().data().await {
206            res.write_all(&chunk?)?;
207        }
208
209        Ok(serde_json::from_slice(&res)?)
210    }
211}