• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

polyphony-chat / polyproto-rs / #90

18 Jun 2025 09:45AM UTC coverage: 64.119% (-9.7%) from 73.783%
#90

push

bitfl0wer
feat: add faq shield

1401 of 2185 relevant lines covered (64.12%)

1.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

56.45
/src/api/mod.rs
1
// This Source Code Form is subject to the terms of the Mozilla Public
2
// License, v. 2.0. If a copy of the MPL was not distributed with this
3
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4

5
/// The `core` module contains all API routes for implementing the core polyproto protocol in a client or server,
6
/// as well as some additional, useful types.
7
pub mod core;
8

9
/// Module containing code for cacheable ID-Certs.
10
#[cfg(feature = "types")]
11
pub mod cacheable_cert;
12

13
#[cfg(feature = "reqwest")]
14
pub use http_client::*;
15

16
#[cfg(feature = "reqwest")]
17
pub(crate) mod http_client {
18
    use std::fmt::Debug;
19
    use std::sync::Arc;
20

21
    use http::{Method, StatusCode};
22
    use reqwest::multipart::Form;
23
    use reqwest::{Client, Request, RequestBuilder, Response};
24
    use serde::Deserialize;
25
    use serde_json::{Value, from_str, json};
26
    use url::Url;
27

28
    use crate::certs::idcert::IdCert;
29
    use crate::errors::{InvalidInput, RequestError};
30
    use crate::key::PrivateKey;
31
    use crate::signature::Signature;
32
    use crate::types::keytrial::KeyTrialResponse;
33
    use crate::types::routes::Route;
34

35
    pub(crate) trait SendsRequest {
36
        async fn send_request(&self, request: Request) -> HttpResult<Response>;
37
        fn get_client(&self) -> Client;
38
    }
39

40
    impl SendsRequest for &HttpClient {
41
        async fn send_request(&self, request: Request) -> HttpResult<Response> {
×
42
            self.client
×
43
                .execute(request)
44
                .await
×
45
                .map_err(crate::errors::composite::RequestError::HttpError)
46
        }
47

48
        fn get_client(&self) -> Client {
×
49
            self.client.clone()
×
50
        }
51
    }
52

53
    impl<S: Signature, T: PrivateKey<S>> SendsRequest for &Session<S, T> {
54
        async fn send_request(&self, request: Request) -> HttpResult<Response> {
4✔
55
            self.get_client()
6✔
56
                .execute(request)
1✔
57
                .await
4✔
58
                .map_err(crate::errors::composite::RequestError::HttpError)
1✔
59
        }
60

61
        fn get_client(&self) -> Client {
1✔
62
            self.client.client.clone()
1✔
63
        }
64
    }
65

66
    pub(crate) struct P2RequestBuilder<'a, T: SendsRequest> {
67
        homeserver: Option<Url>,
68
        key_trials: Vec<KeyTrialResponse>,
69
        sensitive_solution: Option<String>,
70
        body: Option<Value>,
71
        auth_token: Option<String>,
72
        multipart: Option<Form>,
73
        endpoint: Route,
74
        replace_endpoint_substr: Vec<(String, String)>,
75
        query: Vec<(String, String)>,
76
        client: &'a T,
77
    }
78

