1use 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, user_banner_url};
28
29pub(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#[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 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#[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#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
228#[derive(Clone, Debug, Default, Deserialize, Serialize)]
229#[non_exhaustive]
230pub struct User {
231 pub id: UserId,
233 #[serde(rename = "username")]
237 pub name: String,
238 #[serde(default, skip_serializing_if = "Option::is_none", with = "discriminator")]
243 pub discriminator: Option<NonZeroU16>,
244 pub global_name: Option<String>,
247 pub avatar: Option<ImageHash>,
249 #[serde(default)]
251 pub bot: bool,
252 #[serde(default)]
254 pub system: bool,
255 #[serde(default)]
257 pub mfa_enabled: bool,
258 pub banner: Option<ImageHash>,
263 #[serde(rename = "accent_color")]
268 pub accent_colour: Option<Colour>,
269 pub locale: Option<String>,
271 pub verified: Option<bool>,
275 pub email: Option<String>,
279 #[serde(default)]
281 pub flags: UserPublicFlags,
282 #[serde(default)]
284 pub premium_type: PremiumType,
285 pub public_flags: Option<UserPublicFlags>,
287 pub member: Option<Box<PartialMember>>,
292 pub primary_guild: Option<PrimaryGuild>,
296 pub avatar_decoration_data: Option<AvatarDecorationData>,
298 pub collectibles: Option<Collectibles>,
301}
302
303enum_number! {
304 #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
309 #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
310 #[serde(from = "u8", into = "u8")]
311 #[non_exhaustive]
312 pub enum PremiumType {
313 #[default]
314 None = 0,
315 NitroClassic = 1,
316 Nitro = 2,
317 NitroBasic = 3,
318 _ => Unknown(u8),
319 }
320}
321
322bitflags! {
323 #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
327 #[derive(Copy, Clone, Default, Debug, Eq, Hash, PartialEq)]
328 pub struct UserPublicFlags: u32 {
329 const DISCORD_EMPLOYEE = 1 << 0;
331 const PARTNERED_SERVER_OWNER = 1 << 1;
333 const HYPESQUAD_EVENTS = 1 << 2;
335 const BUG_HUNTER_LEVEL_1 = 1 << 3;
337 const HOUSE_BRAVERY = 1 << 6;
339 const HOUSE_BRILLIANCE = 1 << 7;
341 const HOUSE_BALANCE = 1 << 8;
343 const EARLY_SUPPORTER = 1 << 9;
345 const TEAM_USER = 1 << 10;
347 const SYSTEM = 1 << 12;
349 const BUG_HUNTER_LEVEL_2 = 1 << 14;
351 const VERIFIED_BOT = 1 << 16;
353 const EARLY_VERIFIED_BOT_DEVELOPER = 1 << 17;
355 const DISCORD_CERTIFIED_MODERATOR = 1 << 18;
357 const BOT_HTTP_INTERACTIONS = 1 << 19;
359 #[cfg(feature = "unstable_discord_api")]
361 const SPAMMER = 1 << 20;
362 const ACTIVE_DEVELOPER = 1 << 22;
364 }
365}
366
367#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
371#[derive(Clone, Debug, Default, Deserialize, Serialize)]
372#[non_exhaustive]
373pub struct PrimaryGuild {
374 pub identity_guild_id: Option<GuildId>,
376 pub identity_enabled: Option<bool>,
379 pub tag: Option<String>,
381 pub badge: Option<ImageHash>,
383}
384
385#[cfg(feature = "model")]
386impl PrimaryGuild {
387 #[must_use]
388 pub fn badge_url(&self) -> Option<String> {
390 primary_guild_badge_url(self.identity_guild_id, self.badge.as_ref())
391 }
392}
393
394#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
395#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
396#[non_exhaustive]
397pub struct AvatarDecorationData {
401 pub asset: ImageHash,
403 pub sku_id: SkuId,
405}
406
407#[cfg(feature = "model")]
408impl AvatarDecorationData {
409 #[must_use]
410 pub fn decoration_url(&self) -> String {
412 avatar_decoration_url(&self.asset)
413 }
414}
415
416#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
420#[derive(Clone, Debug, Deserialize, Serialize)]
421#[non_exhaustive]
422pub struct Collectibles {
423 pub nameplate: Option<Nameplate>,
425}
426
427#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
431#[derive(Clone, Debug, Deserialize, Serialize)]
432#[non_exhaustive]
433pub struct Nameplate {
434 pub sku_id: SkuId,
436 pub asset: String,
438 pub label: String,
440 pub palette: String,
443}
444
445#[cfg(all(feature = "unstable_discord_api", feature = "model"))]
446impl Nameplate {
447 #[must_use]
449 pub fn static_url(&self) -> String {
450 static_nameplate_url(&self.asset)
451 }
452
453 #[must_use]
455 pub fn url(&self) -> String {
456 nameplate_url(&self.asset)
457 }
458}
459
460use std::hash::{Hash, Hasher};
461
462impl PartialEq for User {
463 fn eq(&self, other: &Self) -> bool {
464 self.id == other.id
465 }
466}
467
468impl Eq for User {}
469
470impl Hash for User {
471 fn hash<H: Hasher>(&self, hasher: &mut H) {
472 self.id.hash(hasher);
473 }
474}
475
476#[cfg(feature = "model")]
477impl User {
478 #[inline]
482 #[must_use]
483 pub fn avatar_url(&self) -> Option<String> {
484 avatar_url(None, self.id, self.avatar.as_ref())
485 }
486
487 #[inline]
491 #[must_use]
492 pub fn banner_url(&self) -> Option<String> {
493 user_banner_url(None, self.id, self.banner.as_ref())
494 }
495
496 #[inline]
505 pub async fn create_dm_channel(&self, cache_http: impl CacheHttp) -> Result<PrivateChannel> {
506 if self.bot {
507 return Err(Error::Model(ModelError::MessagingBot));
508 }
509
510 self.id.create_dm_channel(cache_http).await
511 }
512
513 #[inline]
515 #[must_use]
516 pub fn created_at(&self) -> Timestamp {
517 self.id.created_at()
518 }
519
520 #[inline]
524 #[must_use]
525 pub fn default_avatar_url(&self) -> String {
526 default_avatar_url(self)
527 }
528
529 pub async fn direct_message(
540 &self,
541 cache_http: impl CacheHttp,
542 builder: CreateMessage,
543 ) -> Result<Message> {
544 self.id.direct_message(cache_http, builder).await
545 }
546
547 #[inline]
553 #[must_use]
554 pub fn display_name(&self) -> &str {
555 self.global_name.as_deref().unwrap_or(&self.name)
556 }
557
558 #[allow(clippy::missing_errors_doc)]
560 #[inline]
561 pub async fn dm(&self, cache_http: impl CacheHttp, builder: CreateMessage) -> Result<Message> {
562 self.direct_message(cache_http, builder).await
563 }
564
565 #[must_use]
570 pub fn face(&self) -> String {
571 self.avatar_url().unwrap_or_else(|| self.default_avatar_url())
572 }
573
574 #[must_use]
580 pub fn static_face(&self) -> String {
581 self.static_avatar_url().unwrap_or_else(|| self.default_avatar_url())
582 }
583
584 #[inline]
605 pub async fn has_role(
606 &self,
607 cache_http: impl CacheHttp,
608 guild_id: impl Into<GuildId>,
609 role: impl Into<RoleId>,
610 ) -> Result<bool> {
611 guild_id.into().member(cache_http, self).await.map(|m| m.roles.contains(&role.into()))
612 }
613
614 #[inline]
622 pub async fn refresh(&mut self, cache_http: impl CacheHttp) -> Result<()> {
623 *self = self.id.to_user(cache_http).await?;
624
625 Ok(())
626 }
627
628 #[inline]
632 #[must_use]
633 pub fn static_avatar_url(&self) -> Option<String> {
634 static_avatar_url(self.id, self.avatar.as_ref())
635 }
636
637 #[inline]
662 #[must_use]
663 pub fn tag(&self) -> String {
664 tag(&self.name, self.discriminator)
665 }
666
667 #[inline]
671 pub async fn nick_in(
672 &self,
673 cache_http: impl CacheHttp,
674 guild_id: impl Into<GuildId>,
675 ) -> Option<String> {
676 let guild_id = guild_id.into();
677
678 #[cfg(feature = "cache")]
681 {
682 if let Some(cache) = cache_http.cache() {
683 if let Some(guild) = guild_id.to_guild_cached(cache) {
684 if let Some(member) = guild.members.get(&self.id) {
685 return member.nick.clone();
686 }
687 }
688 }
689 }
690
691 guild_id.member(cache_http, &self.id).await.ok().and_then(|member| member.nick)
693 }
694
695 #[cfg(feature = "collector")]
698 pub fn await_reply(&self, shard_messenger: impl AsRef<ShardMessenger>) -> MessageCollector {
699 MessageCollector::new(shard_messenger).author_id(self.id)
700 }
701
702 #[cfg(feature = "collector")]
704 pub fn await_replies(&self, shard_messenger: impl AsRef<ShardMessenger>) -> MessageCollector {
705 self.await_reply(shard_messenger)
706 }
707
708 #[cfg(feature = "collector")]
711 pub fn await_reaction(&self, shard_messenger: impl AsRef<ShardMessenger>) -> ReactionCollector {
712 ReactionCollector::new(shard_messenger).author_id(self.id)
713 }
714
715 #[cfg(feature = "collector")]
717 pub fn await_reactions(
718 &self,
719 shard_messenger: impl AsRef<ShardMessenger>,
720 ) -> ReactionCollector {
721 self.await_reaction(shard_messenger)
722 }
723}
724
725impl fmt::Display for User {
726 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
729 fmt::Display::fmt(&self.id.mention(), f)
730 }
731}
732
733#[cfg(feature = "model")]
734impl UserId {
735 pub async fn create_dm_channel(self, cache_http: impl CacheHttp) -> Result<PrivateChannel> {
748 #[cfg(feature = "temp_cache")]
749 if let Some(cache) = cache_http.cache() {
750 if let Some(private_channel) = cache.temp_private_channels.get(&self) {
751 return Ok(PrivateChannel::clone(&private_channel));
752 }
753 }
754
755 let map = json!({
756 "recipient_id": self,
757 });
758
759 let channel = cache_http.http().create_private_channel(&map).await?;
760
761 #[cfg(feature = "temp_cache")]
762 if let Some(cache) = cache_http.cache() {
763 use crate::cache::MaybeOwnedArc;
764
765 let cached_channel = MaybeOwnedArc::new(channel.clone());
766 cache.temp_private_channels.insert(self, cached_channel);
767 }
768
769 Ok(channel)
770 }
771
772 pub async fn direct_message(
809 self,
810 cache_http: impl CacheHttp,
811 builder: CreateMessage,
812 ) -> Result<Message> {
813 self.create_dm_channel(&cache_http).await?.send_message(cache_http, builder).await
814 }
815
816 #[allow(clippy::missing_errors_doc)]
818 #[inline]
819 pub async fn dm(self, cache_http: impl CacheHttp, builder: CreateMessage) -> Result<Message> {
820 self.direct_message(cache_http, builder).await
821 }
822
823 #[cfg(feature = "cache")]
825 #[inline]
826 pub fn to_user_cached(self, cache: &impl AsRef<Cache>) -> Option<UserRef<'_>> {
827 cache.as_ref().user(self)
828 }
829
830 #[inline]
845 pub async fn to_user(self, cache_http: impl CacheHttp) -> Result<User> {
846 #[cfg(feature = "cache")]
847 {
848 if let Some(cache) = cache_http.cache() {
849 if let Some(user) = cache.user(self) {
850 return Ok(user.clone());
851 }
852 }
853 }
854
855 let user = cache_http.http().get_user(self).await?;
856
857 #[cfg(all(feature = "cache", feature = "temp_cache"))]
858 {
859 if let Some(cache) = cache_http.cache() {
860 use crate::cache::MaybeOwnedArc;
861
862 let cached_user = MaybeOwnedArc::new(user.clone());
863 cache.temp_users.insert(cached_user.id, cached_user);
864 }
865 }
866
867 Ok(user)
868 }
869}
870
871impl From<Member> for UserId {
872 fn from(member: Member) -> UserId {
874 member.user.id
875 }
876}
877
878impl From<&Member> for UserId {
879 fn from(member: &Member) -> UserId {
881 member.user.id
882 }
883}
884
885impl From<User> for UserId {
886 fn from(user: User) -> UserId {
888 user.id
889 }
890}
891
892impl From<&User> for UserId {
893 fn from(user: &User) -> UserId {
895 user.id
896 }
897}
898
899#[cfg(feature = "model")]
900fn default_avatar_url(user: &User) -> String {
901 let avatar_id = if let Some(discriminator) = user.discriminator {
902 discriminator.get() % 5 } else {
904 ((user.id.get() >> 22) % 6) as u16 };
906
907 cdn!("/embed/avatars/{}.png", avatar_id)
908}
909
910#[cfg(feature = "model")]
911fn static_avatar_url(user_id: UserId, hash: Option<&ImageHash>) -> Option<String> {
912 hash.map(|hash| cdn!("/avatars/{}/{}.webp?size=1024", user_id, hash))
913}
914
915#[cfg(feature = "model")]
916fn tag(name: &str, discriminator: Option<NonZeroU16>) -> String {
917 let mut tag = String::with_capacity(37);
921 tag.push_str(name);
922 if let Some(discriminator) = discriminator {
923 tag.push('#');
924 write!(tag, "{discriminator:04}").expect("writing to a string should never fail");
925 }
926 tag
927}
928
929#[cfg(feature = "model")]
930fn primary_guild_badge_url(guild_id: Option<GuildId>, hash: Option<&ImageHash>) -> Option<String> {
931 if let Some(guild_id) = guild_id {
932 return hash.map(|hash| cdn!("/guild-tag-badges/{}/{}.png?size=1024", guild_id, hash));
933 }
934
935 None
936}
937
938#[cfg(feature = "model")]
939fn avatar_decoration_url(hash: &ImageHash) -> String {
940 cdn!("/avatar-decoration-presets/{}.png?size=1024", hash)
941}
942
943#[cfg(all(feature = "unstable_discord_api", feature = "model"))]
944fn nameplate_url(path: &str) -> String {
945 cdn!("https://cdn.discordapp.com/assets/collectibles/{}/asset.webm", path)
946}
947
948#[cfg(all(feature = "unstable_discord_api", feature = "model"))]
949#[cfg(feature = "model")]
950fn static_nameplate_url(path: &str) -> String {
951 cdn!("https://cdn.discordapp.com/assets/collectibles/{}/static.png", path)
952}
953
954#[cfg(test)]
955mod test {
956 use std::num::NonZeroU16;
957
958 #[test]
959 fn test_discriminator_serde() {
960 use serde::{Deserialize, Serialize};
961
962 use super::discriminator;
963 use crate::json::{assert_json, json};
964
965 #[derive(Debug, PartialEq, Deserialize, Serialize)]
966 struct User {
967 #[serde(default, skip_serializing_if = "Option::is_none", with = "discriminator")]
968 discriminator: Option<NonZeroU16>,
969 }
970
971 let user = User {
972 discriminator: NonZeroU16::new(123),
973 };
974 assert_json(&user, json!({"discriminator": "0123"}));
975
976 let user_no_discriminator = User {
977 discriminator: None,
978 };
979 assert_json(&user_no_discriminator, json!({}));
980 }
981
982 #[cfg(feature = "model")]
983 mod model {
984 use std::num::NonZeroU16;
985 use std::str::FromStr;
986
987 use crate::model::id::UserId;
988 use crate::model::misc::ImageHash;
989 use crate::model::user::User;
990
991 #[test]
992 fn test_core() {
993 let mut user = User {
994 id: UserId::new(210),
995 avatar: Some(ImageHash::from_str("fb211703bcc04ee612c88d494df0272f").unwrap()),
996 discriminator: NonZeroU16::new(1432),
997 name: "test".to_string(),
998 ..Default::default()
999 };
1000
1001 let expected = "/avatars/210/fb211703bcc04ee612c88d494df0272f.webp?size=1024";
1002 assert!(user.avatar_url().unwrap().ends_with(expected));
1003 assert!(user.static_avatar_url().unwrap().ends_with(expected));
1004
1005 user.avatar = Some(ImageHash::from_str("a_fb211703bcc04ee612c88d494df0272f").unwrap());
1006 let expected = "/avatars/210/a_fb211703bcc04ee612c88d494df0272f.gif?size=1024";
1007 assert!(user.avatar_url().unwrap().ends_with(expected));
1008 let expected = "/avatars/210/a_fb211703bcc04ee612c88d494df0272f.webp?size=1024";
1009 assert!(user.static_avatar_url().unwrap().ends_with(expected));
1010
1011 user.avatar = None;
1012 assert!(user.avatar_url().is_none());
1013
1014 assert_eq!(user.tag(), "test#1432");
1015 }
1016
1017 #[test]
1018 fn default_avatars() {
1019 let mut user = User {
1020 discriminator: None,
1021 id: UserId::new(737323631117598811),
1022 ..Default::default()
1023 };
1024
1025 assert!(user.default_avatar_url().ends_with("5.png"));
1027
1028 user.discriminator = NonZeroU16::new(1);
1030 assert!(user.default_avatar_url().ends_with("1.png"));
1031 user.discriminator = NonZeroU16::new(2);
1032 assert!(user.default_avatar_url().ends_with("2.png"));
1033 user.discriminator = NonZeroU16::new(3);
1034 assert!(user.default_avatar_url().ends_with("3.png"));
1035 user.discriminator = NonZeroU16::new(4);
1036 assert!(user.default_avatar_url().ends_with("4.png"));
1037 }
1038 }
1039}