From edce406f343a852e0c2cddeecee98a69661699ed Mon Sep 17 00:00:00 2001 From: Jacob Kiesel Date: Wed, 3 May 2023 21:26:10 -0600 Subject: [PATCH] Make discriminator optional and non-zero in preparation for new usernames --- src/cache/mod.rs | 1 + src/model/channel/message.rs | 6 +- src/model/channel/mod.rs | 2 + src/model/error.rs | 5 -- src/model/gateway.rs | 10 ++- src/model/guild/member.rs | 6 +- src/model/mention.rs | 2 + src/model/user.rs | 161 ++++++++++++++--------------------- src/utils/content_safe.rs | 11 ++- src/utils/mod.rs | 21 +++-- 10 files changed, 104 insertions(+), 121 deletions(-) diff --git a/src/cache/mod.rs b/src/cache/mod.rs index eb9635f8aa9..9bd94389dee 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -978,6 +978,7 @@ impl Default for Cache { #[cfg(test)] mod test { use std::collections::HashMap; + use std::num::NonZeroU16; use crate::cache::{Cache, CacheUpdate, Settings}; use crate::model::prelude::*; diff --git a/src/model/channel/message.rs b/src/model/channel/message.rs index 9767afb6919..0a88f064f3a 100644 --- a/src/model/channel/message.rs +++ b/src/model/channel/message.rs @@ -365,8 +365,10 @@ impl Message { let mut at_distinct = String::with_capacity(38); at_distinct.push('@'); at_distinct.push_str(&u.name); - at_distinct.push('#'); - write!(at_distinct, "{:04}", u.discriminator).unwrap(); + if let Some(discriminator) = u.discriminator { + at_distinct.push('#'); + write!(at_distinct, "{:04}", discriminator.get()).unwrap(); + } let mut m = u.mention().to_string(); // Check whether we're replacing a nickname mention or a normal mention. diff --git a/src/model/channel/mod.rs b/src/model/channel/mod.rs index 7675867b01f..9758a5ce179 100644 --- a/src/model/channel/mod.rs +++ b/src/model/channel/mod.rs @@ -465,6 +465,8 @@ pub struct ThreadsData { mod test { #[cfg(all(feature = "model", feature = "utils"))] mod model_utils { + use std::num::NonZeroU16; + use crate::model::prelude::*; #[test] diff --git a/src/model/error.rs b/src/model/error.rs index 46c55e97f8d..b31a9d6570f 100644 --- a/src/model/error.rs +++ b/src/model/error.rs @@ -28,11 +28,6 @@ use super::Permissions; /// #[serenity::async_trait] /// impl EventHandler for Handler { /// async fn guild_ban_removal(&self, context: Context, guild_id: GuildId, user: User) { -/// // If the user has an even discriminator, don't re-ban them. -/// if user.discriminator % 2 == 0 { -/// return; -/// } -/// /// match guild_id.ban(&context, user, 8).await { /// Ok(()) => { /// // Ban successful. diff --git a/src/model/gateway.rs b/src/model/gateway.rs index c2aae91975d..fc7e14ac2ce 100644 --- a/src/model/gateway.rs +++ b/src/model/gateway.rs @@ -1,6 +1,8 @@ //! Models pertaining to the gateway. use serde::ser::SerializeSeq; +use std::num::NonZeroU16; + use url::Url; use super::prelude::*; @@ -232,8 +234,8 @@ pub struct PresenceUser { pub id: UserId, pub avatar: Option, pub bot: Option, - #[serde(default, skip_serializing_if = "Option::is_none", with = "discriminator::option")] - pub discriminator: Option, + #[serde(default, skip_serializing_if = "Option::is_none", with = "discriminator")] + pub discriminator: Option, pub email: Option, pub mfa_enabled: Option, #[serde(rename = "username")] @@ -251,7 +253,7 @@ impl PresenceUser { Some(User { avatar: self.avatar, bot: self.bot?, - discriminator: self.discriminator?, + discriminator: self.discriminator, id: self.id, name: self.name?, public_flags: self.public_flags, @@ -285,7 +287,7 @@ impl PresenceUser { self.avatar = Some(avatar.clone()); } self.bot = Some(user.bot); - self.discriminator = Some(user.discriminator); + self.discriminator = user.discriminator; self.name = Some(user.name.clone()); if let Some(public_flags) = user.public_flags { self.public_flags = Some(public_flags); diff --git a/src/model/guild/member.rs b/src/model/guild/member.rs index a0c99d7cad7..0447db41984 100644 --- a/src/model/guild/member.rs +++ b/src/model/guild/member.rs @@ -233,7 +233,11 @@ impl Member { #[inline] #[must_use] pub fn distinct(&self) -> String { - format!("{}#{:04}", self.display_name(), self.user.discriminator) + if let Some(discriminator) = self.user.discriminator { + format!("{}#{:04}", self.display_name(), discriminator.get()) + } else { + self.display_name().to_string() + } } /// Edits the member in place with the given data. diff --git a/src/model/mention.rs b/src/model/mention.rs index f2de51116df..26aca30e66d 100644 --- a/src/model/mention.rs +++ b/src/model/mention.rs @@ -183,6 +183,8 @@ mentionable!(value: Role, value.id); #[cfg(feature = "utils")] #[cfg(test)] mod test { + use std::num::NonZeroU16; + use crate::model::prelude::*; #[test] diff --git a/src/model/user.rs b/src/model/user.rs index 8f5a841ae9a..8214d9746de 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -5,6 +5,7 @@ use std::fmt; use std::fmt::Write; #[cfg(feature = "temp_cache")] use std::sync::Arc; +use std::num::NonZeroU16; use serde::{Deserialize, Serialize}; @@ -24,28 +25,29 @@ use crate::internal::prelude::*; use crate::json::json; use crate::json::to_string; use crate::model::mention::Mentionable; - /// Used with `#[serde(with|deserialize_with|serialize_with)]` /// /// # Examples /// /// ```rust,ignore +/// use std::num::NonZeroU16; +/// /// #[derive(Deserialize, Serialize)] /// struct A { /// #[serde(with = "discriminator")] -/// id: u16, +/// id: Option, /// } /// /// #[derive(Deserialize)] /// struct B { /// #[serde(deserialize_with = "discriminator::deserialize")] -/// id: u16, +/// id: Option, /// } /// /// #[derive(Serialize)] /// struct C { /// #[serde(serialize_with = "discriminator::serialize")] -/// id: u16, +/// id: Option, /// } /// ``` pub(crate) mod discriminator { @@ -53,16 +55,6 @@ pub(crate) mod discriminator { use std::fmt; use serde::de::{Error, Visitor}; - use serde::{Deserializer, Serializer}; - - pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result { - deserializer.deserialize_any(DiscriminatorVisitor) - } - - #[allow(clippy::trivially_copy_pass_by_ref)] - pub fn serialize(value: &u16, serializer: S) -> Result { - serializer.collect_str(&format_args!("{value:04}")) - } struct DiscriminatorVisitor; @@ -82,46 +74,19 @@ pub(crate) mod discriminator { } } - /// Used with `#[serde(with|deserialize_with|serialize_with)]` - /// - /// # Examples - /// - /// ```rust,ignore - /// #[derive(Deserialize, Serialize)] - /// struct A { - /// #[serde(with = "discriminator::option")] - /// id: Option, - /// } - /// - /// #[derive(Deserialize)] - /// struct B { - /// #[serde(deserialize_with = "discriminator::option::deserialize")] - /// id: Option, - /// } - /// - /// #[derive(Serialize)] - /// struct C { - /// #[serde(serialize_with = "discriminator::option::serialize")] - /// id: Option, - /// } - /// ``` - pub(crate) mod option { - use std::fmt; - - use serde::de::{Error, Visitor}; - use serde::{Deserializer, Serializer}; + use std::num::NonZeroU16; - use super::DiscriminatorVisitor; + use serde::{Deserializer, Serializer}; - pub fn deserialize<'de, D: Deserializer<'de>>( - deserializer: D, - ) -> Result, D::Error> { - deserializer.deserialize_option(OptionalDiscriminatorVisitor) - } + pub fn deserialize<'de, D: Deserializer<'de>>( + deserializer: D, + ) -> Result, D::Error> { + deserializer.deserialize_option(OptionalDiscriminatorVisitor) + } #[allow(clippy::trivially_copy_pass_by_ref)] pub fn serialize( - value: &Option, + value: &Option, serializer: S, ) -> Result { match value { @@ -130,29 +95,28 @@ pub(crate) mod discriminator { } } - struct OptionalDiscriminatorVisitor; + struct OptionalDiscriminatorVisitor; - impl<'de> Visitor<'de> for OptionalDiscriminatorVisitor { - type Value = Option; + impl<'de> Visitor<'de> for OptionalDiscriminatorVisitor { + type Value = Option; - fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str("optional string or integer discriminator") - } + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("optional string or integer discriminator") + } - fn visit_none(self) -> Result { - Ok(None) - } + fn visit_none(self) -> Result { + Ok(None) + } - fn visit_unit(self) -> Result { - Ok(None) - } + fn visit_unit(self) -> Result { + Ok(None) + } - fn visit_some>( - self, - deserializer: D, - ) -> Result { - Ok(Some(deserializer.deserialize_any(DiscriminatorVisitor)?)) - } + fn visit_some>( + self, + deserializer: D, + ) -> Result { + deserializer.deserialize_any(DiscriminatorVisitor).map(NonZeroU16::new) } } } @@ -277,14 +241,17 @@ impl OnlineStatus { pub struct User { /// The unique Id of the user. Can be used to calculate the account's creation date. pub id: UserId, - /// The account's username. Changing username will trigger a discriminator change if the - /// username+discriminator pair becomes non-unique. + /// The account's username. Changing username will trigger a discriminator + /// change if the username+discriminator pair becomes non-unique. Unless the account has + /// migrated to a next generation username, which does not have a discriminant. #[serde(rename = "username")] pub name: String, - /// The account's discriminator to differentiate the user from others with the same - /// [`Self::name`]. The name+discriminator pair is always unique. - #[serde(with = "discriminator")] - pub discriminator: u16, + /// The account's discriminator to differentiate the user from others with + /// the same [`Self::name`]. The name+discriminator pair is always unique. + /// If the discriminator is not present, then this is a next generation username + /// which is implicitly unique. + #[serde(default, skip_serializing_if = "Option::is_none", with = "discriminator")] + pub discriminator: Option, /// Optional avatar hash. pub avatar: Option, /// Indicator of whether the user is a bot. @@ -838,8 +805,13 @@ fn avatar_url(user_id: UserId, hash: Option<&String>) -> Option { } #[cfg(feature = "model")] -fn default_avatar_url(discriminator: u16) -> String { - cdn!("/embed/avatars/{}.png", discriminator % 5u16) +fn default_avatar_url(discriminator: Option) -> String { + if let Some(discriminator) = discriminator { + cdn!("/embed/avatars/{}.png", discriminator.get() % 5u16) + } else { + // TODO: Replace this with a correct implementation once Discord publishes how this is going to work. + cdn!("/embed/avatars/0.png").to_string() + } } #[cfg(feature = "model")] @@ -857,20 +829,23 @@ fn banner_url(user_id: UserId, hash: Option<&String>) -> Option { } #[cfg(feature = "model")] -fn tag(name: &str, discriminator: u16) -> String { +fn tag(name: &str, discriminator: Option) -> String { // 32: max length of username // 1: `#` // 4: max length of discriminator let mut tag = String::with_capacity(37); tag.push_str(name); - tag.push('#'); - write!(tag, "{discriminator:04}").unwrap(); - + if let Some(discriminator) = discriminator { + tag.push('#'); + write!(tag, "{discriminator:04}").unwrap(); + } tag } #[cfg(test)] mod test { + use std::num::NonZeroU16; + #[test] fn test_discriminator_serde() { use serde::{Deserialize, Serialize}; @@ -880,17 +855,8 @@ mod test { #[derive(Debug, PartialEq, Deserialize, Serialize)] struct User { - #[serde(with = "discriminator")] - discriminator: u16, - } - #[derive(Debug, PartialEq, Deserialize, Serialize)] - struct UserOpt { - #[serde( - default, - skip_serializing_if = "Option::is_none", - with = "discriminator::option" - )] - discriminator: Option, + #[serde(default, skip_serializing_if = "Option::is_none", with = "discriminator")] + discriminator: Option, } let user = User { @@ -912,6 +878,8 @@ mod test { #[cfg(feature = "model")] mod model { use crate::model::id::UserId; + use std::num::NonZeroU16; + use crate::model::user::User; #[test] @@ -942,19 +910,16 @@ mod test { #[test] fn default_avatars() { - let mut user = User { - discriminator: 0, - ..Default::default() - }; + let mut user = User { discriminator: None, ..Default::default() }; assert!(user.default_avatar_url().ends_with("0.png")); - user.discriminator = 1; + user.discriminator = NonZeroU16::new(1); assert!(user.default_avatar_url().ends_with("1.png")); - user.discriminator = 2; + user.discriminator = NonZeroU16::new(2); assert!(user.default_avatar_url().ends_with("2.png")); - user.discriminator = 3; + user.discriminator = NonZeroU16::new(3); assert!(user.default_avatar_url().ends_with("3.png")); - user.discriminator = 4; + user.discriminator = NonZeroU16::new(4); assert!(user.default_avatar_url().ends_with("4.png")); } } diff --git a/src/utils/content_safe.rs b/src/utils/content_safe.rs index 7bd1c849852..ce4c017584a 100644 --- a/src/utils/content_safe.rs +++ b/src/utils/content_safe.rs @@ -55,6 +55,9 @@ impl ContentSafeOptions { /// If set to true, if [`content_safe`] replaces a user mention it will add their four digit /// discriminator with a preceding `#`, turning `@username` to `@username#discriminator`. + /// + /// This option is ignored if the username is a next-gen username, and + /// therefore does not have a discriminator. #[must_use] pub fn show_discriminator(mut self, b: bool) -> Self { self.show_discriminator = b; @@ -323,7 +326,7 @@ mod tests { <@123invalid> <@> <@ "; let without_user_mentions = - "@Crab#0000 <@!000000000000000000> @invalid-user @invalid-user \ + "@Crab <@!000000000000000000> @invalid-user @invalid-user \ <@!123123123123123123123> @invalid-user @invalid-user <@!invalid> \ <@invalid> <@日本語 한국어$§)[/__#\\(/&2032$§#> \ <@!i)/==(<<>z/9080)> <@!1231invalid> <@invalid123> \ @@ -335,13 +338,13 @@ mod tests { let options = ContentSafeOptions::default(); assert_eq!( - format!("@{}#{:04}", user.name, user.discriminator), + format!("@{}", user.name), content_safe(&cache, "<@!100000000000000000>", &options, &[]) ); let options = ContentSafeOptions::default(); assert_eq!( - format!("@{}#{:04}", user.name, user.discriminator), + format!("@{}", user.name), content_safe(&cache, "<@100000000000000000>", &options, &[]) ); @@ -350,7 +353,7 @@ mod tests { let options = ContentSafeOptions::default(); assert_eq!( - format!("@{}#{:04}", outside_cache_user.name, outside_cache_user.discriminator), + format!("@{}", outside_cache_user.name), content_safe(&cache, "<@100000000000000001>", &options, &[outside_cache_user]) ); diff --git a/src/utils/mod.rs b/src/utils/mod.rs index f8912331adb..ea9b13a3488 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -29,6 +29,7 @@ pub use self::token::validate as validate_token; #[cfg(all(feature = "cache", feature = "model"))] use crate::cache::Cache; #[cfg(all(feature = "cache", feature = "model"))] +use std::num::NonZeroU16; use crate::http::CacheHttp; use crate::internal::prelude::*; use crate::model::prelude::*; @@ -84,24 +85,30 @@ pub fn parse_invite(code: &str) -> &str { } /// Retrieves the username and discriminator out of a user tag (`name#discrim`). +/// In order to accomodate next gen Discord usernames, this will also accept `name` style tags. /// /// If the user tag is invalid, None is returned. /// /// # Examples /// ```rust +/// use std::num::NonZeroU16; /// use serenity::utils::parse_user_tag; /// -/// assert_eq!(parse_user_tag("kangalioo#9108"), Some(("kangalioo", 9108))); +/// assert_eq!(parse_user_tag("kangalioo#9108"), Some(("kangalioo", NonZeroU16::new(9108)))); /// assert_eq!(parse_user_tag("kangalioo#10108"), None); +/// assert_eq!(parse_user_tag("kangalioo"), Some(("kangalioo", None))); /// ``` #[must_use] -pub fn parse_user_tag(s: &str) -> Option<(&str, u16)> { - let (name, discrim) = s.split_once('#')?; - let discrim = discrim.parse().ok()?; - if discrim > 9999 { - return None; +pub fn parse_user_tag(s: &str) -> Option<(&str, Option)> { + if let Some((name, discrim)) = s.split_once('#') { + let discrim: u16 = discrim.parse().ok()?; + if discrim > 9999 { + return None; + } + Some((name, NonZeroU16::new(discrim))) + } else { + Some((s, None)) } - Some((name, discrim)) } /// Retrieves an Id from a user mention.