Skip to main content

ronami_core/
bot.rs

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