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}