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