79
    impl<'a, T: SendsRequest> P2RequestBuilder<'a, T> {
80
        /// Construct a new [P2RequestBuilder].
81
        pub(crate) fn new(client: &'a T) -> Self {
1✔
82
            Self {
83
                homeserver: None,
84
                client,
85
                endpoint: Route {
1✔
86
                    method: Method::default(),
87
                    path: "",
88
                },
89
                key_trials: Vec::new(),
1✔
90
                sensitive_solution: None,
91
                body: None,
92
                auth_token: None,
93
                multipart: None,
94
                query: Vec::new(),
1✔
95
                replace_endpoint_substr: Vec::new(),
1✔
96
            }
97
        }
98

99
        pub(crate) fn homeserver(mut self, url: Url) -> Self {
1✔
100
            self.homeserver = Some(url);
2✔
101
            self
1✔
102
        }
103

104
        /// Adds key trials to the response if none were added before. Replaces the currently stored
105
        /// vector of key trials with the one passed in this function, if one has been added before.
106
        pub(crate) fn key_trials(mut self, key_trials: Vec<KeyTrialResponse>) -> Self {
×
107
            self.key_trials = key_trials;
×
108
            self
×
109
        }
110

111
        /// Add a P2 sensitive solution if none was added before. Replaces the previously stored
112
        /// sensitive solution, if applicable.
113
        pub(crate) fn sensitive_solution(mut self, sensitive_solution: String) -> Self {
×
114
            self.sensitive_solution = Some(sensitive_solution);
×
115
            self
×
116
        }
117

118
        /// Add a request body to the request. Replaces the previously stored
119
        /// body, if applicable.
120
        ///
121
        /// ## Errors
122
        ///
123
        /// This function is infallible. However, building the request using `self.build()` will fail,
124
        /// if *both* a body and a multipart are present in the [P2RequestBuilder].
125
        pub(crate) fn body(mut self, body: Value) -> Self {
×
126
            self.body = Some(body); // once told me the world was gonna roll me
127
            self
×
128
        }
129

130
        /// Authorize using a Bearer token. The "Bearer " prefix will be added by `reqwest`. Replaces
131
        /// the previously stored token, if applicable.
132
        pub(crate) fn auth_token(mut self, token: String) -> Self {
1✔
133
            self.auth_token = Some(token);
2✔
134
            self
1✔
135
        }
136

137
        /// Add a multipart form to the request. Replaces the previously stored
138
        /// multipart, if applicable.
139
        ///
140
        /// ## Errors
141
        ///
142
        /// This function is infallible. However, building the request using `self.build()` will fail,
143
        /// if *both* a body and a multipart are present in the [P2RequestBuilder].
144
        pub(crate) fn multipart(mut self, form: Form) -> Self {
×
145
            self.multipart = Some(form);
×
146
            self
×
147
        }
148

149
        /// Set the endpoint of this request by supplying a [Route]. Replaces
150
        /// the previously selected route, if applicable.
151
        pub(crate) fn endpoint(mut self, route: Route) -> Self {
1✔
152
            self.endpoint = route;
2✔
153
            self
1✔
154
        }
155

156
        /// Add a substring replacement to this route. Multiple calls to this method will mean that
157
        /// multiple substring replacements will be performed. Replacements are done in FIFO order.
158
        ///
159
        /// Some [Route]s have path-query placeholders like `{rid}` or `{fid}`. If you are building
160
        /// a request to such a route, you will have to add a substring replacement using this method
161
        /// to send the request to the correct endpoint.
162
        ///
163
        /// ## Example
164
        ///
165
        /// ```rs
166
        /// let mut request_builder = Client::get_request_builder(method, url);
167
        /// request_builder.replace_endpoint_substr(r#"{rid}"#, "actual-resource-id");
168
        /// ```
169
        pub(crate) fn replace_endpoint_substr(mut self, from: &str, to: &str) -> Self {
×
170
            self.replace_endpoint_substr
×
171
                .push((from.to_string(), to.to_string()));
×
172
            self
×
173
        }
174

175
        /// Add a query parameter to this route. Does not replace previous query parameters.
176
        pub(crate) fn query(mut self, key: &str, value: &str) -> Self {
1✔
177
            self.query.push((key.to_string(), value.to_string()));
2✔
178
            self
1✔
179
        }
180

181
        /// Build the request. Fails, if both a body and a multipart are set, or if `reqwest` cannot
182
        /// build the request for any reason.
183
        pub(crate) fn build(self) -> Result<Request, InvalidInput> {
1✔
184
            if self.homeserver.is_none() {
2✔
185
                return Err(InvalidInput::Malformed(
×
186
                    "You forgot to set a homeserver URL".to_string(),
×
187
                ));
188
            }
189
            let mut path = self.endpoint.path.to_string();
1✔
190
            for (from, to) in self.replace_endpoint_substr.iter() {
2✔
191
                path = path.replace(from, to);
×
192
            }
193
            let url = self
2✔
194
                .homeserver
×
195
                .unwrap()
196
                .join(&path)
1✔
197
                .map_err(|e| InvalidInput::Malformed(e.to_string()))?;
1✔
198
            let mut request = self.client.get_client().request(self.endpoint.method, url);
1✔
199
            if let Some(token) = self.auth_token {
2✔
200
                request = request.bearer_auth(token);
2✔
201
            }
202
            if self.body.is_some() && self.multipart.is_some() {
2✔
203
                return Err(InvalidInput::Malformed(
×
204
                    "Cannot have both multipart and body in a request".to_string(),
×
205
                ));
206
            }
207
            if let Some(body) = self.body {
1✔
208
                request = request.body(body.to_string());
×
209
            } else if let Some(multipart) = self.multipart {
1✔
210
                request = request.multipart(multipart);
×
211
            }
212

213
            if !self.key_trials.is_empty() {
2✔
214
                request = request.header("X-P2-core-keytrial", json!(self.key_trials).to_string());
×
215
            }
216

217
            if let Some(sensitive_solution) = self.sensitive_solution {
1✔
218
                request = request.header("X-P2-sensitive-solution", sensitive_solution)
×
219
            }
220

221
            for (key, value) in self.query.iter() {
3✔
222
                request = request.query(&[(key, value)]);
2✔
223
            }
224

225
            request
1✔
226
                .build()
227
                .map_err(|e| InvalidInput::Malformed(e.to_string()))
×
228
        }
229
    }
