robespierre_http/
lib.rs

1use std::result::Result as StdResult;
2
3use reqwest::{
4    header::{HeaderMap, HeaderValue},
5    multipart::{Form, Part},
6    RequestBuilder,
7};
8use robespierre_models::{
9    auth::Session,
10    autumn::{AttachmentId, AttachmentTag},
11    core::RevoltConfiguration,
12    id::UserId,
13};
14
15/// Any error that can happen while requesting / decoding
16#[derive(Debug, thiserror::Error)]
17pub enum HttpError {
18    #[error("reqwest: {0}")]
19    Reqwest(#[from] reqwest::Error),
20
21    #[error("decoding: {0}")]
22    Decoding(#[from] serde_json::Error),
23}
24
25pub type Result<T = ()> = StdResult<T, HttpError>;
26
27/// A value that can be used to authenticate on the REST API, either as a bot or as a non-bot user.
28#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
29pub enum HttpAuthentication<'a> {
30    BotToken { token: &'a str },
31    UserSession { session_token: &'a str },
32}
33trait AuthExt: Sized {
34    fn auth(self, auth: &HttpAuthentication) -> Self;
35}
36
37impl AuthExt for RequestBuilder {
38    fn auth(self, auth: &HttpAuthentication) -> Self {
39        match auth {
40            HttpAuthentication::BotToken { token } => self.header("x-bot-token", *token),
41            HttpAuthentication::UserSession { session_token } => {
42                self.header("x-session-token", *session_token)
43            }
44        }
45    }
46}
47
48impl AuthExt for HeaderMap {
49    fn auth(mut self, auth: &HttpAuthentication) -> Self {
50        match auth {
51            HttpAuthentication::BotToken { token } => {
52                self.insert("x-bot-token", token.parse().unwrap());
53            }
54            HttpAuthentication::UserSession { session_token } => {
55                self.insert("x-session-token", session_token.parse().unwrap());
56            }
57        }
58
59        self
60    }
61}
62
63#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
64pub enum AuthType {
65    Bot,
66    UserSession,
67}
68
69macro_rules! ep {
70    ($self:ident, $ep:literal $($args:tt)*) => {
71        format!(concat!("{}", $ep), $self.api_root, $($args)*)
72    };
73
74    (api_root = $api_root:expr, $ep:literal $($args:tt)*) => {
75        format!(concat!("{}", $ep), $api_root, $($args)*)
76    };
77}
78
79macro_rules! autumn_tag_upload {
80    ($self:expr, $tag:expr) => {
81        format!("{}/{}", $self.revolt_config.features.autumn.url(), $tag)
82    };
83}
84
85impl<'a> From<&'a Session> for HttpAuthentication<'a> {
86    fn from(s: &'a Session) -> Self {
87        HttpAuthentication::UserSession {
88            session_token: &s.token.0,
89        }
90    }
91}
92
93/// An instance of a client to the REST API
94pub struct Http {
95    client: reqwest::Client,
96    api_root: String,
97    revolt_config: RevoltConfiguration,
98
99    auth_type: AuthType,
100}
101
102pub mod core;
103
104pub mod onboarding;
105
106pub mod account;
107
108pub mod session;
109
110pub mod users_information;
111
112pub mod direct_messaging;
113
114pub mod relationships;
115
116pub mod channel_information;
117
118pub mod channel_invites;
119
120pub mod channel_permissions;
121
122pub mod messaging;
123
124pub mod groups;
125
126pub mod voice;
127
128pub mod server_information;
129
130pub mod server_members;
131
132pub mod server_permissions;
133
134pub mod bots;
135
136pub mod invites;
137
138pub mod sync;
139
140pub mod web_push;
141
142mod impl_prelude {
143    pub use super::Http;
144    pub use super::Result;
145}
146
147impl Http {
148    /// Creates a new client from the authentication
149    pub async fn new<'auth>(auth: impl Into<HttpAuthentication<'auth>>) -> Result<Self> {
150        Self::new_with_url(auth, "https://api.revolt.chat").await
151    }
152
153    /// Creates a new client from the authentication and url.
154    ///
155    /// Use this if using a self hosted instance of revolt, otherwise use [`Self::new`].
156    pub async fn new_with_url<'auth>(
157        auth: impl Into<HttpAuthentication<'auth>>,
158        api_root: &str,
159    ) -> Result<Self> {
160        let auth = auth.into();
161        let mut default_headers = HeaderMap::new().auth(&auth);
162        default_headers.insert(reqwest::header::ACCEPT, HeaderValue::from_static("*/*"));
163        let client = reqwest::Client::builder()
164            .default_headers(default_headers)
165            .build()
166            .unwrap();
167        let revolt_config = Self::get_revolt_config(&client, api_root).await?;
168        let auth_type = match auth {
169            HttpAuthentication::BotToken { .. } => AuthType::Bot,
170            HttpAuthentication::UserSession { .. } => AuthType::UserSession,
171        };
172        Ok(Self {
173            client,
174            api_root: api_root.to_string(),
175            revolt_config,
176            auth_type,
177        })
178    }
179
180    fn client_user_session_auth_type(&self) -> &reqwest::Client {
181        match self.auth_type {
182            AuthType::Bot => panic!("Cannot use route when using a bot auth"),
183            AuthType::UserSession => &self.client,
184        }
185    }
186
187    /// Gets the websocket url
188    pub fn get_ws_url(&self) -> &str {
189        &self.revolt_config.ws
190    }
191
192    pub async fn get_self_id(&self) -> Result<UserId> {
193        Ok(self.fetch_account().await?.id)
194    }
195
196    /// Uploads a file to autumn, returning the [`AttachmentId`]
197    pub async fn upload_autumn(
198        &self,
199        tag: AttachmentTag,
200        name: String,
201        bytes: Vec<u8>,
202    ) -> Result<AttachmentId> {
203        #[derive(serde::Deserialize)]
204        struct AutumnUploadResponse {
205            id: AttachmentId,
206        }
207
208        let part = Part::bytes(bytes).file_name(name.clone());
209        let form = Form::new().part(name, part);
210        let req = self
211            .client
212            .post(autumn_tag_upload!(self, tag))
213            .multipart(form);
214        let resp = req
215            .send()
216            .await?
217            .error_for_status()?
218            .json::<AutumnUploadResponse>()
219            .await?;
220        Ok(resp.id)
221    }
222}