mutiny_rs/
http.rs

1use reqwest::StatusCode;
2use serde::de::DeserializeOwned;
3use serde::Serialize;
4use thiserror::Error;
5
6#[derive(Debug, Error)]
7pub enum HttpError {
8    #[error("Unauthorized (401): {0}")]
9    Unauthorized(String),
10    #[error("Forbidden (403): {0}")]
11    Forbidden(String),
12    #[error("Not Found (404): {0}")]
13    NotFound(String),
14    #[error("Bad Request (400): {0}")]
15    BadRequest(String),
16    #[error("Conflict (409): {0}")]
17    Conflict(String),
18    #[error("Too Many Requests (429): {0}")]
19    TooManyRequests(String),
20    #[error("Internal Server Error (500): {0}")]
21    ServerError(String),
22    #[error("Reqwest error: {0}")]
23    Reqwest(#[from] reqwest::Error),
24    #[error("Unhandled status code {status}: {message}")]
25    Unhandled { status: StatusCode, message: String },
26    #[error("Other error: {0}")]
27    Other(String),
28}
29
30#[derive(Clone, Debug)]
31pub struct Http {
32    pub(crate) client: reqwest::Client,
33    pub(crate) base_url: String,
34    pub(crate) token: String,
35}
36
37pub const BASE_URL: &str = "https://api.revolt.chat";
38
39impl Http {
40    pub fn new(token: String) -> Self {
41        Http {
42            client: reqwest::Client::new(),
43            base_url: BASE_URL.to_string(),
44            token,
45        }
46    }
47
48    fn status_to_error(status: StatusCode, body: String) -> HttpError {
49        match status {
50            StatusCode::BAD_REQUEST => HttpError::BadRequest(body),
51            StatusCode::UNAUTHORIZED => HttpError::Unauthorized(body),
52            StatusCode::FORBIDDEN => HttpError::Forbidden(body),
53            StatusCode::NOT_FOUND => HttpError::NotFound(body),
54            StatusCode::CONFLICT => HttpError::Conflict(body),
55            StatusCode::TOO_MANY_REQUESTS => HttpError::TooManyRequests(body),
56            StatusCode::INTERNAL_SERVER_ERROR => HttpError::ServerError(body),
57            _ => HttpError::Unhandled { status, message: body },
58        }
59    }
60
61    pub async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T, HttpError> {
62        self.request_internal::<T, ()>(path, None).await
63    }
64
65    pub async fn get_with_query<T: DeserializeOwned, Q: Serialize>(&self, path: &str, query: &Q) -> Result<T, HttpError> {
66        self.request_internal(path, Some(query)).await
67    }
68    async fn request_internal<T: DeserializeOwned, Q: Serialize>(&self, path: &str, query: Option<&Q>) -> Result<T, HttpError> {
69        let base = self.base_url.trim_end_matches('/');
70        let path = path.trim_start_matches('/');
71        let url = format!("{}/{}", base, path);
72
73        let mut request = self.client
74            .get(&url)
75            .header(reqwest::header::ACCEPT, "application/json")
76            .header("x-bot-token", &self.token);
77
78        if let Some(q) = query {
79            request = request.query(q);
80        }
81
82        let response = request.send().await.map_err(HttpError::from)?;
83        let status = response.status();
84
85        if !status.is_success() {
86            let text = response.text().await.unwrap_or_default();
87            return Err(Self::status_to_error(status, text));
88        }
89
90        let text = response.text().await.map_err(|e| HttpError::Other(format!("Failed to read body: {}", e)))?;
91        serde_json::from_str::<T>(&text).map_err(|e| HttpError::Other(format!("Failed to parse JSON: {}. Body: {}", e, text)))
92    }
93
94    pub async fn post<T: DeserializeOwned, B: Serialize>(&self, path: &str, body: &B) -> Result<T, HttpError> {
95        let base = self.base_url.trim_end_matches('/');
96        let path = path.trim_start_matches('/');
97        let url = format!("{}/{}", base, path);
98        let response = self.client
99            .post(&url)
100            .header("x-bot-token", &self.token)
101            .json(body)
102            .send()
103            .await
104            .map_err(HttpError::from)?;
105
106        let status = response.status();
107        if !status.is_success() {
108            let text = response.text().await.unwrap_or_default();
109            return Err(Self::status_to_error(status, text));
110        }
111
112        response.json::<T>().await.map_err(|e| HttpError::Other(format!("Parse Error: {}", e)))
113    }
114
115    pub async fn patch<T: DeserializeOwned, B: Serialize>(&self, path: &str, body: &B) -> Result<T, HttpError> {
116        let base = self.base_url.trim_end_matches('/');
117        let path = path.trim_start_matches('/');
118        let url = format!("{}/{}", base, path);
119        let response = self.client
120            .patch(&url)
121            .header("x-bot-token", &self.token)
122            .json(body)
123            .send()
124            .await
125            .map_err(HttpError::from)?;
126
127        let status = response.status();
128        if !status.is_success() {
129            let text = response.text().await.unwrap_or_default();
130            return Err(Self::status_to_error(status, text));
131        }
132
133        response.json::<T>().await.map_err(|e| HttpError::Other(format!("Parse Error: {}", e)))
134    }
135
136    pub async fn delete_with_query<Q: Serialize>(&self, path: &str, query: Option<&Q>) -> Result<(), HttpError> {
137        let base = self.base_url.trim_end_matches('/');
138        let path = path.trim_start_matches('/');
139        let url = format!("{}/{}", base, path);
140        let mut request = self.client
141            .delete(&url)
142            .header("x-bot-token", &self.token);
143        if let Some(q) = query {
144            request = request.query(q);
145        }
146        let response = request.send().await.map_err(HttpError::from)?;
147        let status = response.status();
148
149        if !status.is_success() {
150            let text = response.text().await.unwrap_or_default();
151
152            return Err(Self::status_to_error(status, text));
153        }
154        Ok(())
155    }
156    pub async fn delete(&self, path: &str) -> Result<(), HttpError> {
157        // Pass generic unit type () as the query
158        self.delete_with_query::<()>(path, None).await
159    }
160    /// Sends a POST request with no body, and expects no response body.
161    /// Useful for actions like "Pin Message" or "Ack".
162    pub async fn post_empty(&self, path: &str) -> Result<(), HttpError> {
163        let base = self.base_url.trim_end_matches('/');
164        let path = path.trim_start_matches('/');
165        let url = format!("{}/{}", base, path);
166
167        let response = self.client
168            .post(&url)
169            .header("x-bot-token", &self.token)
170            // Note: No .json() body attached here
171            .send()
172            .await
173            .map_err(HttpError::from)?;
174
175        let status = response.status();
176
177        if !status.is_success() {
178            let text = response.text().await.unwrap_or_default();
179            return Err(Self::status_to_error(status, text));
180        }
181        Ok(())
182    }
183}