Compare commits

..

4 Commits

3 changed files with 188 additions and 21 deletions

View File

@ -12,8 +12,7 @@ use crate::{
instance::{ChorusUser, Instance}, instance::{ChorusUser, Instance},
ratelimiter::ChorusRequest, ratelimiter::ChorusRequest,
types::{ types::{
DeleteDisableUserSchema, LimitType, PublicUser, Snowflake, User, UserModifyProfileSchema, DeleteDisableUserSchema, GetPomeloEligibilityReturn, GetPomeloSuggestionsReturn, GetUserProfileSchema, LimitType, PublicUser, Snowflake, User, UserModifyProfileSchema, UserModifySchema, UserProfile, UserProfileMetadata, UserSettings, VerifyUserEmailChangeResponse, VerifyUserEmailChangeSchema
UserModifySchema, UserProfile, UserProfileMetadata, UserSettings, VerifyUserEmailChangeResponse, VerifyUserEmailChangeSchema,
}, },
}; };
@ -44,6 +43,11 @@ impl ChorusUser {
/// ///
/// As of 2024/07/28, Spacebar does not yet implement this endpoint. /// 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: /// Note:
/// ///
/// "Unless the target user is a bot, you must be able to add /// "Unless the target user is a bot, you must be able to add
@ -56,8 +60,12 @@ impl ChorusUser {
/// ///
/// # Reference /// # Reference
/// See <https://docs.discord.sex/resources/user#get-user-by-username> /// See <https://docs.discord.sex/resources/user#get-user-by-username>
pub async fn get_user_by_username(&mut self, username: &String) -> ChorusResult<PublicUser> { pub async fn get_user_by_username(
User::get_by_username(self, username).await &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.
@ -164,8 +172,12 @@ impl ChorusUser {
/// ///
/// # Reference /// # Reference
/// See <https://docs.discord.sex/resources/user#get-user-profile> /// See <https://docs.discord.sex/resources/user#get-user-profile>
pub async fn get_user_profile(&mut self, id: Snowflake) -> ChorusResult<UserProfile> { pub async fn get_user_profile(
User::get_profile(self, id).await &mut self,
id: Snowflake,
query_parameters: GetUserProfileSchema,
) -> ChorusResult<UserProfile> {
User::get_profile(self, id, query_parameters).await
} }
/// Modifies the current user's profile. /// Modifies the current user's profile.
@ -187,8 +199,8 @@ impl ChorusUser {
/// Initiates the email change process. /// Initiates the email change process.
/// ///
/// Sends a verification code to the current user's email. /// Sends a verification code to the current user's email.
/// ///
/// Should be followed up with [Self::verify_email_change] /// Should be followed up with [Self::verify_email_change]
/// ///
/// # Reference /// # Reference
/// See <https://docs.discord.sex/resources/user#modify-user-email> /// See <https://docs.discord.sex/resources/user#modify-user-email>
@ -206,28 +218,89 @@ impl ChorusUser {
chorus_request.handle_request_as_result(self).await chorus_request.handle_request_as_result(self).await
} }
/// Verifies a code sent to change the current user's email. /// Verifies a code sent to change the current user's email.
/// ///
/// Should be the follow-up to [Self::initiate_email_change] /// Should be the follow-up to [Self::initiate_email_change]
/// ///
/// This endpoint returns a token which can be used with [Self::modify] /// This endpoint returns a token which can be used with [Self::modify]
/// to set a new email address (email_token). /// to set a new email address (email_token).
/// ///
/// # Reference /// # Reference
/// See <https://docs.discord.sex/resources/user#modify-user-email> /// See <https://docs.discord.sex/resources/user#modify-user-email>
pub async fn verify_email_change(&mut self, schema: VerifyUserEmailChangeSchema) -> ChorusResult<VerifyUserEmailChangeResponse> { pub async fn verify_email_change(
&mut self,
schema: VerifyUserEmailChangeSchema,
) -> ChorusResult<VerifyUserEmailChangeResponse> {
let request = Client::new() let request = Client::new()
.post(format!( .post(format!(
"{}/users/@me/email/verify-code", "{}/users/@me/email/verify-code",
self.belongs_to.read().unwrap().urls.api self.belongs_to.read().unwrap().urls.api
)) ))
.header("Authorization", self.token()) .header("Authorization", self.token())
.json(&schema); .json(&schema);
let chorus_request = ChorusRequest { let chorus_request = ChorusRequest {
request, request,
limit_type: LimitType::default(), limit_type: LimitType::default(),
}; };
chorus_request.deserialize_response::<VerifyUserEmailChangeResponse>(self).await 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])
///
/// 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.
///
/// 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)
} }
} }
@ -272,6 +345,11 @@ impl User {
/// ///
/// As of 2024/07/28, Spacebar does not yet implement this endpoint. /// 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: /// Note:
/// ///
/// "Unless the target user is a bot, you must be able to add /// "Unless the target user is a bot, you must be able to add
@ -284,12 +362,18 @@ impl User {
pub async fn get_by_username( pub async fn get_by_username(
user: &mut ChorusUser, user: &mut ChorusUser,
username: &String, username: &String,
discriminator: Option<&String>,
) -> ChorusResult<PublicUser> { ) -> ChorusResult<PublicUser> {
let url_api = user.belongs_to.read().unwrap().urls.api.clone(); let url_api = user.belongs_to.read().unwrap().urls.api.clone();
let url = format!("{}/users/username/{username}", url_api); let url = format!("{}/users/username/{username}", url_api);
let request = reqwest::Client::new() let mut request = reqwest::Client::new()
.get(url) .get(url)
.header("Authorization", user.token()); .header("Authorization", user.token());
if let Some(some_discriminator) = discriminator {
request = request.query(&[("discriminator", some_discriminator)]);
}
let chorus_request = ChorusRequest { let chorus_request = ChorusRequest {
request, request,
limit_type: LimitType::Global, limit_type: LimitType::Global,
@ -329,12 +413,17 @@ impl User {
/// ///
/// # Reference /// # Reference
/// See <https://docs.discord.sex/resources/user#get-user-profile> /// See <https://docs.discord.sex/resources/user#get-user-profile>
// TODO: Implement query string parameters for this endpoint pub async fn get_profile(
pub async fn get_profile(user: &mut ChorusUser, id: Snowflake) -> ChorusResult<UserProfile> { user: &mut ChorusUser,
id: Snowflake,
query_parameters: GetUserProfileSchema,
) -> ChorusResult<UserProfile> {
let url_api = user.belongs_to.read().unwrap().urls.api.clone(); let url_api = user.belongs_to.read().unwrap().urls.api.clone();
let request: reqwest::RequestBuilder = Client::new() let request: reqwest::RequestBuilder = Client::new()
.get(format!("{}/users/{}/profile", url_api, id)) .get(format!("{}/users/{}/profile", url_api, id))
.header("Authorization", user.token()); .header("Authorization", user.token())
.query(&query_parameters);
let chorus_request = ChorusRequest { let chorus_request = ChorusRequest {
request, request,
limit_type: LimitType::Global, limit_type: LimitType::Global,

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

@ -181,3 +181,50 @@ pub struct VerifyUserEmailChangeResponse {
#[serde(rename = "token")] #[serde(rename = "token")]
pub email_token: String, 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
}