diff --git a/src/api/auth/login.rs b/src/api/auth/login.rs index 749ed239..1fcda04f 100644 --- a/src/api/auth/login.rs +++ b/src/api/auth/login.rs @@ -12,8 +12,9 @@ use crate::gateway::Gateway; use crate::instance::{ChorusUser, Instance}; use crate::ratelimiter::ChorusRequest; use crate::types::{ - MfaAuthenticationType, GatewayIdentifyPayload, LimitType, LoginResult, LoginSchema, - SendMfaSmsResponse, SendMfaSmsSchema, User, VerifyMFALoginResponse, VerifyMFALoginSchema, + ClientProperties, GatewayIdentifyPayload, LimitType, LoginResult, LoginSchema, + MfaAuthenticationType, SendMfaSmsResponse, SendMfaSmsSchema, User, VerifyMFALoginResponse, + VerifyMFALoginSchema, }; impl Instance { @@ -24,12 +25,11 @@ impl Instance { pub async fn login_account(&mut self, login_schema: LoginSchema) -> ChorusResult { let endpoint_url = self.urls.api.clone() + "/auth/login"; let chorus_request = ChorusRequest { - request: Client::new() - .post(endpoint_url) - .body(to_string(&login_schema).unwrap()) - .header("Content-Type", "application/json"), + request: Client::new().post(endpoint_url).json(&login_schema), limit_type: LimitType::AuthLogin, - }; + } + // Note: yes, this is still sent even for login and register + .with_client_properties(&ClientProperties::default()); // We do not have a user yet, and the UserRateLimits will not be affected by a login // request (since login is an instance wide limit), which is why we are just cloning the @@ -58,12 +58,11 @@ impl Instance { let endpoint_url = self.urls.api.clone() + "/auth/mfa/" + &authenticator.to_string(); let chorus_request = ChorusRequest { - request: Client::new() - .post(endpoint_url) - .header("Content-Type", "application/json") - .json(&schema), + request: Client::new().post(endpoint_url).json(&schema), limit_type: LimitType::AuthLogin, - }; + } + // Note: yes, this is still sent even for login and register + .with_client_properties(&ClientProperties::default()); let mut user = ChorusUser::shell(Arc::new(RwLock::new(self.clone())), "None").await; @@ -94,7 +93,7 @@ impl Instance { /// /// # Reference /// See - // FIXME: This uses ChorusUser::shell, when it *really* shoudln't, but + // FIXME: This uses ChorusUser::shell, when it *really* shouldn't, but // there is no other way to send a ratelimited request pub async fn send_mfa_sms( &mut self, diff --git a/src/api/auth/mod.rs b/src/api/auth/mod.rs index 91d6d73e..ea070e8e 100644 --- a/src/api/auth/mod.rs +++ b/src/api/auth/mod.rs @@ -25,7 +25,7 @@ impl Instance { pub async fn login_with_token(&mut self, token: &str) -> ChorusResult { let mut user = ChorusUser::shell(Arc::new(RwLock::new(self.clone())), token).await; - user.update_with_login_data(token.to_string(), None).await?; + user.update_with_login_data(token.to_string(), None).await?; Ok(user) } diff --git a/src/api/auth/register.rs b/src/api/auth/register.rs index db230f22..28d56422 100644 --- a/src/api/auth/register.rs +++ b/src/api/auth/register.rs @@ -8,7 +8,7 @@ use reqwest::Client; use serde_json::to_string; use crate::gateway::{Gateway, GatewayHandle}; -use crate::types::{GatewayIdentifyPayload, User}; +use crate::types::{ClientProperties, GatewayIdentifyPayload, User}; use crate::{ errors::ChorusResult, instance::{ChorusUser, Instance, Token}, @@ -28,12 +28,12 @@ impl Instance { ) -> ChorusResult { let endpoint_url = self.urls.api.clone() + "/auth/register"; let chorus_request = ChorusRequest { - request: Client::new() - .post(endpoint_url) - .body(to_string(®ister_schema).unwrap()) - .header("Content-Type", "application/json"), + request: Client::new().post(endpoint_url).json(®ister_schema), limit_type: LimitType::AuthRegister, - }; + } + // Note: yes, this is still sent even for login and register + .with_client_properties(&ClientProperties::default()); + // We do not have a user yet, and the UserRateLimits will not be affected by a login // request (since register is an instance wide limit), which is why we are just cloning // the instances' limits to pass them on as user_rate_limits later. diff --git a/src/api/channels/channels.rs b/src/api/channels/channels.rs index 7cb9ee3f..9fca1779 100644 --- a/src/api/channels/channels.rs +++ b/src/api/channels/channels.rs @@ -21,18 +21,15 @@ impl Channel { /// # Reference /// See pub async fn get(user: &mut ChorusUser, channel_id: Snowflake) -> ChorusResult { - let chorus_request = ChorusRequest::new( - http::Method::GET, - &format!( + let chorus_request = ChorusRequest { + request: Client::new().get(format!( "{}/channels/{}", user.belongs_to.read().unwrap().urls.api.clone(), channel_id - ), - None, - None, - Some(user), - LimitType::Channel(channel_id), - ); + )), + limit_type: LimitType::Channel(channel_id), + } + .with_headers_for(user); chorus_request.deserialize_response::(user).await } @@ -55,14 +52,12 @@ impl Channel { self.id, ); - let request = ChorusRequest::new( - http::Method::DELETE, - &url, - None, - audit_log_reason.as_deref(), - Some(user), - LimitType::Channel(self.id), - ); + let request = ChorusRequest { + request: Client::new().delete(url), + limit_type: LimitType::Channel(self.id), + } + .with_maybe_audit_log_reason(audit_log_reason) + .with_headers_for(user); request.handle_request_as_result(user).await } @@ -94,14 +89,12 @@ impl Channel { channel_id ); - let request = ChorusRequest::new( - http::Method::PATCH, - &url, - Some(to_string(&modify_data).unwrap()), - audit_log_reason.as_deref(), - Some(user), - LimitType::Channel(channel_id), - ); + let request = ChorusRequest { + request: Client::new().patch(url).json(&modify_data), + limit_type: LimitType::Channel(channel_id), + } + .with_maybe_audit_log_reason(audit_log_reason) + .with_headers_for(user); request.deserialize_response::(user).await } @@ -126,14 +119,12 @@ impl Channel { channel_id ); - let mut chorus_request = ChorusRequest::new( - http::Method::GET, - &url, - None, - None, - Some(user), - Default::default(), - ); + let mut chorus_request = ChorusRequest { + request: Client::new().get(url), + limit_type: Default::default(), + } + .with_headers_for(user); + chorus_request.request = chorus_request.request.query(&range); chorus_request @@ -151,22 +142,22 @@ impl Channel { user: &mut ChorusUser, add_channel_recipient_schema: Option, ) -> ChorusResult<()> { - let mut request = Client::new() - .put(format!( - "{}/channels/{}/recipients/{}", - user.belongs_to.read().unwrap().urls.api, - self.id, - recipient_id - )) - .header("Authorization", user.token()) - .header("Content-Type", "application/json"); + let mut request = Client::new().put(format!( + "{}/channels/{}/recipients/{}", + user.belongs_to.read().unwrap().urls.api, + self.id, + recipient_id + )); + if let Some(schema) = add_channel_recipient_schema { - request = request.body(to_string(&schema).unwrap()); + request = request.json(&schema); } + ChorusRequest { request, limit_type: LimitType::Channel(self.id), } + .with_headers_for(user) .handle_request_as_result(user) .await } @@ -187,14 +178,11 @@ impl Channel { recipient_id ); - let request = ChorusRequest::new( - http::Method::DELETE, - &url, - None, - None, - Some(user), - LimitType::Channel(self.id), - ); + let request = ChorusRequest { + request: Client::new().delete(url), + limit_type: LimitType::Channel(self.id), + } + .with_headers_for(user); request.handle_request_as_result(user).await } @@ -215,14 +203,11 @@ impl Channel { guild_id ); - let request = ChorusRequest::new( - http::Method::PATCH, - &url, - Some(to_string(&schema).unwrap()), - None, - Some(user), - LimitType::Guild(guild_id), - ); + let request = ChorusRequest { + request: Client::new().patch(url).json(&schema), + limit_type: LimitType::Guild(guild_id), + } + .with_headers_for(user); request.handle_request_as_result(user).await } diff --git a/src/api/channels/messages.rs b/src/api/channels/messages.rs index 53fe64e6..5eef0e39 100644 --- a/src/api/channels/messages.rs +++ b/src/api/channels/messages.rs @@ -33,11 +33,11 @@ impl Message { let chorus_request = ChorusRequest { request: Client::new() .post(format!("{}/channels/{}/messages", url_api, channel_id)) - .header("Authorization", user.token()) - .body(to_string(&message).unwrap()) - .header("Content-Type", "application/json"), + .json(&message), limit_type: LimitType::Channel(channel_id), - }; + } + .with_headers_for(user); + chorus_request.deserialize_response::(user).await } else { for (index, attachment) in message.attachments.iter_mut().enumerate() { @@ -70,10 +70,11 @@ impl Message { let chorus_request = ChorusRequest { request: Client::new() .post(format!("{}/channels/{}/messages", url_api, channel_id)) - .header("Authorization", user.token()) .multipart(form), limit_type: LimitType::Channel(channel_id), - }; + } + .with_headers_for(user); + chorus_request.deserialize_response::(user).await } } @@ -105,10 +106,10 @@ impl Message { &user.belongs_to.read().unwrap().urls.api, endpoint )) - .header("Authorization", user.token()) - .header("Content-Type", "application/json") - .body(to_string(&query).unwrap()), - }; + .json(&query), + } + .with_headers_for(user); + let result = request.send_request(user).await?; let result_json = result.json::().await.unwrap(); if !result_json.is_object() { @@ -142,22 +143,17 @@ impl Message { channel_id: Snowflake, user: &mut ChorusUser, ) -> ChorusResult> { - let chorus_request = ChorusRequest::new( - http::Method::GET, - format!( + let request = ChorusRequest { + request: Client::new().get(format!( "{}/channels/{}/pins", user.belongs_to.read().unwrap().urls.api, channel_id - ) - .as_str(), - None, - None, - Some(user), - LimitType::Channel(channel_id), - ); - chorus_request - .deserialize_response::>(user) - .await + )), + limit_type: LimitType::Channel(channel_id), + } + .with_headers_for(user); + + request.deserialize_response::>(user).await } /// Pins a message in a channel. Requires the `MANAGE_MESSAGES` permission. Returns a 204 empty response on success. @@ -168,23 +164,21 @@ impl Message { pub async fn sticky( channel_id: Snowflake, message_id: Snowflake, - audit_log_reason: Option<&str>, + audit_log_reason: Option, user: &mut ChorusUser, ) -> ChorusResult<()> { - let request = ChorusRequest::new( - http::Method::PUT, - format!( + let request = ChorusRequest { + request: Client::new().put(format!( "{}/channels/{}/pins/{}", user.belongs_to.read().unwrap().urls.api, channel_id, message_id - ) - .as_str(), - None, - audit_log_reason, - Some(user), - LimitType::Channel(channel_id), - ); + )), + limit_type: LimitType::Channel(channel_id), + } + .with_maybe_audit_log_reason(audit_log_reason) + .with_headers_for(user); + request.handle_request_as_result(user).await } @@ -194,23 +188,21 @@ impl Message { pub async fn unsticky( channel_id: Snowflake, message_id: Snowflake, - audit_log_reason: Option<&str>, + audit_log_reason: Option, user: &mut ChorusUser, ) -> ChorusResult<()> { - let request = ChorusRequest::new( - http::Method::DELETE, - format!( + let request = ChorusRequest { + request: Client::new().delete(format!( "{}/channels/{}/pins/{}", user.belongs_to.read().unwrap().urls.api, channel_id, message_id - ) - .as_str(), - None, - audit_log_reason, - Some(user), - LimitType::Channel(channel_id), - ); + )), + limit_type: LimitType::Channel(channel_id), + } + .with_maybe_audit_log_reason(audit_log_reason) + .with_headers_for(user); + request.handle_request_as_result(user).await } @@ -224,17 +216,16 @@ impl Message { user: &mut ChorusUser, ) -> ChorusResult { let chorus_request = ChorusRequest { - request: Client::new() - .get(format!( - "{}/channels/{}/messages/{}", - user.belongs_to.read().unwrap().urls.api, - channel_id, - message_id - )) - .header("Authorization", user.token()) - .header("Content-Type", "application/json"), + request: Client::new().get(format!( + "{}/channels/{}/messages/{}", + user.belongs_to.read().unwrap().urls.api, + channel_id, + message_id + )), limit_type: LimitType::Channel(channel_id), - }; + } + .with_headers_for(user); + chorus_request.deserialize_response::(user).await } @@ -246,19 +237,18 @@ impl Message { schema: CreateGreetMessage, user: &mut ChorusUser, ) -> ChorusResult { - let request = ChorusRequest::new( - http::Method::POST, - format!( - "{}/channels/{}/messages/greet", - user.belongs_to.read().unwrap().urls.api, - channel_id, - ) - .as_str(), - Some(to_string(&schema).unwrap()), - None, - Some(user), - LimitType::Channel(channel_id), - ); + let request = ChorusRequest { + request: Client::new() + .post(format!( + "{}/channels/{}/messages/greet", + user.belongs_to.read().unwrap().urls.api, + channel_id, + )) + .json(&schema), + limit_type: LimitType::Channel(channel_id), + } + .with_headers_for(user); + request.deserialize_response::(user).await } @@ -278,20 +268,19 @@ impl Message { schema: MessageAck, user: &mut ChorusUser, ) -> ChorusResult> { - let request = ChorusRequest::new( - http::Method::POST, - format!( - "{}/channels/{}/messages/{}/ack", - user.belongs_to.read().unwrap().urls.api, - channel_id, - message_id - ) - .as_str(), - Some(to_string(&schema).unwrap()), - None, - Some(user), - LimitType::Channel(channel_id), - ); + let request = ChorusRequest { + request: Client::new() + .post(format!( + "{}/channels/{}/messages/{}/ack", + user.belongs_to.read().unwrap().urls.api, + channel_id, + message_id + )) + .json(&schema), + limit_type: LimitType::Channel(channel_id), + } + .with_headers_for(user); + request.deserialize_response::>(user).await } @@ -306,20 +295,17 @@ impl Message { message_id: Snowflake, user: &mut ChorusUser, ) -> ChorusResult { - let request = ChorusRequest::new( - http::Method::POST, - format!( + let request = ChorusRequest { + request: Client::new().post(format!( "{}/channels/{}/messages/{}/crosspost", user.belongs_to.read().unwrap().urls.api, channel_id, message_id - ) - .as_str(), - None, - None, - Some(user), - LimitType::Channel(channel_id), - ); + )), + limit_type: LimitType::Channel(channel_id), + } + .with_headers_for(user); + request.deserialize_response::(user).await } @@ -338,15 +324,14 @@ impl Message { channel_id, message_id ); - let chorus_request = ChorusRequest::new( - http::Method::DELETE, - &url, - None, - None, - Some(user), - LimitType::Channel(channel_id), - ); - chorus_request.handle_request_as_result(user).await + + let request = ChorusRequest { + request: Client::new().delete(url), + limit_type: LimitType::Channel(channel_id), + } + .with_headers_for(user); + + request.handle_request_as_result(user).await } /// Edits a previously sent message. All fields can be edited by the original message author. @@ -371,15 +356,14 @@ impl Message { channel_id, message_id ); - let chorus_request = ChorusRequest::new( - http::Method::PATCH, - &url, - Some(to_string(&schema).unwrap()), - None, - Some(user), - LimitType::Channel(channel_id), - ); - chorus_request.deserialize_response::(user).await + + let request = ChorusRequest { + request: Client::new().patch(url).json(&schema), + limit_type: LimitType::Channel(channel_id), + } + .with_headers_for(user); + + request.deserialize_response::(user).await } /// Deletes a message. If operating on a guild channel and trying to delete a message that was not sent by the current user, @@ -397,16 +381,14 @@ impl Message { message_id ); - let chorus_request = ChorusRequest::new( - http::Method::DELETE, - &url, - None, - audit_log_reason.as_deref(), - Some(user), - LimitType::Channel(channel_id), - ); + let request = ChorusRequest { + request: Client::new().delete(url), + limit_type: LimitType::Channel(channel_id), + } + .with_maybe_audit_log_reason(audit_log_reason) + .with_headers_for(user); - chorus_request.handle_request_as_result(user).await + request.handle_request_as_result(user).await } /// Deletes multiple messages in a single request. This endpoint can only be used on guild channels and requires the MANAGE_MESSAGES permission. @@ -429,45 +411,42 @@ impl Message { error: "`messages` must contain at least 2 entries.".to_string(), }); } - let request = ChorusRequest::new( - http::Method::POST, - format!( - "{}/channels/{}/messages/bulk-delete", - user.belongs_to.read().unwrap().urls.api, - channel_id, - ) - .as_str(), - Some(to_string(&messages).unwrap()), - audit_log_reason.as_deref(), - Some(user), - LimitType::Channel(channel_id), - ); + + let request = ChorusRequest { + request: Client::new() + .post(format!( + "{}/channels/{}/messages/bulk-delete", + user.belongs_to.read().unwrap().urls.api, + channel_id, + )) + .json(&messages), + limit_type: LimitType::Channel(channel_id), + } + .with_maybe_audit_log_reason(audit_log_reason) + .with_headers_for(user); + request.handle_request_as_result(user).await } /// Acknowledges the currently pinned messages in a channel. Returns a 204 empty response on success. /// /// # Reference: - /// See: + /// See pub async fn acknowledge_pinned( channel_id: Snowflake, user: &mut ChorusUser, ) -> ChorusResult<()> { - let chorus_request = ChorusRequest::new( - http::Method::POST, - format!( + let request = ChorusRequest { + request: Client::new().post(format!( "{}/channels/{}/pins/ack", user.belongs_to.read().unwrap().urls.api, channel_id, - ) - .as_str(), - None, - None, - Some(user), - LimitType::Channel(channel_id), - ); + )), + limit_type: LimitType::Channel(channel_id), + } + .with_headers_for(user); - chorus_request.handle_request_as_result(user).await + request.handle_request_as_result(user).await } } diff --git a/src/api/channels/permissions.rs b/src/api/channels/permissions.rs index 19492cd6..c3ba8fbf 100644 --- a/src/api/channels/permissions.rs +++ b/src/api/channels/permissions.rs @@ -35,26 +35,14 @@ impl types::Channel { channel_id, overwrite.id ); - let body = match to_string(&overwrite) { - Ok(string) => string, - Err(e) => { - return Err(ChorusError::FormCreation { - error: e.to_string(), - }); - } - }; - let mut request = Client::new() - .put(url) - .header("Authorization", user.token()) - .header("Content-Type", "application/json") - .body(body); - if let Some(reason) = audit_log_reason { - request = request.header("X-Audit-Log-Reason", reason); - } + let chorus_request = ChorusRequest { - request, + request: Client::new().put(url).json(&overwrite), limit_type: LimitType::Channel(channel_id), - }; + } + .with_maybe_audit_log_reason(audit_log_reason) + .with_headers_for(user); + chorus_request.handle_request_as_result(user).await } @@ -78,14 +66,11 @@ impl types::Channel { overwrite_id ); - let request = ChorusRequest::new( - http::Method::DELETE, - &url, - None, - None, - Some(user), - LimitType::Channel(channel_id), - ); + let request = ChorusRequest { + request: Client::new().delete(url), + limit_type: LimitType::Channel(channel_id), + } + .with_headers_for(user); request.handle_request_as_result(user).await } diff --git a/src/api/channels/reactions.rs b/src/api/channels/reactions.rs index e9bec801..1a7124e4 100644 --- a/src/api/channels/reactions.rs +++ b/src/api/channels/reactions.rs @@ -2,6 +2,8 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. +use reqwest::Client; + use crate::{ errors::ChorusResult, instance::ChorusUser, @@ -31,14 +33,11 @@ impl ReactionMeta { self.message_id ); - let request = ChorusRequest::new( - http::Method::DELETE, - &url, - None, - None, - Some(user), - LimitType::Channel(self.channel_id), - ); + let request = ChorusRequest { + request: Client::new().delete(url), + limit_type: LimitType::Channel(self.channel_id), + } + .with_headers_for(user); request.handle_request_as_result(user).await } @@ -59,14 +58,11 @@ impl ReactionMeta { emoji ); - let request = ChorusRequest::new( - http::Method::GET, - &url, - None, - None, - Some(user), - LimitType::Channel(self.channel_id), - ); + let request = ChorusRequest { + request: Client::new().get(url), + limit_type: LimitType::Channel(self.channel_id), + } + .with_headers_for(user); request.deserialize_response::>(user).await } @@ -89,14 +85,11 @@ impl ReactionMeta { emoji ); - let request = ChorusRequest::new( - http::Method::DELETE, - &url, - None, - None, - Some(user), - LimitType::Channel(self.channel_id), - ); + let request = ChorusRequest { + request: Client::new().delete(url), + limit_type: LimitType::Channel(self.channel_id), + } + .with_headers_for(user); request.handle_request_as_result(user).await } @@ -122,14 +115,11 @@ impl ReactionMeta { emoji ); - let request = ChorusRequest::new( - http::Method::PUT, - &url, - None, - None, - Some(user), - LimitType::Channel(self.channel_id), - ); + let request = ChorusRequest { + request: Client::new().put(url), + limit_type: LimitType::Channel(self.channel_id), + } + .with_headers_for(user); request.handle_request_as_result(user).await } @@ -150,14 +140,11 @@ impl ReactionMeta { emoji ); - let request = ChorusRequest::new( - http::Method::DELETE, - &url, - None, - None, - Some(user), - LimitType::Channel(self.channel_id), - ); + let request = ChorusRequest { + request: Client::new().delete(url), + limit_type: LimitType::Channel(self.channel_id), + } + .with_headers_for(user); request.handle_request_as_result(user).await } @@ -186,14 +173,11 @@ impl ReactionMeta { user_id ); - let request = ChorusRequest::new( - http::Method::DELETE, - &url, - None, - None, - Some(user), - LimitType::Channel(self.channel_id), - ); + let request = ChorusRequest { + request: Client::new().delete(url), + limit_type: LimitType::Channel(self.channel_id), + } + .with_headers_for(user); request.handle_request_as_result(user).await } diff --git a/src/api/guilds/guilds.rs b/src/api/guilds/guilds.rs index 6469834d..791a102f 100644 --- a/src/api/guilds/guilds.rs +++ b/src/api/guilds/guilds.rs @@ -24,15 +24,15 @@ impl Guild { /// See pub async fn get(guild_id: Snowflake, user: &mut ChorusUser) -> ChorusResult { let chorus_request = ChorusRequest { - request: Client::new() - .get(format!( - "{}/guilds/{}", - user.belongs_to.read().unwrap().urls.api, - guild_id - )) - .header("Authorization", user.token()), + request: Client::new().get(format!( + "{}/guilds/{}", + user.belongs_to.read().unwrap().urls.api, + guild_id + )), limit_type: LimitType::Guild(guild_id), - }; + } + .with_headers_for(user); + let response = chorus_request.deserialize_response::(user).await?; Ok(response) } @@ -47,13 +47,10 @@ impl Guild { ) -> ChorusResult { let url = format!("{}/guilds", user.belongs_to.read().unwrap().urls.api); let chorus_request = ChorusRequest { - request: Client::new() - .post(url.clone()) - .header("Authorization", user.token.clone()) - .header("Content-Type", "application/json") - .body(to_string(&guild_create_schema).unwrap()), + request: Client::new().post(url.clone()).json(&guild_create_schema), limit_type: LimitType::Global, - }; + } + .with_headers_for(user); chorus_request.deserialize_response::(user).await } @@ -80,12 +77,11 @@ impl Guild { user.belongs_to.read().unwrap().urls.api, guild_id, )) - .header("Authorization", user.token()) - .header("Content-Type", "application/json") - .body(to_string(&schema).unwrap()), + .json(&schema), limit_type: LimitType::Guild(guild_id), } - .with_maybe_mfa(&user.mfa_token); + .with_maybe_mfa(&user.mfa_token) + .with_headers_for(user); let response = chorus_request.deserialize_response::(user).await?; Ok(response) @@ -120,13 +116,11 @@ impl Guild { ); let chorus_request = ChorusRequest { - request: Client::new() - .post(url.clone()) - .header("Authorization", user.token.clone()) - .header("Content-Type", "application/json"), + request: Client::new().post(url.clone()), limit_type: LimitType::Global, } - .with_maybe_mfa(&user.mfa_token); + .with_maybe_mfa(&user.mfa_token) + .with_headers_for(user); chorus_request.handle_request_as_result(user).await } @@ -157,15 +151,15 @@ impl Guild { /// See pub async fn channels(&self, user: &mut ChorusUser) -> ChorusResult> { let chorus_request = ChorusRequest { - request: Client::new() - .get(format!( - "{}/guilds/{}/channels", - user.belongs_to.read().unwrap().urls.api, - self.id - )) - .header("Authorization", user.token()), + request: Client::new().get(format!( + "{}/guilds/{}/channels", + user.belongs_to.read().unwrap().urls.api, + self.id + )), limit_type: LimitType::Channel(self.id), - }; + } + .with_headers_for(user); + let result = chorus_request.send_request(user).await?; let stringed_response = match result.text().await { Ok(value) => value, @@ -196,16 +190,15 @@ impl Guild { user: &mut ChorusUser, ) -> ChorusResult { let chorus_request = ChorusRequest { - request: Client::new() - .patch(format!( - "{}/guilds/{}/preview", - user.belongs_to.read().unwrap().urls.api, - guild_id, - )) - .header("Authorization", user.token()) - .header("Content-Type", "application/json"), + request: Client::new().patch(format!( + "{}/guilds/{}/preview", + user.belongs_to.read().unwrap().urls.api, + guild_id, + )), limit_type: LimitType::Guild(guild_id), - }; + } + .with_headers_for(user); + let response = chorus_request .deserialize_response::(user) .await?; @@ -220,19 +213,16 @@ impl Guild { guild_id: Snowflake, user: &mut ChorusUser, ) -> ChorusResult> { - let request = ChorusRequest::new( - http::Method::GET, - format!( + let request = ChorusRequest { + request: Client::new().get(format!( "{}/guilds/{}/members", user.belongs_to.read().unwrap().urls.api, guild_id, - ) - .as_str(), - None, - None, - Some(user), - LimitType::Guild(guild_id), - ); + )), + limit_type: LimitType::Guild(guild_id), + } + .with_headers_for(user); + request.deserialize_response::>(user).await } @@ -245,22 +235,20 @@ impl Guild { query: GuildMemberSearchSchema, user: &mut ChorusUser, ) -> ChorusResult> { - let mut request = ChorusRequest::new( - http::Method::GET, - format!( + let mut request = ChorusRequest { + request: Client::new().get(format!( "{}/guilds/{}/members/search", user.belongs_to.read().unwrap().urls.api, guild_id, - ) - .as_str(), - None, - None, - Some(user), - LimitType::Guild(guild_id), - ); + )), + limit_type: LimitType::Guild(guild_id), + } + .with_headers_for(user); + request.request = request .request .query(&[("query", to_string(&query).unwrap())]); + request.deserialize_response::>(user).await } @@ -276,20 +264,18 @@ impl Guild { audit_log_reason: Option, user: &mut ChorusUser, ) -> ChorusResult<()> { - let request = ChorusRequest::new( - http::Method::DELETE, - format!( + let request = ChorusRequest { + request: Client::new().delete(format!( "{}/guilds/{}/members/{}", user.belongs_to.read().unwrap().urls.api, guild_id, member_id, - ) - .as_str(), - None, - audit_log_reason.as_deref(), - Some(user), - LimitType::Guild(guild_id), - ); + )), + limit_type: LimitType::Guild(guild_id), + } + .with_maybe_audit_log_reason(audit_log_reason) + .with_headers_for(user); + request.handle_request_as_result(user).await } @@ -305,20 +291,20 @@ impl Guild { audit_log_reason: Option, user: &mut ChorusUser, ) -> ChorusResult { - let request = ChorusRequest::new( - http::Method::PATCH, - format!( - "{}/guilds/{}/members/{}", - user.belongs_to.read().unwrap().urls.api, - guild_id, - member_id, - ) - .as_str(), - Some(to_string(&schema).unwrap()), - audit_log_reason.as_deref(), - Some(user), - LimitType::Guild(guild_id), - ); + let request = ChorusRequest { + request: Client::new() + .patch(format!( + "{}/guilds/{}/members/{}", + user.belongs_to.read().unwrap().urls.api, + guild_id, + member_id, + )) + .json(&schema), + limit_type: LimitType::Guild(guild_id), + } + .with_maybe_audit_log_reason(audit_log_reason) + .with_headers_for(user); + request.deserialize_response::(user).await } @@ -332,19 +318,19 @@ impl Guild { audit_log_reason: Option, user: &mut ChorusUser, ) -> ChorusResult { - let request = ChorusRequest::new( - http::Method::PATCH, - format!( - "{}/guilds/{}/members/@me", - user.belongs_to.read().unwrap().urls.api, - guild_id, - ) - .as_str(), - Some(to_string(&schema).unwrap()), - audit_log_reason.as_deref(), - Some(user), - LimitType::Guild(guild_id), - ); + let request = ChorusRequest { + request: Client::new() + .patch(format!( + "{}/guilds/{}/members/@me", + user.belongs_to.read().unwrap().urls.api, + guild_id, + )) + .json(&schema), + limit_type: LimitType::Guild(guild_id), + } + .with_maybe_audit_log_reason(audit_log_reason) + .with_headers_for(user); + request.deserialize_response::(user).await } @@ -357,19 +343,18 @@ impl Guild { schema: ModifyGuildMemberProfileSchema, user: &mut ChorusUser, ) -> ChorusResult { - let request = ChorusRequest::new( - http::Method::PATCH, - format!( - "{}/guilds/{}/profile/@me", - user.belongs_to.read().unwrap().urls.api, - guild_id, - ) - .as_str(), - Some(to_string(&schema).unwrap()), - None, - Some(user), - LimitType::Guild(guild_id), - ); + let request = ChorusRequest { + request: Client::new() + .patch(format!( + "{}/guilds/{}/profile/@me", + user.belongs_to.read().unwrap().urls.api, + guild_id, + )) + .json(&schema), + limit_type: LimitType::Guild(guild_id), + } + .with_headers_for(user); + request .deserialize_response::(user) .await @@ -392,17 +377,16 @@ impl Guild { guild_id, ); - let mut request = ChorusRequest::new( - http::Method::GET, - &url, - None, - None, - Some(user), - LimitType::Guild(guild_id), - ); + let mut request = ChorusRequest { + request: Client::new().get(url), + limit_type: LimitType::Guild(guild_id), + } + .with_headers_for(user); + if let Some(query) = query { request.request = request.request.query(&to_string(&query).unwrap()); } + request.deserialize_response::>(user).await } @@ -424,14 +408,12 @@ impl Guild { user_id ); - let request = ChorusRequest::new( - http::Method::GET, - &url, - None, - None, - Some(user), - LimitType::Guild(guild_id), - ); + let request = ChorusRequest { + request: Client::new().get(url), + limit_type: LimitType::Guild(guild_id), + } + .with_headers_for(user); + request.deserialize_response::(user).await } @@ -447,20 +429,20 @@ impl Guild { user: &mut ChorusUser, ) -> ChorusResult<()> { // FIXME: Return GuildBan instead of (). Requires to be resolved. - let request = ChorusRequest::new( - http::Method::PUT, - format!( - "{}/guilds/{}/bans/{}", - user.belongs_to.read().unwrap().urls.api, - guild_id, - user_id - ) - .as_str(), - Some(to_string(&schema).unwrap()), - audit_log_reason.as_deref(), - Some(user), - LimitType::Guild(guild_id), - ); + let request = ChorusRequest { + request: Client::new() + .put(format!( + "{}/guilds/{}/bans/{}", + user.belongs_to.read().unwrap().urls.api, + guild_id, + user_id + )) + .json(&schema), + limit_type: LimitType::Guild(guild_id), + } + .with_maybe_audit_log_reason(audit_log_reason) + .with_headers_for(user); + request.handle_request_as_result(user).await } @@ -483,14 +465,13 @@ impl Guild { user_id ); - let request = ChorusRequest::new( - http::Method::DELETE, - &url, - None, - audit_log_reason.as_deref(), - Some(user), - LimitType::Guild(guild_id), - ); + let request = ChorusRequest { + request: Client::new().delete(url), + limit_type: LimitType::Guild(guild_id), + } + .with_maybe_audit_log_reason(audit_log_reason) + .with_headers_for(user); + request.handle_request_as_result(user).await } } @@ -508,22 +489,19 @@ impl Channel { audit_log_reason: Option, schema: ChannelCreateSchema, ) -> ChorusResult { - let mut request = Client::new() - .post(format!( - "{}/guilds/{}/channels", - user.belongs_to.read().unwrap().urls.api, - guild_id - )) - .header("Authorization", user.token()) - .header("Content-Type", "application/json") - .body(to_string(&schema).unwrap()); - if let Some(reason) = audit_log_reason { - request = request.header("X-Audit-Log-Reason", reason); - } - let chorus_request = ChorusRequest { - request, + let request = ChorusRequest { + request: Client::new() + .post(format!( + "{}/guilds/{}/channels", + user.belongs_to.read().unwrap().urls.api, + guild_id + )) + .json(&schema), limit_type: LimitType::Guild(guild_id), - }; - chorus_request.deserialize_response::(user).await + } + .with_maybe_audit_log_reason(audit_log_reason) + .with_headers_for(user); + + request.deserialize_response::(user).await } } diff --git a/src/api/guilds/member.rs b/src/api/guilds/member.rs index 0037ec7d..58b5028c 100644 --- a/src/api/guilds/member.rs +++ b/src/api/guilds/member.rs @@ -27,10 +27,13 @@ impl types::GuildMember { guild_id, member_id ); + let chorus_request = ChorusRequest { - request: Client::new().get(url).header("Authorization", user.token()), + request: Client::new().get(url), limit_type: LimitType::Guild(guild_id), - }; + } + .with_headers_for(user); + chorus_request .deserialize_response::(user) .await @@ -55,13 +58,13 @@ impl types::GuildMember { member_id, role_id ); + let chorus_request = ChorusRequest { - request: Client::new() - .put(url) - .header("Authorization", user.token()) - .header("Content-Type", "application/json"), + request: Client::new().put(url), limit_type: LimitType::Guild(guild_id), - }; + } + .with_headers_for(user); + chorus_request.handle_request_as_result(user).await } @@ -84,12 +87,13 @@ impl types::GuildMember { member_id, role_id ); + let chorus_request = ChorusRequest { - request: Client::new() - .delete(url) - .header("Authorization", user.token()), + request: Client::new().delete(url), limit_type: LimitType::Guild(guild_id), - }; + } + .with_headers_for(user); + chorus_request.handle_request_as_result(user).await } } diff --git a/src/api/guilds/roles.rs b/src/api/guilds/roles.rs index 7e760103..67f97566 100644 --- a/src/api/guilds/roles.rs +++ b/src/api/guilds/roles.rs @@ -28,15 +28,16 @@ impl types::RoleObject { user.belongs_to.read().unwrap().urls.api, guild_id ); + let chorus_request = ChorusRequest { - request: Client::new().get(url).header("Authorization", user.token()), + request: Client::new().get(url), limit_type: LimitType::Guild(guild_id), - }; - let roles = chorus_request + } + .with_headers_for(user); + + chorus_request .deserialize_response::>(user) .await - .unwrap(); - Ok(roles) } /// Retrieves a single role for a given guild. @@ -54,10 +55,13 @@ impl types::RoleObject { guild_id, role_id ); + let chorus_request = ChorusRequest { - request: Client::new().get(url).header("Authorization", user.token()), + request: Client::new().get(url), limit_type: LimitType::Guild(guild_id), - }; + } + .with_headers_for(user); + chorus_request .deserialize_response::(user) .await @@ -79,19 +83,13 @@ impl types::RoleObject { user.belongs_to.read().unwrap().urls.api, guild_id ); - let body = to_string::(&role_create_schema).map_err(|e| { - ChorusError::FormCreation { - error: e.to_string(), - } - })?; + let chorus_request = ChorusRequest { - request: Client::new() - .post(url) - .header("Authorization", user.token()) - .header("Content-Type", "application/json") - .body(body), + request: Client::new().post(url).json(&role_create_schema), limit_type: LimitType::Guild(guild_id), - }; + } + .with_headers_for(user); + chorus_request .deserialize_response::(user) .await @@ -113,18 +111,13 @@ impl types::RoleObject { user.belongs_to.read().unwrap().urls.api, guild_id ); - let body = - to_string(&role_position_update_schema).map_err(|e| ChorusError::FormCreation { - error: e.to_string(), - })?; + let chorus_request = ChorusRequest { - request: Client::new() - .patch(url) - .header("Authorization", user.token()) - .header("Content-Type", "application/json") - .body(body), + request: Client::new().patch(url).json(&role_position_update_schema), limit_type: LimitType::Guild(guild_id), - }; + } + .with_headers_for(user); + chorus_request .deserialize_response::(user) .await @@ -148,19 +141,13 @@ impl types::RoleObject { guild_id, role_id ); - let body = to_string::(&role_create_schema).map_err(|e| { - ChorusError::FormCreation { - error: e.to_string(), - } - })?; + let chorus_request = ChorusRequest { - request: Client::new() - .patch(url) - .header("Authorization", user.token()) - .header("Content-Type", "application/json") - .body(body), + request: Client::new().patch(url).json(&role_create_schema), limit_type: LimitType::Guild(guild_id), - }; + } + .with_headers_for(user); + chorus_request .deserialize_response::(user) .await @@ -183,14 +170,13 @@ impl types::RoleObject { role_id ); - let request = ChorusRequest::new( - http::Method::DELETE, - &url, - None, - audit_log_reason.as_deref(), - Some(user), - LimitType::Guild(guild_id), - ); + let request = ChorusRequest { + request: Client::new().delete(url), + limit_type: LimitType::Guild(guild_id), + } + .with_maybe_audit_log_reason(audit_log_reason) + .with_headers_for(user); + request.handle_request_as_result(user).await } } diff --git a/src/api/invites/mod.rs b/src/api/invites/mod.rs index 68f14178..94047f14 100644 --- a/src/api/invites/mod.rs +++ b/src/api/invites/mod.rs @@ -23,15 +23,18 @@ impl ChorusUser { session_id: Option<&str>, ) -> ChorusResult { let mut request = ChorusRequest { - request: Client::new() - .post(format!( - "{}/invites/{}", - self.belongs_to.read().unwrap().urls.api, - invite_code - )) - .header("Authorization", self.token()), + request: Client::new().post(format!( + "{}/invites/{}", + self.belongs_to.read().unwrap().urls.api, + invite_code + )), limit_type: LimitType::Global, - }; + } + .with_headers_for(self); + + // FIXME: Not how this is serialized! + // + // It should be as """{"session_id": "something"}""" if let Some(session_id) = session_id { request.request = request .request @@ -54,11 +57,10 @@ impl ChorusUser { "{}/users/@me/invites", self.belongs_to.read().unwrap().urls.api )) - .body(to_string(&code).unwrap()) - .header("Authorization", self.token()) - .header("Content-Type", "application/json"), + .json(&code), limit_type: LimitType::Global, } + .with_headers_for(self) .deserialize_response::(self) .await } @@ -82,11 +84,10 @@ impl ChorusUser { self.belongs_to.read().unwrap().urls.api, channel_id )) - .header("Authorization", self.token()) - .header("Content-Type", "application/json") - .body(to_string(&create_channel_invite_schema).unwrap()), + .json(&create_channel_invite_schema), limit_type: LimitType::Channel(channel_id), } + .with_headers_for(self) .deserialize_response::(self) .await } diff --git a/src/api/users/channels.rs b/src/api/users/channels.rs index 44ed8975..586a4380 100644 --- a/src/api/users/channels.rs +++ b/src/api/users/channels.rs @@ -23,12 +23,10 @@ impl ChorusUser { self.belongs_to.read().unwrap().urls.api ); ChorusRequest { - request: Client::new() - .get(url) - .header("Authorization", self.token()) - .header("Content-Type", "application/json"), + request: Client::new().get(url), limit_type: LimitType::Global, } + .with_headers_for(self) .deserialize_response::>(self) .await } @@ -49,13 +47,10 @@ impl ChorusUser { self.belongs_to.read().unwrap().urls.api ); ChorusRequest { - request: Client::new() - .post(url) - .header("Authorization", self.token()) - .header("Content-Type", "application/json") - .body(to_string(&create_private_channel_schema).unwrap()), + request: Client::new().post(url).json(&create_private_channel_schema), limit_type: LimitType::Global, } + .with_headers_for(self) .deserialize_response::(self) .await } diff --git a/src/api/users/connections.rs b/src/api/users/connections.rs index 29c40df1..a076d261 100644 --- a/src/api/users/connections.rs +++ b/src/api/users/connections.rs @@ -41,15 +41,15 @@ impl ChorusUser { self.belongs_to.read().unwrap().urls.api, connection_type_string )) - // Note: ommiting this header causes a 401 Unauthorized, - // even though discord.sex mentions it as unauthenticated - .header("Authorization", self.token()) .query(&query_parameters); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), - }; + } + .with_headers_for(self); + // Note: ommiting authorization causes a 401 Unauthorized, + // even though discord.sex mentions it as unauthenticated chorus_request .deserialize_response::(self) @@ -81,13 +81,13 @@ impl ChorusUser { self.belongs_to.read().unwrap().urls.api, connection_type_string )) - .header("Authorization", self.token()) .json(&json_schema); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), - }; + } + .with_headers_for(self); chorus_request.deserialize_response(self).await } @@ -111,13 +111,13 @@ impl ChorusUser { self.belongs_to.read().unwrap().urls.api, connection_account_id )) - .header("Authorization", self.token()) .json(&json_schema); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), - }; + } + .with_headers_for(self); chorus_request.deserialize_response(self).await } @@ -169,18 +169,17 @@ impl ChorusUser { &mut self, domain: &String, ) -> ChorusResult { - let request = Client::new() - .post(format!( - "{}/users/@me/connections/domain/{}", - self.belongs_to.read().unwrap().urls.api, - domain - )) - .header("Authorization", self.token()); + let request = Client::new().post(format!( + "{}/users/@me/connections/domain/{}", + self.belongs_to.read().unwrap().urls.api, + domain + )); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), - }; + } + .with_headers_for(self); let result = chorus_request .deserialize_response::(self) @@ -217,17 +216,16 @@ impl ChorusUser { /// # Reference /// See pub async fn get_connections(&mut self) -> ChorusResult> { - let request = Client::new() - .get(format!( - "{}/users/@me/connections", - self.belongs_to.read().unwrap().urls.api, - )) - .header("Authorization", self.token()); + let request = Client::new().get(format!( + "{}/users/@me/connections", + self.belongs_to.read().unwrap().urls.api, + )); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), - }; + } + .with_headers_for(self); chorus_request.deserialize_response(self).await } @@ -245,19 +243,18 @@ impl ChorusUser { .expect("Failed to serialize connection type!") .replace('"', ""); - let request = Client::new() - .post(format!( - "{}/users/@me/connections/{}/{}/refresh", - self.belongs_to.read().unwrap().urls.api, - connection_type_string, - connection_account_id - )) - .header("Authorization", self.token()); + let request = Client::new().post(format!( + "{}/users/@me/connections/{}/{}/refresh", + self.belongs_to.read().unwrap().urls.api, + connection_type_string, + connection_account_id + )); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), - }; + } + .with_headers_for(self); chorus_request.handle_request_as_result(self).await } @@ -286,13 +283,13 @@ impl ChorusUser { connection_type_string, connection_account_id )) - .header("Authorization", self.token()) .json(&json_schema); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), - }; + } + .with_headers_for(self); chorus_request.deserialize_response(self).await } @@ -310,19 +307,18 @@ impl ChorusUser { .expect("Failed to serialize connection type!") .replace('"', ""); - let request = Client::new() - .delete(format!( - "{}/users/@me/connections/{}/{}", - self.belongs_to.read().unwrap().urls.api, - connection_type_string, - connection_account_id - )) - .header("Authorization", self.token()); + let request = Client::new().delete(format!( + "{}/users/@me/connections/{}/{}", + self.belongs_to.read().unwrap().urls.api, + connection_type_string, + connection_account_id + )); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), - }; + } + .with_headers_for(self); chorus_request.handle_request_as_result(self).await } @@ -342,19 +338,18 @@ impl ChorusUser { .expect("Failed to serialize connection type!") .replace('"', ""); - let request = Client::new() - .get(format!( - "{}/users/@me/connections/{}/{}/access-token", - self.belongs_to.read().unwrap().urls.api, - connection_type_string, - connection_account_id - )) - .header("Authorization", self.token()); + let request = Client::new().get(format!( + "{}/users/@me/connections/{}/{}/access-token", + self.belongs_to.read().unwrap().urls.api, + connection_type_string, + connection_account_id + )); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), - }; + } + .with_headers_for(self); chorus_request .deserialize_response::(self) @@ -373,18 +368,17 @@ impl ChorusUser { &mut self, connection_account_id: &String, ) -> ChorusResult> { - let request = Client::new() - .get(format!( - "{}/users/@me/connections/reddit/{}/subreddits", - self.belongs_to.read().unwrap().urls.api, - connection_account_id - )) - .header("Authorization", self.token()); + let request = Client::new().get(format!( + "{}/users/@me/connections/reddit/{}/subreddits", + self.belongs_to.read().unwrap().urls.api, + connection_account_id + )); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), - }; + } + .with_headers_for(self); chorus_request.deserialize_response(self).await } diff --git a/src/api/users/guilds.rs b/src/api/users/guilds.rs index 46a25fb6..c37019bc 100644 --- a/src/api/users/guilds.rs +++ b/src/api/users/guilds.rs @@ -26,9 +26,8 @@ impl ChorusUser { self.belongs_to.read().unwrap().urls.api, guild_id )) - .header("Authorization", self.token()) - .header("Content-Type", "application/json") - .body(to_string(&lurking).unwrap()), + // FIXME not how you serialize this + .json(&lurking), limit_type: LimitType::Guild(*guild_id), } .handle_request_as_result(self) @@ -44,12 +43,10 @@ impl ChorusUser { &mut self, query: Option, ) -> ChorusResult> { - let query_parameters = { if let Some(query_some) = query { query_some.to_query() - } - else { + } else { Vec::new() } }; @@ -59,14 +56,12 @@ impl ChorusUser { self.belongs_to.read().unwrap().urls.api, ); let chorus_request = ChorusRequest { - request: Client::new() - .get(url) - .header("Authorization", self.token()) - .header("Content-Type", "application/json") - .query(&query_parameters), + request: Client::new().get(url).query(&query_parameters), limit_type: LimitType::Global, - }; + } + .with_headers_for(self); + chorus_request .deserialize_response::>(self) .await diff --git a/src/api/users/mfa.rs b/src/api/users/mfa.rs index 621f0692..c2ab02cd 100644 --- a/src/api/users/mfa.rs +++ b/src/api/users/mfa.rs @@ -32,13 +32,13 @@ impl ChorusUser { "{}/users/@me/mfa/totp/enable", self.belongs_to.read().unwrap().urls.api )) - .header("Authorization", self.token()) .json(&schema); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), - }; + } + .with_headers_for(self); let response: EnableTotpMfaResponse = chorus_request.deserialize_response(self).await?; @@ -61,18 +61,17 @@ impl ChorusUser { /// # Reference /// See pub async fn disable_totp_mfa(&mut self) -> ChorusResult { - let request = Client::new() - .post(format!( - "{}/users/@me/mfa/totp/disable", - self.belongs_to.read().unwrap().urls.api - )) - .header("Authorization", self.token()); + let request = Client::new().post(format!( + "{}/users/@me/mfa/totp/disable", + self.belongs_to.read().unwrap().urls.api + )); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), } - .with_maybe_mfa(&self.mfa_token); + .with_maybe_mfa(&self.mfa_token) + .with_headers_for(self); let response: Token = chorus_request.deserialize_response(self).await?; @@ -98,14 +97,14 @@ impl ChorusUser { "{}/users/@me/mfa/sms/enable", self.belongs_to.read().unwrap().urls.api )) - .header("Authorization", self.token()) .json(&schema); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), } - .with_maybe_mfa(&self.mfa_token); + .with_maybe_mfa(&self.mfa_token) + .with_headers_for(self); chorus_request.handle_request_as_result(self).await } @@ -125,14 +124,14 @@ impl ChorusUser { "{}/users/@me/mfa/sms/disable", self.belongs_to.read().unwrap().urls.api )) - .header("Authorization", self.token()) .json(&schema); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), } - .with_maybe_mfa(&self.mfa_token); + .with_maybe_mfa(&self.mfa_token) + .with_headers_for(self); chorus_request.handle_request_as_result(self).await } @@ -143,17 +142,16 @@ impl ChorusUser { /// # Reference /// See pub async fn get_webauthn_authenticators(&mut self) -> ChorusResult> { - let request = Client::new() - .get(format!( - "{}/users/@me/mfa/webauthn/credentials", - self.belongs_to.read().unwrap().urls.api - )) - .header("Authorization", self.token()); + let request = Client::new().get(format!( + "{}/users/@me/mfa/webauthn/credentials", + self.belongs_to.read().unwrap().urls.api + )); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), - }; + } + .with_headers_for(self); chorus_request.deserialize_response(self).await } @@ -177,18 +175,17 @@ impl ChorusUser { pub async fn begin_webauthn_authenticator_creation( &mut self, ) -> ChorusResult { - let request = Client::new() - .post(format!( - "{}/users/@me/mfa/webauthn/credentials", - self.belongs_to.read().unwrap().urls.api - )) - .header("Authorization", self.token()); + let request = Client::new().post(format!( + "{}/users/@me/mfa/webauthn/credentials", + self.belongs_to.read().unwrap().urls.api + )); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), } - .with_maybe_mfa(&self.mfa_token); + .with_maybe_mfa(&self.mfa_token) + .with_headers_for(self); chorus_request.deserialize_response(self).await } @@ -221,14 +218,14 @@ impl ChorusUser { "{}/users/@me/mfa/webauthn/credentials", self.belongs_to.read().unwrap().urls.api )) - .header("Authorization", self.token()) .json(&schema); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), } - .with_maybe_mfa(&self.mfa_token); + .with_maybe_mfa(&self.mfa_token) + .with_headers_for(self); chorus_request.deserialize_response(self).await } @@ -256,14 +253,14 @@ impl ChorusUser { self.belongs_to.read().unwrap().urls.api, authenticator_id )) - .header("Authorization", self.token()) .json(&schema); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), } - .with_maybe_mfa(&self.mfa_token); + .with_maybe_mfa(&self.mfa_token) + .with_headers_for(self); chorus_request.deserialize_response(self).await } @@ -287,19 +284,18 @@ impl ChorusUser { &mut self, authenticator_id: Snowflake, ) -> ChorusResult<()> { - let request = Client::new() - .delete(format!( - "{}/users/@me/mfa/webauthn/credentials/{}", - self.belongs_to.read().unwrap().urls.api, - authenticator_id - )) - .header("Authorization", self.token()); + let request = Client::new().delete(format!( + "{}/users/@me/mfa/webauthn/credentials/{}", + self.belongs_to.read().unwrap().urls.api, + authenticator_id + )); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), } - .with_maybe_mfa(&self.mfa_token); + .with_maybe_mfa(&self.mfa_token) + .with_headers_for(self); chorus_request.handle_request_as_result(self).await } @@ -323,13 +319,13 @@ impl ChorusUser { "{}/auth/verify/view-backup-codes-challenge", self.belongs_to.read().unwrap().urls.api, )) - .header("Authorization", self.token()) .json(&schema); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), - }; + } + .with_headers_for(self); chorus_request.deserialize_response(self).await } @@ -359,13 +355,13 @@ impl ChorusUser { "{}/users/@me/mfa/codes-verification", self.belongs_to.read().unwrap().urls.api, )) - .header("Authorization", self.token()) .json(&schema); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), - }; + } + .with_headers_for(self); chorus_request.deserialize_response(self).await } diff --git a/src/api/users/relationships.rs b/src/api/users/relationships.rs index c77db447..7d9f2ed3 100644 --- a/src/api/users/relationships.rs +++ b/src/api/users/relationships.rs @@ -30,9 +30,10 @@ impl ChorusUser { user_id ); let chorus_request = ChorusRequest { - request: Client::new().get(url).header("Authorization", self.token()), + request: Client::new().get(url), limit_type: LimitType::Global, - }; + } + .with_headers_for(self); chorus_request .deserialize_response::>(self) .await @@ -48,9 +49,10 @@ impl ChorusUser { self.belongs_to.read().unwrap().urls.api ); let chorus_request = ChorusRequest { - request: Client::new().get(url).header("Authorization", self.token()), + request: Client::new().get(url), limit_type: LimitType::Global, - }; + } + .with_headers_for(self); chorus_request .deserialize_response::>(self) .await @@ -68,15 +70,11 @@ impl ChorusUser { "{}/users/@me/relationships", self.belongs_to.read().unwrap().urls.api ); - let body = to_string(&schema).unwrap(); let chorus_request = ChorusRequest { - request: Client::new() - .post(url) - .header("Authorization", self.token()) - .header("Content-Type", "application/json") - .body(body), + request: Client::new().post(url).json(&schema), limit_type: LimitType::Global, - }; + } + .with_headers_for(self); chorus_request.handle_request_as_result(self).await } @@ -93,14 +91,14 @@ impl ChorusUser { RelationshipType::None => { let chorus_request = ChorusRequest { request: Client::new() - .delete(format!("{}/users/@me/relationships/{}", api_url, user_id)) - .header("Authorization", self.token()), + .delete(format!("{}/users/@me/relationships/{}", api_url, user_id)), limit_type: LimitType::Global, - }; + } + .with_headers_for(self); chorus_request.handle_request_as_result(self).await } RelationshipType::Friends | RelationshipType::Incoming | RelationshipType::Outgoing => { - let body = CreateUserRelationshipSchema { + let schema = CreateUserRelationshipSchema { relationship_type: None, // Selecting 'None' here will accept an incoming FR or send a new FR. from_friend_suggestion: None, friend_token: None, @@ -108,14 +106,14 @@ impl ChorusUser { let chorus_request = ChorusRequest { request: Client::new() .put(format!("{}/users/@me/relationships/{}", api_url, user_id)) - .header("Authorization", self.token()) - .body(to_string(&body).unwrap()), + .json(&schema), limit_type: LimitType::Global, - }; + } + .with_headers_for(self); chorus_request.handle_request_as_result(self).await } RelationshipType::Blocked => { - let body = CreateUserRelationshipSchema { + let schema = CreateUserRelationshipSchema { relationship_type: Some(RelationshipType::Blocked), from_friend_suggestion: None, friend_token: None, @@ -123,10 +121,10 @@ impl ChorusUser { let chorus_request = ChorusRequest { request: Client::new() .put(format!("{}/users/@me/relationships/{}", api_url, user_id)) - .header("Authorization", self.token()) - .body(to_string(&body).unwrap()), + .json(&schema), limit_type: LimitType::Global, - }; + } + .with_headers_for(self); chorus_request.handle_request_as_result(self).await } RelationshipType::Suggestion | RelationshipType::Implicit => Ok(()), @@ -144,11 +142,10 @@ impl ChorusUser { user_id ); let chorus_request = ChorusRequest { - request: Client::new() - .delete(url) - .header("Authorization", self.token()), + request: Client::new().delete(url), limit_type: LimitType::Global, - }; + } + .with_headers_for(self); chorus_request.handle_request_as_result(self).await } } diff --git a/src/api/users/users.rs b/src/api/users/users.rs index a430d545..d9968a9a 100644 --- a/src/api/users/users.rs +++ b/src/api/users/users.rs @@ -109,15 +109,14 @@ impl ChorusUser { "{}/users/@me", self.belongs_to.read().unwrap().urls.api )) - .body(to_string(&modify_schema).unwrap()) - .header("Authorization", self.token()) - .header("Content-Type", "application/json"); + .json(&modify_schema); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), } - .with_maybe_mfa(&self.mfa_token); + .with_maybe_mfa(&self.mfa_token) + .with_headers_for(self); chorus_request.deserialize_response::(self).await } @@ -139,14 +138,14 @@ impl ChorusUser { "{}/users/@me/disable", self.belongs_to.read().unwrap().urls.api )) - .header("Authorization", self.token()) .json(&schema); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), } - .with_maybe_mfa(&self.mfa_token); + .with_maybe_mfa(&self.mfa_token) + .with_headers_for(self); chorus_request.handle_request_as_result(self).await } @@ -166,14 +165,14 @@ impl ChorusUser { "{}/users/@me/delete", self.belongs_to.read().unwrap().urls.api )) - .header("Authorization", self.token()) .json(&schema); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), } - .with_maybe_mfa(&self.mfa_token); + .with_maybe_mfa(&self.mfa_token) + .with_headers_for(self); chorus_request.handle_request_as_result(self).await } @@ -226,16 +225,15 @@ impl ChorusUser { /// # Reference /// See pub async fn initiate_email_change(&mut self) -> ChorusResult<()> { - let request = Client::new() - .put(format!( - "{}/users/@me/email", - self.belongs_to.read().unwrap().urls.api - )) - .header("Authorization", self.token()); + let request = Client::new().put(format!( + "{}/users/@me/email", + self.belongs_to.read().unwrap().urls.api + )); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), - }; + } + .with_headers_for(self); chorus_request.handle_request_as_result(self).await } @@ -261,12 +259,12 @@ impl ChorusUser { "{}/users/@me/email/verify-code", self.belongs_to.read().unwrap().urls.api )) - .header("Authorization", self.token()) .json(&schema); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), - }; + } + .with_headers_for(self); chorus_request .deserialize_response::(self) .await @@ -287,17 +285,16 @@ impl ChorusUser { /// # Reference /// See pub async fn get_pomelo_suggestions(&mut self) -> ChorusResult { - let request = Client::new() - .get(format!( - "{}/users/@me/pomelo-suggestions", - self.belongs_to.read().unwrap().urls.api - )) - .header("Authorization", self.token()); + let request = Client::new().get(format!( + "{}/users/@me/pomelo-suggestions", + self.belongs_to.read().unwrap().urls.api + )); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), - }; + } + .with_headers_for(self); chorus_request .deserialize_response::(self) .await @@ -319,7 +316,6 @@ impl ChorusUser { "{}/users/@me/pomelo-attempt", self.belongs_to.read().unwrap().urls.api )) - .header("Authorization", self.token()) // FIXME: should we create a type for this? .body(format!(r#"{{ "username": {:?} }}"#, username)) .header("Content-Type", "application/json"); @@ -327,7 +323,8 @@ impl ChorusUser { let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), - }; + } + .with_headers_for(self); chorus_request .deserialize_response::(self) .await @@ -359,7 +356,6 @@ impl ChorusUser { "{}/users/@me/pomelo", self.belongs_to.read().unwrap().urls.api )) - .header("Authorization", self.token()) // FIXME: should we create a type for this? .body(format!(r#"{{ "username": {:?} }}"#, username)) .header("Content-Type", "application/json"); @@ -367,7 +363,8 @@ impl ChorusUser { let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), - }; + } + .with_headers_for(self); let result = chorus_request.deserialize_response::(self).await; @@ -397,13 +394,13 @@ impl ChorusUser { "{}/users/@me/mentions", self.belongs_to.read().unwrap().urls.api )) - .header("Authorization", self.token()) .query(&query_parameters); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), - }; + } + .with_headers_for(self); chorus_request .deserialize_response::>(self) @@ -420,18 +417,17 @@ impl ChorusUser { /// # Reference /// See pub async fn delete_recent_mention(&mut self, message_id: Snowflake) -> ChorusResult<()> { - let request = Client::new() - .delete(format!( - "{}/users/@me/mentions/{}", - self.belongs_to.read().unwrap().urls.api, - message_id - )) - .header("Authorization", self.token()); + let request = Client::new().delete(format!( + "{}/users/@me/mentions/{}", + self.belongs_to.read().unwrap().urls.api, + message_id + )); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), - }; + } + .with_headers_for(self); chorus_request.handle_request_as_result(self).await } @@ -446,17 +442,16 @@ impl ChorusUser { /// # Reference /// See pub async fn get_harvest(&mut self) -> ChorusResult> { - let request = Client::new() - .get(format!( - "{}/users/@me/harvest", - self.belongs_to.read().unwrap().urls.api, - )) - .header("Authorization", self.token()); + let request = Client::new().get(format!( + "{}/users/@me/harvest", + self.belongs_to.read().unwrap().urls.api, + )); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), - }; + } + .with_headers_for(self); // Manual handling, because a 204 with no harvest is a success state // TODO: Maybe make this a method on ChorusRequest if we need it a lot @@ -523,13 +518,13 @@ impl ChorusUser { "{}/users/@me/harvest", self.belongs_to.read().unwrap().urls.api, )) - .header("Authorization", self.token()) .json(&schema); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), - }; + } + .with_headers_for(self); chorus_request.deserialize_response(self).await } @@ -542,17 +537,16 @@ impl ChorusUser { /// # Reference /// See pub async fn get_user_notes(&mut self) -> ChorusResult> { - let request = Client::new() - .get(format!( - "{}/users/@me/notes", - self.belongs_to.read().unwrap().urls.api, - )) - .header("Authorization", self.token()); + let request = Client::new().get(format!( + "{}/users/@me/notes", + self.belongs_to.read().unwrap().urls.api, + )); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), - }; + } + .with_headers_for(self); chorus_request.deserialize_response(self).await } @@ -595,17 +589,16 @@ impl ChorusUser { /// # Reference /// See pub async fn get_user_affinities(&mut self) -> ChorusResult { - let request = Client::new() - .get(format!( - "{}/users/@me/affinities/users", - self.belongs_to.read().unwrap().urls.api, - )) - .header("Authorization", self.token()); + let request = Client::new().get(format!( + "{}/users/@me/affinities/users", + self.belongs_to.read().unwrap().urls.api, + )); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), - }; + } + .with_headers_for(self); chorus_request.deserialize_response(self).await } @@ -615,17 +608,16 @@ impl ChorusUser { /// # Reference /// See pub async fn get_guild_affinities(&mut self) -> ChorusResult { - let request = Client::new() - .get(format!( - "{}/users/@me/affinities/guilds", - self.belongs_to.read().unwrap().urls.api, - )) - .header("Authorization", self.token()); + let request = Client::new().get(format!( + "{}/users/@me/affinities/guilds", + self.belongs_to.read().unwrap().urls.api, + )); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), - }; + } + .with_headers_for(self); chorus_request.deserialize_response(self).await } @@ -641,17 +633,16 @@ impl ChorusUser { /// # Reference /// See pub async fn get_premium_usage(&mut self) -> ChorusResult { - let request = Client::new() - .get(format!( - "{}/users/@me/premium-usage", - self.belongs_to.read().unwrap().urls.api, - )) - .header("Authorization", self.token()); + let request = Client::new().get(format!( + "{}/users/@me/premium-usage", + self.belongs_to.read().unwrap().urls.api, + )); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), - }; + } + .with_headers_for(self); chorus_request.deserialize_response(self).await } @@ -664,17 +655,16 @@ impl ChorusUser { /// # Notes /// As of 2024/08/18, Spacebar does not yet implement this endpoint. pub async fn get_burst_credits(&mut self) -> ChorusResult { - let request = Client::new() - .get(format!( - "{}/users/@me/burst-credits", - self.belongs_to.read().unwrap().urls.api, - )) - .header("Authorization", self.token()); + let request = Client::new().get(format!( + "{}/users/@me/burst-credits", + self.belongs_to.read().unwrap().urls.api, + )); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), - }; + } + .with_headers_for(self); chorus_request.deserialize_response(self).await } @@ -688,13 +678,12 @@ impl User { pub async fn get_current(user: &mut ChorusUser) -> ChorusResult { let url_api = user.belongs_to.read().unwrap().urls.api.clone(); let url = format!("{}/users/@me", url_api); - let request = reqwest::Client::new() - .get(url) - .header("Authorization", user.token()); + let request = reqwest::Client::new().get(url); let chorus_request = ChorusRequest { request, limit_type: LimitType::Global, - }; + } + .with_headers_for(user); chorus_request.deserialize_response::(user).await } @@ -705,13 +694,12 @@ impl User { pub async fn get(user: &mut ChorusUser, id: Snowflake) -> ChorusResult { let url_api = user.belongs_to.read().unwrap().urls.api.clone(); let url = format!("{}/users/{}", url_api, id); - let request = reqwest::Client::new() - .get(url) - .header("Authorization", user.token()); + let request = reqwest::Client::new().get(url); let chorus_request = ChorusRequest { request, limit_type: LimitType::Global, - }; + } + .with_headers_for(user); chorus_request .deserialize_response::(user) .await @@ -742,9 +730,7 @@ impl User { ) -> ChorusResult { let url_api = user.belongs_to.read().unwrap().urls.api.clone(); let url = format!("{}/users/username/{username}", url_api); - let mut request = reqwest::Client::new() - .get(url) - .header("Authorization", user.token()); + let mut request = reqwest::Client::new().get(url); if let Some(some_discriminator) = discriminator { request = request.query(&[("discriminator", some_discriminator)]); @@ -753,7 +739,8 @@ impl User { let chorus_request = ChorusRequest { request, limit_type: LimitType::Global, - }; + } + .with_headers_for(user); chorus_request .deserialize_response::(user) .await @@ -765,13 +752,13 @@ impl User { /// See pub async fn get_settings(user: &mut ChorusUser) -> ChorusResult { let url_api = user.belongs_to.read().unwrap().urls.api.clone(); - let request: reqwest::RequestBuilder = Client::new() - .get(format!("{}/users/@me/settings", url_api)) - .header("Authorization", user.token()); + let request: reqwest::RequestBuilder = + Client::new().get(format!("{}/users/@me/settings", url_api)); let chorus_request = ChorusRequest { request, limit_type: LimitType::Global, - }; + } + .with_headers_for(user); chorus_request .deserialize_response::(user) .await @@ -797,13 +784,13 @@ impl User { let url_api = user.belongs_to.read().unwrap().urls.api.clone(); let request: reqwest::RequestBuilder = Client::new() .get(format!("{}/users/{}/profile", url_api, id)) - .header("Authorization", user.token()) .query(&query_parameters); let chorus_request = ChorusRequest { request, limit_type: LimitType::Global, - }; + } + .with_headers_for(user); chorus_request .deserialize_response::(user) .await @@ -822,12 +809,12 @@ impl User { let url_api = user.belongs_to.read().unwrap().urls.api.clone(); let request: reqwest::RequestBuilder = Client::new() .patch(format!("{}/users/@me/profile", url_api)) - .header("Authorization", user.token()) .json(&schema); let chorus_request = ChorusRequest { request, limit_type: LimitType::Global, - }; + } + .with_headers_for(user); chorus_request .deserialize_response::(user) .await @@ -844,18 +831,17 @@ impl User { user: &mut ChorusUser, target_user_id: Snowflake, ) -> ChorusResult { - let request = Client::new() - .get(format!( - "{}/users/@me/notes/{}", - user.belongs_to.read().unwrap().urls.api, - target_user_id - )) - .header("Authorization", user.token()); + let request = Client::new().get(format!( + "{}/users/@me/notes/{}", + user.belongs_to.read().unwrap().urls.api, + target_user_id + )); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), - }; + } + .with_headers_for(user); chorus_request.deserialize_response(user).await } @@ -879,13 +865,13 @@ impl User { user.belongs_to.read().unwrap().urls.api, target_user_id )) - .header("Authorization", user.token()) .json(&schema); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), - }; + } + .with_headers_for(user); chorus_request.handle_request_as_result(user).await } diff --git a/src/instance.rs b/src/instance.rs index aa891ede..a4c60463 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -19,8 +19,8 @@ use crate::gateway::{Gateway, GatewayHandle, GatewayOptions}; use crate::ratelimiter::ChorusRequest; use crate::types::types::subconfigs::limits::rates::RateLimits; use crate::types::{ - GatewayIdentifyPayload, GeneralConfiguration, Limit, LimitType, LimitsConfiguration, MfaToken, - MfaTokenSchema, MfaVerifySchema, Shared, User, UserSettings, + ClientProperties, GatewayIdentifyPayload, GeneralConfiguration, Limit, LimitType, + LimitsConfiguration, MfaToken, MfaTokenSchema, MfaVerifySchema, Shared, User, UserSettings, }; use crate::UrlBundle; @@ -256,15 +256,35 @@ impl fmt::Display for Token { #[derive(Debug, Clone)] /// A ChorusUser is a representation of an authenticated user on an [Instance]. +/// /// It is used for most authenticated actions on a Spacebar server. +/// /// It also has its own [Gateway] connection. pub struct ChorusUser { + /// A reference to the [Instance] the user is registered on pub belongs_to: Shared, + + /// The user's authentication token pub token: String, + + /// Telemetry data sent to the instance. + /// + /// See [ClientProperties] for more information + pub client_properties: ClientProperties, + + /// A token for bypassing mfa, if any pub mfa_token: Option, + + /// Ratelimit data pub limits: Option>, + + /// The user's settings pub settings: Shared, + + /// Information about the user pub object: Shared, + + /// The user's connection to the gateway pub gateway: GatewayHandle, } @@ -285,6 +305,7 @@ impl ChorusUser { pub fn new( belongs_to: Shared, token: String, + client_properties: ClientProperties, limits: Option>, settings: Shared, object: Shared, @@ -293,6 +314,7 @@ impl ChorusUser { ChorusUser { belongs_to, token, + client_properties, mfa_token: None, limits, settings, @@ -305,7 +327,7 @@ impl ChorusUser { /// /// Fetches all the other required data from the api. /// - /// If the received_settings can be None, since not all login methods + /// The received_settings can be None, since not all login methods /// return user settings. If this is the case, we'll fetch them via an api route. pub(crate) async fn update_with_login_data( &mut self, @@ -314,8 +336,9 @@ impl ChorusUser { ) -> ChorusResult<()> { self.token = token.clone(); - let mut identify = GatewayIdentifyPayload::common(); + let mut identify = GatewayIdentifyPayload::default_w_client_capabilities(); identify.token = token; + identify.properties = self.client_properties.clone(); self.gateway.send_identify(identify).await; *self.object.write().unwrap() = self.get_current_user().await?; @@ -345,6 +368,7 @@ impl ChorusUser { let gateway = Gateway::spawn(wss_url, gateway_options).await.unwrap(); ChorusUser { token: token.to_string(), + client_properties: ClientProperties::default(), mfa_token: None, belongs_to: instance.clone(), limits: instance diff --git a/src/ratelimiter.rs b/src/ratelimiter.rs index 785c57db..48ded41c 100644 --- a/src/ratelimiter.rs +++ b/src/ratelimiter.rs @@ -13,7 +13,10 @@ use serde_json::from_str; use crate::{ errors::{ChorusError, ChorusResult}, instance::ChorusUser, - types::{types::subconfigs::limits::rates::RateLimits, Limit, LimitType, LimitsConfiguration, MfaRequiredSchema}, + types::{ + types::subconfigs::limits::rates::RateLimits, Limit, LimitType, LimitsConfiguration, + MfaRequiredSchema, + }, }; /// Chorus' request struct. This struct is used to send rate-limited requests to the Spacebar server. @@ -25,53 +28,6 @@ pub struct ChorusRequest { } impl ChorusRequest { - /// Makes a new [`ChorusRequest`]. - /// # Arguments - /// * `method` - The HTTP method to use. Must be one of the following: - /// * [`http::Method::GET`] - /// * [`http::Method::POST`] - /// * [`http::Method::PUT`] - /// * [`http::Method::DELETE`] - /// * [`http::Method::PATCH`] - /// * [`http::Method::HEAD`] - #[allow(unused_variables)] - pub fn new( - method: http::Method, - url: &str, - body: Option, - audit_log_reason: Option<&str>, - chorus_user: Option<&mut ChorusUser>, - limit_type: LimitType, - ) -> ChorusRequest { - let request = Client::new(); - let mut request = match method { - http::Method::GET => request.get(url), - http::Method::POST => request.post(url), - http::Method::PUT => request.put(url), - http::Method::DELETE => request.delete(url), - http::Method::PATCH => request.patch(url), - http::Method::HEAD => request.head(url), - _ => panic!("Illegal state: Method not supported."), - }; - if let Some(user) = chorus_user { - request = request.header("Authorization", user.token()); - } - if let Some(body) = body { - // ONCE TOLD ME THE WORLD WAS GONNA ROLL ME - request = request - .body(body) - .header("Content-Type", "application/json"); - } - if let Some(reason) = audit_log_reason { - request = request.header("X-Audit-Log-Reason", reason); - } - - ChorusRequest { - request, - limit_type, - } - } - /// Sends a [`ChorusRequest`]. Checks if the user is rate limited, and if not, sends the request. /// If the user is not rate limited and the instance has rate limits enabled, it will update the /// rate limits. @@ -83,8 +39,13 @@ impl ChorusRequest { bucket: format!("{:?}", self.limit_type), }); } + + let request = self + .request + .header("User-Agent", user.client_properties.user_agent.clone().0); + let client = user.belongs_to.read().unwrap().client.clone(); - let result = match client.execute(self.request.build().unwrap()).await { + let result = match client.execute(request.build().unwrap()).await { Ok(result) => { log::trace!("Request successful: {:?}", result); result @@ -524,6 +485,56 @@ impl ChorusRequest { }; Ok(object) } + + /// Adds an audit log reason to the request. + /// + /// Sets the X-Audit-Log-Reason header + pub(crate) fn with_audit_log_reason(self, reason: String) -> ChorusRequest { + let mut request = self; + + request.request = request.request.header("X-Audit-Log-Reason", reason); + request + } + + /// Adds an audit log reason to the request, if it is [Some] + /// + /// Sets the X-Audit-Log-Reason header + pub(crate) fn with_maybe_audit_log_reason(self, reason: Option) -> ChorusRequest { + if let Some(reason_some) = reason { + return self.with_audit_log_reason(reason_some); + } + + self + } + + /// Adds an authorization token to the request. + /// + /// Sets the Authorization header + pub(crate) fn with_authorization(self, token: &String) -> ChorusRequest { + let mut request = self; + + request.request = request.request.header("Authorization", token); + request + } + + /// Adds authorization for a [ChorusUser] to the request. + /// + /// Sets the Authorization header + pub(crate) fn with_authorization_for(self, user: &ChorusUser) -> ChorusRequest { + self.with_authorization(&user.token) + } + + /// Adds user-specific headers for a [ChorusUser] to the request. + /// + /// Adds authorization and telemetry; for specific details see + /// [Self::with_authorization_for] and [Self::with_client_properties_for] + /// + /// If a route you're adding involves authorization as the user, you + /// should likely use this method. + pub(crate) fn with_headers_for(self, user: &ChorusUser) -> ChorusRequest { + self.with_authorization_for(user) + .with_client_properties_for(user) + } } enum LimitOrigin { diff --git a/src/types/events/identify.rs b/src/types/events/identify.rs index f6d0e071..a248f285 100644 --- a/src/types/events/identify.rs +++ b/src/types/events/identify.rs @@ -2,16 +2,15 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -use crate::types::events::WebSocketEvent; +use crate::types::{events::WebSocketEvent, ClientProperties}; use serde::{Deserialize, Serialize}; -use serde_with::serde_as; use super::GatewayIdentifyPresenceUpdate; #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, WebSocketEvent)] pub struct GatewayIdentifyPayload { pub token: String, - pub properties: GatewayIdentifyConnectionProps, + pub properties: ClientProperties, #[serde(skip_serializing_if = "Option::is_none")] pub compress: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -32,29 +31,31 @@ pub struct GatewayIdentifyPayload { impl Default for GatewayIdentifyPayload { fn default() -> Self { - Self::common() + Self { + token: String::new(), + properties: ClientProperties::default(), + compress: None, + large_threshold: None, + shard: None, + presence: None, + intents: None, + capabilities: None, + } } } impl GatewayIdentifyPayload { - /// Uses the most common, 25% data along with client capabilities + /// Uses the most common data along with client capabilities /// - /// Basically pretends to be an official client on Windows 10, with Chrome 113.0.0.0 + /// Basically pretends to be an official client on Windows 10 pub fn common() -> Self { Self { - token: "".to_string(), - properties: GatewayIdentifyConnectionProps::default(), - compress: Some(false), - large_threshold: None, - shard: None, - presence: None, - intents: None, + properties: ClientProperties::default(), capabilities: Some(8189), + ..Self::default() } } -} -impl GatewayIdentifyPayload { /// Creates an identify payload with the same default capabilities as the official client pub fn default_w_client_capabilities() -> Self { Self { @@ -71,108 +72,3 @@ impl GatewayIdentifyPayload { } } } - -#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, WebSocketEvent)] -#[serde_as] -pub struct GatewayIdentifyConnectionProps { - /// Almost always sent - /// - /// ex: "Linux", "Windows", "Mac OS X" - /// - /// ex (mobile): "Windows Mobile", "iOS", "Android", "BlackBerry" - #[serde(default)] - pub os: String, - /// Almost always sent - /// - /// ex: "Firefox", "Chrome", "Opera Mini", "Opera", "Blackberry", "Facebook Mobile", "Chrome iOS", "Mobile Safari", "Safari", "Android Chrome", "Android Mobile", "Edge", "Konqueror", "Internet Explorer", "Mozilla", "Discord Client" - #[serde(default)] - pub browser: String, - /// Sometimes not sent, acceptable to be "" - /// - /// Speculation: - /// Only sent for mobile devices - /// - /// ex: "BlackBerry", "Windows Phone", "Android", "iPhone", "iPad", "" - #[serde_as(as = "NoneAsEmptyString")] - pub device: Option, - /// Almost always sent, most commonly en-US - /// - /// ex: "en-US" - #[serde(default)] - pub system_locale: String, - /// Almost always sent - /// - /// ex: any user agent, most common is "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36" - #[serde(default)] - pub browser_user_agent: String, - /// Almost always sent - /// - /// ex: "113.0.0.0" - #[serde(default)] - pub browser_version: String, - /// Sometimes not sent, acceptable to be "" - /// - /// ex: "10" (For os = "Windows") - #[serde_as(as = "NoneAsEmptyString")] - pub os_version: Option, - /// Sometimes not sent, acceptable to be "" - #[serde_as(as = "NoneAsEmptyString")] - pub referrer: Option, - /// Sometimes not sent, acceptable to be "" - #[serde_as(as = "NoneAsEmptyString")] - pub referring_domain: Option, - /// Sometimes not sent, acceptable to be "" - #[serde_as(as = "NoneAsEmptyString")] - pub referrer_current: Option, - /// Almost always sent, most commonly "stable" - #[serde(default)] - pub release_channel: String, - /// Almost always sent, identifiable if default is 0, should be around 199933 - #[serde(default)] - pub client_build_number: u64, - //pub client_event_source: Option -} - -impl Default for GatewayIdentifyConnectionProps { - /// Uses the most common, 25% data - fn default() -> Self { - Self::common() - } -} - -impl GatewayIdentifyConnectionProps { - /// Returns a minimal, least data possible default - fn minimal() -> Self { - Self { - os: String::new(), - browser: String::new(), - device: None, - system_locale: String::from("en-US"), - browser_user_agent: String::new(), - browser_version: String::new(), - os_version: None, - referrer: None, - referring_domain: None, - referrer_current: None, - release_channel: String::from("stable"), - client_build_number: 0, - } - } - - /// Returns the most common connection props so we can't be tracked - pub fn common() -> Self { - Self { - // See https://www.useragents.me/#most-common-desktop-useragents - // 25% of the web - //default.browser_user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36".to_string(); - browser: String::from("Chrome"), - browser_version: String::from("126.0.0.0"), - system_locale: String::from("en-US"), - os: String::from("Windows"), - os_version: Some(String::from("10")), - client_build_number: 222963, - release_channel: String::from("stable"), - ..Self::minimal() - } - } -} diff --git a/src/types/interfaces/client_properties.rs b/src/types/interfaces/client_properties.rs new file mode 100644 index 00000000..9d549ea3 --- /dev/null +++ b/src/types/interfaces/client_properties.rs @@ -0,0 +1,1036 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use base64::Engine; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +#[cfg(feature = "client")] +use crate::{instance::ChorusUser, ratelimiter::ChorusRequest}; + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[serde_as] +/// Tracking information for the current client, which is sent to +/// the discord servers in every gateway connection and http api request. +/// +/// This includes data about the os, distribution, browser, client, device +/// and even window manager the user is running. +/// +/// Chorus by default uses the most common data possible, pretending to be +/// an official client. +/// +/// Note that these values are (for now) written into the source code, meaning that +/// older version of the crate will be less effective. +/// +/// Unless a user of the library specifically goes out of their way to implement +/// it, chorus never sends real tracking data. +/// +/// # Why? +/// +/// You may ask, why would a library such as chorus even implement this? +/// +/// There are two main reasons: +/// +/// ## 1. Identifiability +/// +/// Not sending tracking data at all makes users of software built with chorus +/// stick out like a sore thumb; it screams "I am using a 3rd party client". +/// +/// Ideally, users of chorus-based software would blend in with users of the +/// official clients. +/// +/// ## 2. Anti-abuse systems +/// +/// Sadly, tracking data is also the most common way distinguish between real users +/// and poorly made self-bots abusing the user api. +/// +/// # Disabling +/// +/// By setting [ClientProperties::send_telemetry_headers] to false, it is possible to disable +/// sending these properties via headers in the HTTP API. +/// +/// (Sending them via the gateway is required, since it is a non-optional field in the schema) +/// +/// **Note that unless connecting to a server you specifically know doesn't care about these +/// headers, it is recommended to leave them enabled.** +/// +/// # Profiles +/// +/// Chorus contains a bunch of premade profiles: +/// - [ClientProperties::minimal] +/// - [ClientProperties::common_desktop_windows] - the most common data for a desktop client on +/// windows - this is also the profile used by default ([ClientProperties::default]) +/// - [ClientProperties::common_web_windows] - the most common settings for a web client on windows +/// - [ClientProperties::common_desktop_mac_os] - the most common settings for a desktop client on macos +/// - [ClientProperties::common_desktop_linux] - the most common settings for a desktop client on linux +/// +/// If you wish to create your own profile, please use `..ClientProperties::minimal()` instead of `..ClientProperties::default()` for unset fields. +/// +/// # Reference +/// See +pub struct ClientProperties { + /// **Not part of the sent data** + /// + /// If set to false, disables sending X-Super-Properties, X-Discord-Locale and X-Debug-Options + /// headers in the HTTP API. + /// + /// Note that unless connecting to a server you specifically know doesn't care about these + /// headers, it is recommended to leave them enabled. + #[serde(skip_serializing)] + pub send_telemetry_headers: bool, + + /// Always sent, must be provided + /// + /// See [ClientOs] for more details + #[serde(default)] + pub os: ClientOs, + + /// Always sent, must be provided + /// + /// Can also be an empty string + /// + /// See [ClientOsVersion] for more details + #[serde(default)] + pub os_version: ClientOsVersion, + + /// The operating system SDK version + /// + /// Not required + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub os_sdk_version: Option, + + /// The operating system architecture + /// + /// e.g. "x64" or "arm64" + /// + /// Not required + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub os_arch: Option, + + /// The architecture of the desktop app + /// + /// e.g. "x64" or "arm64" + /// + /// Not required + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub app_arch: Option, + + /// Always sent, must be provided + /// + /// Also includes desktop clients, not just web browsers + /// + /// See [ClientBrowser] for more details + #[serde(default)] + pub browser: ClientBrowser, + + /// Always sent, must be provided + /// + /// May be an empty string for mobile clients + /// + /// See [ClientUserAgent] for more details + #[serde(default)] + #[serde(rename = "browser_user_agent")] + pub user_agent: ClientUserAgent, + + /// Always sent, must be provided + /// + /// ex: "130.0.0.0" + #[serde(default)] + pub browser_version: String, + + /// Always sent, must be provided + /// + /// Current is ~ 355624 + /// + /// See [ClientBuildNumber] for more details + #[serde(default)] + pub client_build_number: ClientBuildNumber, + + /// The native metadata version of the desktop client, if using the new update system. + #[serde(default)] + pub native_build_number: Option, + + /// The mobile client version + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub client_version: Option, + + /// The alternate event source this request originated from + /// + /// # Reference + /// See + #[serde(default)] + pub client_event_source: Option, + + /// The release channel of the desktop / web client + /// + /// See [ClientReleaseChannel] for more details + #[serde(default)] + pub release_channel: ClientReleaseChannel, + + /// Always sent, must be provided + /// + /// most commonly "en-US" ([ClientSystemLocale::en_us]) + /// + /// See [ClientSystemLocale] for more details + #[serde(default)] + pub system_locale: ClientSystemLocale, + + /// Sometimes not sent, acceptable to be an empty string + /// + /// Speculation: + /// Only sent for mobile devices + /// + /// ex: "BlackBerry", "Windows Phone", "Android", "iPhone", "iPad", "" + #[serde(skip_serializing_if = "Option::is_none")] + pub device: Option, + + /// A unique identifier for the mobile device, + /// (random UUID on android, IdentifierForVendor on iOS) + #[serde(skip_serializing_if = "Option::is_none")] + pub device_vendor_id: Option, + + /// The linux window manager running the client. + /// + /// Acquired by the official client as + /// (env.XDG_CURRENT_DESKTOP ?? "unknown" + "," + env.GDMSESSION ?? "unknown") + /// + #[serde(skip_serializing_if = "Option::is_none")] + pub window_manager: Option, + + /// The linux distribution running the client. + /// + /// Acquired by the official client as the output of lsb_release -ds + /// + #[serde(skip_serializing_if = "Option::is_none")] + pub distro: Option, + + /// The url that referred the user to the client + /// + /// Sent as an empty string if there is no referrer + #[serde_as(as = "NoneAsEmptyString")] + pub referrer: Option, + + /// Same as referrer, but for the current session + /// + /// Sent as an empty string if there is no referrer + #[serde_as(as = "NoneAsEmptyString")] + pub referrer_current: Option, + + /// The domain of the url that referred the user to the client + /// + /// Sent as an empty string if there is no referrer + #[serde_as(as = "NoneAsEmptyString")] + pub referring_domain: Option, + + /// Same as referring_domain but for the current session + /// + /// Sent as an empty string if there is no referrer + #[serde_as(as = "NoneAsEmptyString")] + pub referring_domain_current: Option, + + /// The search engine which referred the user to the client. + /// + /// Common values are "google", "bing", "yahoo" and "duckduckgo" + /// + /// # Reference + /// See + #[serde(skip_serializing_if = "Option::is_none")] + pub search_engine: Option, + + /// Same as search_engine, but for the current session + #[serde(skip_serializing_if = "Option::is_none")] + pub search_engine_current: Option, + + /// Whether the client has modifications, e.g. BetterDiscord + /// + /// Note that these modifications usually don't make themselves known + #[serde(skip_serializing_if = "Option::is_none")] + pub has_client_mods: Option, +} + +impl Default for ClientProperties { + /// Uses the most common, data + fn default() -> Self { + Self::common() + } +} + +impl ClientProperties { + /// Returns a minimal, least data possible default + /// + /// If creating your own profile, use `..Self::minimal()` instead of `..Self::default()` + pub fn minimal() -> Self { + Self { + send_telemetry_headers: true, + os: ClientOs::custom(String::new()), + os_version: ClientOsVersion::custom(String::new()), + browser: ClientBrowser::custom(String::new()), + system_locale: ClientSystemLocale::en_us(), + user_agent: ClientUserAgent::custom(String::new()), + browser_version: String::new(), + release_channel: ClientReleaseChannel::stable(), + client_build_number: ClientBuildNumber::custom(0), + os_sdk_version: None, + os_arch: None, + app_arch: None, + native_build_number: None, + client_version: None, + device: None, + device_vendor_id: None, + window_manager: None, + distro: None, + referrer: None, + referrer_current: None, + referring_domain: None, + referring_domain_current: None, + search_engine: None, + search_engine_current: None, + has_client_mods: None, + client_event_source: None, + } + } + + /// Returns the most common properties for desktop web on windows + /// + /// Currently chrome 131.0.0 on windows 10 + /// + /// See + pub fn common_web_windows() -> Self { + // 24% of the web + Self { + os: ClientOs::windows(), + os_version: ClientOsVersion::common_windows(), + browser: ClientBrowser::chrome_desktop(), + browser_version: String::from("131.0.0"), + user_agent: ClientUserAgent::common_web_windows(), + system_locale: ClientSystemLocale::en_us(), + client_build_number: ClientBuildNumber::latest(), + release_channel: ClientReleaseChannel::stable(), + has_client_mods: Some(false), + ..Self::minimal() + } + } + + /// Returns the most common properties for the desktop client on windows + /// + /// See + pub fn common_desktop_windows() -> Self { + Self { + os: ClientOs::windows(), + os_version: ClientOsVersion::common_windows(), + browser: ClientBrowser::discord_desktop(), + browser_version: String::from("130.0.0"), + user_agent: ClientUserAgent::common_desktop_windows(), + system_locale: ClientSystemLocale::en_us(), + client_build_number: ClientBuildNumber::latest(), + os_arch: Some(String::from("x64")), + app_arch: Some(String::from("x64")), + has_client_mods: Some(false), + release_channel: ClientReleaseChannel::stable(), + ..Self::minimal() + } + } + + /// Returns the most common properties for the desktop client on mac os + /// + /// See + pub fn common_desktop_mac_os() -> Self { + Self { + os: ClientOs::mac_os(), + os_version: ClientOsVersion::common_mac_os(), + browser: ClientBrowser::discord_desktop(), + browser_version: String::from("130.0.0"), + user_agent: ClientUserAgent::common_desktop_macos(), + system_locale: ClientSystemLocale::en_us(), + client_build_number: ClientBuildNumber::latest(), + os_arch: Some(String::from("arm64")), + app_arch: Some(String::from("arm64")), + has_client_mods: Some(false), + release_channel: ClientReleaseChannel::stable(), + ..Self::minimal() + } + } + + /// Returns the most common properties for the desktop client on linux + /// + /// See + pub fn common_desktop_linux() -> Self { + Self { + os: ClientOs::linux(), + os_version: ClientOsVersion::latest_linux(), + browser: ClientBrowser::discord_desktop(), + browser_version: String::from("130.0.0"), + user_agent: ClientUserAgent::common_desktop_windows(), + system_locale: ClientSystemLocale::en_us(), + client_build_number: ClientBuildNumber::latest(), + os_arch: Some(String::from("x64")), + app_arch: Some(String::from("x64")), + has_client_mods: Some(false), + release_channel: ClientReleaseChannel::stable(), + ..Self::minimal() + } + } + + /// Returns the most common properties to reduce tracking, currently pretends to be a desktop client on Windows 10 + pub fn common() -> Self { + Self::common_desktop_windows() + } + + /// Encodes self to base64, for the X-Super-Properties header + pub fn to_base64(&self) -> String { + let as_json = serde_json::to_string(self).unwrap(); + + base64::prelude::BASE64_STANDARD.encode(as_json) + } +} + +#[cfg(feature = "client")] +impl ChorusRequest { + /// Adds client telemetry data to the request. + /// + /// Sets the X-Super-Properties, X-Discord-Locale and X-Debug-Options headers + /// + /// For more info, see [ClientProperties] + pub(crate) fn with_client_properties(self, properties: &ClientProperties) -> ChorusRequest { + // If they are specifically disabled, just return the unmodified request + if !properties.send_telemetry_headers { + return self; + } + + let mut request = self; + + let properties_as_b64 = properties.to_base64(); + + request.request = request + .request + .header("X-Super-Properties", properties_as_b64); + + // Fake discord locale as being the same as system locale + request.request = request + .request + .header("X-Discord-Locale", &properties.system_locale.0); + request.request = request + .request + .header("X-Debug-Options", "bugReporterEnabled"); + + // TODO: X-Discord-Timezone + // + // Does spoofing this have any real effect? + // + // Also, there is no clear "most common" option + // the most populous is UTC+08, but who's to say + // it's discord's most common one + // + // The US has the biggest market share per country (30%), + // which makes something like eastern time an option + // + // My speculation however is that just sending EST still + // sticks out, since most users will send a timezone like + // America/Toronto + // + // koza, 30/12/2024 + + // Note: User-Agent is set in ChorusRequest::send_request, since + // we want to send it for every request, even if it isn't + // to discord's servers directly + + request + } + + /// Adds client telemetry data for a [ChorusUser] to the request. + /// + /// Sets the X-Super-Properties, X-Discord-Locale and X-Debug-Options headers + /// + /// For more info, see [ClientProperties] + pub(crate) fn with_client_properties_for(self, user: &ChorusUser) -> ChorusRequest { + self.with_client_properties(&user.client_properties) + } +} + +/// The operating system the client is running on. +/// +/// # Notes +/// This is used for [ClientProperties] +/// +/// # Reference +/// See +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[serde(transparent)] +pub struct ClientOs(String); + +impl From for ClientOs { + fn from(value: String) -> Self { + ClientOs(value) + } +} + +impl From for String { + fn from(value: ClientOs) -> Self { + value.0 + } +} + +impl ClientOs { + pub fn android() -> ClientOs { + ClientOs(String::from("Android")) + } + + pub fn blackberry() -> ClientOs { + ClientOs(String::from("BlackBerry")) + } + + pub fn mac_os() -> ClientOs { + ClientOs(String::from("Mac OS X")) + } + + pub fn ios() -> ClientOs { + ClientOs(String::from("iOS")) + } + + pub fn linux() -> ClientOs { + ClientOs(String::from("Linux")) + } + + pub fn windows_mobile() -> ClientOs { + ClientOs(String::from("Windows Mobile")) + } + + pub fn windows() -> ClientOs { + ClientOs(String::from("Windows")) + } + + /// The most common os, currently Windows + pub fn common() -> ClientOs { + Self::windows() + } + + pub fn custom(value: String) -> ClientOs { + value.into() + } +} + +impl Default for ClientOs { + fn default() -> Self { + ClientOs::common() + } +} + +/// The operating system version the client is running on. +/// +/// For windows, this is 10, 11, ... +/// +/// For linux, this is the kernel version +/// +/// For android, this is the sdk version +/// +/// # Notes +/// This is used for [ClientProperties] +/// +/// # Reference +/// See +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[serde(transparent)] +pub struct ClientOsVersion(String); + +impl From for ClientOsVersion { + fn from(value: String) -> Self { + ClientOsVersion(value) + } +} + +impl From for String { + fn from(value: ClientOsVersion) -> Self { + value.0 + } +} + +impl ClientOsVersion { + /// The latest os version for [ClientOs::android] + // See https://apilevels.com/ and https://telemetrydeck.com/survey/android/Android/sdkVersions/ + pub fn latest_android() -> ClientOsVersion { + ClientOsVersion(String::from("35")) + } + + /// The currently most common os version for [ClientOs::android] + // See https://apilevels.com/ and https://telemetrydeck.com/survey/android/Android/sdkVersions/ + pub fn common_android() -> ClientOsVersion { + ClientOsVersion(String::from("34")) + } + + /// The latest os version for [ClientOs::mac_os] + // See https://en.wikipedia.org/wiki/MacOS_version_history and https://www.statista.com/statistics/944559/worldwide-macos-version-market-share/ + pub fn latest_mac_os() -> ClientOsVersion { + ClientOsVersion(String::from("15")) + } + + /// The currently most common os version for [ClientOs::mac_os] + // See https://en.wikipedia.org/wiki/MacOS_version_history and https://www.statista.com/statistics/944559/worldwide-macos-version-market-share/ + pub fn common_mac_os() -> ClientOsVersion { + ClientOsVersion(String::from("10.15")) + } + + /// The latest os version for [ClientOs::ios] + // See https://en.wikipedia.org/wiki/IOS_version_history and https://gs.statcounter.com/ios-version-market-share/ + pub fn latest_ios() -> ClientOsVersion { + ClientOsVersion(String::from("18.2")) + } + + /// The currently most common os version for [ClientOs::ios] + // See https://en.wikipedia.org/wiki/IOS_version_history and https://gs.statcounter.com/ios-version-market-share/ + pub fn common_ios() -> ClientOsVersion { + ClientOsVersion(String::from("17.6")) + } + + /// The latest os version for [ClientOs::linux] + // See https://en.wikipedia.org/wiki/Linux_kernel_version_history + pub fn latest_linux() -> ClientOsVersion { + ClientOsVersion(String::from("6.12.7")) + } + + // Note: I couldn't find which is the most commonly used + + /// The latest os version for [ClientOs::windows] + // See https://en.wikipedia.org/wiki/List_of_Microsoft_Windows_versions and https://gs.statcounter.com/os-version-market-share/windows/desktop/worldwide + pub fn latest_windows() -> ClientOsVersion { + ClientOsVersion(String::from("11")) + } + + /// The currently most common os version for [ClientOs::windows] + // See https://en.wikipedia.org/wiki/List_of_Microsoft_Windows_versions and https://gs.statcounter.com/os-version-market-share/windows/desktop/worldwide + pub fn common_windows() -> ClientOsVersion { + ClientOsVersion(String::from("10")) + } + + pub fn custom(value: String) -> ClientOsVersion { + value.into() + } +} + +// Empty string by default +impl Default for ClientOsVersion { + fn default() -> Self { + ClientOsVersion::custom(String::new()) + } +} + +/// The browser the client is running on. +/// +/// This also includes the desktop clients. +/// +/// # Notes +/// This is used for [ClientProperties] +/// +/// # Reference +/// See +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[serde(transparent)] +pub struct ClientBrowser(String); + +impl From for ClientBrowser { + fn from(value: String) -> Self { + ClientBrowser(value) + } +} + +impl From for String { + fn from(value: ClientBrowser) -> Self { + value.0 + } +} + +impl ClientBrowser { + /// The official discord client, on desktop + pub fn discord_desktop() -> ClientBrowser { + ClientBrowser(String::from("Discord Client")) + } + + /// The official discord client, on android + pub fn discord_android() -> ClientBrowser { + ClientBrowser(String::from("Discord Android")) + } + + /// The official discord client, on ios + pub fn discord_ios() -> ClientBrowser { + ClientBrowser(String::from("Discord iOS")) + } + + /// The official discord client, on e.g. consoles, Xbox + pub fn discord_embedded() -> ClientBrowser { + ClientBrowser(String::from("Discord Embedded")) + } + + pub fn chrome_android() -> ClientBrowser { + ClientBrowser(String::from("Android Chrome")) + } + + pub fn chrome_ios() -> ClientBrowser { + ClientBrowser(String::from("Chrome iOS")) + } + + pub fn chrome_desktop() -> ClientBrowser { + ClientBrowser(String::from("Chrome")) + } + + /// Generic android web browser + pub fn generic_android() -> ClientBrowser { + ClientBrowser(String::from("Android Mobile")) + } + + /// Blackberry web browser + pub fn blackberry() -> ClientBrowser { + ClientBrowser(String::from("BlackBerry")) + } + + /// Legacy microsoft edge + /// + /// Probably shouldn't be used + pub fn edge() -> ClientBrowser { + ClientBrowser(String::from("Edge")) + } + + /// Facebook mobile browser + pub fn facebook_mobile() -> ClientBrowser { + ClientBrowser(String::from("Facebook Mobile")) + } + + pub fn firefox() -> ClientBrowser { + ClientBrowser(String::from("Firefox")) + } + + pub fn internet_explorer() -> ClientBrowser { + ClientBrowser(String::from("Internet Explorer")) + } + + pub fn kde_konqueror() -> ClientBrowser { + ClientBrowser(String::from("Konqueror")) + } + + pub fn safari_ios() -> ClientBrowser { + ClientBrowser(String::from("Mobile Safari")) + } + + pub fn safari_desktop() -> ClientBrowser { + ClientBrowser(String::from("Safari")) + } + + /// Generic Mozilla-like browser + pub fn generic_mozilla() -> ClientBrowser { + ClientBrowser(String::from("Mozilla")) + } + + pub fn opera() -> ClientBrowser { + ClientBrowser(String::from("Opera")) + } + + pub fn opera_mini() -> ClientBrowser { + ClientBrowser(String::from("Opera Mini")) + } + + /// The most common web browser, currently chrome on desktop + // See https://en.wikipedia.org/wiki/Usage_share_of_web_browsers + pub fn common() -> ClientBrowser { + Self::chrome_desktop() + } + + pub fn custom(value: String) -> ClientBrowser { + value.into() + } +} + +impl Default for ClientBrowser { + fn default() -> Self { + ClientBrowser::common() + } +} + +/// The user agent of the browser the client is running on. +/// +/// May be blank on mobile clients +/// +/// # Notes +/// This is used for [ClientProperties] +/// +/// # Reference +/// See +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[serde(transparent)] +pub struct ClientUserAgent(pub(crate) String); + +impl From for ClientUserAgent { + fn from(value: String) -> Self { + ClientUserAgent(value) + } +} + +impl From for String { + fn from(value: ClientUserAgent) -> Self { + value.0 + } +} + +impl ClientUserAgent { + /// Returns the most common user agent used for the web client on windows + /// + /// Currently Chrome 131.0.0 on Windows 10, 24% of the web + /// + /// See + pub fn common_web_windows() -> ClientUserAgent { + ClientUserAgent(String::from("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.3")) + } + + /// Returns the most common user agent used on Android web + /// + /// Currently Chrome 131.0.0 on android + /// + /// See + pub fn common_web_android() -> ClientUserAgent { + ClientUserAgent(String::from("Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.3")) + } + + /// Returns the most common user agent used for the ios web + /// + /// Currently Safari on ios 18.1.1 + /// + /// See + pub fn common_web_ios() -> ClientUserAgent { + ClientUserAgent(String::from("Mozilla/5.0 (iPhone; CPU iPhone OS 18_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.1.1 Mobile/15E148 Safari/604.")) + } + + /// Returns the most common user agent used for the Mac os web client + /// + /// Currently Safari 17.6 on Mac os 10.15.7 + /// + /// See + pub fn common_web_mac_os() -> ClientUserAgent { + ClientUserAgent(String::from("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.1")) + } + + /// Returns the most common user agent used for the Linux web client + /// + /// Currently Chrome 131.0.0 on Linux + /// + /// See + pub fn common_web_linux() -> ClientUserAgent { + ClientUserAgent(String::from("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36")) + } + + /// Returns the most common user agent used for the desktop client on Windows + /// + /// (this is mostly a guess, since we can't get statistics from discord themselves) + /// + /// Desktop useragents look similar to ones on Chrome; + /// this behaves like Windows 10 on Chrome 130.0.0.0 + /// + /// See + pub fn common_desktop_windows() -> ClientUserAgent { + ClientUserAgent(String::from("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36")) + } + + /// Returns the most common user agent used for the desktop client on macos + /// + /// (this is mostly a guess, since we can't get statistics from discord themselves) + /// + /// Desktop useragents look similar to ones on Chrome; + /// this behaves like a Mac 10.15.7 running Chrome 130.0.0.0 + /// + /// See + pub fn common_desktop_macos() -> ClientUserAgent { + ClientUserAgent(String::from("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36")) + } + + /// Returns the most common user agent used for the desktop client on linux + /// + /// (this is mostly a guess, since we can't get statistics from discord themselves) + /// + /// Desktop useragents look similar to ones on Chrome; + /// this behaves like Linux running Chrome 130.0.0.0 + /// + /// See + pub fn common_desktop_linux() -> ClientUserAgent { + ClientUserAgent(String::from("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36")) + } + + /// Returns the most common user agent used for the web client + /// + /// Currently Chrome 131.0.0 on Windows 10, 24% of the web + /// + /// See + pub fn common_web() -> ClientUserAgent { + Self::common_web_windows() + } + + /// Returns the most common user agent + pub fn common() -> ClientUserAgent { + Self::common_desktop_windows() + } + + pub fn custom(value: String) -> ClientUserAgent { + value.into() + } +} + +impl Default for ClientUserAgent { + fn default() -> Self { + ClientUserAgent::common() + } +} + +/// The build number of the client we are running. +/// +/// # Notes +/// This is used for [ClientProperties] +/// +/// # Reference +/// See +#[derive(Debug, Deserialize, Serialize, Copy, Clone, PartialEq, Eq)] +#[serde(transparent)] +pub struct ClientBuildNumber(u64); + +impl From for ClientBuildNumber { + fn from(value: u64) -> Self { + ClientBuildNumber(value) + } +} + +impl From for u64 { + fn from(value: ClientBuildNumber) -> Self { + value.0 + } +} + +impl ClientBuildNumber { + pub fn latest() -> ClientBuildNumber { + 355624.into() + } + + pub fn custom(value: u64) -> ClientBuildNumber { + value.into() + } +} + +impl Default for ClientBuildNumber { + fn default() -> Self { + ClientBuildNumber::latest() + } +} + +/// The release channel of the official client we are running on. +/// +/// The main channels are +/// - [Self::stable] +/// - [Self::ptb] (public test build) +/// - [Self::canary] (alpha test build) +/// +/// The desktop client has an additional [Self::development] channel, which follows +/// the [Self::canary] channel but is not recommended, as it can be broken or unstable at any time. +/// +/// For more information about the main channels, see +/// +/// # Notes +/// This is used for [ClientProperties] +/// +/// # Reference +/// See and +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[serde(transparent)] +pub struct ClientReleaseChannel(String); + +impl From for ClientReleaseChannel { + fn from(value: String) -> Self { + ClientReleaseChannel(value) + } +} + +impl From for String { + fn from(value: ClientReleaseChannel) -> Self { + value.0 + } +} + +impl ClientReleaseChannel { + /// Stable web / desktop channel + pub fn stable() -> ClientReleaseChannel { + ClientReleaseChannel::custom(String::from("stable")) + } + + /// Public test build web / desktop channel + pub fn ptb() -> ClientReleaseChannel { + ClientReleaseChannel::custom(String::from("ptb")) + } + + /// Alpha test build web channel + pub fn canary() -> ClientReleaseChannel { + ClientReleaseChannel::custom(String::from("canary")) + } + + /// Alpha test build desktop only channel, can be unstable or broken + pub fn development() -> ClientReleaseChannel { + ClientReleaseChannel::custom(String::from("development")) + } + + /// The most commonly used release channel, currently stable + pub fn common() -> ClientReleaseChannel { + Self::stable() + } + + pub fn custom(value: String) -> ClientReleaseChannel { + value.into() + } +} + +impl Default for ClientReleaseChannel { + fn default() -> Self { + ClientReleaseChannel::common() + } +} + +/// The locale ([IETF language tag](https://en.wikipedia.org/wiki/IETF_language_tag)) of the system +/// running the client. +/// +/// # Notes +/// This is used for [ClientProperties] +/// +/// # Reference +/// See +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[serde(transparent)] +pub struct ClientSystemLocale(String); + +impl From for ClientSystemLocale { + fn from(value: String) -> Self { + ClientSystemLocale(value) + } +} + +impl From for String { + fn from(value: ClientSystemLocale) -> Self { + value.0 + } +} + +impl ClientSystemLocale { + /// The en-US locale + pub fn en_us() -> ClientSystemLocale { + ClientSystemLocale(String::from("en-US")) + } + + /// The most commonly used system locale, currently en-US + pub fn common() -> ClientSystemLocale { + Self::en_us() + } + + pub fn custom(value: String) -> ClientSystemLocale { + value.into() + } +} + +impl Default for ClientSystemLocale { + fn default() -> Self { + ClientSystemLocale::common() + } +} diff --git a/src/types/interfaces/mod.rs b/src/types/interfaces/mod.rs index b5742d6b..9f18d17c 100644 --- a/src/types/interfaces/mod.rs +++ b/src/types/interfaces/mod.rs @@ -8,9 +8,11 @@ pub use connected_account::*; pub use guild_welcome_screen::*; pub use interaction::*; pub use status::*; +pub use client_properties::*; mod activity; mod connected_account; mod guild_welcome_screen; mod interaction; mod status; +mod client_properties; diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 4de7c68d..7f31f9f1 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -52,6 +52,7 @@ impl TestBundle { } pub(crate) async fn clone_user_without_gateway(&self) -> ChorusUser { ChorusUser { + client_properties: Default::default(), belongs_to: self.user.belongs_to.clone(), token: self.user.token.clone(), mfa_token: None,