1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
use std::fmt::{self, Formatter};

use anyhow::Result;
use derive_more::*;
use serde::{de::DeserializeOwned, Serialize};

use crate::ApiResponse;

/// This is a wrapper around the Telegram API token string. Get your token from
/// [@BotFather](https://t.me/BotFather).
#[derive(Debug, Clone, From, Into, FromStr, Display)]
pub struct ApiToken(String);

/// PostFn lets you override the default POST request handler. This is useful
/// for testing.
pub struct PostFn(pub Box<dyn Fn(String, String) -> Result<String> + Send + Sync>);

/// From trait for PostFn to make it easier to use.
impl<T> From<T> for PostFn
where
    T: Fn(String, String) -> Result<String> + Send + Sync + 'static,
{
    fn from(f: T) -> Self {
        Self(Box::new(f))
    }
}

/// Debug trait for PostFn (because Client derives Debug)
impl fmt::Debug for PostFn {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        f.write_str("PostFn")
    }
}

#[async_trait::async_trait]
pub trait Post {
    async fn post(&self, method: String, req: String) -> Result<String>;
}

/// This is a thin shim around the Telegram HTTP client. Requires a valid API token.
pub struct Client {
    /// This base URL is used for all requests and is constructed from the
    /// provided API token.
    base_url: String,

    /// The underlying HTTP client.
    client: reqwest::Client,

    /// A post handler that implements the Post trait. Useful for testing.
    post_handler: Option<Box<dyn Post + Send + Sync>>,

    /// A function that handles POST requests. This is useful for testing.
    post_handler_fn: Option<PostFn>,
}

impl Client {
    /// Returns a new Telegram API client.
    pub fn new(token: ApiToken) -> Self {
        Self {
            base_url: format!("https://api.telegram.org/bot{token}"),
            client: reqwest::Client::new(),
            post_handler: None,
            post_handler_fn: None,
        }
    }

    /// Sets a function that handles POST requests. This is useful for testing.
    pub fn with_post_handler_fn(mut self, post_fn: impl Into<PostFn>) -> Self {
        self.post_handler_fn = Some(post_fn.into());
        self
    }

    pub fn with_post_handler(mut self, post_handler: impl Post + Send + Sync + 'static) -> Self {
        self.post_handler = Some(Box::new(post_handler));
        self
    }

    /// Send `method` with `req` as the request body to the Telegram API.
    pub async fn post<Req, Resp>(&self, method: &str, req: &Req) -> Result<Resp>
    where
        Req: crate::Request,
        Resp: Serialize + DeserializeOwned + Clone,
    {
        let body;
        if let Some(ref post_handler) = self.post_handler_fn {
            body = (post_handler.0)(method.to_string(), serde_json::to_string(req)?).unwrap();
        } else if let Some(ref post_handler) = self.post_handler {
            body = post_handler
                .post(method.to_string(), serde_json::to_string(req)?)
                .await?;
        } else {
            debug!(
                "POST /{}:\n{}",
                method,
                serde_json::to_string_pretty(req).unwrap()
            );
            body = self
                .client
                .post(format!("{}/{}", self.base_url, method))
                .json(&req)
                .send()
                .await?
                .text()
                .await?;
        }
        let response = ApiResponse::<Resp>::from_str(&body)?;
        debug!(
            "Response /{}:\n{}",
            method,
            serde_json::to_string_pretty(&response).unwrap()
        );
        Ok(response.result()?.clone())
    }
}