Skip to main content

mutiny_rs/http/
mod.rs

1pub mod routing;
2mod user;
3
4use crate::http::routing::Route;
5use reqwest::StatusCode;
6use serde::de::DeserializeOwned;
7use serde::Serialize;
8use thiserror::Error;
9
10#[derive(Debug, Error)]
11pub enum HttpError {
12    #[error("Unauthorized (401): {0}")]
13    Unauthorized(String),
14    #[error("Forbidden (403): {0}")]
15    Forbidden(String),
16    #[error("Not Found (404): {0}")]
17    NotFound(String),
18    #[error("Bad Request (400): {0}")]
19    BadRequest(String),
20    #[error("Conflict (409): {0}")]
21    Conflict(String),
22    #[error("Too Many Requests (429): {0}")]
23    TooManyRequests(String),
24    #[error("Internal Server Error (500): {0}")]
25    ServerError(String),
26    #[error("Reqwest error: {0}")]
27    Reqwest(#[from] reqwest::Error),
28    #[error("Unhandled status code {status}: {message}")]
29    Unhandled { status: StatusCode, message: String },
30    #[error("Other error: {0}")]
31    Other(String),
32}
33
34#[derive(Clone, Debug)]
35pub struct HttpClient {
36    pub(crate) client: reqwest::Client,
37    pub(crate) base_url: String,
38    pub(crate) token: String,
39}
40
41pub const BASE_URL: &str = "https://api.revolt.chat";
42
43impl HttpClient {
44    pub fn new(token: String) -> Self {
45        HttpClient {
46            client: reqwest::Client::new(),
47            base_url: BASE_URL.to_string(),
48            token,
49        }
50    }
51
52    /// The "Master" request function.
53    /// It handles URL generation, Authentication, Error Checking, and Parsing.
54    ///
55    /// * `route`: The Route enum (provides path and HTTP method).
56    /// * `body`: Optional JSON body (for POST/PATCH).
57    /// * `query`: Optional Query params (for GET/DELETE).
58    pub async fn request<B, Q, T>(
59        &self,
60        route: Route<'_>,
61        body: Option<B>,
62        query: Option<&Q>,
63    ) -> Result<T, HttpError>
64    where
65        B: Serialize,
66        Q: Serialize,
67        T: DeserializeOwned,
68    {
69        let base = self.base_url.trim_end_matches('/');
70        let path = route.path().trim_start_matches('/').to_string();
71        let url = format!("{}/{}", base, path);
72        let method = route.method();
73
74        let mut builder = self.client
75            .request(method, &url)
76            .header(reqwest::header::ACCEPT, "application/json")
77            .header("x-bot-token", &self.token);
78
79        if let Some(data) = body {
80            builder = builder.json(&data);
81        }
82
83        if let Some(q) = query {
84            builder = builder.query(q);
85        }
86
87        let response = builder.send().await?;
88        let status = response.status();
89
90        if !status.is_success() {
91            let text = response.text().await.unwrap_or_default();
92            return Err(Self::status_to_error(status, text));
93        }
94
95        // If the expected type T is generic unit `()`, we don't try to parse JSON.
96        // This is a "hack" to handle empty responses like 204 No Content.
97        // (Note: Rust doesn't easily let us check `T == ()` at runtime,
98        // so we try to parse; if T is (), serde_json::from_str("null") or empty might fail
99        // depending on the API, so we handle the specific case of empty body).
100
101        let text = response.text().await.map_err(|e| HttpError::Other(format!("Failed to read body: {}", e)))?;
102
103        if text.is_empty() {
104            // Try to deserialize "nothing" -> T.
105            // If T is (), this usually requires specific handling,
106            // but often APIs return "{}" or "null" for empty.
107            // If the API returns literally 0 bytes, serde might error unless we handle it.
108            // For strict correctness with `()`, we often return Ok(serde_json::from_str("null")?)
109            // But simpler:
110            if std::any::type_name::<T>() == "()" {
111                return Ok(serde_json::from_str("null").unwrap_or_else(|_| unsafe { std::mem::zeroed() }));
112            }
113        }
114
115        serde_json::from_str::<T>(&text).map_err(|e| {
116            HttpError::Other(format!("Failed to parse JSON: {}. Body: {}", e, text))
117        })
118    }
119    // Helper for simple GETs
120    pub async fn get<T: DeserializeOwned>(&self, route: Route<'_>) -> Result<T, HttpError> {
121        self.request::<(), (), T>(route, None, None).await
122    }
123
124    // Helper for simple Body requests (POST/PATCH)
125    pub async fn execute<B: Serialize, T: DeserializeOwned>(&self, route: Route<'_>, body: B) -> Result<T, HttpError> {
126        self.request::<B, (), T>(route, Some(body), None).await
127    }
128
129    /// Helper to map status codes to our custom errors
130    fn status_to_error(status: StatusCode, body: String) -> HttpError {
131        match status {
132            StatusCode::BAD_REQUEST => HttpError::BadRequest(body),
133            StatusCode::UNAUTHORIZED => HttpError::Unauthorized(body),
134            StatusCode::FORBIDDEN => HttpError::Forbidden(body),
135            StatusCode::NOT_FOUND => HttpError::NotFound(body),
136            StatusCode::CONFLICT => HttpError::Conflict(body),
137            StatusCode::TOO_MANY_REQUESTS => HttpError::TooManyRequests(body),
138            StatusCode::INTERNAL_SERVER_ERROR => HttpError::ServerError(body),
139            _ => HttpError::Unhandled { status, message: body },
140        }
141    }
142}