//! 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 { 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 { 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(&self, path: &str) -> Result 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(&self, path: &str, body: &B) -> Result 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(&self, path: &str, token: &str) -> Result 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(&self, path: &str, token: &str, body: &B) -> Result 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(&self, path: &str, token: &str) -> Result 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(response: Response) -> Result { 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::(&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, }) } } }