twilight_model/user/
mod.rs

1mod avatar_decoration_data;
2mod connection;
3mod connection_visibility;
4mod current_user;
5mod current_user_guild;
6mod flags;
7mod premium_type;
8mod primary_guild;
9
10pub use self::{
11    avatar_decoration_data::AvatarDecorationData, connection::Connection,
12    connection_visibility::ConnectionVisibility, current_user::CurrentUser,
13    current_user_guild::CurrentUserGuild, flags::UserFlags, premium_type::PremiumType,
14    primary_guild::PrimaryGuild,
15};
16
17use crate::{
18    id::{Id, marker::UserMarker},
19    util::image_hash::ImageHash,
20};
21use serde::{Deserialize, Serialize};
22use std::fmt::{Display, Formatter, Result as FmtResult};
23
24pub(crate) mod discriminator {
25    use super::DiscriminatorDisplay;
26    use serde::{
27        de::{Deserializer, Error as DeError, Visitor},
28        ser::Serializer,
29    };
30    use std::fmt::{Formatter, Result as FmtResult};
31
32    struct DiscriminatorVisitor;
33
34    impl Visitor<'_> for DiscriminatorVisitor {
35        type Value = u16;
36
37        fn expecting(&self, f: &mut Formatter<'_>) -> FmtResult {
38            f.write_str("string or integer discriminator")
39        }
40
41        fn visit_u64<E: DeError>(self, value: u64) -> Result<Self::Value, E> {
42            value.try_into().map_err(DeError::custom)
43        }
44
45        fn visit_str<E: DeError>(self, value: &str) -> Result<Self::Value, E> {
46            value.parse().map_err(DeError::custom)
47        }
48    }
49
50    pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<u16, D::Error> {
51        deserializer.deserialize_any(DiscriminatorVisitor)
52    }
53
54    // Allow this lint because taking a reference is required by serde.
55    #[allow(clippy::trivially_copy_pass_by_ref)]
56    pub fn serialize<S: Serializer>(value: &u16, serializer: S) -> Result<S::Ok, S::Error> {
57        serializer.collect_str(&DiscriminatorDisplay(*value))
58    }
59}
60
61/// Display formatter for a user discriminator.
62///
63/// When formatted this will pad a discriminator with zeroes.
64///
65/// This may be preferable to use instead of using `format!` to avoid a String
66/// allocation, and may also be preferable to use rather than defining your own
67/// implementations via `format_args!("{discriminator:04}")`.
68///
69/// # Examples
70///
71/// Display the discriminator value `16` as a string:
72///
73/// ```
74/// use twilight_model::user::DiscriminatorDisplay;
75///
76/// let display = DiscriminatorDisplay::new(16);
77/// assert_eq!("0016", display.to_string());
78/// ```
79#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
80#[must_use = "display implementations should be formatted"]
81pub struct DiscriminatorDisplay(u16);
82
83impl DiscriminatorDisplay {
84    /// Create a new display formatter for a discriminator.
85    ///
86    /// # Examples
87    ///
88    /// Display the discriminator value `5` as a string:
89    ///
90    /// ```
91    /// use twilight_model::user::DiscriminatorDisplay;
92    ///
93    /// let display = DiscriminatorDisplay::new(5);
94    /// assert_eq!("0005", display.to_string());
95    /// ```
96    pub const fn new(discriminator: u16) -> Self {
97        Self(discriminator)
98    }
99
100    /// Retrieve the inner discriminator value.
101    pub const fn get(self) -> u16 {
102        self.0
103    }
104}
105
106impl Display for DiscriminatorDisplay {
107    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
108        // Pad the formatted value with zeroes depending on the number of
109        // digits.
110        //
111        // If the value is [1000, u16::MAX] then we don't need to pad.
112        match self.0 {
113            1..=9 => f.write_str("000")?,
114            10..=99 => f.write_str("00")?,
115            100..=999 => f.write_str("0")?,
116            _ => {}
117        }
118
119        Display::fmt(&self.0, f)
120    }
121}
122
123#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
124pub struct User {
125    /// Accent color of the user's banner.
126    ///
127    /// This is an integer representation of a hexadecimal color code.
128    pub accent_color: Option<u32>,
129    pub avatar: Option<ImageHash>,
130    /// Hash of the user's avatar decoration.
131    pub avatar_decoration: Option<ImageHash>,
132    /// Data for the user's avatar decoration.
133    pub avatar_decoration_data: Option<AvatarDecorationData>,
134    /// Hash of the user's banner image.
135    pub banner: Option<ImageHash>,
136    #[serde(default)]
137    pub bot: bool,
138    /// Discriminator used to differentiate people with the same username.
139    ///
140    /// Note: Users that have migrated to the new username system will have a
141    /// discriminator of `0`.
142    ///
143    /// # Formatting
144    ///
145    /// Because discriminators are stored as an integer they're not in the
146    /// format of Discord user tags due to a lack of padding with zeros. The
147    /// [`discriminator`] method can be used to retrieve a formatter to pad the
148    /// discriminator with zeros.
149    ///
150    /// # serde
151    ///
152    /// The discriminator field can be deserialized from either a string or an
153    /// integer. The field will always serialize into a string due to that being
154    /// the type Discord's API uses.
155    ///
156    /// [`discriminator`]: Self::discriminator
157    #[serde(with = "discriminator")]
158    pub discriminator: u16,
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub email: Option<String>,
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub flags: Option<UserFlags>,
163    /// User's global display name, if set. For bots, this is the application name.
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub global_name: Option<String>,
166    pub id: Id<UserMarker>,
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub locale: Option<String>,
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub mfa_enabled: Option<bool>,
171    #[serde(rename = "username")]
172    pub name: String,
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub premium_type: Option<PremiumType>,
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub primary_guild: Option<PrimaryGuild>,
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub public_flags: Option<UserFlags>,
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub system: Option<bool>,
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub verified: Option<bool>,
183}
184
185impl User {
186    /// Create a [`Display`] formatter for a user discriminator that pads the
187    /// discriminator with zeros up to 4 digits.
188    ///
189    /// [`Display`]: core::fmt::Display
190    pub const fn discriminator(&self) -> DiscriminatorDisplay {
191        DiscriminatorDisplay::new(self.discriminator)
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::{DiscriminatorDisplay, PremiumType, User, UserFlags};
198    use crate::user::primary_guild::PrimaryGuild;
199    use crate::{id::Id, test::image_hash};
200    use serde_test::Token;
201    use static_assertions::assert_impl_all;
202    use std::{fmt::Debug, hash::Hash};
203
204    assert_impl_all!(
205        DiscriminatorDisplay: Clone,
206        Copy,
207        Debug,
208        Eq,
209        Hash,
210        PartialEq,
211        Send,
212        Sync
213    );
214
215    fn user_tokens(discriminator_token: Token) -> Vec<Token> {
216        vec![
217            Token::Struct {
218                name: "User",
219                len: 18,
220            },
221            Token::Str("accent_color"),
222            Token::None,
223            Token::Str("avatar"),
224            Token::Some,
225            Token::Str(image_hash::AVATAR_INPUT),
226            Token::Str("avatar_decoration"),
227            Token::Some,
228            Token::Str(image_hash::AVATAR_DECORATION_INPUT),
229            Token::Str("avatar_decoration_data"),
230            Token::None,
231            Token::Str("banner"),
232            Token::Some,
233            Token::Str(image_hash::BANNER_INPUT),
234            Token::Str("bot"),
235            Token::Bool(false),
236            Token::Str("discriminator"),
237            discriminator_token,
238            Token::Str("email"),
239            Token::Some,
240            Token::Str("address@example.com"),
241            Token::Str("flags"),
242            Token::Some,
243            Token::U64(131_584),
244            Token::Str("global_name"),
245            Token::Some,
246            Token::Str("test"),
247            Token::Str("id"),
248            Token::NewtypeStruct { name: "Id" },
249            Token::Str("1"),
250            Token::Str("locale"),
251            Token::Some,
252            Token::Str("en-us"),
253            Token::Str("mfa_enabled"),
254            Token::Some,
255            Token::Bool(true),
256            Token::Str("username"),
257            Token::Str("test"),
258            Token::Str("premium_type"),
259            Token::Some,
260            Token::U8(2),
261            Token::Str("primary_guild"),
262            Token::Some,
263            Token::Struct {
264                name: "PrimaryGuild",
265                len: 4,
266            },
267            Token::Str("identity_guild_id"),
268            Token::Some,
269            Token::NewtypeStruct { name: "Id" },
270            Token::Str("169256939211980800"),
271            Token::Str("identity_enabled"),
272            Token::Some,
273            Token::Bool(true),
274            Token::Str("tag"),
275            Token::Some,
276            Token::Str("DISC"),
277            Token::Str("badge"),
278            Token::Some,
279            Token::Str("1269e74af4df7417b13759eae50c83dc"),
280            Token::StructEnd,
281            Token::Str("public_flags"),
282            Token::Some,
283            Token::U64(131_584),
284            Token::Str("verified"),
285            Token::Some,
286            Token::Bool(true),
287            Token::StructEnd,
288        ]
289    }
290
291    fn user_tokens_complete(discriminator_token: Token) -> Vec<Token> {
292        vec![
293            Token::Struct {
294                name: "User",
295                len: 19,
296            },
297            Token::Str("accent_color"),
298            Token::None,
299            Token::Str("avatar"),
300            Token::Some,
301            Token::Str(image_hash::AVATAR_INPUT),
302            Token::Str("avatar_decoration"),
303            Token::Some,
304            Token::Str(image_hash::AVATAR_DECORATION_INPUT),
305            Token::Str("avatar_decoration_data"),
306            Token::None,
307            Token::Str("banner"),
308            Token::Some,
309            Token::Str(image_hash::BANNER_INPUT),
310            Token::Str("bot"),
311            Token::Bool(false),
312            Token::Str("discriminator"),
313            discriminator_token,
314            Token::Str("email"),
315            Token::Some,
316            Token::Str("address@example.com"),
317            Token::Str("flags"),
318            Token::Some,
319            Token::U64(131_584),
320            Token::Str("global_name"),
321            Token::Some,
322            Token::Str("test"),
323            Token::Str("id"),
324            Token::NewtypeStruct { name: "Id" },
325            Token::Str("1"),
326            Token::Str("locale"),
327            Token::Some,
328            Token::Str("en-us"),
329            Token::Str("mfa_enabled"),
330            Token::Some,
331            Token::Bool(true),
332            Token::Str("username"),
333            Token::Str("test"),
334            Token::Str("premium_type"),
335            Token::Some,
336            Token::U8(2),
337            Token::Str("primary_guild"),
338            Token::Some,
339            Token::Struct {
340                name: "PrimaryGuild",
341                len: 4,
342            },
343            Token::Str("identity_guild_id"),
344            Token::Some,
345            Token::NewtypeStruct { name: "Id" },
346            Token::Str("169256939211980800"),
347            Token::Str("identity_enabled"),
348            Token::Some,
349            Token::Bool(true),
350            Token::Str("tag"),
351            Token::Some,
352            Token::Str("DISC"),
353            Token::Str("badge"),
354            Token::Some,
355            Token::Str("1269e74af4df7417b13759eae50c83dc"),
356            Token::StructEnd,
357            Token::Str("public_flags"),
358            Token::Some,
359            Token::U64(131_584),
360            Token::Str("system"),
361            Token::Some,
362            Token::Bool(true),
363            Token::Str("verified"),
364            Token::Some,
365            Token::Bool(true),
366            Token::StructEnd,
367        ]
368    }
369
370    #[test]
371    fn discriminator_display() {
372        assert_eq!(3030, DiscriminatorDisplay::new(3030).get());
373        assert_eq!("0003", DiscriminatorDisplay::new(3).to_string());
374        assert_eq!("0033", DiscriminatorDisplay::new(33).to_string());
375        assert_eq!("0333", DiscriminatorDisplay::new(333).to_string());
376        assert_eq!("3333", DiscriminatorDisplay::new(3333).to_string());
377        assert_eq!("0", DiscriminatorDisplay::new(0).to_string());
378    }
379
380    #[test]
381    fn user() {
382        let value = User {
383            accent_color: None,
384            avatar: Some(image_hash::AVATAR),
385            avatar_decoration: Some(image_hash::AVATAR_DECORATION),
386            avatar_decoration_data: None,
387            banner: Some(image_hash::BANNER),
388            bot: false,
389            discriminator: 1,
390            email: Some("address@example.com".to_owned()),
391            flags: Some(UserFlags::PREMIUM_EARLY_SUPPORTER | UserFlags::VERIFIED_DEVELOPER),
392            global_name: Some("test".to_owned()),
393            id: Id::new(1),
394            locale: Some("en-us".to_owned()),
395            mfa_enabled: Some(true),
396            name: "test".to_owned(),
397            premium_type: Some(PremiumType::Nitro),
398            primary_guild: Some(PrimaryGuild {
399                identity_guild_id: Some(Id::new(169_256_939_211_980_800)),
400                identity_enabled: Some(true),
401                tag: Some("DISC".to_owned()),
402                badge: Some("1269e74af4df7417b13759eae50c83dc".parse().unwrap()),
403            }),
404            public_flags: Some(UserFlags::PREMIUM_EARLY_SUPPORTER | UserFlags::VERIFIED_DEVELOPER),
405            system: None,
406            verified: Some(true),
407        };
408
409        // Deserializing a user with a string discriminator (which Discord
410        // provides)
411        serde_test::assert_tokens(&value, &user_tokens(Token::Str("0001")));
412
413        // Deserializing a user with an integer discriminator. Userland code
414        // may have this due to being a more compact memory representation of a
415        // discriminator.
416        serde_test::assert_de_tokens(&value, &user_tokens(Token::U64(1)));
417    }
418
419    #[test]
420    fn user_no_discriminator() {
421        let value = User {
422            accent_color: None,
423            avatar: Some(image_hash::AVATAR),
424            avatar_decoration: Some(image_hash::AVATAR_DECORATION),
425            avatar_decoration_data: None,
426            banner: Some(image_hash::BANNER),
427            bot: false,
428            discriminator: 0,
429            email: Some("address@example.com".to_owned()),
430            flags: Some(UserFlags::PREMIUM_EARLY_SUPPORTER | UserFlags::VERIFIED_DEVELOPER),
431            global_name: Some("test".to_owned()),
432            id: Id::new(1),
433            locale: Some("en-us".to_owned()),
434            mfa_enabled: Some(true),
435            name: "test".to_owned(),
436            premium_type: Some(PremiumType::Nitro),
437            primary_guild: Some(PrimaryGuild {
438                identity_guild_id: Some(Id::new(169_256_939_211_980_800)),
439                identity_enabled: Some(true),
440                tag: Some("DISC".to_owned()),
441                badge: Some("1269e74af4df7417b13759eae50c83dc".parse().unwrap()),
442            }),
443            public_flags: Some(UserFlags::PREMIUM_EARLY_SUPPORTER | UserFlags::VERIFIED_DEVELOPER),
444            system: None,
445            verified: Some(true),
446        };
447
448        // Users migrated to the new username system will have a placeholder discriminator of 0,
449        // You can check if a user has migrated by seeing if their discriminator is 0.
450        // Read more here: https://discord.com/developers/docs/change-log#identifying-migrated-users
451        serde_test::assert_tokens(&value, &user_tokens(Token::Str("0")));
452        serde_test::assert_de_tokens(&value, &user_tokens(Token::U64(0)));
453    }
454
455    #[test]
456    fn user_complete() {
457        let value = User {
458            accent_color: None,
459            avatar: Some(image_hash::AVATAR),
460            avatar_decoration: Some(image_hash::AVATAR_DECORATION),
461            avatar_decoration_data: None,
462            banner: Some(image_hash::BANNER),
463            bot: false,
464            discriminator: 1,
465            email: Some("address@example.com".to_owned()),
466            flags: Some(UserFlags::PREMIUM_EARLY_SUPPORTER | UserFlags::VERIFIED_DEVELOPER),
467            global_name: Some("test".to_owned()),
468            id: Id::new(1),
469            locale: Some("en-us".to_owned()),
470            mfa_enabled: Some(true),
471            name: "test".to_owned(),
472            premium_type: Some(PremiumType::Nitro),
473            primary_guild: Some(PrimaryGuild {
474                identity_guild_id: Some(Id::new(169_256_939_211_980_800)),
475                identity_enabled: Some(true),
476                tag: Some("DISC".to_owned()),
477                badge: Some("1269e74af4df7417b13759eae50c83dc".parse().unwrap()),
478            }),
479            public_flags: Some(UserFlags::PREMIUM_EARLY_SUPPORTER | UserFlags::VERIFIED_DEVELOPER),
480            system: Some(true),
481            verified: Some(true),
482        };
483
484        // Deserializing a user with a string discriminator (which Discord
485        // provides)
486        serde_test::assert_tokens(&value, &user_tokens_complete(Token::Str("0001")));
487
488        // Deserializing a user with an integer discriminator. Userland code
489        // may have this due to being a more compact memory representation of a
490        // discriminator.
491        serde_test::assert_de_tokens(&value, &user_tokens_complete(Token::U64(1)));
492    }
493}