Skip to main content

teloxide_core_ng/
bot.rs

1use std::{future::Future, sync::Arc};
2
3use reqwest::Client;
4use serde::{Serialize, de::DeserializeOwned};
5
6use crate::{
7    net,
8    requests::{MultipartPayload, Payload, ResponseResult},
9    serde_multipart,
10};
11
12mod api;
13mod download;
14
15const TELOXIDE_TOKEN: &str = "TELOXIDE_TOKEN";
16const TELOXIDE_API_URL: &str = "TELOXIDE_API_URL";
17
18/// A requests sender.
19///
20/// This is the main type of the library, it allows to send requests to the
21/// [Telegram Bot API] and download files.
22///
23/// ## TBA methods
24///
25/// All TBA methods are located in the [`Requester`] [`impl for Bot`]. This
26/// allows for opt-in behaviours using requester [adaptors].
27///
28/// ```
29/// # async {
30/// use teloxide_core_ng::prelude::*;
31///
32/// let bot = Bot::new("TOKEN");
33/// dbg!(bot.get_me().await?);
34/// # Ok::<_, teloxide_core_ng::RequestError>(()) };
35/// ```
36///
37/// [`Requester`]: crate::requests::Requester
38/// [`impl for Bot`]: Bot#impl-Requester
39/// [adaptors]: crate::adaptors
40///
41/// ## File download
42///
43/// In the similar way as with TBA methods, file downloading methods are located
44/// in a trait — [`Download<'_>`]. See its documentation for more.
45///
46/// [`Download<'_>`]: crate::net::Download
47///
48/// ## Clone cost
49///
50/// `Bot::clone` is relatively cheap, so if you need to share `Bot`, it's
51/// recommended to clone it, instead of wrapping it in [`Arc<_>`].
52///
53/// [`Arc`]: std::sync::Arc
54/// [Telegram Bot API]: https://core.telegram.org/bots/api
55#[must_use]
56#[derive(Debug, Clone)]
57pub struct Bot {
58    token: Arc<str>,
59    api_url: Arc<reqwest::Url>,
60    client: Client,
61}
62
63/// Constructors
64impl Bot {
65    /// Creates a new `Bot` with the specified token and the default
66    /// [http-client](reqwest::Client).
67    ///
68    /// # Panics
69    ///
70    /// If it cannot create [`reqwest::Client`].
71    pub fn new<S>(token: S) -> Self
72    where
73        S: Into<String>,
74    {
75        let client = net::default_reqwest_settings().build().expect("Client creation failed");
76
77        Self::with_client(token, client)
78    }
79
80    /// Creates a new `Bot` with the specified token and your
81    /// [`reqwest::Client`].
82    ///
83    /// # Caution
84    ///
85    /// Your custom client might not be configured correctly to be able to work
86    /// in long time durations, see [issue 223].
87    ///
88    /// [`reqwest::Client`]: https://docs.rs/reqwest/latest/reqwest/struct.Client.html
89    /// [issue 223]: https://github.com/teloxide/teloxide/issues/223
90    pub fn with_client<S>(token: S, client: Client) -> Self
91    where
92        S: Into<String>,
93    {
94        let token = Into::<String>::into(token).into();
95        let api_url = Arc::new(
96            reqwest::Url::parse(net::TELEGRAM_API_URL)
97                .expect("Failed to parse the default TBA URL"),
98        );
99
100        Self { token, api_url, client }
101    }
102
103    /// Creates a new `Bot` with the `TELOXIDE_TOKEN` & `TELOXIDE_API_URL` &
104    /// `TELOXIDE_PROXY` environmental variables (the bot's token & the bot's
105    /// API URL & the proxy) and the default [`reqwest::Client`].
106    ///
107    /// If `TELOXIDE_API_URL` doesn't exist, returns to the default TBA URL.
108    ///
109    /// This function passes the value of `TELOXIDE_PROXY` into
110    /// [`reqwest::Proxy::all`], if it exists, otherwise returns the default
111    /// client.
112    ///
113    /// # Panics
114    ///  - If cannot get the `TELOXIDE_TOKEN`  environmental variable.
115    ///  - If `TELOXIDE_API_URL` exists, but isn't a correct URL.
116    ///  - If it cannot create [`reqwest::Client`].
117    ///
118    /// [`reqwest::Client`]: https://docs.rs/reqwest/0.10.1/reqwest/struct.Client.html
119    /// [`reqwest::Proxy::all`]: https://docs.rs/reqwest/latest/reqwest/struct.Proxy.html#method.all
120    pub fn from_env() -> Self {
121        Self::from_env_with_client(crate::net::client_from_env())
122    }
123
124    /// Creates a new `Bot` with the `TELOXIDE_TOKEN` environmental variable
125    /// (the bot's token), `TELOXIDE_API_URL` environmental variable (the bot's
126    /// API URL) and your [`reqwest::Client`].
127    ///
128    /// If `TELOXIDE_API_URL` doesn't exist, returns to the default TBA URL.
129    ///
130    /// # Panics
131    ///  - If cannot get the `TELOXIDE_TOKEN` environmental variable.
132    ///  - If `TELOXIDE_API_URL` exists, but isn't a correct URL.
133    ///
134    /// # Caution
135    /// Your custom client might not be configured correctly to be able to work
136    /// in long time durations, see [issue 223].
137    ///
138    /// [`reqwest::Client`]: https://docs.rs/reqwest/0.10.1/reqwest/struct.Client.html
139    /// [issue 223]: https://github.com/teloxide/teloxide/issues/223
140    pub fn from_env_with_client(client: Client) -> Self {
141        let bot = Self::with_client(get_env(TELOXIDE_TOKEN), client);
142
143        match std::env::var(TELOXIDE_API_URL) {
144            Ok(env_api_url) => {
145                // Remove the trailing slash if it exists
146                let env_api_url = if env_api_url.ends_with('/') {
147                    env_api_url.trim_end_matches('/')
148                } else {
149                    &env_api_url
150                };
151                let api_url = reqwest::Url::parse(env_api_url)
152                    .expect("Failed to parse the `TELOXIDE_API_URL` env variable");
153                bot.set_api_url(api_url)
154            }
155            Err(_) => bot,
156        }
157    }
158
159    /// Sets a custom API URL.
160    ///
161    /// For example, you can run your own [TBA server][tbas] and set its URL
162    /// using this method.
163    ///
164    /// [tbas]: https://github.com/tdlib/telegram-bot-api
165    ///
166    /// ## Examples
167    ///
168    /// ```
169    /// use teloxide_core_ng::{
170    ///     Bot,
171    ///     requests::{Request, Requester},
172    /// };
173    ///
174    /// # async {
175    /// let url = reqwest::Url::parse("https://localhost/tbas").unwrap();
176    /// let bot = Bot::new("TOKEN").set_api_url(url);
177    /// // From now all methods will use "https://localhost/tbas" as an API URL.
178    /// bot.get_me().await
179    /// # };
180    /// ```
181    ///
182    /// ## Multi-instance behaviour
183    ///
184    /// This method only sets the URL for one bot instace, older clones are
185    /// unaffected.
186    ///
187    /// ```
188    /// use teloxide_core_ng::Bot;
189    ///
190    /// let bot = Bot::new("TOKEN");
191    /// let bot2 = bot.clone();
192    /// let bot = bot.set_api_url(reqwest::Url::parse("https://example.com/").unwrap());
193    ///
194    /// assert_eq!(bot.api_url().as_str(), "https://example.com/");
195    /// assert_eq!(bot.clone().api_url().as_str(), "https://example.com/");
196    /// assert_ne!(bot2.api_url().as_str(), "https://example.com/");
197    /// ```
198    pub fn set_api_url(mut self, url: reqwest::Url) -> Self {
199        self.api_url = Arc::new(url);
200        self
201    }
202}
203
204/// Getters
205impl Bot {
206    /// Returns currently used token.
207    #[must_use]
208    pub fn token(&self) -> &str {
209        &self.token
210    }
211
212    /// Returns currently used http-client.
213    #[must_use]
214    pub fn client(&self) -> &Client {
215        &self.client
216    }
217
218    /// Returns currently used token API URL.
219    #[must_use]
220    pub fn api_url(&self) -> reqwest::Url {
221        reqwest::Url::clone(&*self.api_url)
222    }
223}
224
225impl Bot {
226    pub(crate) fn execute_json<P>(
227        &self,
228        payload: &P,
229    ) -> impl Future<Output = ResponseResult<P::Output>> + 'static
230    where
231        P: Payload + Serialize,
232        P::Output: DeserializeOwned + 'static,
233    {
234        let client = self.client.clone();
235        let token = Arc::clone(&self.token);
236        let api_url = Arc::clone(&self.api_url);
237
238        let timeout_hint = payload.timeout_hint();
239        let params = stacker::maybe_grow(256 * 1024, 1024 * 1024, || serde_json::to_vec(payload))
240            // this `expect` should be ok since we don't write request those may trigger error here
241            .expect("serialization of request to be infallible");
242
243        // async move to capture client&token&api_url&params
244        async move {
245            net::request_json(
246                &client,
247                token.as_ref(),
248                reqwest::Url::clone(&*api_url),
249                P::NAME,
250                params,
251                timeout_hint,
252            )
253            .await
254        }
255    }
256
257    pub(crate) fn execute_multipart<P>(
258        &self,
259        payload: &mut P,
260    ) -> impl Future<Output = ResponseResult<P::Output>>
261    where
262        P: MultipartPayload + Serialize,
263        P::Output: DeserializeOwned + 'static,
264    {
265        let client = self.client.clone();
266        let token = Arc::clone(&self.token);
267        let api_url = Arc::clone(&self.api_url);
268
269        let timeout_hint = payload.timeout_hint();
270        let params = serde_multipart::to_form(payload);
271
272        // async move to capture client&token&api_url&params
273        async move {
274            let params = params?.await;
275            net::request_multipart(
276                &client,
277                token.as_ref(),
278                reqwest::Url::clone(&*api_url),
279                P::NAME,
280                params,
281                timeout_hint,
282            )
283            .await
284        }
285    }
286
287    pub(crate) fn execute_multipart_ref<P>(
288        &self,
289        payload: &P,
290    ) -> impl Future<Output = ResponseResult<P::Output>>
291    where
292        P: MultipartPayload + Serialize,
293        P::Output: DeserializeOwned + 'static,
294    {
295        let client = self.client.clone();
296        let token = Arc::clone(&self.token);
297        let api_url = self.api_url.clone();
298
299        let timeout_hint = payload.timeout_hint();
300        let params = serde_multipart::to_form_ref(payload);
301
302        // async move to capture client&token&api_url&params
303        async move {
304            let params = params?.await;
305            net::request_multipart(
306                &client,
307                token.as_ref(),
308                reqwest::Url::clone(&*api_url),
309                P::NAME,
310                params,
311                timeout_hint,
312            )
313            .await
314        }
315    }
316}
317
318fn get_env(env: &'static str) -> String {
319    std::env::var(env).unwrap_or_else(|_| panic!("Cannot get the {env} env variable"))
320}