Compare commits

..

4 Commits

3 changed files with 216 additions and 14 deletions

View File

@ -11,7 +11,10 @@ use crate::{
errors::{ChorusError, ChorusResult}, errors::{ChorusError, ChorusResult},
instance::{ChorusUser, Instance}, instance::{ChorusUser, Instance},
ratelimiter::ChorusRequest, ratelimiter::ChorusRequest,
types::{LimitType, PublicUser, Snowflake, User, UserModifySchema, UserProfile, UserSettings}, types::{
DeleteDisableUserSchema, LimitType, PublicUser, Snowflake, User, UserModifyProfileSchema,
UserModifySchema, UserProfile, UserProfileMetadata, UserSettings, VerifyUserEmailChangeResponse, VerifyUserEmailChangeSchema,
},
}; };
impl ChorusUser { impl ChorusUser {
@ -96,23 +99,54 @@ 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. /// Gets a user's profile object by their id.
@ -133,6 +167,68 @@ impl ChorusUser {
pub async fn get_user_profile(&mut self, id: Snowflake) -> ChorusResult<UserProfile> { pub async fn get_user_profile(&mut self, id: Snowflake) -> ChorusResult<UserProfile> {
User::get_profile(self, id).await User::get_profile(self, id).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).
///
/// # 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
}
} }
impl User { impl User {
@ -247,4 +343,28 @@ impl User {
.deserialize_response::<UserProfile>(user) .deserialize_response::<UserProfile>(user)
.await .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

@ -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")]
@ -31,7 +31,7 @@ pub struct UserModifySchema {
// TODO: Add a CDN data type // TODO: Add a CDN data type
pub avatar: Option<String>, pub avatar: Option<String>,
/// Note: This is not yet implemented on Spacebar /// Note: This is not yet implemented on Spacebar
pub avatar_decoration_id: Option<Snowflake>, pub avatar_decoration_id: Option<Snowflake>,
/// Note: This is not yet implemented on Spacebar /// Note: This is not yet implemented on Spacebar
pub avatar_decoration_sku_id: Option<Snowflake>, pub avatar_decoration_sku_id: Option<Snowflake>,
/// The user's email address; if changing from a verified email, email_token must be provided /// The user's email address; if changing from a verified email, email_token must be provided
@ -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,73 @@ 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,
}

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()
} }