serenity/model/
user.rs

1//! User information-related models.
2
3use std::fmt;
4#[cfg(feature = "model")]
5use std::fmt::Write;
6use std::num::NonZeroU16;
7use std::ops::{Deref, DerefMut};
8
9use serde::{Deserialize, Serialize};
10
11use super::prelude::*;
12#[cfg(feature = "model")]
13use crate::builder::{Builder, CreateMessage, EditProfile};
14#[cfg(all(feature = "cache", feature = "model"))]
15use crate::cache::{Cache, UserRef};
16#[cfg(feature = "collector")]
17use crate::collector::{MessageCollector, ReactionCollector};
18#[cfg(feature = "collector")]
19use crate::gateway::ShardMessenger;
20#[cfg(feature = "model")]
21use crate::http::CacheHttp;
22#[cfg(feature = "model")]
23use crate::internal::prelude::*;
24#[cfg(feature = "model")]
25use crate::json::json;
26#[cfg(feature = "model")]
27use crate::model::utils::avatar_url;
28
29/// Used with `#[serde(with|deserialize_with|serialize_with)]`
30///
31/// # Examples
32///
33/// ```rust,ignore
34/// use std::num::NonZeroU16;
35///
36/// #[derive(Deserialize, Serialize)]
37/// struct A {
38///     #[serde(with = "discriminator")]
39///     id: Option<NonZeroU16>,
40/// }
41///
42/// #[derive(Deserialize)]
43/// struct B {
44///     #[serde(deserialize_with = "discriminator::deserialize")]
45///     id: Option<NonZeroU16>,
46/// }
47///
48/// #[derive(Serialize)]
49/// struct C {
50///     #[serde(serialize_with = "discriminator::serialize")]
51///     id: Option<NonZeroU16>,
52/// }
53/// ```
54pub(crate) mod discriminator {
55    use std::fmt;
56
57    use serde::de::{Error, Visitor};
58
59    struct DiscriminatorVisitor;
60
61    impl Visitor<'_> for DiscriminatorVisitor {
62        type Value = u16;
63
64        fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
65            formatter.write_str("string or integer discriminator")
66        }
67
68        fn visit_u64<E: Error>(self, value: u64) -> Result<Self::Value, E> {
69            u16::try_from(value).map_err(Error::custom)
70        }
71
72        fn visit_str<E: Error>(self, s: &str) -> Result<Self::Value, E> {
73            s.parse().map_err(Error::custom)
74        }
75    }
76
77    use std::num::NonZeroU16;
78
79    use serde::{Deserializer, Serializer};
80
81    pub fn deserialize<'de, D: Deserializer<'de>>(
82        deserializer: D,
83    ) -> Result<Option<NonZeroU16>, D::Error> {
84        deserializer.deserialize_option(OptionalDiscriminatorVisitor)
85    }
86
87    #[allow(clippy::trivially_copy_pass_by_ref, clippy::ref_option)]
88    pub fn serialize<S: Serializer>(
89        value: &Option<NonZeroU16>,
90        serializer: S,
91    ) -> Result<S::Ok, S::Error> {
92        match value {
93            Some(value) => serializer.serialize_some(&format_args!("{value:04}")),
94            None => serializer.serialize_none(),
95        }
96    }
97
98    struct OptionalDiscriminatorVisitor;
99
100    impl<'de> Visitor<'de> for OptionalDiscriminatorVisitor {
101        type Value = Option<NonZeroU16>;
102
103        fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
104            formatter.write_str("optional string or integer discriminator")
105        }
106
107        fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
108            Ok(None)
109        }
110
111        fn visit_unit<E: Error>(self) -> Result<Self::Value, E> {
112            Ok(None)
113        }
114
115        fn visit_some<D: Deserializer<'de>>(
116            self,
117            deserializer: D,
118        ) -> Result<Self::Value, D::Error> {
119            deserializer.deserialize_any(DiscriminatorVisitor).map(NonZeroU16::new)
120        }
121    }
122}
123
124/// Information about the current user.
125///
126/// [Discord docs](https://discord.com/developers/docs/resources/user#user-object).
127#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
128#[derive(Clone, Debug, Default, Deserialize, Serialize)]
129#[serde(transparent)]
130pub struct CurrentUser(User);
131
132impl Deref for CurrentUser {
133    type Target = User;
134
135    fn deref(&self) -> &Self::Target {
136        &self.0
137    }
138}
139
140impl DerefMut for CurrentUser {
141    fn deref_mut(&mut self) -> &mut Self::Target {
142        &mut self.0
143    }
144}
145
146impl From<CurrentUser> for User {
147    fn from(user: CurrentUser) -> Self {
148        user.0
149    }
150}
151
152#[cfg(feature = "model")]
153impl CurrentUser {
154    /// Edits the current user's profile settings.
155    ///
156    /// This mutates the current user in-place.
157    ///
158    /// Refer to [`EditProfile`]'s documentation for its methods.
159    ///
160    /// # Examples
161    ///
162    /// Change the avatar:
163    ///
164    /// ```rust,no_run
165    /// # use serenity::builder::{EditProfile, CreateAttachment};
166    /// # use serenity::http::Http;
167    /// # use serenity::model::user::CurrentUser;
168    /// #
169    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
170    /// # let http: Http = unimplemented!();
171    /// # let mut user = CurrentUser::default();
172    /// let avatar = CreateAttachment::path("./avatar.png").await?;
173    /// user.edit(&http, EditProfile::new().avatar(&avatar)).await;
174    /// # Ok(())
175    /// # }
176    /// ```
177    ///
178    /// # Errors
179    ///
180    /// Returns an [`Error::Http`] if an invalid value is set. May also return an [`Error::Json`]
181    /// if there is an error in deserializing the API response.
182    pub async fn edit(&mut self, cache_http: impl CacheHttp, builder: EditProfile) -> Result<()> {
183        *self = builder.execute(cache_http, ()).await?;
184        Ok(())
185    }
186}
187
188/// The representation of a user's status.
189///
190/// [Discord docs](https://discord.com/developers/docs/topics/gateway-events#update-presence-status-types).
191#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
192#[derive(
193    Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize,
194)]
195#[non_exhaustive]
196pub enum OnlineStatus {
197    #[serde(rename = "dnd")]
198    DoNotDisturb,
199    #[serde(rename = "idle")]
200    Idle,
201    #[serde(rename = "invisible")]
202    Invisible,
203    #[serde(rename = "offline")]
204    Offline,
205    #[serde(rename = "online")]
206    #[default]
207    Online,
208}
209
210impl OnlineStatus {
211    #[must_use]
212    pub fn name(&self) -> &str {
213        match *self {
214            OnlineStatus::DoNotDisturb => "dnd",
215            OnlineStatus::Idle => "idle",
216            OnlineStatus::Invisible => "invisible",
217            OnlineStatus::Offline => "offline",
218            OnlineStatus::Online => "online",
219        }
220    }
221}
222
223/// Information about a user.
224///
225/// [Discord docs](https://discord.com/developers/docs/resources/user#user-object), existence of
226/// additional partial member field documented [here](https://discord.com/developers/docs/topics/gateway-events#message-create).
227#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
228#[derive(Clone, Debug, Default, Deserialize, Serialize)]
229#[non_exhaustive]
230pub struct User {
231    /// The unique Id of the user. Can be used to calculate the account's creation date.
232    pub id: UserId,
233    /// The account's username. Changing username will trigger a discriminator
234    /// change if the username+discriminator pair becomes non-unique. Unless the account has
235    /// migrated to a next generation username, which does not have a discriminant.
236    #[serde(rename = "username")]
237    pub name: String,
238    /// The account's discriminator to differentiate the user from others with
239    /// the same [`Self::name`]. The name+discriminator pair is always unique.
240    /// If the discriminator is not present, then this is a next generation username
241    /// which is implicitly unique.
242    #[serde(default, skip_serializing_if = "Option::is_none", with = "discriminator")]
243    pub discriminator: Option<NonZeroU16>,
244    /// The account's display name, if it is set.
245    /// For bots this is the application name.
246    pub global_name: Option<String>,
247    /// Optional avatar hash.
248    pub avatar: Option<ImageHash>,
249    /// Indicator of whether the user is a bot.
250    #[serde(default)]
251    pub bot: bool,
252    /// Whether the user is an Official Discord System user (part of the urgent message system).
253    #[serde(default)]
254    pub system: bool,
255    /// Whether the user has two factor enabled on their account
256    #[serde(default)]
257    pub mfa_enabled: bool,
258    /// Optional banner hash.
259    ///
260    /// **Note**: This will only be present if the user is fetched via Rest API, e.g. with
261    /// [`crate::http::Http::get_user`].
262    pub banner: Option<ImageHash>,
263    /// The user's banner colour encoded as an integer representation of hexadecimal colour code
264    ///
265    /// **Note**: This will only be present if the user is fetched via Rest API, e.g. with
266    /// [`crate::http::Http::get_user`].
267    #[serde(rename = "accent_color")]
268    pub accent_colour: Option<Colour>,
269    /// The user's chosen language option
270    pub locale: Option<String>,
271    /// Whether the email on this account has been verified
272    ///
273    /// Requires [`Scope::Email`]
274    pub verified: Option<bool>,
275    /// The user's email
276    ///
277    /// Requires [`Scope::Email`]
278    pub email: Option<String>,
279    /// The flags on a user's account
280    #[serde(default)]
281    pub flags: UserPublicFlags,
282    /// The type of Nitro subscription on a user's account
283    #[serde(default)]
284    pub premium_type: PremiumType,
285    /// The public flags on a user's account
286    pub public_flags: Option<UserPublicFlags>,
287    /// Only included in [`Message::mentions`] for messages from the gateway.
288    ///
289    /// [Discord docs](https://discord.com/developers/docs/topics/gateway-events#message-create-message-create-extra-fields).
290    // Box required to avoid infinitely recursive types
291    pub member: Option<Box<PartialMember>>,
292}
293
294enum_number! {
295    /// Premium types denote the level of premium a user has. Visit the [Nitro](https://discord.com/nitro)
296    /// page to learn more about the premium plans Discord currently offers.
297    ///
298    /// [Discord docs](https://discord.com/developers/docs/resources/user#user-object-premium-types).
299    #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
300    #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
301    #[serde(from = "u8", into = "u8")]
302    #[non_exhaustive]
303    pub enum PremiumType {
304        #[default]
305        None = 0,
306        NitroClassic = 1,
307        Nitro = 2,
308        NitroBasic = 3,
309        _ => Unknown(u8),
310    }
311}
312
313bitflags! {
314    /// User's public flags
315    ///
316    /// [Discord docs](https://discord.com/developers/docs/resources/user#user-object-user-flags).
317    #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
318    #[derive(Copy, Clone, Default, Debug, Eq, Hash, PartialEq)]
319    pub struct UserPublicFlags: u32 {
320        /// User's flag as discord employee
321        const DISCORD_EMPLOYEE = 1 << 0;
322        /// User's flag as partnered server owner
323        const PARTNERED_SERVER_OWNER = 1 << 1;
324        /// User's flag as hypesquad events
325        const HYPESQUAD_EVENTS = 1 << 2;
326        /// User's flag as bug hunter level 1
327        const BUG_HUNTER_LEVEL_1 = 1 << 3;
328        /// User's flag as house bravery
329        const HOUSE_BRAVERY = 1 << 6;
330        /// User's flag as house brilliance
331        const HOUSE_BRILLIANCE = 1 << 7;
332        /// User's flag as house balance
333        const HOUSE_BALANCE = 1 << 8;
334        /// User's flag as early supporter
335        const EARLY_SUPPORTER = 1 << 9;
336        /// User's flag as team user
337        const TEAM_USER = 1 << 10;
338        /// User's flag as system
339        const SYSTEM = 1 << 12;
340        /// User's flag as bug hunter level 2
341        const BUG_HUNTER_LEVEL_2 = 1 << 14;
342        /// User's flag as verified bot
343        const VERIFIED_BOT = 1 << 16;
344        /// User's flag as early verified bot developer
345        const EARLY_VERIFIED_BOT_DEVELOPER = 1 << 17;
346        /// User's flag as discord certified moderator
347        const DISCORD_CERTIFIED_MODERATOR = 1 << 18;
348        /// Bot's running with HTTP interactions
349        const BOT_HTTP_INTERACTIONS = 1 << 19;
350        /// User's flag for suspected spam activity.
351        #[cfg(feature = "unstable_discord_api")]
352        const SPAMMER = 1 << 20;
353        /// User's flag as active developer
354        const ACTIVE_DEVELOPER = 1 << 22;
355    }
356}
357
358use std::hash::{Hash, Hasher};
359
360impl PartialEq for User {
361    fn eq(&self, other: &Self) -> bool {
362        self.id == other.id
363    }
364}
365
366impl Eq for User {}
367
368impl Hash for User {
369    fn hash<H: Hasher>(&self, hasher: &mut H) {
370        self.id.hash(hasher);
371    }
372}
373
374#[cfg(feature = "model")]
375impl User {
376    /// Returns the formatted URL of the user's icon, if one exists.
377    ///
378    /// This will produce a WEBP image URL, or GIF if the user has a GIF avatar.
379    #[inline]
380    #[must_use]
381    pub fn avatar_url(&self) -> Option<String> {
382        avatar_url(None, self.id, self.avatar.as_ref())
383    }
384
385    /// Returns the formatted URL of the user's banner, if one exists.
386    ///
387    /// This will produce a WEBP image URL, or GIF if the user has a GIF banner.
388    ///
389    /// **Note**: This will only be present if the user is fetched via Rest API, e.g. with
390    /// [`crate::http::Http::get_user`].
391    #[inline]
392    #[must_use]
393    pub fn banner_url(&self) -> Option<String> {
394        banner_url(self.id, self.banner.as_ref())
395    }
396
397    /// Creates a direct message channel between the [current user] and the user. This can also
398    /// retrieve the channel if one already exists.
399    ///
400    /// [current user]: CurrentUser
401    ///
402    /// # Errors
403    ///
404    /// See [`UserId::create_dm_channel`] for what errors may be returned.
405    #[inline]
406    pub async fn create_dm_channel(&self, cache_http: impl CacheHttp) -> Result<PrivateChannel> {
407        if self.bot {
408            return Err(Error::Model(ModelError::MessagingBot));
409        }
410
411        self.id.create_dm_channel(cache_http).await
412    }
413
414    /// Retrieves the time that this user was created at.
415    #[inline]
416    #[must_use]
417    pub fn created_at(&self) -> Timestamp {
418        self.id.created_at()
419    }
420
421    /// Returns the formatted URL to the user's default avatar URL.
422    ///
423    /// This will produce a PNG URL.
424    #[inline]
425    #[must_use]
426    pub fn default_avatar_url(&self) -> String {
427        default_avatar_url(self)
428    }
429
430    /// Sends a message to a user through a direct message channel. This is a channel that can only
431    /// be accessed by you and the recipient.
432    ///
433    /// # Examples
434    ///
435    /// See [`UserId::direct_message`] for examples.
436    ///
437    /// # Errors
438    ///
439    /// See [`UserId::direct_message`] for errors.
440    pub async fn direct_message(
441        &self,
442        cache_http: impl CacheHttp,
443        builder: CreateMessage,
444    ) -> Result<Message> {
445        self.id.direct_message(cache_http, builder).await
446    }
447
448    /// Calculates the user's display name.
449    ///
450    /// The global name takes priority over the user's username if it exists.
451    ///
452    /// Note: Guild specific information is not included as this is only available on the [Member].
453    #[inline]
454    #[must_use]
455    pub fn display_name(&self) -> &str {
456        self.global_name.as_deref().unwrap_or(&self.name)
457    }
458
459    /// This is an alias of [`Self::direct_message`].
460    #[allow(clippy::missing_errors_doc)]
461    #[inline]
462    pub async fn dm(&self, cache_http: impl CacheHttp, builder: CreateMessage) -> Result<Message> {
463        self.direct_message(cache_http, builder).await
464    }
465
466    /// Retrieves the URL to the user's avatar, falling back to the default avatar if needed.
467    ///
468    /// This will call [`Self::avatar_url`] first, and if that returns [`None`], it then falls back
469    /// to [`Self::default_avatar_url`].
470    #[must_use]
471    pub fn face(&self) -> String {
472        self.avatar_url().unwrap_or_else(|| self.default_avatar_url())
473    }
474
475    /// Retrieves the URL to the static version of the user's avatar, falling back to the default
476    /// avatar if needed.
477    ///
478    /// This will call [`Self::static_avatar_url`] first, and if that returns [`None`], it then
479    /// falls back to [`Self::default_avatar_url`].
480    #[must_use]
481    pub fn static_face(&self) -> String {
482        self.static_avatar_url().unwrap_or_else(|| self.default_avatar_url())
483    }
484
485    /// Check if a user has a [`Role`]. This will retrieve the [`Guild`] from the [`Cache`] if it
486    /// is available, and then check if that guild has the given [`Role`].
487    ///
488    /// # Examples
489    ///
490    /// Check if a guild has a [`Role`] by Id:
491    ///
492    /// ```rust,ignore
493    /// // Assumes a 'guild_id' and `role_id` have already been bound
494    /// let _ = message.author.has_role(guild_id, role_id);
495    /// ```
496    ///
497    /// [`Cache`]: crate::cache::Cache
498    ///
499    /// # Errors
500    ///
501    /// Returns an [`Error::Http`] if the given [`Guild`] is unavailable, if that [`Role`] does not
502    /// exist in the given [`Guild`], or if the given [`User`] is not in that [`Guild`].
503    ///
504    /// May also return an [`Error::Json`] if there is an error in deserializing the API response.
505    #[inline]
506    pub async fn has_role(
507        &self,
508        cache_http: impl CacheHttp,
509        guild_id: impl Into<GuildId>,
510        role: impl Into<RoleId>,
511    ) -> Result<bool> {
512        guild_id.into().member(cache_http, self).await.map(|m| m.roles.contains(&role.into()))
513    }
514
515    /// Refreshes the information about the user.
516    ///
517    /// Replaces the instance with the data retrieved over the REST API.
518    ///
519    /// # Errors
520    ///
521    /// See [`UserId::to_user`] for what errors may be returned.
522    #[inline]
523    pub async fn refresh(&mut self, cache_http: impl CacheHttp) -> Result<()> {
524        *self = self.id.to_user(cache_http).await?;
525
526        Ok(())
527    }
528
529    /// Returns a static formatted URL of the user's icon, if one exists.
530    ///
531    /// This will always produce a WEBP image URL.
532    #[inline]
533    #[must_use]
534    pub fn static_avatar_url(&self) -> Option<String> {
535        static_avatar_url(self.id, self.avatar.as_ref())
536    }
537
538    /// Returns the "tag" for the user.
539    ///
540    /// The "tag" is defined as "username#discriminator", such as "zeyla#5479".
541    ///
542    /// # Examples
543    ///
544    /// Make a command to tell the user what their tag is:
545    ///
546    /// ```rust,no_run
547    /// # use serenity::prelude::*;
548    /// # use serenity::model::prelude::*;
549    /// # struct Handler;
550    ///
551    /// #[serenity::async_trait]
552    /// # #[cfg(feature = "client")]
553    /// impl EventHandler for Handler {
554    ///     async fn message(&self, context: Context, msg: Message) {
555    ///         if msg.content == "!mytag" {
556    ///             let content = format!("Your tag is: {}", msg.author.tag());
557    ///             let _ = msg.channel_id.say(&context.http, &content).await;
558    ///         }
559    ///     }
560    /// }
561    /// ```
562    #[inline]
563    #[must_use]
564    pub fn tag(&self) -> String {
565        tag(&self.name, self.discriminator)
566    }
567
568    /// Returns the user's nickname in the given `guild_id`.
569    ///
570    /// If none is used, it returns [`None`].
571    #[inline]
572    pub async fn nick_in(
573        &self,
574        cache_http: impl CacheHttp,
575        guild_id: impl Into<GuildId>,
576    ) -> Option<String> {
577        let guild_id = guild_id.into();
578
579        // This can't be removed because `GuildId::member` clones the entire `Member` struct if
580        // it's present in the cache, which is expensive.
581        #[cfg(feature = "cache")]
582        {
583            if let Some(cache) = cache_http.cache() {
584                if let Some(guild) = guild_id.to_guild_cached(cache) {
585                    if let Some(member) = guild.members.get(&self.id) {
586                        return member.nick.clone();
587                    }
588                }
589            }
590        }
591
592        // At this point we're guaranteed to do an API call.
593        guild_id.member(cache_http, &self.id).await.ok().and_then(|member| member.nick)
594    }
595
596    /// Returns a builder which can be awaited to obtain a message or stream of messages sent by
597    /// this user.
598    #[cfg(feature = "collector")]
599    pub fn await_reply(&self, shard_messenger: impl AsRef<ShardMessenger>) -> MessageCollector {
600        MessageCollector::new(shard_messenger).author_id(self.id)
601    }
602
603    /// Same as [`Self::await_reply`].
604    #[cfg(feature = "collector")]
605    pub fn await_replies(&self, shard_messenger: impl AsRef<ShardMessenger>) -> MessageCollector {
606        self.await_reply(shard_messenger)
607    }
608
609    /// Returns a builder which can be awaited to obtain a reaction or stream of reactions sent by
610    /// this user.
611    #[cfg(feature = "collector")]
612    pub fn await_reaction(&self, shard_messenger: impl AsRef<ShardMessenger>) -> ReactionCollector {
613        ReactionCollector::new(shard_messenger).author_id(self.id)
614    }
615
616    /// Same as [`Self::await_reaction`].
617    #[cfg(feature = "collector")]
618    pub fn await_reactions(
619        &self,
620        shard_messenger: impl AsRef<ShardMessenger>,
621    ) -> ReactionCollector {
622        self.await_reaction(shard_messenger)
623    }
624}
625
626impl fmt::Display for User {
627    /// Formats a string which will mention the user.
628    // This is in the format of: `<@USER_ID>`
629    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
630        fmt::Display::fmt(&self.id.mention(), f)
631    }
632}
633
634#[cfg(feature = "model")]
635impl UserId {
636    /// Creates a direct message channel between the [current user] and the user. This can also
637    /// retrieve the channel if one already exists.
638    ///
639    /// # Errors
640    ///
641    /// Returns [`Error::Http`] if a [`User`] with that [`UserId`] does not exist, or is otherwise
642    /// unavailable.
643    ///
644    /// May also return an [`Error::Json`] if there is an error in deserializing the channel data
645    /// returned by the Discord API.
646    ///
647    /// [current user]: CurrentUser
648    pub async fn create_dm_channel(self, cache_http: impl CacheHttp) -> Result<PrivateChannel> {
649        #[cfg(feature = "temp_cache")]
650        if let Some(cache) = cache_http.cache() {
651            if let Some(private_channel) = cache.temp_private_channels.get(&self) {
652                return Ok(PrivateChannel::clone(&private_channel));
653            }
654        }
655
656        let map = json!({
657            "recipient_id": self,
658        });
659
660        let channel = cache_http.http().create_private_channel(&map).await?;
661
662        #[cfg(feature = "temp_cache")]
663        if let Some(cache) = cache_http.cache() {
664            use crate::cache::MaybeOwnedArc;
665
666            let cached_channel = MaybeOwnedArc::new(channel.clone());
667            cache.temp_private_channels.insert(self, cached_channel);
668        }
669
670        Ok(channel)
671    }
672
673    /// Sends a message to a user through a direct message channel. This is a channel that can only
674    /// be accessed by you and the recipient.
675    ///
676    /// # Examples
677    ///
678    /// When a user sends a message with a content of `"~help"`, DM the author a help message
679    ///
680    /// ```rust,no_run
681    /// # use serenity::prelude::*;
682    /// # use serenity::model::prelude::*;
683    /// # struct Handler;
684    /// use serenity::builder::CreateMessage;
685    ///
686    /// #[serenity::async_trait]
687    /// # #[cfg(feature = "client")]
688    /// impl EventHandler for Handler {
689    ///     async fn message(&self, ctx: Context, msg: Message) {
690    ///         if msg.content == "~help" {
691    ///             let builder = CreateMessage::new().content("Helpful info here.");
692    ///
693    ///             if let Err(why) = msg.author.id.direct_message(&ctx, builder).await {
694    ///                 println!("Err sending help: {why:?}");
695    ///                 let _ = msg.reply(&ctx, "There was an error DMing you help.").await;
696    ///             };
697    ///         }
698    ///     }
699    /// }
700    /// ```
701    ///
702    /// # Errors
703    ///
704    /// Returns a [`ModelError::MessagingBot`] if the user being direct messaged is a bot user.
705    ///
706    /// May also return an [`Error::Http`] if the user cannot be sent a direct message.
707    ///
708    /// Returns an [`Error::Json`] if there is an error deserializing the API response.
709    pub async fn direct_message(
710        self,
711        cache_http: impl CacheHttp,
712        builder: CreateMessage,
713    ) -> Result<Message> {
714        self.create_dm_channel(&cache_http).await?.send_message(cache_http, builder).await
715    }
716
717    /// This is an alias of [`Self::direct_message`].
718    #[allow(clippy::missing_errors_doc)]
719    #[inline]
720    pub async fn dm(self, cache_http: impl CacheHttp, builder: CreateMessage) -> Result<Message> {
721        self.direct_message(cache_http, builder).await
722    }
723
724    /// Attempts to find a [`User`] by its Id in the cache.
725    #[cfg(feature = "cache")]
726    #[inline]
727    pub fn to_user_cached(self, cache: &impl AsRef<Cache>) -> Option<UserRef<'_>> {
728        cache.as_ref().user(self)
729    }
730
731    /// First attempts to find a [`User`] by its Id in the cache, upon failure requests it via the
732    /// REST API.
733    ///
734    /// **Note**: If the cache is not enabled, REST API will be used only.
735    ///
736    /// **Note**: If the cache is enabled, you might want to enable the `temp_cache` feature to
737    /// cache user data retrieved by this function for a short duration.
738    ///
739    /// # Errors
740    ///
741    /// May return an [`Error::Http`] if a [`User`] with that [`UserId`] does not exist, or
742    /// otherwise cannot be fetched.
743    ///
744    /// May also return an [`Error::Json`] if there is an error in deserializing the user.
745    #[inline]
746    pub async fn to_user(self, cache_http: impl CacheHttp) -> Result<User> {
747        #[cfg(feature = "cache")]
748        {
749            if let Some(cache) = cache_http.cache() {
750                if let Some(user) = cache.user(self) {
751                    return Ok(user.clone());
752                }
753            }
754        }
755
756        let user = cache_http.http().get_user(self).await?;
757
758        #[cfg(all(feature = "cache", feature = "temp_cache"))]
759        {
760            if let Some(cache) = cache_http.cache() {
761                use crate::cache::MaybeOwnedArc;
762
763                let cached_user = MaybeOwnedArc::new(user.clone());
764                cache.temp_users.insert(cached_user.id, cached_user);
765            }
766        }
767
768        Ok(user)
769    }
770}
771
772impl From<Member> for UserId {
773    /// Gets the Id of a [`Member`].
774    fn from(member: Member) -> UserId {
775        member.user.id
776    }
777}
778
779impl From<&Member> for UserId {
780    /// Gets the Id of a [`Member`].
781    fn from(member: &Member) -> UserId {
782        member.user.id
783    }
784}
785
786impl From<User> for UserId {
787    /// Gets the Id of a [`User`].
788    fn from(user: User) -> UserId {
789        user.id
790    }
791}
792
793impl From<&User> for UserId {
794    /// Gets the Id of a [`User`].
795    fn from(user: &User) -> UserId {
796        user.id
797    }
798}
799
800#[cfg(feature = "model")]
801fn default_avatar_url(user: &User) -> String {
802    let avatar_id = if let Some(discriminator) = user.discriminator {
803        discriminator.get() % 5 // Legacy username system
804    } else {
805        ((user.id.get() >> 22) % 6) as u16 // New username system
806    };
807
808    cdn!("/embed/avatars/{}.png", avatar_id)
809}
810
811#[cfg(feature = "model")]
812fn static_avatar_url(user_id: UserId, hash: Option<&ImageHash>) -> Option<String> {
813    hash.map(|hash| cdn!("/avatars/{}/{}.webp?size=1024", user_id, hash))
814}
815
816#[cfg(feature = "model")]
817fn banner_url(user_id: UserId, hash: Option<&ImageHash>) -> Option<String> {
818    hash.map(|hash| {
819        let ext = if hash.is_animated() { "gif" } else { "webp" };
820        cdn!("/banners/{}/{}.{}?size=1024", user_id, hash, ext)
821    })
822}
823
824#[cfg(feature = "model")]
825fn tag(name: &str, discriminator: Option<NonZeroU16>) -> String {
826    // 32: max length of username
827    // 1: `#`
828    // 4: max length of discriminator
829    let mut tag = String::with_capacity(37);
830    tag.push_str(name);
831    if let Some(discriminator) = discriminator {
832        tag.push('#');
833        write!(tag, "{discriminator:04}").unwrap();
834    }
835    tag
836}
837
838#[cfg(test)]
839mod test {
840    use std::num::NonZeroU16;
841
842    #[test]
843    fn test_discriminator_serde() {
844        use serde::{Deserialize, Serialize};
845
846        use super::discriminator;
847        use crate::json::{assert_json, json};
848
849        #[derive(Debug, PartialEq, Deserialize, Serialize)]
850        struct User {
851            #[serde(default, skip_serializing_if = "Option::is_none", with = "discriminator")]
852            discriminator: Option<NonZeroU16>,
853        }
854
855        let user = User {
856            discriminator: NonZeroU16::new(123),
857        };
858        assert_json(&user, json!({"discriminator": "0123"}));
859
860        let user_no_discriminator = User {
861            discriminator: None,
862        };
863        assert_json(&user_no_discriminator, json!({}));
864    }
865
866    #[cfg(feature = "model")]
867    mod model {
868        use std::num::NonZeroU16;
869        use std::str::FromStr;
870
871        use crate::model::id::UserId;
872        use crate::model::misc::ImageHash;
873        use crate::model::user::User;
874
875        #[test]
876        fn test_core() {
877            let mut user = User {
878                id: UserId::new(210),
879                avatar: Some(ImageHash::from_str("fb211703bcc04ee612c88d494df0272f").unwrap()),
880                discriminator: NonZeroU16::new(1432),
881                name: "test".to_string(),
882                ..Default::default()
883            };
884
885            let expected = "/avatars/210/fb211703bcc04ee612c88d494df0272f.webp?size=1024";
886            assert!(user.avatar_url().unwrap().ends_with(expected));
887            assert!(user.static_avatar_url().unwrap().ends_with(expected));
888
889            user.avatar = Some(ImageHash::from_str("a_fb211703bcc04ee612c88d494df0272f").unwrap());
890            let expected = "/avatars/210/a_fb211703bcc04ee612c88d494df0272f.gif?size=1024";
891            assert!(user.avatar_url().unwrap().ends_with(expected));
892            let expected = "/avatars/210/a_fb211703bcc04ee612c88d494df0272f.webp?size=1024";
893            assert!(user.static_avatar_url().unwrap().ends_with(expected));
894
895            user.avatar = None;
896            assert!(user.avatar_url().is_none());
897
898            assert_eq!(user.tag(), "test#1432");
899        }
900
901        #[test]
902        fn default_avatars() {
903            let mut user = User {
904                discriminator: None,
905                id: UserId::new(737323631117598811),
906                ..Default::default()
907            };
908
909            // New username system
910            assert!(user.default_avatar_url().ends_with("5.png"));
911
912            // Legacy username system
913            user.discriminator = NonZeroU16::new(1);
914            assert!(user.default_avatar_url().ends_with("1.png"));
915            user.discriminator = NonZeroU16::new(2);
916            assert!(user.default_avatar_url().ends_with("2.png"));
917            user.discriminator = NonZeroU16::new(3);
918            assert!(user.default_avatar_url().ends_with("3.png"));
919            user.discriminator = NonZeroU16::new(4);
920            assert!(user.default_avatar_url().ends_with("4.png"));
921        }
922    }
923}