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;
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}
293
294enum_number! {
295 #[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 #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
318 #[derive(Copy, Clone, Default, Debug, Eq, Hash, PartialEq)]
319 pub struct UserPublicFlags: u32 {
320 const DISCORD_EMPLOYEE = 1 << 0;
322 const PARTNERED_SERVER_OWNER = 1 << 1;
324 const HYPESQUAD_EVENTS = 1 << 2;
326 const BUG_HUNTER_LEVEL_1 = 1 << 3;
328 const HOUSE_BRAVERY = 1 << 6;
330 const HOUSE_BRILLIANCE = 1 << 7;
332 const HOUSE_BALANCE = 1 << 8;
334 const EARLY_SUPPORTER = 1 << 9;
336 const TEAM_USER = 1 << 10;
338 const SYSTEM = 1 << 12;
340 const BUG_HUNTER_LEVEL_2 = 1 << 14;
342 const VERIFIED_BOT = 1 << 16;
344 const EARLY_VERIFIED_BOT_DEVELOPER = 1 << 17;
346 const DISCORD_CERTIFIED_MODERATOR = 1 << 18;
348 const BOT_HTTP_INTERACTIONS = 1 << 19;
350 #[cfg(feature = "unstable_discord_api")]
352 const SPAMMER = 1 << 20;
353 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 #[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 #[inline]
392 #[must_use]
393 pub fn banner_url(&self) -> Option<String> {
394 banner_url(self.id, self.banner.as_ref())
395 }
396
397 #[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 #[inline]
416 #[must_use]
417 pub fn created_at(&self) -> Timestamp {
418 self.id.created_at()
419 }
420
421 #[inline]
425 #[must_use]
426 pub fn default_avatar_url(&self) -> String {
427 default_avatar_url(self)
428 }
429
430 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 #[inline]
454 #[must_use]
455 pub fn display_name(&self) -> &str {
456 self.global_name.as_deref().unwrap_or(&self.name)
457 }
458
459 #[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 #[must_use]
471 pub fn face(&self) -> String {
472 self.avatar_url().unwrap_or_else(|| self.default_avatar_url())
473 }
474
475 #[must_use]
481 pub fn static_face(&self) -> String {
482 self.static_avatar_url().unwrap_or_else(|| self.default_avatar_url())
483 }
484
485 #[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 #[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 #[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 #[inline]
563 #[must_use]
564 pub fn tag(&self) -> String {
565 tag(&self.name, self.discriminator)
566 }
567
568 #[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 #[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 guild_id.member(cache_http, &self.id).await.ok().and_then(|member| member.nick)
594 }
595
596 #[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 #[cfg(feature = "collector")]
605 pub fn await_replies(&self, shard_messenger: impl AsRef<ShardMessenger>) -> MessageCollector {
606 self.await_reply(shard_messenger)
607 }
608
609 #[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 #[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 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 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 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 #[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 #[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 #[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 fn from(member: Member) -> UserId {
775 member.user.id
776 }
777}
778
779impl From<&Member> for UserId {
780 fn from(member: &Member) -> UserId {
782 member.user.id
783 }
784}
785
786impl From<User> for UserId {
787 fn from(user: User) -> UserId {
789 user.id
790 }
791}
792
793impl From<&User> for UserId {
794 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 } else {
805 ((user.id.get() >> 22) % 6) as u16 };
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 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 assert!(user.default_avatar_url().ends_with("5.png"));
911
912 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}