From 7db5015c07412e5c4372d43c3cd41736c37388ce Mon Sep 17 00:00:00 2001 From: kozabrada123 Date: Sun, 28 Jul 2024 11:42:26 +0200 Subject: [PATCH 01/16] feat: Add UserProfile and other types --- src/types/entities/guild.rs | 13 +- src/types/entities/user.rs | 557 +++++++++++++++++++++++++++++++++++- 2 files changed, 563 insertions(+), 7 deletions(-) diff --git a/src/types/entities/guild.rs b/src/types/entities/guild.rs index cca9b94..4de8569 100644 --- a/src/types/entities/guild.rs +++ b/src/types/entities/guild.rs @@ -444,7 +444,7 @@ pub enum VerificationLevel { #[cfg_attr(feature = "sqlx", derive(sqlx::Type))] #[repr(u8)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] -/// See +/// See pub enum MFALevel { #[default] None = 0, @@ -467,7 +467,7 @@ pub enum MFALevel { #[cfg_attr(feature = "sqlx", derive(sqlx::Type))] #[repr(u8)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] -/// See +/// See pub enum NSFWLevel { #[default] Default = 0, @@ -492,12 +492,19 @@ pub enum NSFWLevel { #[cfg_attr(feature = "sqlx", derive(sqlx::Type))] #[repr(u8)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] -/// See +// Note: Maybe rename this to GuildPremiumTier? +/// **Guild** premium (Boosting) tier +/// +/// See pub enum PremiumTier { #[default] + /// No server boost perks None = 0, + /// Level 1 server boost perks Tier1 = 1, + /// Level 2 server boost perks Tier2 = 2, + /// Level 3 server boost perks Tier3 = 3, } diff --git a/src/types/entities/user.rs b/src/types/entities/user.rs index 674c931..1db4b6f 100644 --- a/src/types/entities/user.rs +++ b/src/types/entities/user.rs @@ -7,6 +7,7 @@ use crate::types::utils::Snowflake; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_aux::prelude::deserialize_option_number_from_string; +use serde_repr::{Deserialize_repr, Serialize_repr}; use std::array::TryFromSliceError; use std::fmt::Debug; @@ -22,7 +23,7 @@ use crate::gateway::GatewayHandle; #[cfg(feature = "client")] use chorus_macros::{Composite, Updateable}; -use super::Emoji; +use super::{Emoji, GuildMember}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "sqlx", derive(sqlx::Type))] @@ -39,6 +40,8 @@ impl User { #[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "client", derive(Updateable, Composite))] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] +/// # Reference +/// See pub struct User { pub id: Snowflake, pub username: String, @@ -57,8 +60,10 @@ pub struct User { #[serde(default)] #[serde(deserialize_with = "deserialize_option_number_from_string")] pub flags: Option, + pub premium: Option, + /// The type of premium (Nitro) a user has + pub premium_type: Option, pub premium_since: Option>, - pub premium_type: Option, pub pronouns: Option, pub public_flags: Option, pub banner: Option, @@ -66,13 +71,15 @@ pub struct User { pub theme_colors: Option, pub phone: Option, pub nsfw_allowed: Option, - pub premium: Option, pub purchased_flags: Option, pub premium_usage_flags: Option, pub disabled: Option, } #[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 { #[serde(flatten)] inner: (u32, u32), @@ -139,6 +146,8 @@ impl sqlx::Type for ThemeColors { } #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] +/// # Reference +/// See pub struct PublicUser { pub id: Snowflake, pub username: Option, @@ -150,7 +159,9 @@ pub struct PublicUser { pub pronouns: Option, pub bot: Option, pub bio: Option, - pub premium_type: Option, + /// The type of premium (Nitro) a user has + pub premium_type: Option, + /// The date the user's premium (Nitro) subscribtion started pub premium_since: Option>, pub public_flags: Option, } @@ -181,6 +192,8 @@ const CUSTOM_USER_FLAG_OFFSET: u64 = 1 << 32; bitflags::bitflags! { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, chorus_macros::SerdeBitFlags)] #[cfg_attr(feature = "sqlx", derive(chorus_macros::SqlxBitFlags))] + /// # Reference + /// See pub struct UserFlags: u64 { const DISCORD_EMPLOYEE = 1 << 0; const PARTNERED_SERVER_OWNER = 1 << 1; @@ -194,6 +207,7 @@ bitflags::bitflags! { const EARLY_SUPPORTER = 1 << 9; const TEAM_USER = 1 << 10; const TRUST_AND_SAFETY = 1 << 11; + /// Note: deprecated by Discord const SYSTEM = 1 << 12; const HAS_UNREAD_URGENT_MESSAGES = 1 << 13; 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 +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)] +/// # Reference +/// See pub struct UserProfileMetadata { + /// The guild ID this profile applies to, if it is a guild profile. pub guild_id: Option, + /// The user's pronouns, up to 40 characters pub pronouns: String, + /// The user's bio / description, up to 190 characters pub bio: Option, + /// The hash used to retrieve the user's banned from the CDN pub banner: Option, + /// Banner color encoded as an i32 representation of a hex color code pub accent_color: Option, + /// See [ThemeColors] pub theme_colors: Option, pub popout_animation_particle_type: Option, pub emoji: Option, } + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +/// A user's publically facing profile +/// +/// # Reference +/// See +pub struct UserProfile { + // TODO: add profile application object + pub user: PublicUser, + + #[serde(rename = "user_profile")] + pub profile_metadata: UserProfileMetadata, + + #[serde(default)] + pub badges: Vec, + + pub guild_member: Option, + + #[serde(rename = "guild_member_profile")] + pub guild_member_profile_metadata: Option, + + #[serde(default)] + pub guild_badges: Vec, + + /// The user's legacy username#discriminator, if existing and shown + pub legacy_username: Option, + + #[serde(default)] + pub mutual_guilds: Vec, + + #[serde(default)] + pub mutual_friends: Vec, + + pub mutual_friends_count: Option, + + // TODO: Add connections! + // TODO: And application role connections! + /// The type of premium (Nitro) a user has + pub premium_type: Option, + /// The date the user's premium (Nitro) subscribtion started + pub premium_since: Option>, + /// The date the user's premium guild (Boosting) subscribtion started + pub premium_guild_since: Option>, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +/// Info about a badge on a user's profile ([UserProfile]) +/// +/// # Reference +/// See +/// +/// For a list of know badges, see +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, +} + +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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 +#[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, +} From 20bdb3247a19e5053bbf0619d32e4228f1abf93d Mon Sep 17 00:00:00 2001 From: kozabrada123 Date: Sun, 28 Jul 2024 12:20:27 +0200 Subject: [PATCH 02/16] api: re-do a large part of the users api --- src/api/auth/login.rs | 4 +- src/api/auth/mod.rs | 2 +- src/api/auth/register.rs | 6 +- src/api/users/users.rs | 170 ++++++++++++++++++++++++++++++++------- 4 files changed, 146 insertions(+), 36 deletions(-) diff --git a/src/api/auth/login.rs b/src/api/auth/login.rs index 7c58e0e..1242953 100644 --- a/src/api/auth/login.rs +++ b/src/api/auth/login.rs @@ -32,14 +32,14 @@ impl Instance { // instances' limits to pass them on as user_rate_limits later. let mut user = ChorusUser::shell(Arc::new(RwLock::new(self.clone())), "None".to_string()).await; - + let login_result = chorus_request .deserialize_response::(&mut user) .await?; user.set_token(login_result.token); 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; let mut identify = GatewayIdentifyPayload::common(); diff --git a/src/api/auth/mod.rs b/src/api/auth/mod.rs index 498080e..3aa8b3b 100644 --- a/src/api/auth/mod.rs +++ b/src/api/auth/mod.rs @@ -26,7 +26,7 @@ impl Instance { let mut user = 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?; *user.object.write().unwrap() = object; diff --git a/src/api/auth/register.rs b/src/api/auth/register.rs index 6b94a4d..34e628a 100644 --- a/src/api/auth/register.rs +++ b/src/api/auth/register.rs @@ -39,14 +39,14 @@ impl Instance { // the instances' limits to pass them on as user_rate_limits later. let mut user = ChorusUser::shell(Arc::new(RwLock::new(self.clone())), "None".to_string()).await; - + let token = chorus_request .deserialize_response::(&mut user) .await? .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?; *user.object.write().unwrap() = object; diff --git a/src/api/users/users.rs b/src/api/users/users.rs index 4f6ef57..15f0aa6 100644 --- a/src/api/users/users.rs +++ b/src/api/users/users.rs @@ -11,22 +11,52 @@ use crate::{ errors::{ChorusError, ChorusResult}, instance::{ChorusUser, Instance}, ratelimiter::ChorusRequest, - types::{LimitType, User, UserModifySchema, UserSettings}, + types::{LimitType, PublicUser, Snowflake, User, UserModifySchema, UserProfile, UserSettings}, }; 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 + pub async fn get_current_user(&mut self) -> ChorusResult { + User::get_current(self).await + } + + /// Gets a non-local user by their id /// /// # Notes /// This function is a wrapper around [`User::get`]. /// /// # Reference - /// See and - /// - pub async fn get_user(&mut self, id: Option<&String>) -> ChorusResult { + /// See + pub async fn get_user(&mut self, id: Snowflake) -> ChorusResult { 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. + /// + /// 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 + pub async fn get_user_by_username(&mut self, username: &String) -> ChorusResult { + User::get_by_username(self, username).await + } + /// Gets the user's settings. /// /// # Notes @@ -40,7 +70,6 @@ impl ChorusUser { /// # Reference /// See pub async fn modify(&mut self, modify_schema: UserModifySchema) -> ChorusResult { - // See , note 1 let requires_current_password = modify_schema.username.is_some() || modify_schema.discriminator.is_some() @@ -85,21 +114,35 @@ impl ChorusUser { }; chorus_request.handle_request_as_result(&mut 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 + pub async fn get_user_profile(&mut self, id: Snowflake) -> ChorusResult { + User::get_profile(self, id).await + } } impl User { - /// Gets a user by id, or if the id is None, gets the current user. + /// Gets the local / current user. /// /// # Reference - /// See and - /// - pub async fn get(user: &mut ChorusUser, id: Option<&String>) -> ChorusResult { + /// See + pub async fn get_current(user: &mut ChorusUser) -> ChorusResult { let url_api = user.belongs_to.read().unwrap().urls.api.clone(); - let url = if id.is_none() { - format!("{}/users/@me", url_api) - } else { - format!("{}/users/{}", url_api, id.unwrap()) - }; + let url = format!("{}/users/@me", url_api); let request = reqwest::Client::new() .get(url) .header("Authorization", user.token()); @@ -107,16 +150,60 @@ impl User { request, limit_type: LimitType::Global, }; - match chorus_request.send_request(user).await { - Ok(result) => { - let result_text = result.text().await.unwrap(); - Ok(serde_json::from_str::(&result_text).unwrap()) - } - Err(e) => Err(e), - } + chorus_request.deserialize_response::(user).await } - /// Gets the user's settings. + /// Gets a non-local user by their id + /// + /// # Reference + /// See + 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 chorus_request = ChorusRequest { + request, + limit_type: LimitType::Global, + }; + chorus_request + .deserialize_response::(user) + .await + } + + /// Gets a user by their unique username. + /// + /// As of 2024/07/28, Spacebar does not yet implement this endpoint. + /// + /// 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 + pub async fn get_by_username( + user: &mut ChorusUser, + username: &String, + ) -> ChorusResult { + let url_api = user.belongs_to.read().unwrap().urls.api.clone(); + let url = format!("{}/users/username/{username}", url_api); + let request = reqwest::Client::new() + .get(url) + .header("Authorization", user.token()); + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::Global, + }; + chorus_request + .deserialize_response::(user) + .await + } + + /// Gets the current user's settings. /// /// # Reference /// See @@ -129,12 +216,35 @@ impl User { request, limit_type: LimitType::Global, }; - match chorus_request.send_request(user).await { - Ok(result) => { - let result_text = result.text().await.unwrap(); - Ok(serde_json::from_str(&result_text).unwrap()) - } - Err(e) => Err(e), - } + chorus_request + .deserialize_response::(user) + .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 + /// + /// # Reference + /// See + // TODO: Implement query string parameters for this endpoint + pub async fn get_profile(user: &mut ChorusUser, id: Snowflake) -> ChorusResult { + 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()); + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::Global, + }; + chorus_request + .deserialize_response::(user) + .await } } From 62d48d61fe5782054e9283e4a42d41a63b809ca4 Mon Sep 17 00:00:00 2001 From: kozabrada123 Date: Mon, 29 Jul 2024 08:50:10 +0200 Subject: [PATCH 03/16] feat: add modify user profile --- src/api/users/users.rs | 45 +++++++++++++++++++++++++++++++++++++++- src/types/schema/user.rs | 45 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/src/api/users/users.rs b/src/api/users/users.rs index 15f0aa6..9357615 100644 --- a/src/api/users/users.rs +++ b/src/api/users/users.rs @@ -11,7 +11,10 @@ use crate::{ errors::{ChorusError, ChorusResult}, instance::{ChorusUser, Instance}, ratelimiter::ChorusRequest, - types::{LimitType, PublicUser, Snowflake, User, UserModifySchema, UserProfile, UserSettings}, + types::{ + LimitType, PublicUser, Snowflake, User, UserModifyProfileSchema, UserModifySchema, + UserProfile, UserProfileMetadata, UserSettings, + }, }; impl ChorusUser { @@ -133,6 +136,22 @@ impl ChorusUser { pub async fn get_user_profile(&mut self, id: Snowflake) -> ChorusResult { 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 + pub async fn modify_profile( + &mut self, + schema: UserModifyProfileSchema, + ) -> ChorusResult { + User::modify_profile(self, schema).await + } } impl User { @@ -247,4 +266,28 @@ impl User { .deserialize_response::(user) .await } + + /// Modifies the current user's profile. + /// + /// Returns the updated [UserProfileMetadata]. + /// + /// # Reference + /// See + pub async fn modify_profile( + user: &mut ChorusUser, + schema: UserModifyProfileSchema, + ) -> ChorusResult { + 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::(user) + .await + } } diff --git a/src/types/schema/user.rs b/src/types/schema/user.rs index e2600a4..c7f89ba 100644 --- a/src/types/schema/user.rs +++ b/src/types/schema/user.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use chrono::NaiveDate; use serde::{Deserialize, Serialize}; -use crate::types::Snowflake; +use crate::types::{Snowflake, ThemeColors}; #[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] #[serde(rename_all = "snake_case")] @@ -31,7 +31,7 @@ pub struct UserModifySchema { // TODO: Add a CDN data type pub avatar: Option, /// Note: This is not yet implemented on Spacebar - pub avatar_decoration_id: Option, + pub avatar_decoration_id: Option, /// Note: This is not yet implemented on Spacebar pub avatar_decoration_sku_id: Option, /// The user's email address; if changing from a verified email, email_token must be provided @@ -106,3 +106,44 @@ pub struct PrivateChannelCreateSchema { pub access_tokens: Option>, pub nicks: Option>, } + +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] +/// A schema used to modify the current user's profile. +/// +/// Similar to [crate::types::UserProfileMetadata] +/// +/// See +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, + + #[serde(skip_serializing_if = "Option::is_none")] + /// The user's new bio (max 190 characters) + pub bio: Option, + + #[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, + + // 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, + + #[serde(skip_serializing_if = "Option::is_none")] + /// The user's new profile popup animation particle type + pub popout_animation_particle_type: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + /// The user's new profile emoji id + pub emoji_id: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + /// The user's new profile ffect id + pub profile_effect_id: Option, +} From 5d0a65a9a9e5cb51710318b76fcfc646ddcdbeec Mon Sep 17 00:00:00 2001 From: kozabrada123 Date: Mon, 29 Jul 2024 09:23:09 +0200 Subject: [PATCH 04/16] feat: delete and disable user endpoints --- src/api/users/users.rs | 44 +++++++++++++++++++++++++++++++++------- src/types/schema/user.rs | 10 +++++++++ tests/common/mod.rs | 17 +++++++++++----- 3 files changed, 59 insertions(+), 12 deletions(-) diff --git a/src/api/users/users.rs b/src/api/users/users.rs index 9357615..a4c763d 100644 --- a/src/api/users/users.rs +++ b/src/api/users/users.rs @@ -12,8 +12,7 @@ use crate::{ instance::{ChorusUser, Instance}, ratelimiter::ChorusRequest, types::{ - LimitType, PublicUser, Snowflake, User, UserModifyProfileSchema, UserModifySchema, - UserProfile, UserProfileMetadata, UserSettings, + DeleteDisableUserSchema, LimitType, PublicUser, Snowflake, User, UserModifyProfileSchema, UserModifySchema, UserProfile, UserProfileMetadata, UserSettings }, }; @@ -99,23 +98,54 @@ impl ChorusUser { chorus_request.deserialize_response::(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 - /// See - pub async fn delete(mut self) -> ChorusResult<()> { + /// See + 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 + pub async fn delete(&mut self, schema: DeleteDisableUserSchema) -> ChorusResult<()> { let request = Client::new() .post(format!( "{}/users/@me/delete", self.belongs_to.read().unwrap().urls.api )) .header("Authorization", self.token()) - .header("Content-Type", "application/json"); + .json(&schema); let chorus_request = ChorusRequest { request, 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. diff --git a/src/types/schema/user.rs b/src/types/schema/user.rs index c7f89ba..418fd66 100644 --- a/src/types/schema/user.rs +++ b/src/types/schema/user.rs @@ -147,3 +147,13 @@ pub struct UserModifyProfileSchema { /// The user's new profile ffect id pub profile_effect_id: Option, } + +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] +/// A schema used to delete or disable the current user's profile. +/// +/// See and +/// +pub struct DeleteDisableUserSchema { + /// The user's current password, if any + pub password: Option, +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs index f2f0663..5bc0691 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -5,12 +5,12 @@ use std::str::FromStr; use chorus::gateway::{Gateway, GatewayOptions}; -use chorus::types::{IntoShared, PermissionFlags}; +use chorus::types::{DeleteDisableUserSchema, IntoShared, PermissionFlags}; use chorus::{ instance::{ChorusUser, Instance}, types::{ Channel, ChannelCreateSchema, Guild, GuildCreateSchema, RegisterSchema, - RoleCreateModifySchema, RoleObject, Shared + RoleCreateModifySchema, RoleObject, Shared, }, UrlBundle, }; @@ -59,9 +59,12 @@ impl TestBundle { // Set up a test by creating an Instance and a User. Reduces Test boilerplate. pub(crate) async fn setup() -> TestBundle { - // 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(); // 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) { let id = bundle.guild.read().unwrap().id; Guild::delete(&mut bundle.user, id).await.unwrap(); - bundle.user.delete().await.unwrap() + bundle + .user + .delete(DeleteDisableUserSchema { password: None }) + .await + .unwrap() } From 0a1c51dddd86e22bb2e4f734ca20c8cdecb2f82d Mon Sep 17 00:00:00 2001 From: kozabrada123 Date: Mon, 29 Jul 2024 10:42:14 +0200 Subject: [PATCH 05/16] feat: modify email and verify email endpoints --- src/api/users/users.rs | 79 ++++++++++++++++++++++++++++++++-------- src/types/schema/user.rs | 26 ++++++++++++- 2 files changed, 88 insertions(+), 17 deletions(-) diff --git a/src/api/users/users.rs b/src/api/users/users.rs index a4c763d..70f448d 100644 --- a/src/api/users/users.rs +++ b/src/api/users/users.rs @@ -12,7 +12,8 @@ use crate::{ instance::{ChorusUser, Instance}, ratelimiter::ChorusRequest, types::{ - DeleteDisableUserSchema, LimitType, PublicUser, Snowflake, User, UserModifyProfileSchema, UserModifySchema, UserProfile, UserProfileMetadata, UserSettings + DeleteDisableUserSchema, LimitType, PublicUser, Snowflake, User, UserModifyProfileSchema, + UserModifySchema, UserProfile, UserProfileMetadata, UserSettings, VerifyUserEmailChangeResponse, VerifyUserEmailChangeSchema, }, }; @@ -98,14 +99,14 @@ impl ChorusUser { chorus_request.deserialize_response::(self).await } - /// Disables the current user's account. - /// - /// Invalidates all active tokens. - /// - /// Requires the user's current password (if any) - /// - /// # Notes - /// Requires MFA + /// Disables the current user's account. + /// + /// Invalidates all active tokens. + /// + /// Requires the user's current password (if any) + /// + /// # Notes + /// Requires MFA /// /// # Reference /// See @@ -116,7 +117,7 @@ impl ChorusUser { self.belongs_to.read().unwrap().urls.api )) .header("Authorization", self.token()) - .json(&schema); + .json(&schema); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), @@ -125,11 +126,11 @@ impl ChorusUser { } /// Deletes the current user from the Instance. - /// - /// Requires the user's current password (if any) - /// - /// # Notes - /// Requires MFA + /// + /// Requires the user's current password (if any) + /// + /// # Notes + /// Requires MFA /// /// # Reference /// See @@ -140,7 +141,7 @@ impl ChorusUser { self.belongs_to.read().unwrap().urls.api )) .header("Authorization", self.token()) - .json(&schema); + .json(&schema); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), @@ -182,6 +183,52 @@ impl ChorusUser { ) -> ChorusResult { 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 + 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 + pub async fn verify_email_change(&mut self, schema: VerifyUserEmailChangeSchema) -> ChorusResult { + 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::(self).await + } } impl User { diff --git a/src/types/schema/user.rs b/src/types/schema/user.rs index 418fd66..904ff54 100644 --- a/src/types/schema/user.rs +++ b/src/types/schema/user.rs @@ -41,7 +41,12 @@ pub struct UserModifySchema { pub email: Option, /// The user's email token from their previous email, required if a new email is set. /// - /// See and + /// See: + /// + /// - the endpoints and + /// + /// - 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. /// /// # Note @@ -157,3 +162,22 @@ pub struct DeleteDisableUserSchema { /// The user's current password, if any pub password: Option, } + +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] +/// A schema used for [ChorusUser::verify_email_change](crate::instance::ChorusUser::verify_email_change) +/// +/// See +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 +pub struct VerifyUserEmailChangeResponse { + /// The email_token to be used in [ChorusUser::modify](crate::instance::ChorusUser::modify) + #[serde(rename = "token")] + pub email_token: String, +} From e6a4cc30a6400ca00db58dfc311530b84410c90d Mon Sep 17 00:00:00 2001 From: kozabrada123 Date: Thu, 8 Aug 2024 08:37:08 +0200 Subject: [PATCH 06/16] feat!: add discriminator parameter to get_user_by_username --- src/api/users/users.rs | 54 +++++++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/src/api/users/users.rs b/src/api/users/users.rs index 70f448d..c8ef4b5 100644 --- a/src/api/users/users.rs +++ b/src/api/users/users.rs @@ -13,7 +13,8 @@ use crate::{ ratelimiter::ChorusRequest, types::{ DeleteDisableUserSchema, LimitType, PublicUser, Snowflake, User, UserModifyProfileSchema, - UserModifySchema, UserProfile, UserProfileMetadata, UserSettings, VerifyUserEmailChangeResponse, VerifyUserEmailChangeSchema, + UserModifySchema, UserProfile, UserProfileMetadata, UserSettings, + VerifyUserEmailChangeResponse, VerifyUserEmailChangeSchema, }, }; @@ -44,6 +45,11 @@ impl ChorusUser { /// /// 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 @@ -56,8 +62,12 @@ impl ChorusUser { /// /// # Reference /// See - pub async fn get_user_by_username(&mut self, username: &String) -> ChorusResult { - User::get_by_username(self, username).await + pub async fn get_user_by_username( + &mut self, + username: &String, + discriminator: Option<&String>, + ) -> ChorusResult { + User::get_by_username(self, username, discriminator).await } /// Gets the user's settings. @@ -187,8 +197,8 @@ impl ChorusUser { /// Initiates the email change process. /// /// 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 /// See @@ -206,28 +216,33 @@ impl ChorusUser { 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] - /// - /// This endpoint returns a token which can be used with [Self::modify] - /// to set a new email address (email_token). + /// 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 - pub async fn verify_email_change(&mut self, schema: VerifyUserEmailChangeSchema) -> ChorusResult { + pub async fn verify_email_change( + &mut self, + schema: VerifyUserEmailChangeSchema, + ) -> ChorusResult { let request = Client::new() .post(format!( "{}/users/@me/email/verify-code", self.belongs_to.read().unwrap().urls.api )) .header("Authorization", self.token()) - .json(&schema); + .json(&schema); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), }; - chorus_request.deserialize_response::(self).await + chorus_request + .deserialize_response::(self) + .await } } @@ -272,6 +287,11 @@ impl User { /// /// 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 @@ -284,12 +304,18 @@ impl User { pub async fn get_by_username( user: &mut ChorusUser, username: &String, + discriminator: Option<&String>, ) -> ChorusResult { let url_api = user.belongs_to.read().unwrap().urls.api.clone(); let url = format!("{}/users/username/{username}", url_api); - let request = reqwest::Client::new() + 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, From 1fa84b4b63b993e938daab8353bcc68b5d306b9c Mon Sep 17 00:00:00 2001 From: kozabrada123 Date: Thu, 8 Aug 2024 08:58:57 +0200 Subject: [PATCH 07/16] feat!: add get_user_profile query string schema --- src/api/users/users.rs | 15 +++++++-------- src/types/schema/user.rs | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/api/users/users.rs b/src/api/users/users.rs index c8ef4b5..b49f4a6 100644 --- a/src/api/users/users.rs +++ b/src/api/users/users.rs @@ -12,9 +12,7 @@ use crate::{ instance::{ChorusUser, Instance}, ratelimiter::ChorusRequest, types::{ - DeleteDisableUserSchema, LimitType, PublicUser, Snowflake, User, UserModifyProfileSchema, - UserModifySchema, UserProfile, UserProfileMetadata, UserSettings, - VerifyUserEmailChangeResponse, VerifyUserEmailChangeSchema, + DeleteDisableUserSchema, GetUserProfileSchema, LimitType, PublicUser, Snowflake, User, UserModifyProfileSchema, UserModifySchema, UserProfile, UserProfileMetadata, UserSettings, VerifyUserEmailChangeResponse, VerifyUserEmailChangeSchema }, }; @@ -174,8 +172,8 @@ impl ChorusUser { /// /// # Reference /// See - pub async fn get_user_profile(&mut self, id: Snowflake) -> ChorusResult { - User::get_profile(self, id).await + pub async fn get_user_profile(&mut self, id: Snowflake, query_parameters: GetUserProfileSchema) -> ChorusResult { + User::get_profile(self, id, query_parameters).await } /// Modifies the current user's profile. @@ -355,12 +353,13 @@ impl User { /// /// # Reference /// See - // TODO: Implement query string parameters for this endpoint - pub async fn get_profile(user: &mut ChorusUser, id: Snowflake) -> ChorusResult { + pub async fn get_profile(user: &mut ChorusUser, id: Snowflake, query_parameters: GetUserProfileSchema) -> ChorusResult { 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()); + .header("Authorization", user.token()) + .query(&query_parameters); + let chorus_request = ChorusRequest { request, limit_type: LimitType::Global, diff --git a/src/types/schema/user.rs b/src/types/schema/user.rs index 904ff54..a53298a 100644 --- a/src/types/schema/user.rs +++ b/src/types/schema/user.rs @@ -181,3 +181,35 @@ pub struct VerifyUserEmailChangeResponse { #[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 +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, + /// Whether to include the mutual friends between the current user. + /// + /// If unset it will default to false + pub with_mutual_friends: Option, + /// 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, + /// 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, + /// The role id to get the user's application role connection metadata in, if any. + pub connections_role_id: Option, +} + From 80c99753c4c05db4f82c877258a5c177f7e3ad6c Mon Sep 17 00:00:00 2001 From: kozabrada123 Date: Thu, 8 Aug 2024 09:09:46 +0200 Subject: [PATCH 08/16] chore: add integration expire behavior --- src/types/entities/integration.rs | 33 ++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/types/entities/integration.rs b/src/types/entities/integration.rs index 16cd991..37aac42 100644 --- a/src/types/entities/integration.rs +++ b/src/types/entities/integration.rs @@ -4,6 +4,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; use crate::types::{ entities::{Application, User}, @@ -23,7 +24,7 @@ pub struct Integration { pub syncing: Option, pub role_id: Option, pub enabled_emoticons: Option, - pub expire_behaviour: Option, + pub expire_behaviour: Option, pub expire_grace_period: Option, #[cfg_attr(feature = "sqlx", sqlx(skip))] pub user: Option>, @@ -50,6 +51,7 @@ pub struct IntegrationAccount { #[serde(rename_all = "snake_case")] #[cfg_attr(feature = "sqlx", derive(sqlx::Type))] #[cfg_attr(feature = "sqlx", sqlx(rename_all = "snake_case"))] +/// See pub enum IntegrationType { #[default] Twitch, @@ -57,3 +59,32 @@ pub enum IntegrationType { Discord, 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 +pub enum IntegrationExpireBehaviour { + #[default] + /// Remove the subscriber role from the user + RemoveRole = 0, + /// Kick the user from the guild + Kick = 1, +} + + From 926f89e1cf0ed2c104c52a0ec7ddc43f0c4e7afe Mon Sep 17 00:00:00 2001 From: kozabrada123 Date: Thu, 8 Aug 2024 09:57:23 +0200 Subject: [PATCH 09/16] feat: add get_pomelo_suggestions and get_pomelo_eligibility --- src/api/users/users.rs | 72 +++++++++++++++++++++++++++++++++++++--- src/types/schema/user.rs | 15 +++++++++ 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/src/api/users/users.rs b/src/api/users/users.rs index b49f4a6..1c07b8c 100644 --- a/src/api/users/users.rs +++ b/src/api/users/users.rs @@ -12,7 +12,7 @@ use crate::{ instance::{ChorusUser, Instance}, ratelimiter::ChorusRequest, types::{ - DeleteDisableUserSchema, GetUserProfileSchema, LimitType, PublicUser, Snowflake, User, UserModifyProfileSchema, UserModifySchema, UserProfile, UserProfileMetadata, UserSettings, VerifyUserEmailChangeResponse, VerifyUserEmailChangeSchema + DeleteDisableUserSchema, GetPomeloEligibilityReturn, GetPomeloSuggestionsReturn, GetUserProfileSchema, LimitType, PublicUser, Snowflake, User, UserModifyProfileSchema, UserModifySchema, UserProfile, UserProfileMetadata, UserSettings, VerifyUserEmailChangeResponse, VerifyUserEmailChangeSchema }, }; @@ -172,7 +172,11 @@ impl ChorusUser { /// /// # Reference /// See - pub async fn get_user_profile(&mut self, id: Snowflake, query_parameters: GetUserProfileSchema) -> ChorusResult { + pub async fn get_user_profile( + &mut self, + id: Snowflake, + query_parameters: GetUserProfileSchema, + ) -> ChorusResult { User::get_profile(self, id, query_parameters).await } @@ -242,6 +246,62 @@ impl ChorusUser { .deserialize_response::(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 + 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 chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + chorus_request + .deserialize_response::(self) + .await + .map(|returned| returned.username) + } + + /// Checks whether a unique username is available. + /// + /// Returns whether the username is not taken yet. + /// + /// See + pub async fn get_pomelo_eligibility(&mut self, username: &String) -> ChorusResult { + 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::(self) + .await + .map(|returned| !returned.taken) + } } impl User { @@ -353,12 +413,16 @@ impl User { /// /// # Reference /// See - pub async fn get_profile(user: &mut ChorusUser, id: Snowflake, query_parameters: GetUserProfileSchema) -> ChorusResult { + pub async fn get_profile( + user: &mut ChorusUser, + id: Snowflake, + query_parameters: GetUserProfileSchema, + ) -> ChorusResult { 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); + .query(&query_parameters); let chorus_request = ChorusRequest { request, diff --git a/src/types/schema/user.rs b/src/types/schema/user.rs index a53298a..b336a63 100644 --- a/src/types/schema/user.rs +++ b/src/types/schema/user.rs @@ -213,3 +213,18 @@ pub struct GetUserProfileSchema { pub connections_role_id: Option, } +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +/// Internal type for the [crate::instance::ChorusUser::get_pomelo_suggestions] endpoint. +/// +/// See +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 +pub(crate) struct GetPomeloEligibilityReturn { + pub taken: bool +} From 7683ce49a39e65f78c438520bd571fc10806b332 Mon Sep 17 00:00:00 2001 From: kozabrada123 Date: Thu, 8 Aug 2024 19:14:39 +0200 Subject: [PATCH 10/16] feat: add create_pomelo_migration --- src/api/users/users.rs | 78 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 67 insertions(+), 11 deletions(-) diff --git a/src/api/users/users.rs b/src/api/users/users.rs index 1c07b8c..6d47303 100644 --- a/src/api/users/users.rs +++ b/src/api/users/users.rs @@ -12,7 +12,10 @@ use crate::{ instance::{ChorusUser, Instance}, ratelimiter::ChorusRequest, types::{ - DeleteDisableUserSchema, GetPomeloEligibilityReturn, GetPomeloSuggestionsReturn, GetUserProfileSchema, LimitType, PublicUser, Snowflake, User, UserModifyProfileSchema, UserModifySchema, UserProfile, UserProfileMetadata, UserSettings, VerifyUserEmailChangeResponse, VerifyUserEmailChangeSchema + DeleteDisableUserSchema, GetPomeloEligibilityReturn, GetPomeloSuggestionsReturn, + GetUserProfileSchema, LimitType, PublicUser, Snowflake, User, UserModifyProfileSchema, + UserModifySchema, UserProfile, UserProfileMetadata, UserSettings, + VerifyUserEmailChangeResponse, VerifyUserEmailChangeSchema, }, }; @@ -225,6 +228,9 @@ impl ChorusUser { /// 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 pub async fn verify_email_change( @@ -254,9 +260,11 @@ impl ChorusUser { /// "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]) + /// + /// 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 pub async fn get_pomelo_suggestions(&mut self) -> ChorusResult { @@ -274,12 +282,14 @@ impl ChorusUser { chorus_request .deserialize_response::(self) .await - .map(|returned| returned.username) + .map(|returned| returned.username) } - /// Checks whether a unique username is available. - /// - /// Returns whether the username is not taken yet. + /// 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 pub async fn get_pomelo_eligibility(&mut self, username: &String) -> ChorusResult { @@ -289,8 +299,8 @@ impl ChorusUser { self.belongs_to.read().unwrap().urls.api )) .header("Authorization", self.token()) - // FIXME: should we create a type for this? - .body(format!(r#"{{ "username": {:?} }}"#, username)) + // FIXME: should we create a type for this? + .body(format!(r#"{{ "username": {:?} }}"#, username)) .header("Content-Type", "application/json"); let chorus_request = ChorusRequest { @@ -300,7 +310,53 @@ impl ChorusUser { chorus_request .deserialize_response::(self) .await - .map(|returned| !returned.taken) + .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 + 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::(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()) } } From fa70c5594402a1bfb273ec95def58357f48f8157 Mon Sep 17 00:00:00 2001 From: kozabrada123 Date: Fri, 9 Aug 2024 09:11:20 +0200 Subject: [PATCH 11/16] fix: rustdoc lints --- src/types/entities/user.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/entities/user.rs b/src/types/entities/user.rs index 88b4746..caf497d 100644 --- a/src/types/entities/user.rs +++ b/src/types/entities/user.rs @@ -331,8 +331,8 @@ pub struct ProfileBadge { /// 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" + /// `"staff"` badge links to `"https://discord.com/company"` + /// `"certified_moderator"` links to `"https://discord.com/safety"` pub link: Option, } From 9d847906be67874acb88ad39fd5cf5f557291fd7 Mon Sep 17 00:00:00 2001 From: kozabrada123 Date: Fri, 9 Aug 2024 09:29:48 +0200 Subject: [PATCH 12/16] feat: recent_mentions endpoints Adds GET /users/@me/mentions and DELETE /users/@me/mentions/{message.id} --- src/api/users/users.rs | 56 ++++++++++++++++++++++++++++++++++++++-- src/types/schema/user.rs | 29 +++++++++++++++++++++ 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/src/api/users/users.rs b/src/api/users/users.rs index 6d47303..6596782 100644 --- a/src/api/users/users.rs +++ b/src/api/users/users.rs @@ -13,8 +13,8 @@ use crate::{ ratelimiter::ChorusRequest, types::{ DeleteDisableUserSchema, GetPomeloEligibilityReturn, GetPomeloSuggestionsReturn, - GetUserProfileSchema, LimitType, PublicUser, Snowflake, User, UserModifyProfileSchema, - UserModifySchema, UserProfile, UserProfileMetadata, UserSettings, + GetRecentMentionsSchema, GetUserProfileSchema, LimitType, PublicUser, Snowflake, User, + UserModifyProfileSchema, UserModifySchema, UserProfile, UserProfileMetadata, UserSettings, VerifyUserEmailChangeResponse, VerifyUserEmailChangeSchema, }, }; @@ -358,6 +358,58 @@ impl ChorusUser { ChorusResult::Err(result.err().unwrap()) } + + /// Fetches a list of [Message](crate::types::Message)s that the current user has been + /// mentioned in during the last 7 days. + /// + /// As of 2024/08/09, Spacebar does not yet implement this endpoint. + /// + /// See + pub async fn get_recent_mentions( + &mut self, + query_parameters: GetRecentMentionsSchema, + ) -> ChorusResult> { + let request = Client::new() + .get(format!( + "{}/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(), + }; + + chorus_request + .deserialize_response::>(self) + .await + } + + /// Acknowledges a message the current user has been mentioned in. + /// + /// Fires a RecentMentionDelete gateway event. (Note: yet to be implemented in chorus, see [#545](https://github.com/polyphony-chat/chorus/issues/545)) + /// + /// As of 2024/08/09, Spacebar does not yet implement this endpoint. + /// + /// 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 chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + chorus_request.handle_request_as_result(self).await + } } impl User { diff --git a/src/types/schema/user.rs b/src/types/schema/user.rs index b336a63..f7a97d2 100644 --- a/src/types/schema/user.rs +++ b/src/types/schema/user.rs @@ -228,3 +228,32 @@ pub(crate) struct GetPomeloSuggestionsReturn { pub(crate) struct GetPomeloEligibilityReturn { pub taken: bool } + +#[derive(Debug, Default, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)] +/// Query string parameters for the route GET /users/@me/mentions +/// ([crate::instance::ChorusUser::get_recent_mentions]) +/// +/// See +pub struct GetRecentMentionsSchema { + /// Only fetch messages before this message id + /// + /// Due to the nature of snowflakes, this can be easily used to fetch + /// messages before a certain timestamp + pub before: Option, + /// Max number of messages to return + /// + /// Should be between 1 and 100. + /// + /// If unset the limit is 25 messages + pub limit: Option, + /// Limit messages to a specific guild + pub guild_id: Option, + /// Whether to include role mentions. + /// + /// If unset the server assumes true + pub roles: Option, + /// Whether to include @everyone and @here mentions. + /// + /// If unset the server assumes true + pub everyone: Option, +} From 87aad461fd4a08e02800a46ab6367819a3df02b1 Mon Sep 17 00:00:00 2001 From: kozabrada123 Date: Fri, 9 Aug 2024 10:24:38 +0200 Subject: [PATCH 13/16] feat: add get_user_harvest & create_user_harvest Also adds /types/entities/harvest.rs, types for Harvest --- src/api/users/users.rs | 103 ++++++++++++++++++++++++++++++++-- src/types/entities/harvest.rs | 96 +++++++++++++++++++++++++++++++ src/types/entities/mod.rs | 2 + src/types/schema/user.rs | 58 +++++++++++-------- 4 files changed, 231 insertions(+), 28 deletions(-) create mode 100644 src/types/entities/harvest.rs diff --git a/src/api/users/users.rs b/src/api/users/users.rs index 6596782..92fbb6e 100644 --- a/src/api/users/users.rs +++ b/src/api/users/users.rs @@ -12,9 +12,10 @@ use crate::{ instance::{ChorusUser, Instance}, ratelimiter::ChorusRequest, types::{ - DeleteDisableUserSchema, GetPomeloEligibilityReturn, GetPomeloSuggestionsReturn, - GetRecentMentionsSchema, GetUserProfileSchema, LimitType, PublicUser, Snowflake, User, - UserModifyProfileSchema, UserModifySchema, UserProfile, UserProfileMetadata, UserSettings, + CreateUserHarvestSchema, DeleteDisableUserSchema, GetPomeloEligibilityReturn, + GetPomeloSuggestionsReturn, GetRecentMentionsSchema, GetUserProfileSchema, Harvest, + HarvestBackendType, LimitType, PublicUser, Snowflake, User, UserModifyProfileSchema, + UserModifySchema, UserProfile, UserProfileMetadata, UserSettings, VerifyUserEmailChangeResponse, VerifyUserEmailChangeSchema, }, }; @@ -392,7 +393,7 @@ impl ChorusUser { /// Fires a RecentMentionDelete gateway event. (Note: yet to be implemented in chorus, see [#545](https://github.com/polyphony-chat/chorus/issues/545)) /// /// As of 2024/08/09, Spacebar does not yet implement this endpoint. - /// + /// /// See pub async fn delete_recent_mention(&mut self, message_id: Snowflake) -> ChorusResult<()> { let request = Client::new() @@ -410,6 +411,100 @@ impl ChorusUser { chorus_request.handle_request_as_result(self).await } + + /// If it exists, returns the most recent [Harvest] (personal data harvest request). + /// + /// To create a new [Harvest], see [Self::create_harvest]. + /// + /// As of 2024/08/09, Spacebar does not yet implement this endpoint. (Or data harvesting) + /// + /// 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 chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + // 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 + let response = chorus_request.send_request(self).await?; + log::trace!("Got response: {:?}", response); + + if response.status() == http::StatusCode::NO_CONTENT { + return Ok(None); + } + + let response_text = match response.text().await { + Ok(string) => string, + Err(e) => { + return Err(ChorusError::InvalidResponse { + error: format!( + "Error while trying to process the HTTP response into a String: {}", + e + ), + }); + } + }; + + let object = match serde_json::from_str::(&response_text) { + Ok(object) => object, + Err(e) => { + return Err(ChorusError::InvalidResponse { + error: format!( + "Error while trying to deserialize the JSON response into requested type T: {}. JSON Response: {}", + e, response_text + ), + }) + } + }; + Ok(Some(object)) + } + + /// Creates a personal data harvest request ([Harvest]) for the current user. + /// + /// To fetch the latest existing harvest, see [Self::get_harvest]. + /// + /// Invalid options in the backends array are ignored. + /// + /// If the array is empty (after ignoring), it requests all [HarvestBackendType]s. + /// + /// As of 2024/08/09, Spacebar does not yet implement this endpoint. (Or data harvesting) + /// + /// See + pub async fn create_harvest( + &mut self, + backends: Vec, + ) -> ChorusResult { + let schema = if backends.is_empty() { + CreateUserHarvestSchema { backends: None } + } else { + CreateUserHarvestSchema { + backends: Some(backends), + } + }; + + let request = Client::new() + .post(format!( + "{}/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(), + }; + + chorus_request.deserialize_response(self).await + } } impl User { diff --git a/src/types/entities/harvest.rs b/src/types/entities/harvest.rs new file mode 100644 index 0000000..d9f86ac --- /dev/null +++ b/src/types/entities/harvest.rs @@ -0,0 +1,96 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; + +use crate::types::Snowflake; + +#[cfg(feature = "client")] +use crate::gateway::Updateable; + +// FIXME: Should this type be Composite? +#[derive(Serialize, Deserialize, Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] +/// A user's data harvest. +/// +/// # Reference +/// +/// See +pub struct Harvest { + pub harvest_id: Snowflake, + /// The id of the user being harvested + pub user_id: Snowflake, + pub status: HarvestStatus, + /// The time the harvest was created + pub created_at: DateTime, + /// The time the harvest was last polled + pub polled_at: Option>, + /// The time the harvest was completed + pub completed_at: Option>, +} + +#[cfg(feature = "client")] +impl Updateable for Harvest { + #[cfg(not(tarpaulin_include))] + fn id(&self) -> Snowflake { + self.harvest_id + } +} + +#[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")] +/// Current status of a [Harvest] +/// +/// See and +pub enum HarvestStatus { + /// The harvest is queued and has not been started + Queued = 0, + /// The harvest is currently running / being processed + Running = 1, + /// The harvest has failed + Failed = 2, + /// The harvest has been completed successfully + Completed = 3, + #[default] + Unknown = 4, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash, Copy, PartialOrd, Ord)] +#[cfg_attr(feature = "sqlx", derive(sqlx::Type))] +/// A type of backend / service a harvest can be requested for. +/// +/// See and +pub enum HarvestBackendType { + /// All account information; + Accounts, + /// Actions the user has taken; + /// + /// Represented as "Your Activity" in the discord client + Analytics, + /// First-party embedded activity information; + /// + /// e.g.: Chess in the Park, Checkers in the Park, Poker Night 2.0; + /// Sketch Heads, Watch Together, Letter League, Land-io, Know What I Meme + Activities, + /// The user's messages + Messages, + /// Official Discord programes; + /// + /// e.g.: Partner, HypeSquad, Verified Server + Programs, + /// Guilds the user is a member of; + Servers, +} diff --git a/src/types/entities/mod.rs b/src/types/entities/mod.rs index 2fec4ca..c736b99 100644 --- a/src/types/entities/mod.rs +++ b/src/types/entities/mod.rs @@ -11,6 +11,7 @@ pub use config::*; pub use emoji::*; pub use guild::*; pub use guild_member::*; +pub use harvest::*; pub use integration::*; pub use invite::*; pub use message::*; @@ -52,6 +53,7 @@ mod config; mod emoji; mod guild; mod guild_member; +mod harvest; mod integration; mod invite; mod message; diff --git a/src/types/schema/user.rs b/src/types/schema/user.rs index f7a97d2..bc641d6 100644 --- a/src/types/schema/user.rs +++ b/src/types/schema/user.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use chrono::NaiveDate; use serde::{Deserialize, Serialize}; -use crate::types::{Snowflake, ThemeColors}; +use crate::types::{HarvestBackendType, Snowflake, ThemeColors}; #[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] #[serde(rename_all = "snake_case")] @@ -218,7 +218,7 @@ pub struct GetUserProfileSchema { /// /// See pub(crate) struct GetPomeloSuggestionsReturn { - pub username: String + pub username: String, } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] @@ -226,7 +226,7 @@ pub(crate) struct GetPomeloSuggestionsReturn { /// /// See pub(crate) struct GetPomeloEligibilityReturn { - pub taken: bool + pub taken: bool, } #[derive(Debug, Default, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)] @@ -235,25 +235,35 @@ pub(crate) struct GetPomeloEligibilityReturn { /// /// See pub struct GetRecentMentionsSchema { - /// Only fetch messages before this message id - /// - /// Due to the nature of snowflakes, this can be easily used to fetch - /// messages before a certain timestamp - pub before: Option, - /// Max number of messages to return - /// - /// Should be between 1 and 100. - /// - /// If unset the limit is 25 messages - pub limit: Option, - /// Limit messages to a specific guild - pub guild_id: Option, - /// Whether to include role mentions. - /// - /// If unset the server assumes true - pub roles: Option, - /// Whether to include @everyone and @here mentions. - /// - /// If unset the server assumes true - pub everyone: Option, + /// Only fetch messages before this message id + /// + /// Due to the nature of snowflakes, this can be easily used to fetch + /// messages before a certain timestamp + pub before: Option, + /// Max number of messages to return + /// + /// Should be between 1 and 100. + /// + /// If unset the limit is 25 messages + pub limit: Option, + /// Limit messages to a specific guild + pub guild_id: Option, + /// Whether to include role mentions. + /// + /// If unset the server assumes true + pub roles: Option, + /// Whether to include @everyone and @here mentions. + /// + /// If unset the server assumes true + pub everyone: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +/// Internal type for the [crate::instance::ChorusUser::create_harvest] endpoint. +// (koza): imo it's nicer if the user can just pass a vec, instead of having to bother with +// a specific type +/// +/// See +pub(crate) struct CreateUserHarvestSchema { + pub backends: Option>, } From 1dfbd216a4c8d9a90ad802a050d7135998a038e7 Mon Sep 17 00:00:00 2001 From: kozabrada123 Date: Fri, 9 Aug 2024 11:10:25 +0200 Subject: [PATCH 14/16] feat: user notes endpoints Adds: get_user_notes, get_user_note, set_user_note --- src/api/users/users.rs | 144 ++++++++++++++++++++++++++++++++++--- src/types/entities/user.rs | 19 +++++ src/types/schema/user.rs | 8 +++ 3 files changed, 160 insertions(+), 11 deletions(-) diff --git a/src/api/users/users.rs b/src/api/users/users.rs index 92fbb6e..1de9edc 100644 --- a/src/api/users/users.rs +++ b/src/api/users/users.rs @@ -2,7 +2,10 @@ // 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 std::sync::{Arc, RwLock}; +use std::{ + collections::HashMap, + sync::{Arc, RwLock}, +}; use reqwest::Client; use serde_json::to_string; @@ -14,9 +17,9 @@ use crate::{ types::{ CreateUserHarvestSchema, DeleteDisableUserSchema, GetPomeloEligibilityReturn, GetPomeloSuggestionsReturn, GetRecentMentionsSchema, GetUserProfileSchema, Harvest, - HarvestBackendType, LimitType, PublicUser, Snowflake, User, UserModifyProfileSchema, - UserModifySchema, UserProfile, UserProfileMetadata, UserSettings, - VerifyUserEmailChangeResponse, VerifyUserEmailChangeSchema, + HarvestBackendType, LimitType, ModifyUserNoteSchema, PublicUser, Snowflake, User, + UserModifyProfileSchema, UserModifySchema, UserNote, UserProfile, UserProfileMetadata, + UserSettings, VerifyUserEmailChangeResponse, VerifyUserEmailChangeSchema, }, }; @@ -224,11 +227,12 @@ impl ChorusUser { /// 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). /// + /// # Notes + /// Should be the follow-up to [Self::initiate_email_change] + /// /// As of 2024/08/08, Spacebar does not yet implement this endpoint. // FIXME: Does this mean PUT users/@me/email is different? /// @@ -256,8 +260,7 @@ impl ChorusUser { /// Returns a suggested unique username based on the current user's username. /// - /// Note: - /// + /// # Notes: /// "This endpoint is used during the pomelo migration flow. /// /// The user must be in the rollout to use this endpoint." @@ -267,6 +270,7 @@ impl ChorusUser { /// /// As of 2024/08/08, Spacebar does not yet implement this endpoint. /// + /// # Reference /// See pub async fn get_pomelo_suggestions(&mut self) -> ChorusResult { let request = Client::new() @@ -290,8 +294,10 @@ impl ChorusUser { /// /// Returns whether the username is not taken yet. /// + /// # Notes /// As of 2024/08/08, Spacebar does not yet implement this endpoint. /// + /// # Reference /// See pub async fn get_pomelo_eligibility(&mut self, username: &String) -> ChorusResult { let request = Client::new() @@ -321,8 +327,7 @@ impl ChorusUser { /// Updates [Self::object] to an updated representation returned by the server. // FIXME: Is this appropriate behaviour? /// - /// Note: - /// + /// # Notes /// "This endpoint is used during the pomelo migration flow. /// /// The user must be in the rollout to use this endpoint." @@ -332,6 +337,7 @@ impl ChorusUser { // /// As of 2024/08/08, Spacebar does not yet implement this endpoint. /// + /// # Reference /// See pub async fn create_pomelo_migration(&mut self, username: &String) -> ChorusResult<()> { let request = Client::new() @@ -363,8 +369,10 @@ impl ChorusUser { /// Fetches a list of [Message](crate::types::Message)s that the current user has been /// mentioned in during the last 7 days. /// + /// # Notes /// As of 2024/08/09, Spacebar does not yet implement this endpoint. /// + /// # Reference /// See pub async fn get_recent_mentions( &mut self, @@ -390,10 +398,12 @@ impl ChorusUser { /// Acknowledges a message the current user has been mentioned in. /// - /// Fires a RecentMentionDelete gateway event. (Note: yet to be implemented in chorus, see [#545](https://github.com/polyphony-chat/chorus/issues/545)) + /// Fires a `RecentMentionDelete` gateway event. (Note: yet to be implemented in chorus, see [#545](https://github.com/polyphony-chat/chorus/issues/545)) /// + /// # Notes /// As of 2024/08/09, Spacebar does not yet implement this endpoint. /// + /// # Reference /// See pub async fn delete_recent_mention(&mut self, message_id: Snowflake) -> ChorusResult<()> { let request = Client::new() @@ -416,8 +426,10 @@ impl ChorusUser { /// /// To create a new [Harvest], see [Self::create_harvest]. /// + /// # Notes /// As of 2024/08/09, Spacebar does not yet implement this endpoint. (Or data harvesting) /// + /// # Reference /// See pub async fn get_harvest(&mut self) -> ChorusResult> { let request = Client::new() @@ -469,6 +481,7 @@ impl ChorusUser { /// Creates a personal data harvest request ([Harvest]) for the current user. /// + /// # Notes /// To fetch the latest existing harvest, see [Self::get_harvest]. /// /// Invalid options in the backends array are ignored. @@ -477,6 +490,7 @@ impl ChorusUser { /// /// As of 2024/08/09, Spacebar does not yet implement this endpoint. (Or data harvesting) /// + /// # Reference /// See pub async fn create_harvest( &mut self, @@ -505,6 +519,57 @@ impl ChorusUser { chorus_request.deserialize_response(self).await } + + /// Returns a mapping of user IDs ([Snowflake]s) to notes ([String]s) for the current user. + /// + /// # 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 chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + chorus_request.deserialize_response(self).await + } + + /// Fetches the note ([UserNote]) for the given user. + /// + /// If the current user has no note for the target, this endpoint + /// returns `Err(NotFound { error: "{\"message\": \"Unknown User\", \"code\": 10013}" })` + /// + /// # Notes + /// This function is a wrapper around [`User::get_note`]. + /// + /// # Reference + /// See + pub async fn get_user_note(&mut self, target_user_id: Snowflake) -> ChorusResult { + User::get_note(self, target_user_id).await + } + + /// Sets the note for the given user. + /// + /// Fires a `UserNoteUpdate` gateway event. (Note: yet to be implemented in chorus, see [#546](https://github.com/polyphony-chat/chorus/issues/546)) + /// + /// # Notes + /// This function is a wrapper around [`User::set_note`]. + /// + /// # Reference + /// See + pub async fn set_user_note( + &mut self, + target_user_id: Snowflake, + note: Option, + ) -> ChorusResult<()> { + User::set_note(self, target_user_id, note).await + } } impl User { @@ -659,4 +724,61 @@ impl User { .deserialize_response::(user) .await } + + /// Fetches the note ([UserNote]) for the given user. + /// + /// If the current user has no note for the target, this endpoint + /// returns `Err(NotFound { error: "{\"message\": \"Unknown User\", \"code\": 10013}" })` + /// + /// # Reference + /// See + pub async fn get_note( + 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 chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + chorus_request.deserialize_response(user).await + } + + /// Sets the note for the given user. + /// + /// Fires a `UserNoteUpdate` gateway event. (Note: yet to be implemented in chorus, see [#546](https://github.com/polyphony-chat/chorus/issues/546)) + /// + /// # Reference + /// See + pub async fn set_note( + user: &mut ChorusUser, + target_user_id: Snowflake, + note: Option, + ) -> ChorusResult<()> { + let schema = ModifyUserNoteSchema { note }; + + let request = Client::new() + .put(format!( + "{}/users/@me/notes/{}", + 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(), + }; + + chorus_request.handle_request_as_result(user).await + } } diff --git a/src/types/entities/user.rs b/src/types/entities/user.rs index caf497d..09ed96b 100644 --- a/src/types/entities/user.rs +++ b/src/types/entities/user.rs @@ -765,3 +765,22 @@ pub struct MutualGuild { /// The user's nickname in the guild, if any pub nick: Option, } + +/// Structure which is returned by the [crate::instance::ChorusUser::get_user_note] endpoint. +/// +/// Note that [crate::instance::ChorusUser::get_user_notes] endpoint +/// returns a completely different structure; +// Specualation: this is probably how Discord stores notes internally +/// +/// # Reference +/// See +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] +pub struct UserNote { + /// Actual note contents; max 256 characters + pub note: String, + /// The ID of the user the note is on + pub note_user_id: Snowflake, + /// The ID of the user who created the note (always the current user) + pub user_id: Snowflake, +} diff --git a/src/types/schema/user.rs b/src/types/schema/user.rs index bc641d6..cc6e36b 100644 --- a/src/types/schema/user.rs +++ b/src/types/schema/user.rs @@ -267,3 +267,11 @@ pub struct GetRecentMentionsSchema { pub(crate) struct CreateUserHarvestSchema { pub backends: Option>, } + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +/// Internal type for the [crate::instance::ChorusUser::set_user_note] endpoint. +/// +/// See +pub(crate) struct ModifyUserNoteSchema { + pub note: Option, +} From 6c6a87ce5b8b11f48e3b0b8e12a23299e3e5fcca Mon Sep 17 00:00:00 2001 From: kozabrada123 Date: Fri, 9 Aug 2024 11:23:38 +0200 Subject: [PATCH 15/16] feat: add #545 and #546 Adds the RECENT_MENTION_DELETE and USER_NOTE_UPDATE gateway events. The events can be accessed at: message.recent_mention_delete & user.note_update --- src/gateway/events.rs | 2 ++ src/gateway/gateway.rs | 2 ++ src/types/events/message.rs | 9 +++++++++ src/types/events/user.rs | 12 ++++++++++++ 4 files changed, 25 insertions(+) diff --git a/src/gateway/events.rs b/src/gateway/events.rs index 049434b..ae1ce85 100644 --- a/src/gateway/events.rs +++ b/src/gateway/events.rs @@ -69,12 +69,14 @@ pub struct Message { pub reaction_remove: Publisher, pub reaction_remove_all: Publisher, pub reaction_remove_emoji: Publisher, + pub recent_mention_delete: Publisher, pub ack: Publisher, } #[derive(Default, Debug)] pub struct User { pub update: Publisher, + pub note_update: Publisher, pub guild_settings_update: Publisher, pub presence_update: Publisher, pub typing_start: Publisher, diff --git a/src/gateway/gateway.rs b/src/gateway/gateway.rs index f1fc03c..32180b1 100644 --- a/src/gateway/gateway.rs +++ b/src/gateway/gateway.rs @@ -404,6 +404,7 @@ impl Gateway { "MESSAGE_REACTION_REMOVE" => message.reaction_remove, // TODO "MESSAGE_REACTION_REMOVE_ALL" => message.reaction_remove_all, // TODO "MESSAGE_REACTION_REMOVE_EMOJI" => message.reaction_remove_emoji, // TODO + "RECENT_MENTION_DELETE" => message.recent_mention_delete, "MESSAGE_ACK" => message.ack, "PRESENCE_UPDATE" => user.presence_update, // TODO "RELATIONSHIP_ADD" => relationship.add, @@ -413,6 +414,7 @@ impl Gateway { "STAGE_INSTANCE_DELETE" => stage_instance.delete, "TYPING_START" => user.typing_start, "USER_UPDATE" => user.update, // TODO + "USER_NOTE_UPDATE" => user.note_update, "USER_GUILD_SETTINGS_UPDATE" => user.guild_settings_update, "VOICE_STATE_UPDATE" => voice.state_update, // TODO "VOICE_SERVER_UPDATE" => voice.server_update, diff --git a/src/types/events/message.rs b/src/types/events/message.rs index 1b855df..d2ca308 100644 --- a/src/types/events/message.rs +++ b/src/types/events/message.rs @@ -149,6 +149,15 @@ pub struct MessageReactionRemoveEmoji { pub emoji: Emoji, } +#[derive(Debug, Serialize, Deserialize, Default, Clone, Copy, WebSocketEvent)] +/// Sent when a message that mentioned the current user in the last week is acknowledged and deleted. +/// +/// # Reference +/// See +pub struct RecentMentionDelete { + pub message_id: Snowflake, +} + #[derive(Debug, Deserialize, Serialize, Default, Clone, WebSocketEvent)] /// Officially Undocumented /// diff --git a/src/types/events/user.rs b/src/types/events/user.rs index 877c96c..acfc298 100644 --- a/src/types/events/user.rs +++ b/src/types/events/user.rs @@ -16,6 +16,18 @@ pub struct UserUpdate { pub user: PublicUser, } +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq, WebSocketEvent)] +/// See ; +/// +/// Sent when a note the current user has on another user is modified; +/// +/// If the field "note" is an empty string, the note was removed. +pub struct UserNoteUpdate { + /// Id of the user the note is for + pub id: Snowflake, + pub note: String, +} + #[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq, WebSocketEvent)] /// Undocumented; /// From 170f79bbd1ea7ecac33c6725c50f2b7b66a44dca Mon Sep 17 00:00:00 2001 From: kozabrada123 Date: Sat, 10 Aug 2024 11:34:56 +0200 Subject: [PATCH 16/16] feat: add authorize_connection Also adds: src/types/entities/connection.rs, Connection and PublicConnection, src/api/users/connections.rs --- src/api/users/connections.rs | 47 ++++++++ src/api/users/mod.rs | 2 + src/api/users/users.rs | 11 +- src/types/entities/connection.rs | 191 +++++++++++++++++++++++++++++++ src/types/entities/mod.rs | 2 + src/types/schema/user.rs | 24 +++- 6 files changed, 271 insertions(+), 6 deletions(-) create mode 100644 src/api/users/connections.rs create mode 100644 src/types/entities/connection.rs diff --git a/src/api/users/connections.rs b/src/api/users/connections.rs new file mode 100644 index 0000000..2fd9580 --- /dev/null +++ b/src/api/users/connections.rs @@ -0,0 +1,47 @@ +use reqwest::Client; + +use crate::{ + errors::ChorusResult, + instance::ChorusUser, + ratelimiter::ChorusRequest, + types::{AuthorizeConnectionReturn, AuthorizeConnectionSchema, ConnectionType, LimitType}, +}; + +impl ChorusUser { + /// Fetches a url that can be used for authorizing a new connection. + /// + /// # Reference + /// See + /// + /// Note: it doesn't seem to be actually unauthenticated + pub async fn authorize_connection( + &mut self, + connection_type: ConnectionType, + query_parameters: AuthorizeConnectionSchema, + ) -> ChorusResult { + let connection_type_string = serde_json::to_string(&connection_type) + .expect("Failed to serialize connection type!") + .replace('"', ""); + + let request = Client::new() + .get(format!( + "{}/connections/{}/authorize", + 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(), + }; + + chorus_request + .deserialize_response::(self) + .await + .map(|response| response.url) + } +} diff --git a/src/api/users/mod.rs b/src/api/users/mod.rs index b11772a..702233c 100644 --- a/src/api/users/mod.rs +++ b/src/api/users/mod.rs @@ -4,11 +4,13 @@ #![allow(unused_imports)] pub use channels::*; +pub use connections::*; pub use guilds::*; pub use relationships::*; pub use users::*; pub mod channels; +pub mod connections; pub mod guilds; pub mod relationships; pub mod users; diff --git a/src/api/users/users.rs b/src/api/users/users.rs index 1de9edc..9064efd 100644 --- a/src/api/users/users.rs +++ b/src/api/users/users.rs @@ -15,11 +15,12 @@ use crate::{ instance::{ChorusUser, Instance}, ratelimiter::ChorusRequest, types::{ - CreateUserHarvestSchema, DeleteDisableUserSchema, GetPomeloEligibilityReturn, - GetPomeloSuggestionsReturn, GetRecentMentionsSchema, GetUserProfileSchema, Harvest, - HarvestBackendType, LimitType, ModifyUserNoteSchema, PublicUser, Snowflake, User, - UserModifyProfileSchema, UserModifySchema, UserNote, UserProfile, UserProfileMetadata, - UserSettings, VerifyUserEmailChangeResponse, VerifyUserEmailChangeSchema, + AuthorizeConnectionSchema, ConnectionType, CreateUserHarvestSchema, + DeleteDisableUserSchema, GetPomeloEligibilityReturn, GetPomeloSuggestionsReturn, + GetRecentMentionsSchema, GetUserProfileSchema, Harvest, HarvestBackendType, LimitType, + ModifyUserNoteSchema, PublicUser, Snowflake, User, UserModifyProfileSchema, + UserModifySchema, UserNote, UserProfile, UserProfileMetadata, UserSettings, + VerifyUserEmailChangeResponse, VerifyUserEmailChangeSchema, }, }; diff --git a/src/types/entities/connection.rs b/src/types/entities/connection.rs new file mode 100644 index 0000000..b4e6b3d --- /dev/null +++ b/src/types/entities/connection.rs @@ -0,0 +1,191 @@ +use std::{ + collections::HashMap, + fmt::Display, +}; + +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; + +/// A 3rd party service connection to a user's account. +/// +/// # Reference +/// See +// TODO: Should (could) this type be Updateable and Composite? +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] +pub struct Connection { + /// The id of the account on the 3rd party service + #[serde(rename = "id")] + pub connected_account_id: String, + + #[serde(rename = "type")] + pub connection_type: ConnectionType, + + /// The username of the connection account + pub name: String, + + /// If the connection is verified + pub verified: bool, + + /// Service specific metadata about the connection / connected account + // FIXME: Is there a better type? As far as I see the value is always encoded as a string + pub metadata: Option>, + pub metadata_visibility: ConnectionVisibilityType, + + /// If the connection if revoked + pub revoked: bool, + + // TODO: Add integrations + pub friend_sync: bool, + + /// Whether activities related to this connection will be shown in presence + pub show_activity: bool, + + /// Whether this connection has a corresponding 3rd party OAuth2 token + pub two_way_link: bool, + + pub visibility: ConnectionVisibilityType, + + /// The access token for the connection account + /// + /// Note: not included when fetching a user's connections via OAuth2 + pub access_token: Option, +} + +/// A partial / public [Connection] type. +/// +/// # Reference +/// See +// FIXME: Should (could) this type also be Updateable and Composite? +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct PublicConnection { + /// The id of the account on the 3rd party service + #[serde(rename = "id")] + pub connected_account_id: String, + + #[serde(rename = "type")] + pub connection_type: ConnectionType, + + /// The username of the connection account + pub name: String, + + /// If the connection is verified + pub verified: bool, + + /// Service specific metadata about the connection / connected account + // FIXME: Is there a better type? As far as I see the value is always encoded as a string + pub metadata: Option>, +} + +impl From for PublicConnection { + fn from(value: Connection) -> Self { + Self { + connected_account_id: value.connected_account_id, + connection_type: value.connection_type, + name: value.name, + verified: value.verified, + metadata: value.metadata, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash, Copy, PartialOrd, Ord)] +#[cfg_attr(feature = "sqlx", derive(sqlx::Type))] +#[serde(rename_all = "lowercase")] +/// A type of connection; the service the connection is for +/// +/// # Reference +/// See +pub enum ConnectionType { + #[serde(rename = "amazon-music")] + AmazonMusic, + /// Battle.net + BattleNet, + /// Bungie.net + Bungie, + /// Discord?'s contact sync + /// + /// (Not returned in Get User Profile or when fetching connections) + Contacts, + Crunchyroll, + Domain, + Ebay, + EpicGames, + Facebook, + GitHub, + Instagram, + LeagueOfLegends, + PayPal, + /// Playstation network + Playstation, + Reddit, + Roblox, + RiotGames, + /// Samsung Galaxy + /// + /// Users can no longer add this service + Samsung, + Spotify, + /// Users can no longer add this service + Skype, + Steam, + TikTok, + Twitch, + Twitter, + Xbox, + YouTube, +} + +impl Display for ConnectionType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match *self { + Self::AmazonMusic => f.write_str("Amazon Music"), + Self::BattleNet => f.write_str("Battle.net"), + Self::Bungie => f.write_str("Bungie.net"), + Self::Ebay => f.write_str("eBay"), + Self::EpicGames => f.write_str("Epic Games"), + Self::LeagueOfLegends => f.write_str("League of Legends"), + Self::Playstation => f.write_str("PlayStation Network"), + Self::RiotGames => f.write_str("Riot Games"), + Self::Samsung => f.write_str("Samsung Galaxy"), + _ => f.write_str(format!("{:?}", self).as_str()), + } + } +} + +#[derive( + Serialize_repr, Deserialize_repr, Debug, Clone, Eq, PartialEq, Hash, Copy, PartialOrd, Ord, +)] +#[cfg_attr(feature = "sqlx", derive(sqlx::Type))] +#[repr(u8)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +/// # Reference +/// See +pub enum ConnectionVisibilityType { + /// Invisible to everyone except the user themselves + None = 0, + /// Visible to everyone + Everyone = 1, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash, Copy, PartialOrd, Ord)] +#[cfg_attr(feature = "sqlx", derive(sqlx::Type))] +#[serde(rename_all = "lowercase")] +/// A type of two-way connection link +/// +/// # Reference +/// See +pub enum TwoWayLinkType { + /// The connection is linked via web + Web, + /// The connection is linked via mobile + Mobile, + /// The connection is linked via desktop + Desktop, +} + +impl Display for TwoWayLinkType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(format!("{:?}", self).as_str()) + } +} diff --git a/src/types/entities/mod.rs b/src/types/entities/mod.rs index c736b99..6ac2629 100644 --- a/src/types/entities/mod.rs +++ b/src/types/entities/mod.rs @@ -8,6 +8,7 @@ pub use audit_log::*; pub use auto_moderation::*; pub use channel::*; pub use config::*; +pub use connection::*; pub use emoji::*; pub use guild::*; pub use guild_member::*; @@ -50,6 +51,7 @@ mod audit_log; mod auto_moderation; mod channel; mod config; +mod connection; mod emoji; mod guild; mod guild_member; diff --git a/src/types/schema/user.rs b/src/types/schema/user.rs index cc6e36b..93202f2 100644 --- a/src/types/schema/user.rs +++ b/src/types/schema/user.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use chrono::NaiveDate; use serde::{Deserialize, Serialize}; -use crate::types::{HarvestBackendType, Snowflake, ThemeColors}; +use crate::types::{HarvestBackendType, Snowflake, ThemeColors, TwoWayLinkType}; #[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] #[serde(rename_all = "snake_case")] @@ -275,3 +275,25 @@ pub(crate) struct CreateUserHarvestSchema { pub(crate) struct ModifyUserNoteSchema { pub note: Option, } + +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] +/// Query string parameters for the route GET /connections/{connection.type}/authorize +/// ([crate::instance::ChorusUser::authorize_connection]) +/// +/// See +pub struct AuthorizeConnectionSchema { + /// The type of two-way link ([TwoWayLinkType]) to create + pub two_way_link_type: Option, + /// The device code to use for the two-way link + pub two_way_user_code: Option, + /// If this is a continuation of a previous authorization + pub continuation: bool, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +/// Internal type for the [crate::instance::ChorusUser::authorize_connection] endpoint. +/// +/// See +pub(crate) struct AuthorizeConnectionReturn { + pub url: String, +}