230

231
    #[derive(Debug, Clone)]
232
    /// A client for making HTTP requests to a polyproto home server. Stores headers such as the
233
    /// authentication token, and the base URL of the server. Both the headers and the URL can be
234
    /// modified after the client is created. However, the intended use case is to create one client
235
    /// per actor, and use it for all requests made by that actor.
236
    ///
237
    /// # Example
238
    ///
239
    /// ```rs
240
    /// let mut header_map = reqwest::header::HeaderMap::new();
241
    /// header_map.insert("Authorization", "nx8r902hjkxlo2n8n72x0");
242
    /// let client = HttpClient::new("https://example.com").unwrap();
243
    /// client.headers(header_map);
244
    /// ```
245
    pub struct HttpClient {
246
        /// The reqwest client used to make requests.
247
        pub client: reqwest::Client,
248
        headers: reqwest::header::HeaderMap,
249
    }
250

251
    /// A type alias for the result of an HTTP request.
252
    pub type HttpResult<T> = Result<T, RequestError>;
253

254
    impl HttpClient {
255
        /// Creates a new instance of the client with no further configuration. To access routes which
256
        /// require authentication, you must set the authentication header using the `headers` method.
257
        ///
258
        /// # Arguments
259
        ///
260
        /// * `url` - The base URL of a polyproto home server.
261
        ///
262
        /// # Errors
263
        ///
264
        /// Will fail if the URL is invalid or if there are issues creating the reqwest client.
265
        pub fn new() -> HttpResult<Self> {
2✔
266
            #[cfg(target_arch = "wasm32")]
267
            let client = reqwest::ClientBuilder::new()
268
                .user_agent(format!("polyproto-rs/{}", env!("CARGO_PKG_VERSION")))
269
                .build()?;
270
            #[cfg(not(target_arch = "wasm32"))]
271
            let client = reqwest::ClientBuilder::new()
6✔
272
                .zstd(true)
273
                .user_agent(format!("polyproto-rs/{}", env!("CARGO_PKG_VERSION")))
4✔
274
                .build()?;
275
            let headers = reqwest::header::HeaderMap::new();
2✔
276

277
            Ok(Self { client, headers })
2✔
278
        }
279

280
        /// Creates a new instance of the client with the specified arguments. To access routes which
281
        /// require authentication, you must set an authentication header.
282
        ///
283
        /// # Arguments
284
        ///
285
        /// * `url` - The base URL of a polyproto home server.
286
        /// * `headers`: [reqwest::header::HeaderMap]
287
        /// * `version`: Version of the HTTP spec
288
        /// * `zstd_compression`: Whether to use zstd compression for responses.
289
        ///
290
        /// # Errors
291
        ///
292
        /// Will fail if the URL is invalid or if there are issues creating the reqwest client.
293
        #[cfg(not(target_arch = "wasm32"))] // WASM doesn't support zstd, so this function can just be left out
294
        pub fn new_with_args(
×
295
            headers: reqwest::header::HeaderMap,
296
            zstd_compression: bool,
297
        ) -> HttpResult<Self> {
298
            let client = reqwest::ClientBuilder::new()
×
299
                .zstd(zstd_compression)
×
300
                .user_agent(format!("polyproto-rs/{}", env!("CARGO_PKG_VERSION")))
×
301
                .build()?;
302
            Ok(Self { client, headers })
×
303
        }
304

305
        /// Sets the headers for the client.
306
        pub fn headers(&mut self, headers: reqwest::header::HeaderMap) {
×
307
            self.headers = headers;
×
308
        }
309

310
        /// Boilerplate reducing request builder when using [Route]s to make basic requests.
311
        pub(crate) fn request_route(
1✔
312
            &self,
313
            instance_url: &Url,
314
            route: Route,
315
        ) -> Result<RequestBuilder, url::ParseError> {
316
            Ok(self
3✔
317
                .client
318
                .request(route.method, instance_url.join(route.path)?))
1✔
319
        }
320

321
        /// Sends an HTTP request to the specified URL using the given method and optional body.
322
        ///
323
        /// ## Errors
324
        ///
325
        /// This function will return an error in the following cases:
326
        /// * The URL is invalid and cannot be parsed.
327
        /// * There are issues sending the request using the underlying reqwest client.
328
        /// * The request fails due to network problems or other errors encountered during execution.
329
        pub async fn request(
×
330
            &self,
331
            method: reqwest::Method,
332
            url: &str,
333
            body: Option<reqwest::Body>,
334
        ) -> HttpResult<reqwest::Response> {
335
            Url::parse(url)?;
×
336
            let mut request = self.client.request(method, url);
×
337
            request = request.headers(self.headers.clone());
×
338
            if let Some(body) = body {
×
339
                request = request.body(body);
×
340
            }
341
            Ok(request.send().await?)
×
342
        }
343

344
        /// Sends an HTTP request to the specified URL using the given method and optional body, and attempts
345
        /// to deserialize the response into a specified type.
346
        ///
347
        /// ## Returns
348
        ///
349
        /// Returns an `HttpResult<T>` containing the deserialized response of type `T` if successful.
350
        ///
351
        /// ## Errors
352
        ///
353
        /// This function will return an error in the following cases:
354
        /// * The URL is invalid and cannot be parsed.
355
        /// * There are issues sending the request using the underlying reqwest client.
356
        /// * The request fails due to network problems or other errors encountered during execution.
357
        /// * The response body cannot be deserialized into the specified type `T`. This might happen,
358
        ///   if the server responds with no body or an empty string to indicate an empty container,
359
        ///   like `None` when `T = Option<...>`, an empty vector if `T = Vec<...>` and so on. If such
360
        ///   behaviour is expected, use `request` instead and "manually" deserialize the result into
361
        ///   the desired type.
362
        pub async fn request_as<T: for<'a> Deserialize<'a>>(
1✔
363
            &self,
364
            method: reqwest::Method,
365
            url: &str,
366
            body: Option<reqwest::Body>,
367
        ) -> HttpResult<T> {
368
            let url = Url::parse(url)?;
2✔
369
            let mut request = self.client.request(method, url);
1✔
370
            request = request.headers(self.headers.clone());
1✔
371
            if let Some(body) = body {
1✔
372
                request = request.body(body);
×
373
            }
374
            let response = request.send().await;
3✔
375
            Self::handle_response(response).await
1✔
376
        }
377

378
        /// Sends a request, handles the response, and returns the deserialized object.
379
        pub(crate) async fn handle_response<T: for<'a> Deserialize<'a>>(
9✔
380
            response: Result<reqwest::Response, reqwest::Error>,
381
        ) -> HttpResult<T> {
382
            let response = response?;
18✔
383
            let response_text = response.text().await?;
18✔
384
            let object = from_str::<T>(&response_text)?;
18✔
385
            Ok(object)
9✔
386
        }
387
    }
