This commit is contained in:
kozabrada123 2024-08-08 17:14:57 +00:00 committed by GitHub
commit 9791b13921
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 1148 additions and 57 deletions

View File

@ -39,7 +39,7 @@ impl Instance {
user.set_token(login_result.token); user.set_token(login_result.token);
user.settings = login_result.settings; user.settings = login_result.settings;
let object = User::get(&mut user, None).await?; let object = User::get_current(&mut user).await?;
*user.object.write().unwrap() = object; *user.object.write().unwrap() = object;
let mut identify = GatewayIdentifyPayload::common(); let mut identify = GatewayIdentifyPayload::common();

View File

@ -26,7 +26,7 @@ impl Instance {
let mut user = let mut user =
ChorusUser::shell(Arc::new(RwLock::new(self.clone())), token).await; ChorusUser::shell(Arc::new(RwLock::new(self.clone())), token).await;
let object = User::get(&mut user, None).await?; let object = User::get_current(&mut user).await?;
let settings = User::get_settings(&mut user).await?; let settings = User::get_settings(&mut user).await?;
*user.object.write().unwrap() = object; *user.object.write().unwrap() = object;

View File

@ -46,7 +46,7 @@ impl Instance {
.token; .token;
user.set_token(token); user.set_token(token);
let object = User::get(&mut user, None).await?; let object = User::get_current(&mut user).await?;
let settings = User::get_settings(&mut user).await?; let settings = User::get_settings(&mut user).await?;
*user.object.write().unwrap() = object; *user.object.write().unwrap() = object;

View File

@ -11,22 +11,66 @@ use crate::{
errors::{ChorusError, ChorusResult}, errors::{ChorusError, ChorusResult},
instance::{ChorusUser, Instance}, instance::{ChorusUser, Instance},
ratelimiter::ChorusRequest, ratelimiter::ChorusRequest,
types::{LimitType, User, UserModifySchema, UserSettings}, types::{
DeleteDisableUserSchema, GetPomeloEligibilityReturn, GetPomeloSuggestionsReturn,
GetUserProfileSchema, LimitType, PublicUser, Snowflake, User, UserModifyProfileSchema,
UserModifySchema, UserProfile, UserProfileMetadata, UserSettings,
VerifyUserEmailChangeResponse, VerifyUserEmailChangeSchema,
},
}; };
impl ChorusUser { impl ChorusUser {
/// Gets a user by id, or if the id is None, gets the current user. /// Gets the local / current user.
///
/// # Notes
/// This function is a wrapper around [`User::get_current`].
///
/// # Reference
/// See <https://docs.discord.sex/resources/user#get-current-user>
pub async fn get_current_user(&mut self) -> ChorusResult<User> {
User::get_current(self).await
}
/// Gets a non-local user by their id
/// ///
/// # Notes /// # Notes
/// This function is a wrapper around [`User::get`]. /// This function is a wrapper around [`User::get`].
/// ///
/// # Reference /// # Reference
/// See <https://discord-userdoccers.vercel.app/resources/user#get-user> and /// See <https://docs.discord.sex/resources/user#get-user>
/// <https://discord-userdoccers.vercel.app/resources/user#get-current-user> pub async fn get_user(&mut self, id: Snowflake) -> ChorusResult<PublicUser> {
pub async fn get_user(&mut self, id: Option<&String>) -> ChorusResult<User> {
User::get(self, id).await User::get(self, id).await
} }
/// Gets a non-local user by their unique username.
///
/// As of 2024/07/28, Spacebar does not yet implement this endpoint.
///
/// If fetching with a pomelo username, discriminator should be set to None.
///
/// This route also permits fetching users with their old pre-pomelo username#discriminator
/// combo.
///
/// Note:
///
/// "Unless the target user is a bot, you must be able to add
/// the user as a friend to resolve them by username.
///
/// Due to this restriction, you are not able to resolve your own username."
///
/// # Notes
/// This function is a wrapper around [`User::get_by_username`].
///
/// # Reference
/// See <https://docs.discord.sex/resources/user#get-user-by-username>
pub async fn get_user_by_username(
&mut self,
username: &String,
discriminator: Option<&String>,
) -> ChorusResult<PublicUser> {
User::get_by_username(self, username, discriminator).await
}
/// Gets the user's settings. /// Gets the user's settings.
/// ///
/// # Notes /// # Notes
@ -40,7 +84,6 @@ impl ChorusUser {
/// # Reference /// # Reference
/// See <https://discord-userdoccers.vercel.app/resources/user#modify-current-user> /// See <https://discord-userdoccers.vercel.app/resources/user#modify-current-user>
pub async fn modify(&mut self, modify_schema: UserModifySchema) -> ChorusResult<User> { pub async fn modify(&mut self, modify_schema: UserModifySchema) -> ChorusResult<User> {
// See <https://docs.discord.sex/resources/user#json-params>, note 1 // See <https://docs.discord.sex/resources/user#json-params>, note 1
let requires_current_password = modify_schema.username.is_some() let requires_current_password = modify_schema.username.is_some()
|| modify_schema.discriminator.is_some() || modify_schema.discriminator.is_some()
@ -67,39 +110,264 @@ impl ChorusUser {
chorus_request.deserialize_response::<User>(self).await chorus_request.deserialize_response::<User>(self).await
} }
/// Deletes the user from the Instance. /// Disables the current user's account.
///
/// Invalidates all active tokens.
///
/// Requires the user's current password (if any)
///
/// # Notes
/// Requires MFA
/// ///
/// # Reference /// # Reference
/// See <https://discord-userdoccers.vercel.app/resources/user#disable-user> /// See <https://docs.discord.sex/resources/user#disable-user>
pub async fn delete(mut self) -> ChorusResult<()> { pub async fn disable(&mut self, schema: DeleteDisableUserSchema) -> ChorusResult<()> {
let request = Client::new()
.post(format!(
"{}/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(),
};
chorus_request.handle_request_as_result(self).await
}
/// Deletes the current user from the Instance.
///
/// Requires the user's current password (if any)
///
/// # Notes
/// Requires MFA
///
/// # Reference
/// See <https://docs.discord.sex/resources/user#delete-user>
pub async fn delete(&mut self, schema: DeleteDisableUserSchema) -> ChorusResult<()> {
let request = Client::new() let request = Client::new()
.post(format!( .post(format!(
"{}/users/@me/delete", "{}/users/@me/delete",
self.belongs_to.read().unwrap().urls.api self.belongs_to.read().unwrap().urls.api
)) ))
.header("Authorization", self.token()) .header("Authorization", self.token())
.header("Content-Type", "application/json"); .json(&schema);
let chorus_request = ChorusRequest { let chorus_request = ChorusRequest {
request, request,
limit_type: LimitType::default(), limit_type: LimitType::default(),
}; };
chorus_request.handle_request_as_result(&mut self).await chorus_request.handle_request_as_result(self).await
}
/// Gets a user's profile object by their id.
///
/// This endpoint requires one of the following:
///
/// - The other user is a bot
/// - The other user shares a mutual guild with the current user
/// - The other user is a friend of the current user
/// - The other user is a friend suggestion of the current user
/// - The other user has an outgoing friend request to the current user
///
/// # Notes
/// This function is a wrapper around [`User::get_profile`].
///
/// # Reference
/// See <https://docs.discord.sex/resources/user#get-user-profile>
pub async fn get_user_profile(
&mut self,
id: Snowflake,
query_parameters: GetUserProfileSchema,
) -> ChorusResult<UserProfile> {
User::get_profile(self, id, query_parameters).await
}
/// Modifies the current user's profile.
///
/// Returns the updated [UserProfileMetadata].
///
/// # Notes
/// This function is a wrapper around [`User::modify_profile`].
///
/// # Reference
/// See <https://docs.discord.sex/resources/user#modify-user-profile>
pub async fn modify_profile(
&mut self,
schema: UserModifyProfileSchema,
) -> ChorusResult<UserProfileMetadata> {
User::modify_profile(self, schema).await
}
/// Initiates the email change process.
///
/// Sends a verification code to the current user's email.
///
/// Should be followed up with [Self::verify_email_change]
///
/// # Reference
/// See <https://docs.discord.sex/resources/user#modify-user-email>
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 chorus_request = ChorusRequest {
request,
limit_type: LimitType::default(),
};
chorus_request.handle_request_as_result(self).await
}
/// Verifies a code sent to change the current user's email.
///
/// Should be the follow-up to [Self::initiate_email_change]
///
/// This endpoint returns a token which can be used with [Self::modify]
/// to set a new email address (email_token).
///
/// As of 2024/08/08, Spacebar does not yet implement this endpoint.
// FIXME: Does this mean PUT users/@me/email is different?
///
/// # Reference
/// See <https://docs.discord.sex/resources/user#modify-user-email>
pub async fn verify_email_change(
&mut self,
schema: VerifyUserEmailChangeSchema,
) -> ChorusResult<VerifyUserEmailChangeResponse> {
let request = Client::new()
.post(format!(
"{}/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(),
};
chorus_request
.deserialize_response::<VerifyUserEmailChangeResponse>(self)
.await
}
/// Returns a suggested unique username based on the current user's username.
///
/// Note:
///
/// "This endpoint is used during the pomelo migration flow.
///
/// The user must be in the rollout to use this endpoint."
///
/// If a user has already migrated, this endpoint will likely return a 401 Unauthorized
/// ([ChorusError::NoPermission])
///
/// As of 2024/08/08, Spacebar does not yet implement this endpoint.
///
/// See <https://docs.discord.sex/resources/user#get-pomelo-suggestions>
pub async fn get_pomelo_suggestions(&mut self) -> ChorusResult<String> {
let request = Client::new()
.get(format!(
"{}/users/@me/pomelo-suggestions",
self.belongs_to.read().unwrap().urls.api
))
.header("Authorization", self.token());
let chorus_request = ChorusRequest {
request,
limit_type: LimitType::default(),
};
chorus_request
.deserialize_response::<GetPomeloSuggestionsReturn>(self)
.await
.map(|returned| returned.username)
}
/// Checks whether a unique username is available.
///
/// Returns whether the username is not taken yet.
///
/// As of 2024/08/08, Spacebar does not yet implement this endpoint.
///
/// See <https://docs.discord.sex/resources/user#get-pomelo-eligibility>
pub async fn get_pomelo_eligibility(&mut self, username: &String) -> ChorusResult<bool> {
let request = Client::new()
.post(format!(
"{}/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");
let chorus_request = ChorusRequest {
request,
limit_type: LimitType::default(),
};
chorus_request
.deserialize_response::<GetPomeloEligibilityReturn>(self)
.await
.map(|returned| !returned.taken)
}
/// Migrates the user from the username#discriminator to the unique username system.
///
/// Fires a [UserUpdate](crate::types::UserUpdate) gateway event.
///
/// Updates [Self::object] to an updated representation returned by the server.
// FIXME: Is this appropriate behaviour?
///
/// Note:
///
/// "This endpoint is used during the pomelo migration flow.
///
/// The user must be in the rollout to use this endpoint."
///
/// If a user has already migrated, this endpoint will likely return a 401 Unauthorized
/// ([ChorusError::NoPermission])
//
/// As of 2024/08/08, Spacebar does not yet implement this endpoint.
///
/// See <https://docs.discord.sex/resources/user#create-pomelo-migration>
pub async fn create_pomelo_migration(&mut self, username: &String) -> ChorusResult<()> {
let request = Client::new()
.post(format!(
"{}/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");
let chorus_request = ChorusRequest {
request,
limit_type: LimitType::default(),
};
let result = chorus_request.deserialize_response::<User>(self).await;
// FIXME: Does UserUpdate do this automatically? or would a user need to manually observe ChorusUser::object
if let Ok(new_object) = result {
*self.object.write().unwrap() = new_object;
return ChorusResult::Ok(());
}
ChorusResult::Err(result.err().unwrap())
} }
} }
impl User { impl User {
/// Gets a user by id, or if the id is None, gets the current user. /// Gets the local / current user.
/// ///
/// # Reference /// # Reference
/// See <https://discord-userdoccers.vercel.app/resources/user#get-user> and /// See <https://docs.discord.sex/resources/user#get-current-user>
/// <https://discord-userdoccers.vercel.app/resources/user#get-current-user> pub async fn get_current(user: &mut ChorusUser) -> ChorusResult<User> {
pub async fn get(user: &mut ChorusUser, id: Option<&String>) -> ChorusResult<User> {
let url_api = user.belongs_to.read().unwrap().urls.api.clone(); let url_api = user.belongs_to.read().unwrap().urls.api.clone();
let url = if id.is_none() { let url = format!("{}/users/@me", url_api);
format!("{}/users/@me", url_api)
} else {
format!("{}/users/{}", url_api, id.unwrap())
};
let request = reqwest::Client::new() let request = reqwest::Client::new()
.get(url) .get(url)
.header("Authorization", user.token()); .header("Authorization", user.token());
@ -107,16 +375,71 @@ impl User {
request, request,
limit_type: LimitType::Global, limit_type: LimitType::Global,
}; };
match chorus_request.send_request(user).await { chorus_request.deserialize_response::<User>(user).await
Ok(result) => {
let result_text = result.text().await.unwrap();
Ok(serde_json::from_str::<User>(&result_text).unwrap())
}
Err(e) => Err(e),
}
} }
/// Gets the user's settings. /// Gets a non-local user by their id
///
/// # Reference
/// See <https://discord-userdoccers.vercel.app/resources/user#get-user>
pub async fn get(user: &mut ChorusUser, id: Snowflake) -> ChorusResult<PublicUser> {
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 chorus_request = ChorusRequest {
request,
limit_type: LimitType::Global,
};
chorus_request
.deserialize_response::<PublicUser>(user)
.await
}
/// Gets a user by their unique username.
///
/// As of 2024/07/28, Spacebar does not yet implement this endpoint.
///
/// If fetching with a pomelo username, discriminator should be set to None.
///
/// This route also permits fetching users with their old pre-pomelo username#discriminator
/// combo.
///
/// Note:
///
/// "Unless the target user is a bot, you must be able to add
/// the user as a friend to resolve them by username.
///
/// Due to this restriction, you are not able to resolve your own username."
///
/// # Reference
/// See <https://docs.discord.sex/resources/user#get-user-by-username>
pub async fn get_by_username(
user: &mut ChorusUser,
username: &String,
discriminator: Option<&String>,
) -> ChorusResult<PublicUser> {
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());
if let Some(some_discriminator) = discriminator {
request = request.query(&[("discriminator", some_discriminator)]);
}
let chorus_request = ChorusRequest {
request,
limit_type: LimitType::Global,
};
chorus_request
.deserialize_response::<PublicUser>(user)
.await
}
/// Gets the current user's settings.
/// ///
/// # Reference /// # Reference
/// See <https://luna.gitlab.io/discord-unofficial-docs/docs/user_settings.html#get-usersmesettings> /// See <https://luna.gitlab.io/discord-unofficial-docs/docs/user_settings.html#get-usersmesettings>
@ -129,12 +452,64 @@ impl User {
request, request,
limit_type: LimitType::Global, limit_type: LimitType::Global,
}; };
match chorus_request.send_request(user).await { chorus_request
Ok(result) => { .deserialize_response::<UserSettings>(user)
let result_text = result.text().await.unwrap(); .await
Ok(serde_json::from_str(&result_text).unwrap())
}
Err(e) => Err(e),
} }
/// Gets a user's profile object by their id.
///
/// This endpoint requires one of the following:
///
/// - The other user is a bot
/// - The other user shares a mutual guild with the current user
/// - The other user is a friend of the current user
/// - The other user is a friend suggestion of the current user
/// - The other user has an outgoing friend request to the current user
///
/// # Reference
/// See <https://docs.discord.sex/resources/user#get-user-profile>
pub async fn get_profile(
user: &mut ChorusUser,
id: Snowflake,
query_parameters: GetUserProfileSchema,
) -> ChorusResult<UserProfile> {
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,
};
chorus_request
.deserialize_response::<UserProfile>(user)
.await
}
/// Modifies the current user's profile.
///
/// Returns the updated [UserProfileMetadata].
///
/// # Reference
/// See <https://docs.discord.sex/resources/user#modify-user-profile>
pub async fn modify_profile(
user: &mut ChorusUser,
schema: UserModifyProfileSchema,
) -> ChorusResult<UserProfileMetadata> {
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,
};
chorus_request
.deserialize_response::<UserProfileMetadata>(user)
.await
} }
} }

