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 self.delete_with_query::<()>(path, None).await
159 }
160 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 .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}