388

389
    /// Returns `Ok(())` if `expected` contains `actual`, or an appropriate `RequestError::StatusCode`
390
    /// otherwise.
391
    pub(crate) fn matches_status_code(
1✔
392
        expected: &[StatusCode],
393
        actual: StatusCode,
394
    ) -> HttpResult<()> {
395
        if expected.contains(&actual) {
1✔
396
            Ok(())
1✔
397
        } else {
398
            Err(RequestError::StatusCode {
×
399
                received: actual,
400
                expected: expected.into(),
×
401
            })
402
        }
403
    }
404

405
    // i would like to move all routes requiring auth to the Session struct. all other routes can stay
406
    // at HttpClient.
407

408
    #[derive(Debug, Clone)]
409
    /// An authenticated polyproto session on an instance. Can optionally store the corresponding [IdCert]
410
    /// and [PrivateKey] for easy access to APIs requiring these parameters. Also gives access to
411
    /// unauthenticated APIs by exposing the inner [HttpClient].
412
    pub struct Session<S: Signature, T: PrivateKey<S>> {
413
        /// The authentication token of this session.
414
        pub token: String,
415
        /// A reference to the underlying [HttpClient].
416
        pub client: Arc<HttpClient>,
417
        /// The URL of the instance this session belongs to.
418
        pub instance_url: Url,
419
        pub(crate) certificate: Option<IdCert<S, T::PublicKey>>,
420
        pub(crate) signing_key: Option<T>,
421
    }
