nigig-org/crates/matrix_client/src/http.rs
2026-07-26 19:38:26 +03:00

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,
})
}
}
}