View File

@ -444,7 +444,7 @@ pub enum VerificationLevel {
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))] #[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
#[repr(u8)] #[repr(u8)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")] #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
/// See <https://discord-userdoccers.vercel.app/resources/guild#verification-level> /// See <https://docs.discord.sex/resources/guild#mfa-level>
pub enum MFALevel { pub enum MFALevel {
#[default] #[default]
None = 0, None = 0,
@ -467,7 +467,7 @@ pub enum MFALevel {
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))] #[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
#[repr(u8)] #[repr(u8)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")] #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
/// See <https://discord-userdoccers.vercel.app/resources/guild#verification-level> /// See <https://docs.discord.sex/resources/guild#nsfw-level>
pub enum NSFWLevel { pub enum NSFWLevel {
#[default] #[default]
Default = 0, Default = 0,
@ -492,12 +492,19 @@ pub enum NSFWLevel {
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))] #[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
#[repr(u8)] #[repr(u8)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")] #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
/// See <https://discord-userdoccers.vercel.app/resources/guild#verification-level> // Note: Maybe rename this to GuildPremiumTier?
/// **Guild** premium (Boosting) tier
///
/// See <https://docs.discord.sex/resources/guild#premium-tier>
pub enum PremiumTier { pub enum PremiumTier {
#[default] #[default]
/// No server boost perks
None = 0, None = 0,
/// Level 1 server boost perks
Tier1 = 1, Tier1 = 1,
/// Level 2 server boost perks
Tier2 = 2, Tier2 = 2,
/// Level 3 server boost perks
Tier3 = 3, Tier3 = 3,
} }

View File

@ -4,6 +4,7 @@
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use crate::types::{ use crate::types::{
entities::{Application, User}, entities::{Application, User},
@ -23,7 +24,7 @@ pub struct Integration {
pub syncing: Option<bool>, pub syncing: Option<bool>,
pub role_id: Option<String>, pub role_id: Option<String>,
pub enabled_emoticons: Option<bool>, pub enabled_emoticons: Option<bool>,
pub expire_behaviour: Option<u8>, pub expire_behaviour: Option<IntegrationExpireBehaviour>,
pub expire_grace_period: Option<u16>, pub expire_grace_period: Option<u16>,
#[cfg_attr(feature = "sqlx", sqlx(skip))] #[cfg_attr(feature = "sqlx", sqlx(skip))]
pub user: Option<Shared<User>>, pub user: Option<Shared<User>>,
@ -50,6 +51,7 @@ pub struct IntegrationAccount {
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))] #[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
#[cfg_attr(feature = "sqlx", sqlx(rename_all = "snake_case"))] #[cfg_attr(feature = "sqlx", sqlx(rename_all = "snake_case"))]
/// See <https://docs.discord.sex/resources/integration#integration-type>
pub enum IntegrationType { pub enum IntegrationType {
#[default] #[default]
Twitch, Twitch,
@ -57,3 +59,32 @@ pub enum IntegrationType {
Discord, Discord,
GuildSubscription, GuildSubscription,
} }
#[derive(
Serialize_repr,
Deserialize_repr,
Debug,
Default,
Clone,
Eq,
PartialEq,
Hash,
Copy,
PartialOrd,
Ord,
)]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
#[repr(u8)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
/// Defines the behaviour that is executed when a user's subscription to the integration expires.
///
/// See <https://docs.discord.sex/resources/integration#integration-expire-behavior>
pub enum IntegrationExpireBehaviour {
#[default]
/// Remove the subscriber role from the user
RemoveRole = 0,
/// Kick the user from the guild
Kick = 1,
}

View File

@ -7,6 +7,7 @@ use crate::types::utils::Snowflake;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_aux::prelude::deserialize_option_number_from_string; use serde_aux::prelude::deserialize_option_number_from_string;
use serde_repr::{Deserialize_repr, Serialize_repr};
use std::array::TryFromSliceError; use std::array::TryFromSliceError;
use std::fmt::Debug; use std::fmt::Debug;
@ -22,7 +23,7 @@ use crate::gateway::GatewayHandle;
#[cfg(feature = "client")] #[cfg(feature = "client")]
use chorus_macros::{Composite, Updateable}; use chorus_macros::{Composite, Updateable};
use super::Emoji; use super::{Emoji, GuildMember};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))] #[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
@ -39,6 +40,8 @@ impl User {
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, Hash)] #[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "client", derive(Updateable, Composite))] #[cfg_attr(feature = "client", derive(Updateable, Composite))]
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
/// # Reference
/// See <https://docs.discord.sex/resources/user#user-structure>
pub struct User { pub struct User {
pub id: Snowflake, pub id: Snowflake,
pub username: String, pub username: String,
@ -57,8 +60,10 @@ pub struct User {
#[serde(default)] #[serde(default)]
#[serde(deserialize_with = "deserialize_option_number_from_string")] #[serde(deserialize_with = "deserialize_option_number_from_string")]
pub flags: Option<UserFlags>, pub flags: Option<UserFlags>,
pub premium: Option<bool>,
/// The type of premium (Nitro) a user has
pub premium_type: Option<PremiumType>,
pub premium_since: Option<DateTime<Utc>>, pub premium_since: Option<DateTime<Utc>>,
pub premium_type: Option<u8>,
pub pronouns: Option<String>, pub pronouns: Option<String>,
pub public_flags: Option<UserFlags>, pub public_flags: Option<UserFlags>,
pub banner: Option<String>, pub banner: Option<String>,
@ -66,13 +71,15 @@ pub struct User {
pub theme_colors: Option<ThemeColors>, pub theme_colors: Option<ThemeColors>,
pub phone: Option<String>, pub phone: Option<String>,
pub nsfw_allowed: Option<bool>, pub nsfw_allowed: Option<bool>,
pub premium: Option<bool>,
pub purchased_flags: Option<i32>, pub purchased_flags: Option<i32>,
pub premium_usage_flags: Option<i32>, pub premium_usage_flags: Option<i32>,
pub disabled: Option<bool>, pub disabled: Option<bool>,
} }
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Copy)] #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Copy)]
/// A user's theme colors, as u32s representing hex color codes
///
/// found in [UserProfileMetadata]
pub struct ThemeColors { pub struct ThemeColors {
#[serde(flatten)] #[serde(flatten)]
inner: (u32, u32), inner: (u32, u32),
@ -139,6 +146,8 @@ impl sqlx::Type<sqlx::Any> for ThemeColors {
} }
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
/// # Reference
/// See <https://docs.discord.sex/resources/user#partial-user-structure>
pub struct PublicUser { pub struct PublicUser {
pub id: Snowflake, pub id: Snowflake,
pub username: Option<String>, pub username: Option<String>,
@ -150,7 +159,9 @@ pub struct PublicUser {
pub pronouns: Option<String>, pub pronouns: Option<String>,
pub bot: Option<bool>, pub bot: Option<bool>,
pub bio: Option<String>, pub bio: Option<String>,
pub premium_type: Option<u8>, /// The type of premium (Nitro) a user has
pub premium_type: Option<PremiumType>,
/// The date the user's premium (Nitro) subscribtion started
pub premium_since: Option<DateTime<Utc>>, pub premium_since: Option<DateTime<Utc>>,
pub public_flags: Option<UserFlags>, pub public_flags: Option<UserFlags>,
} }
@ -181,6 +192,8 @@ const CUSTOM_USER_FLAG_OFFSET: u64 = 1 << 32;
bitflags::bitflags! { bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, chorus_macros::SerdeBitFlags)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, chorus_macros::SerdeBitFlags)]
#[cfg_attr(feature = "sqlx", derive(chorus_macros::SqlxBitFlags))] #[cfg_attr(feature = "sqlx", derive(chorus_macros::SqlxBitFlags))]
/// # Reference
/// See <https://docs.discord.sex/resources/user#user-flags>
pub struct UserFlags: u64 { pub struct UserFlags: u64 {
const DISCORD_EMPLOYEE = 1 << 0; const DISCORD_EMPLOYEE = 1 << 0;
const PARTNERED_SERVER_OWNER = 1 << 1; const PARTNERED_SERVER_OWNER = 1 << 1;
@ -194,6 +207,7 @@ bitflags::bitflags! {
const EARLY_SUPPORTER = 1 << 9; const EARLY_SUPPORTER = 1 << 9;
const TEAM_USER = 1 << 10; const TEAM_USER = 1 << 10;
const TRUST_AND_SAFETY = 1 << 11; const TRUST_AND_SAFETY = 1 << 11;
/// Note: deprecated by Discord
const SYSTEM = 1 << 12; const SYSTEM = 1 << 12;
const HAS_UNREAD_URGENT_MESSAGES = 1 << 13; const HAS_UNREAD_URGENT_MESSAGES = 1 << 13;
const BUGHUNTER_LEVEL_2 = 1 << 14; const BUGHUNTER_LEVEL_2 = 1 << 14;
@ -205,14 +219,549 @@ bitflags::bitflags! {
} }
} }
#[derive(
Serialize_repr,
Deserialize_repr,
Debug,
Default,
Clone,
Eq,
PartialEq,
Hash,
Copy,
PartialOrd,
Ord,
)]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
#[repr(u8)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
/// **User** premium (Nitro) type
///
/// See <https://docs.discord.sex/resources/user#premium-type>
pub enum PremiumType {
#[default]
/// No Nitro
None = 0,
/// Nitro Classic
Tier1 = 1,
/// Nitro
Tier2 = 2,
/// Nitro Basic
Tier3 = 3,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
/// # Reference
/// See <https://docs.discord.sex/resources/user#profile-metadata-object>
pub struct UserProfileMetadata { pub struct UserProfileMetadata {
/// The guild ID this profile applies to, if it is a guild profile.
pub guild_id: Option<Snowflake>, pub guild_id: Option<Snowflake>,
/// The user's pronouns, up to 40 characters
pub pronouns: String, pub pronouns: String,
/// The user's bio / description, up to 190 characters
pub bio: Option<String>, pub bio: Option<String>,
/// The hash used to retrieve the user's banned from the CDN
pub banner: Option<String>, pub banner: Option<String>,
/// Banner color encoded as an i32 representation of a hex color code
pub accent_color: Option<i32>, pub accent_color: Option<i32>,
/// See [ThemeColors]
pub theme_colors: Option<ThemeColors>, pub theme_colors: Option<ThemeColors>,
pub popout_animation_particle_type: Option<Snowflake>, pub popout_animation_particle_type: Option<Snowflake>,
pub emoji: Option<Emoji>, pub emoji: Option<Emoji>,
} }
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
/// A user's publically facing profile
///
/// # Reference
/// See <https://docs.discord.sex/resources/user#profile-metadata-object>
pub struct UserProfile {
// TODO: add profile application object
pub user: PublicUser,
#[serde(rename = "user_profile")]
pub profile_metadata: UserProfileMetadata,
#[serde(default)]
pub badges: Vec<ProfileBadge>,
pub guild_member: Option<GuildMember>,
#[serde(rename = "guild_member_profile")]
pub guild_member_profile_metadata: Option<UserProfileMetadata>,
#[serde(default)]
pub guild_badges: Vec<ProfileBadge>,
/// The user's legacy username#discriminator, if existing and shown
pub legacy_username: Option<String>,
#[serde(default)]
pub mutual_guilds: Vec<MutualGuild>,
#[serde(default)]
pub mutual_friends: Vec<PublicUser>,
pub mutual_friends_count: Option<u32>,
// TODO: Add connections!
// TODO: And application role connections!
/// The type of premium (Nitro) a user has
pub premium_type: Option<PremiumType>,
/// The date the user's premium (Nitro) subscribtion started
pub premium_since: Option<DateTime<Utc>>,
/// The date the user's premium guild (Boosting) subscribtion started
pub premium_guild_since: Option<DateTime<Utc>>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
/// Info about a badge on a user's profile ([UserProfile])
///
/// # Reference
/// See <https://docs.discord.sex/resources/user#profile-badge-structure>
///
/// For a list of know badges, see <https://gist.github.com/XYZenix/c45156b7c883b5301c9028e39d71b479>
pub struct ProfileBadge {
/// The badge's unique id, e.g. "staff", "partner", "premium", ...
pub id: String,
/// Description of what the badge represents, e.g. "Discord Staff"
pub description: String,
/// An icon hash, to get the badge's icon from the CDN
pub icon: String,
/// A link (potentially used for href) for the badge.
///
/// e.g.:
/// "staff" badge links to "https://discord.com/company"
/// "certified_moderator" links to "https://discord.com/safety"
pub link: Option<String>,
}
impl PartialEq for ProfileBadge {
fn eq(&self, other: &Self) -> bool {
// Note: does not include description, since it changes for some badges
//
// Think nitro "Subscriber since ...", "Server boosting since ..."
self.id.eq(&other.id) && self.icon.eq(&other.icon) && self.link.eq(&other.link)
}
}
impl ProfileBadge {
/// Returns a badge representing the "staff" badge on Discord.com
///
/// # Reference
/// See <https://gist.github.com/XYZenix/c45156b7c883b5301c9028e39d71b479>
pub fn discord_staff() -> Self {
Self {
id: "staff".to_string(),
description: "Discord Staff".to_string(),
icon: "5e74e9b61934fc1f67c65515d1f7e60d".to_string(),
link: Some("https://discord.com/company".to_string()),
}
}
/// Returns a badge representing the partnered server owner badge on Discord.com
///
/// # Reference
/// See <https://gist.github.com/XYZenix/c45156b7c883b5301c9028e39d71b479>
pub fn discord_partner() -> Self {
Self {
id: "partner".to_string(),
description: "Partnered Server Owner".to_string(),
icon: "3f9748e53446a137a052f3454e2de41e".to_string(),
link: Some("https://discord.com/partners".to_string()),
}
}
/// Returns a badge representing the certified moderator badge on Discord.com
///
/// # Reference
/// See <https://gist.github.com/XYZenix/c45156b7c883b5301c9028e39d71b479>
pub fn discord_certified_moderator() -> Self {
Self {
id: "certified_moderator".to_string(),
description: "Moderator Programs Alumni".to_string(),
icon: "fee1624003e2fee35cb398e125dc479b".to_string(),
link: Some("https://discord.com/safety".to_string()),
}
}
/// Returns a badge representing the hypesquad events badge on Discord.com
///
/// # Reference
/// See <https://gist.github.com/XYZenix/c45156b7c883b5301c9028e39d71b479>
pub fn discord_hypesquad() -> Self {
Self {
id: "hypesquad".to_string(),
description: "HypeSquad Events".to_string(),
icon: "bf01d1073931f921909045f3a39fd264".to_string(),
link: Some("https://support.discord.com/hc/en-us/articles/360035962891-Profile-Badges-101#h_01GM67K5EJ16ZHYZQ5MPRW3JT3".to_string()),
}
}
/// Returns a badge representing the hypesquad bravery badge on Discord.com
///
/// # Reference
/// See <https://gist.github.com/XYZenix/c45156b7c883b5301c9028e39d71b479>
pub fn discord_hypesquad_bravery() -> Self {
Self {
id: "hypesquad_house_1".to_string(),
description: "HypeSquad Bravery".to_string(),
icon: "8a88d63823d8a71cd5e390baa45efa02".to_string(),
link: Some("https://discord.com/settings/hypesquad-online".to_string()),
}
}
/// Returns a badge representing the hypesquad brilliance badge on Discord.com
///
/// # Reference
/// See <https://gist.github.com/XYZenix/c45156b7c883b5301c9028e39d71b479>
pub fn discord_hypesquad_brilliance() -> Self {
Self {
id: "hypesquad_house_2".to_string(),
description: "HypeSquad Brilliance".to_string(),
icon: "011940fd013da3f7fb926e4a1cd2e618".to_string(),
link: Some("https://discord.com/settings/hypesquad-online".to_string()),
}
}
/// Returns a badge representing the hypesquad balance badge on Discord.com
///
/// # Reference
/// See <https://gist.github.com/XYZenix/c45156b7c883b5301c9028e39d71b479>
pub fn discord_hypesquad_balance() -> Self {
Self {
id: "hypesquad_house_3".to_string(),
description: "HypeSquad Balance".to_string(),
icon: "3aa41de486fa12454c3761e8e223442e".to_string(),
link: Some("https://discord.com/settings/hypesquad-online".to_string()),
}
}
/// Returns a badge representing the bug hunter level 1 badge on Discord.com
///
/// # Reference
/// See <https://gist.github.com/XYZenix/c45156b7c883b5301c9028e39d71b479>
pub fn discord_bug_hunter_1() -> Self {
Self {
id: "bug_hunter_level_1".to_string(),
description: "Discord Bug Hunter".to_string(),
icon: "2717692c7dca7289b35297368a940dd0".to_string(),
link: Some(
"https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs"
.to_string(),
),
}
}
/// Returns a badge representing the bug hunter level 2 badge on Discord.com
///
/// # Reference
/// See <https://gist.github.com/XYZenix/c45156b7c883b5301c9028e39d71b479>
pub fn discord_bug_hunter_2() -> Self {
Self {
id: "bug_hunter_level_2".to_string(),
description: "Discord Bug Hunter".to_string(),
icon: "848f79194d4be5ff5f81505cbd0ce1e6".to_string(),
link: Some(
"https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs"
.to_string(),
),
}
}
/// Returns a badge representing the active developer badge on Discord.com
///
/// # Reference
/// See <https://gist.github.com/XYZenix/c45156b7c883b5301c9028e39d71b479>
pub fn discord_active_developer() -> Self {
Self {
id: "active_developer".to_string(),
description: "Active Developer".to_string(),
icon: "6bdc42827a38498929a4920da12695d9".to_string(),
link: Some(
"https://support-dev.discord.com/hc/en-us/articles/10113997751447?ref=badge"
.to_string(),
),
}
}
/// Returns a badge representing the early verified bot developer badge on Discord.com
///
/// # Reference
/// See <https://gist.github.com/XYZenix/c45156b7c883b5301c9028e39d71b479>
pub fn discord_early_verified_developer() -> Self {
Self {
id: "verified_developer".to_string(),
description: "Early Verified Bot Developer".to_string(),
icon: "6df5892e0f35b051f8b61eace34f4967".to_string(),
link: None,
}
}
/// Returns a badge representing the early supporter badge on Discord.com
///
/// # Reference
/// See <https://gist.github.com/XYZenix/c45156b7c883b5301c9028e39d71b479>
pub fn discord_early_supporter() -> Self {
Self {
id: "early_supporter".to_string(),
description: "Early Supporter".to_string(),
icon: "7060786766c9c840eb3019e725d2b358".to_string(),
link: Some("https://discord.com/settings/premium".to_string()),
}
}
/// Returns a badge representing the nitro subscriber badge on Discord.com
///
/// Note: The description updates for the start date
///
/// # Reference
/// See <https://gist.github.com/XYZenix/c45156b7c883b5301c9028e39d71b479>
pub fn discord_nitro() -> Self {
Self {
id: "premium".to_string(),
description: "Subscriber since 1 Jan 2015".to_string(),
icon: "2ba85e8026a8614b640c2837bcdfe21b".to_string(),
link: Some("https://discord.com/settings/premium".to_string()),
}
}
/// Returns a badge representing the level 1 server boosting badge on Discord.com
///
/// Note: The description updates for the start date
///
/// # Reference
/// See <https://gist.github.com/XYZenix/c45156b7c883b5301c9028e39d71b479>
pub fn discord_server_boosting_1() -> Self {
Self {
id: "guild_booster_lvl1".to_string(),
description: "Server boosting since 1 Jan 2015".to_string(),
icon: "51040c70d4f20a921ad6674ff86fc95c".to_string(),
link: Some("https://discord.com/settings/premium".to_string()),
}
}
/// Returns a badge representing the level 2 server boosting badge on Discord.com
///
/// Note: The description updates for the start date
///
/// # Reference
/// See <https://gist.github.com/XYZenix/c45156b7c883b5301c9028e39d71b479>
pub fn discord_server_boosting_2() -> Self {
Self {
id: "guild_booster_lvl2".to_string(),
description: "Server boosting since 1 Jan 2015".to_string(),
icon: "0e4080d1d333bc7ad29ef6528b6f2fb7".to_string(),
link: Some("https://discord.com/settings/premium".to_string()),
}
}
/// Returns a badge representing the level 3 server boosting badge on Discord.com
///
/// Note: The description updates for the start date
///
/// # Reference
/// See <https://gist.github.com/XYZenix/c45156b7c883b5301c9028e39d71b479>
pub fn discord_server_boosting_3() -> Self {
Self {
id: "guild_booster_lvl3".to_string(),
description: "Server boosting since 1 Jan 2015".to_string(),
icon: "72bed924410c304dbe3d00a6e593ff59".to_string(),
link: Some("https://discord.com/settings/premium".to_string()),
}
}
/// Returns a badge representing the level 4 server boosting badge on Discord.com
///
/// Note: The description updates for the start date
///
/// # Reference
/// See <https://gist.github.com/XYZenix/c45156b7c883b5301c9028e39d71b479>
pub fn discord_server_boosting_4() -> Self {
Self {
id: "guild_booster_lvl4".to_string(),
description: "Server boosting since 1 Jan 2015".to_string(),
icon: "df199d2050d3ed4ebf84d64ae83989f8".to_string(),
link: Some("https://discord.com/settings/premium".to_string()),
}
}
/// Returns a badge representing the level 5 server boosting badge on Discord.com
///
/// Note: The description updates for the start date
///
/// # Reference
/// See <https://gist.github.com/XYZenix/c45156b7c883b5301c9028e39d71b479>
pub fn discord_server_boosting_5() -> Self {
Self {
id: "guild_booster_lvl5".to_string(),
description: "Server boosting since 1 Jan 2015".to_string(),
icon: "996b3e870e8a22ce519b3a50e6bdd52f".to_string(),
link: Some("https://discord.com/settings/premium".to_string()),
}
}
/// Returns a badge representing the level 6 server boosting badge on Discord.com
///
/// Note: The description updates for the start date
///
/// # Reference
/// See <https://gist.github.com/XYZenix/c45156b7c883b5301c9028e39d71b479>
pub fn discord_server_boosting_6() -> Self {
Self {
id: "guild_booster_lvl6".to_string(),
description: "Server boosting since 1 Jan 2015".to_string(),
icon: "991c9f39ee33d7537d9f408c3e53141e".to_string(),
link: Some("https://discord.com/settings/premium".to_string()),
}
}
/// Returns a badge representing the level 7 server boosting badge on Discord.com
///
/// Note: The description updates for the start date
///
/// # Reference
/// See <https://gist.github.com/XYZenix/c45156b7c883b5301c9028e39d71b479>
pub fn discord_server_boosting_7() -> Self {
Self {
id: "guild_booster_lvl7".to_string(),
description: "Server boosting since 1 Jan 2015".to_string(),
icon: "cb3ae83c15e970e8f3d410bc62cb8b99".to_string(),
link: Some("https://discord.com/settings/premium".to_string()),
}
}
/// Returns a badge representing the level 8 server boosting badge on Discord.com
///
/// Note: The description updates for the start date
///
/// # Reference
/// See <https://gist.github.com/XYZenix/c45156b7c883b5301c9028e39d71b479>
pub fn discord_server_boosting_8() -> Self {
Self {
id: "guild_booster_lvl8".to_string(),
description: "Server boosting since 1 Jan 2015".to_string(),
icon: "7142225d31238f6387d9f09efaa02759".to_string(),
link: Some("https://discord.com/settings/premium".to_string()),
}
}
/// Returns a badge representing the level 9 server boosting badge on Discord.com
///
/// Note: The description updates for the start date
///
/// # Reference
/// See <https://gist.github.com/XYZenix/c45156b7c883b5301c9028e39d71b479>
pub fn discord_server_boosting_9() -> Self {
Self {
id: "guild_booster_lvl9".to_string(),
description: "Server boosting since 1 Jan 2015".to_string(),
icon: "ec92202290b48d0879b7413d2dde3bab".to_string(),
link: Some("https://discord.com/settings/premium".to_string()),
}
}
/// Returns a badge representing the legacy username badge on Discord.com
///
/// # Reference
/// See <https://gist.github.com/XYZenix/c45156b7c883b5301c9028e39d71b479>
pub fn discord_legacy_username() -> Self {
Self {
id: "legacy_username".to_string(),
description: "Originally known as USERNAME".to_string(),
icon: "6de6d34650760ba5551a79732e98ed60".to_string(),
link: None,
}
}
/// Returns a badge representing the legacy username badge on Discord.com,
/// with the provided username (which should already contain the #DISCRIM part)
///
/// # Reference
/// See <https://gist.github.com/XYZenix/c45156b7c883b5301c9028e39d71b479>
pub fn discord_legacy_username_with_username(username: String) -> Self {
Self {
id: "legacy_username".to_string(),
description: format!("Originally known as {username}"),
icon: "6de6d34650760ba5551a79732e98ed60".to_string(),
link: None,
}
}
/// Returns a badge representing the legacy username badge on Discord.com,
/// with the provided username and discriminator
///
/// # Reference
/// See <https://gist.github.com/XYZenix/c45156b7c883b5301c9028e39d71b479>
pub fn discord_legacy_username_with_username_and_discriminator(
username: String,
discriminator: String,
) -> Self {
Self {
id: "legacy_username".to_string(),
description: format!("Originally known as {username}#{discriminator}"),
icon: "6de6d34650760ba5551a79732e98ed60".to_string(),
link: None,
}
}
/// Returns a badge representing the bot commands badge on Discord.com
///
/// Note: This badge is only for bot accounts
///
/// # Reference
/// See <https://gist.github.com/XYZenix/c45156b7c883b5301c9028e39d71b479>
pub fn discord_bot_commands() -> Self {
Self {
id: "bot_commands".to_string(),
description: "Supports Commands".to_string(),
icon: "6f9e37f9029ff57aef81db857890005e".to_string(),
link: Some(
"https://discord.com/blog/welcome-to-the-new-era-of-discord-apps?ref=badge"
.to_string(),
),
}
}
/// Returns a badge representing the bot automod badge on Discord.com
///
/// Note: This badge is only for bot accounts
///
/// # Reference
/// See <https://gist.github.com/XYZenix/c45156b7c883b5301c9028e39d71b479>
pub fn discord_bot_automod() -> Self {
Self {
id: "automod".to_string(),
description: "Uses AutoMod".to_string(),
icon: "f2459b691ac7453ed6039bbcfaccbfcd".to_string(),
link: None,
}
}
/// Returns a badge representing the application guild subscription badge on Discord.com
///
/// No idea where this badge could show up, but apparently it means a guild has an
/// application's premium
///
/// # Reference
/// See <https://gist.github.com/XYZenix/c45156b7c883b5301c9028e39d71b479>
pub fn discord_application_guild_subscription() -> Self {
Self {
id: "application_guild_subscription".to_string(),
description: "This server has APPLICATION Premium".to_string(),
icon: "d2010c413a8da2208b7e4f35bd8cd4ac".to_string(),
link: None,
}
}
}
/// Structure which shows a mutual guild with a user
///
/// # Reference
/// See <https://docs.discord.sex/resources/user#mutual-guild-structure>
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct MutualGuild {
pub id: Snowflake,
/// The user's nickname in the guild, if any
pub nick: Option<String>,
}

View File

@ -7,7 +7,7 @@ use std::collections::HashMap;
use chrono::NaiveDate; use chrono::NaiveDate;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::types::Snowflake; use crate::types::{Snowflake, ThemeColors};
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] #[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
@ -41,7 +41,12 @@ pub struct UserModifySchema {
pub email: Option<String>, pub email: Option<String>,
/// The user's email token from their previous email, required if a new email is set. /// The user's email token from their previous email, required if a new email is set.
/// ///
/// See <https://docs.discord.sex/resources/user#modify-user-email> and <https://docs.discord.sex/resources/user#verify-user-email-change> /// See:
///
/// - the endpoints <https://docs.discord.sex/resources/user#modify-user-email> and <https://docs.discord.sex/resources/user#verify-user-email-change>
///
/// - the relevant methods [`ChorusUser::initiate_email_change`](crate::instance::ChorusUser::initiate_email_change) and [`ChorusUser::verify_email_change`](crate::instance::ChorusUser::verify_email_change)
///
/// for changing the user's email. /// for changing the user's email.
/// ///
/// # Note /// # Note
@ -106,3 +111,120 @@ pub struct PrivateChannelCreateSchema {
pub access_tokens: Option<Vec<String>>, pub access_tokens: Option<Vec<String>>,
pub nicks: Option<HashMap<Snowflake, String>>, pub nicks: Option<HashMap<Snowflake, String>>,
} }
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)]
/// A schema used to modify the current user's profile.
///
/// Similar to [crate::types::UserProfileMetadata]
///
/// See <https://docs.discord.sex/resources/user#modify-user-profile>
pub struct UserModifyProfileSchema {
// Note: one of these causes a 500 if it is sent
#[serde(skip_serializing_if = "Option::is_none")]
/// The user's new pronouns (max 40 characters)
pub pronouns: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
/// The user's new bio (max 190 characters)
pub bio: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
// TODO: Add banner -- do we have an image data struct
/// The user's new accent color encoded as an i32 representation of a hex color code
pub accent_color: Option<i32>,
// Note: without the skip serializing this currently (2024/07/28) causes a 500!
//
// Which in turns locks the user's account, requiring phone number verification
#[serde(skip_serializing_if = "Option::is_none")]
/// The user's new [ThemeColors]
pub theme_colors: Option<ThemeColors>,
#[serde(skip_serializing_if = "Option::is_none")]
/// The user's new profile popup animation particle type
pub popout_animation_particle_type: Option<Snowflake>,
#[serde(skip_serializing_if = "Option::is_none")]
/// The user's new profile emoji id
pub emoji_id: Option<Snowflake>,
#[serde(skip_serializing_if = "Option::is_none")]
/// The user's new profile ffect id
pub profile_effect_id: Option<Snowflake>,
}
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)]
/// A schema used to delete or disable the current user's profile.
///
/// See <https://docs.discord.sex/resources/user#disable-user> and
/// <https://docs.discord.sex/resources/user#delete-user>
pub struct DeleteDisableUserSchema {
/// The user's current password, if any
pub password: Option<String>,
}
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)]
/// A schema used for [ChorusUser::verify_email_change](crate::instance::ChorusUser::verify_email_change)
///
/// See <https://docs.discord.sex/resources/user#verify-user-email-change>
pub struct VerifyUserEmailChangeSchema {
/// The verification code sent to the user's email
pub code: String,
}
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)]
/// The return type of [ChorusUser::verify_email_change](crate::instance::ChorusUser::verify_email_change)
///
/// See <https://docs.discord.sex/resources/user#verify-user-email-change>
pub struct VerifyUserEmailChangeResponse {
/// The email_token to be used in [ChorusUser::modify](crate::instance::ChorusUser::modify)
#[serde(rename = "token")]
pub email_token: String,
}
#[derive(Debug, Default, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)]
/// Query string parameters for the route GET /users/{user.id}/profile
/// ([crate::types::User::get_profile])
///
/// See <https://docs.discord.sex/resources/user#get-user-profile>
pub struct GetUserProfileSchema {
/// Whether to include the mutual guilds between the current user.
///
/// If unset it will default to true
pub with_mutual_guilds: Option<bool>,
/// Whether to include the mutual friends between the current user.
///
/// If unset it will default to false
pub with_mutual_friends: Option<bool>,
/// Whether to include the number of mutual friends between the current user
///
/// If unset it will default to false
pub with_mutual_friends_count: Option<bool>,
/// The guild id to get the user's member profile in, if any.
///
/// Note:
///
/// when you click on a user in the member list in the discord client, a request is sent with
/// this property set to the selected guild id.
///
/// This makes the request include fields such as guild_member and guild_member_profile
pub guild_id: Option<Snowflake>,
/// The role id to get the user's application role connection metadata in, if any.
pub connections_role_id: Option<Snowflake>,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
/// Internal type for the [crate::instance::ChorusUser::get_pomelo_suggestions] endpoint.
///
/// See <https://docs.discord.sex/resources/user#get-pomelo-suggestions>
pub(crate) struct GetPomeloSuggestionsReturn {
pub username: String
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
/// Internal type for the [crate::instance::ChorusUser::get_pomelo_eligibility] endpoint.
///
/// See <https://docs.discord.sex/resources/user#get-pomelo-eligibility>
pub(crate) struct GetPomeloEligibilityReturn {
pub taken: bool
}

View File

@ -5,12 +5,12 @@
use std::str::FromStr; use std::str::FromStr;
use chorus::gateway::{Gateway, GatewayOptions}; use chorus::gateway::{Gateway, GatewayOptions};
use chorus::types::{IntoShared, PermissionFlags}; use chorus::types::{DeleteDisableUserSchema, IntoShared, PermissionFlags};
use chorus::{ use chorus::{
instance::{ChorusUser, Instance}, instance::{ChorusUser, Instance},
types::{ types::{
Channel, ChannelCreateSchema, Guild, GuildCreateSchema, RegisterSchema, Channel, ChannelCreateSchema, Guild, GuildCreateSchema, RegisterSchema,
RoleCreateModifySchema, RoleObject, Shared RoleCreateModifySchema, RoleObject, Shared,
}, },
UrlBundle, UrlBundle,
}; };
@ -59,9 +59,12 @@ impl TestBundle {
// Set up a test by creating an Instance and a User. Reduces Test boilerplate. // Set up a test by creating an Instance and a User. Reduces Test boilerplate.
pub(crate) async fn setup() -> TestBundle { pub(crate) async fn setup() -> TestBundle {
// So we can get logs when tests fail // So we can get logs when tests fail
let _ = simple_logger::SimpleLogger::with_level(simple_logger::SimpleLogger::new(), log::LevelFilter::Debug).init(); let _ = simple_logger::SimpleLogger::with_level(
simple_logger::SimpleLogger::new(),
log::LevelFilter::Debug,
)
.init();
let instance = Instance::new("http://localhost:3001/api").await.unwrap(); let instance = Instance::new("http://localhost:3001/api").await.unwrap();
// Requires the existence of the below user. // Requires the existence of the below user.
@ -141,5 +144,9 @@ pub(crate) async fn setup() -> TestBundle {
pub(crate) async fn teardown(mut bundle: TestBundle) { pub(crate) async fn teardown(mut bundle: TestBundle) {
let id = bundle.guild.read().unwrap().id; let id = bundle.guild.read().unwrap().id;
Guild::delete(&mut bundle.user, id).await.unwrap(); Guild::delete(&mut bundle.user, id).await.unwrap();
bundle.user.delete().await.unwrap() bundle
.user
.delete(DeleteDisableUserSchema { password: None })
.await
.unwrap()
} }