422

423
    impl<S: Signature, T: PrivateKey<S>> Session<S, T> {
424
        /// Creates a new authenticated `Session` instance.
425
        ///
426
        /// # Parameters
427
        /// - `client`: A reference to the [`HttpClient`] used for making API requests.
428
        /// - `token`: A string slice representing the authentication token.
429
        /// - `instance_url`: The [`Url`] of the instance to which the session connects.
430
        /// - `cert_and_key`: An optional tuple containing an [`IdCert`] and a corresponding private key.
431
        ///   If provided, these values will be used for authenticated operations requiring signing.
432
        ///
433
        /// # Returns
434
        /// A new `Session` instance initialized with the provided parameters.
435
        ///
436
        /// The returned `Session` provides access to authenticated and unauthenticated APIs,
437
        /// and stores optional credentials for signing requests when required.
438
        pub fn new(
2✔
439
            client: &HttpClient,
440
            token: &str,
441
            instance_url: Url,
442
            cert_and_key: Option<(IdCert<S, T::PublicKey>, T)>,
443
        ) -> Self {
444
            let (certificate, signing_key) = match cert_and_key {
4✔
445
                Some((c, s)) => (Some(c), Some(s)),
×
446
                None => (None, None),
2✔
447
            };
448
            Self {
449
                token: token.to_string(),
2✔
450
                client: Arc::new(client.clone()),
4✔
451
                instance_url,
452
                certificate,
453
                signing_key,
454
            }
455
        }
456

457
        /// Re-set the token, in case it changes.
458
        pub fn set_token(&mut self, token: &str) {
×
459
            self.token = token.to_string();
×
460
        }
461

462
        /// Add or update the [IdCert] and [PrivateKey] stored by the [Session], used for authenticated
463
        /// operations requiring signing.
464
        pub fn set_cert_and_key(&mut self, cert: IdCert<S, T::PublicKey>, signing_key: T) {
×
465
            self.certificate = Some(cert);
×
466
            self.signing_key = Some(signing_key);
×
467
        }
468
    }
469
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc