Skip to main content

matrix_bot_sdk/
http.rs

1use anyhow::Context;
2use async_trait::async_trait;
3use reqwest::Method;
4use serde::de::DeserializeOwned;
5use serde_json::Value;
6use url::Url;
7
8#[derive(Debug, Clone)]
9pub struct MatrixHttp {
10    client: reqwest::Client,
11    homeserver: Url,
12}
13
14impl MatrixHttp {
15    pub fn new(homeserver: Url) -> Self {
16        Self {
17            client: reqwest::Client::new(),
18            homeserver,
19        }
20    }
21
22    pub fn homeserver(&self) -> &Url {
23        &self.homeserver
24    }
25
26    pub async fn send_json(
27        &self,
28        method: Method,
29        endpoint: &str,
30        access_token: Option<&str>,
31        body: Option<&Value>,
32    ) -> anyhow::Result<Value> {
33        let url = self.url(endpoint)?;
34        let mut req = self.client.request(method, url);
35        if let Some(token) = access_token {
36            req = req.bearer_auth(token);
37        }
38        if let Some(payload) = body {
39            req = req.json(payload);
40        }
41        let response = req.send().await?;
42        let status = response.status();
43        if status == reqwest::StatusCode::NO_CONTENT {
44            return Ok(Value::Null);
45        }
46        let payload: Value = response
47            .json()
48            .await
49            .with_context(|| format!("failed to decode Matrix response with status {status}"))?;
50        Ok(payload)
51    }
52
53    fn url(&self, endpoint: &str) -> anyhow::Result<Url> {
54        let normalized = endpoint.trim_start_matches('/');
55        Ok(self.homeserver.join(normalized)?)
56    }
57}
58
59#[async_trait]
60pub trait Request {
61    type Response: DeserializeOwned + Send + Sync + 'static;
62
63    fn method(&self) -> Method;
64    fn endpoint(&self) -> &str;
65    fn body(&self) -> Option<Value> {
66        None
67    }
68}
69
70pub async fn request<R: Request>(
71    http: &MatrixHttp,
72    req: &R,
73    access_token: Option<&str>,
74) -> anyhow::Result<R::Response> {
75    let json = http
76        .send_json(
77            req.method(),
78            req.endpoint(),
79            access_token,
80            req.body().as_ref(),
81        )
82        .await?;
83    Ok(serde_json::from_value(json)?)
84}
85
86pub mod b64 {
87    use base64::Engine;
88    use base64::engine::general_purpose::STANDARD_NO_PAD;
89
90    pub fn encode_unpadded(bytes: &[u8]) -> String {
91        STANDARD_NO_PAD.encode(bytes)
92    }
93
94    pub fn decode_unpadded(value: &str) -> anyhow::Result<Vec<u8>> {
95        Ok(STANDARD_NO_PAD.decode(value)?)
96    }
97}
98
99pub mod simple_validation {
100    pub fn required_non_empty(name: &'static str, value: &str) -> anyhow::Result<()> {
101        if value.trim().is_empty() {
102            anyhow::bail!("{name} cannot be empty");
103        }
104        Ok(())
105    }
106
107    pub fn ensure_prefix(
108        value: &str,
109        prefix: &str,
110        field_name: &'static str,
111    ) -> anyhow::Result<()> {
112        if !value.starts_with(prefix) {
113            anyhow::bail!("{field_name} must start with '{prefix}'");
114        }
115        Ok(())
116    }
117}