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 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 let text = response.text().await.map_err(|e| HttpError::Other(format!("Failed to read body: {}", e)))?;
102
103 if text.is_empty() {
104 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 pub async fn get<T: DeserializeOwned>(&self, route: Route<'_>) -> Result<T, HttpError> {
121 self.request::<(), (), T>(route, None, None).await
122 }
123
124 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 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}