179 lines
5.6 KiB
Rust
179 lines
5.6 KiB
Rust
//! Low-level HTTP helpers that talk directly to the Matrix homeserver.
|
|
//!
|
|
//! All Matrix Client-Server API calls go through these functions.
|
|
//! They handle:
|
|
//! - Setting the `Authorization: Bearer …` header
|
|
//! - Parsing the standard Matrix error body
|
|
//! - Surfacing `M_UNKNOWN_TOKEN` as [`MatrixError::TokenExpired`]
|
|
|
|
use reqwest::{header, Client as ReqwestClient, Response, Url}; // ← Use reqwest::Url
|
|
use serde::{de::DeserializeOwned, Serialize};
|
|
|
|
use crate::{
|
|
error::{MatrixError, Result},
|
|
types::MatrixApiError,
|
|
};
|
|
|
|
/// Thin wrapper around `reqwest::Client` that knows the homeserver base URL
|
|
/// and the current access token.
|
|
#[derive(Debug, Clone)]
|
|
pub struct HttpClient {
|
|
inner: ReqwestClient,
|
|
pub homeserver: Url,
|
|
}
|
|
|
|
impl HttpClient {
|
|
/// Build a new HTTP client for the given homeserver.
|
|
pub fn new(homeserver: Url) -> Result<Self> {
|
|
let inner = ReqwestClient::builder()
|
|
.timeout(std::time::Duration::from_secs(60))
|
|
.https_only(false) // allow http for local test servers
|
|
.build()
|
|
.map_err(|e| MatrixError::Network {
|
|
message: e.to_string(),
|
|
})?;
|
|
|
|
Ok(Self { inner, homeserver })
|
|
}
|
|
|
|
/// Resolve a path relative to the homeserver base URL.
|
|
///
|
|
/// Example: `self.url("/_matrix/client/v3/login")`
|
|
pub fn url(&self, path: &str) -> Result<Url> {
|
|
self.homeserver
|
|
.join(path)
|
|
.map_err(|e| MatrixError::UrlParse {
|
|
message: e.to_string(),
|
|
})
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// Unauthenticated requests
|
|
// ------------------------------------------------------------------
|
|
|
|
/// `GET {path}` — no auth header.
|
|
pub async fn get_unauthenticated<R>(&self, path: &str) -> Result<R>
|
|
where
|
|
R: DeserializeOwned,
|
|
{
|
|
let url = self.url(path)?;
|
|
let response = self
|
|
.inner
|
|
.get(url)
|
|
.header(header::ACCEPT, "application/json")
|
|
.send()
|
|
.await?;
|
|
|
|
parse_response(response).await
|
|
}
|
|
|
|
/// `POST {path}` with a JSON body — no auth header.
|
|
pub async fn post_unauthenticated<B, R>(&self, path: &str, body: &B) -> Result<R>
|
|
where
|
|
B: Serialize + ?Sized,
|
|
R: DeserializeOwned,
|
|
{
|
|
let url = self.url(path)?;
|
|
let response = self
|
|
.inner
|
|
.post(url)
|
|
.header(header::CONTENT_TYPE, "application/json")
|
|
.header(header::ACCEPT, "application/json")
|
|
.json(body)
|
|
.send()
|
|
.await?;
|
|
|
|
parse_response(response).await
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// Authenticated requests
|
|
// ------------------------------------------------------------------
|
|
|
|
/// `GET {path}` with a Bearer token.
|
|
pub async fn get<R>(&self, path: &str, token: &str) -> Result<R>
|
|
where
|
|
R: DeserializeOwned,
|
|
{
|
|
let url = self.url(path)?;
|
|
let response = self
|
|
.inner
|
|
.get(url)
|
|
.header(header::AUTHORIZATION, format!("Bearer {token}"))
|
|
.header(header::ACCEPT, "application/json")
|
|
.send()
|
|
.await?;
|
|
|
|
parse_response(response).await
|
|
}
|
|
|
|
/// `POST {path}` with a JSON body and a Bearer token.
|
|
pub async fn post<B, R>(&self, path: &str, token: &str, body: &B) -> Result<R>
|
|
where
|
|
B: Serialize + ?Sized,
|
|
R: DeserializeOwned,
|
|
{
|
|
let url = self.url(path)?;
|
|
let response = self
|
|
.inner
|
|
.post(url)
|
|
.header(header::AUTHORIZATION, format!("Bearer {token}"))
|
|
.header(header::CONTENT_TYPE, "application/json")
|
|
.header(header::ACCEPT, "application/json")
|
|
.json(body)
|
|
.send()
|
|
.await?;
|
|
|
|
parse_response(response).await
|
|
}
|
|
|
|
/// `POST {path}` with an empty JSON body `{}` and a Bearer token.
|
|
pub async fn post_empty<R>(&self, path: &str, token: &str) -> Result<R>
|
|
where
|
|
R: DeserializeOwned,
|
|
{
|
|
#[derive(Serialize)]
|
|
struct Empty {}
|
|
self.post(path, token, &Empty {}).await
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Response parsing helper
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Parses a raw `Response`, mapping Matrix API errors to [`MatrixError`].
|
|
async fn parse_response<R: DeserializeOwned>(response: Response) -> Result<R> {
|
|
let status = response.status();
|
|
|
|
if status.is_success() {
|
|
let body = response.text().await.map_err(|e| MatrixError::Network {
|
|
message: e.to_string(),
|
|
})?;
|
|
serde_json::from_str(&body).map_err(|e| MatrixError::Json {
|
|
message: format!("Deserializing success response: {e}\nBody: {body}"),
|
|
})
|
|
} else {
|
|
// Attempt to parse the standard Matrix error body.
|
|
let body = response.text().await.unwrap_or_default();
|
|
if let Ok(api_err) = serde_json::from_str::<MatrixApiError>(&body) {
|
|
if api_err.is_unknown_token() {
|
|
return Err(MatrixError::TokenExpired);
|
|
}
|
|
Err(MatrixError::Http {
|
|
status: status.as_u16(),
|
|
message: format!(
|
|
"{}: {}",
|
|
api_err.errcode,
|
|
api_err.error.unwrap_or_default()
|
|
),
|
|
})
|
|
} else {
|
|
Err(MatrixError::Http {
|
|
status: status.as_u16(),
|
|
message: body,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|