discord_sdk/
user.rs

1//! Provides types and functionality around [Users](https://discord.com/developers/docs/game-sdk/users)
2
3pub mod events;
4
5use crate::Error;
6use serde::Deserialize;
7use std::{convert::TryFrom, fmt};
8
9pub type UserId = crate::types::Snowflake;
10
11/// The MD5 hash of a user's avatar
12#[derive(Clone)]
13pub struct Avatar(pub [u8; 16]);
14
15impl Avatar {
16    pub(crate) fn from_str(ava_str: &str) -> Option<Self> {
17        let avatar = ava_str.strip_prefix("a_").unwrap_or(ava_str);
18
19        if avatar.len() != 32 {
20            None
21        } else {
22            let mut md5 = [0u8; 16];
23
24            for (ind, exp) in avatar.as_bytes().chunks(2).enumerate() {
25                let mut cur = match exp[0] {
26                    b'A'..=b'F' => exp[0] - b'A' + 10,
27                    b'a'..=b'f' => exp[0] - b'a' + 10,
28                    b'0'..=b'9' => exp[0] - b'0',
29                    c => {
30                        tracing::debug!("invalid character '{}' found in avatar", c);
31                        return None;
32                    }
33                };
34
35                cur <<= 4;
36
37                cur |= match exp[1] {
38                    b'A'..=b'F' => exp[1] - b'A' + 10,
39                    b'a'..=b'f' => exp[1] - b'a' + 10,
40                    b'0'..=b'9' => exp[1] - b'0',
41                    c => {
42                        tracing::debug!("invalid character '{}' found in avatar", c);
43                        return None;
44                    }
45                };
46
47                md5[ind] = cur;
48            }
49
50            Some(Self(md5))
51        }
52    }
53}
54
55#[cfg(test)]
56impl serde::Serialize for Avatar {
57    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
58    where
59        S: serde::ser::Serializer,
60    {
61        use std::fmt::Write;
62        let mut hex_str = String::with_capacity(32);
63
64        for byte in self.0 {
65            let _ = write!(&mut hex_str, "{:02x}", byte);
66        }
67
68        serializer.serialize_str(&hex_str)
69    }
70}
71
72/// A Discord user.
73///
74/// [API docs](https://discord.com/developers/docs/game-sdk/users#data-models-user-struct)
75#[derive(Clone)]
76pub struct User {
77    /// The user's id
78    pub id: UserId,
79    /// The username
80    pub username: String,
81    /// The user's unique discriminator (ie. the #<number> after their name) to
82    /// disambiguate between users with the same username
83    pub discriminator: Option<u32>,
84    /// The MD5 hash of the user's avatar
85    pub avatar: Option<Avatar>,
86    /// Whether the user belongs to an `OAuth2` application
87    pub is_bot: bool,
88}
89
90impl<'de> Deserialize<'de> for User {
91    fn deserialize<D>(d: D) -> Result<Self, D::Error>
92    where
93        D: serde::de::Deserializer<'de>,
94    {
95        let u: DeUser<'de> = serde::de::Deserialize::deserialize(d)?;
96        Self::try_from(u).map_err(serde::de::Error::custom)
97    }
98}
99
100#[cfg(test)]
101impl serde::Serialize for User {
102    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
103    where
104        S: serde::ser::Serializer,
105    {
106        use serde::ser::SerializeStruct;
107
108        let mut state = serializer.serialize_struct("User", 5)?;
109        state.serialize_field("id", &self.id)?;
110        state.serialize_field("username", &self.username)?;
111        state.serialize_field(
112            "discriminator",
113            &self.discriminator.unwrap_or_default().to_string(),
114        )?;
115        state.serialize_field("avatar", &self.avatar)?;
116        state.serialize_field("bot", &self.is_bot)?;
117        state.end()
118    }
119}
120
121impl fmt::Debug for User {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        f.debug_struct("User")
124            .field("id", &self.id)
125            .field("username", &self.username)
126            .field("discriminator", &self.discriminator)
127            .finish()
128    }
129}
130
131/// Display the name of the user exactly as Discord does, eg `john.smith#1337`.
132impl fmt::Display for User {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        f.write_str(&self.username)?;
135
136        if let Some(disc) = self.discriminator {
137            write!(f, "#{}", disc)?;
138        }
139
140        Ok(())
141    }
142}
143
144/// Inner type purely used for deserialization because writing it manually is
145/// annoying.
146///
147/// [API docs](https://discord.com/developers/docs/game-sdk/activities#data-models-user-struct)
148#[derive(Deserialize)]
149struct DeUser<'u> {
150    /// The i64 unique id of the user, but serialized as a string for I guess
151    /// backwards compatiblity
152    id: Option<UserId>,
153    /// The user's username
154    username: Option<&'u str>,
155    /// A u32 discriminator (serialized as a string, again) to disambiguate
156    /// between users with the same username
157    discriminator: Option<&'u str>,
158    /// A hex-encoded MD5 hash of the user's avatar
159    avatar: Option<&'u str>,
160    /// Whether the user belongs to an `OAuth2` application
161    bot: Option<bool>,
162}
163
164impl<'de> TryFrom<DeUser<'de>> for User {
165    type Error = Error;
166
167    fn try_from(u: DeUser<'de>) -> Result<Self, Self::Error> {
168        let id = u.id.ok_or(Error::MissingField("id"))?;
169        let username = u
170            .username
171            .ok_or(Error::MissingField("username"))?
172            .to_owned();
173
174        // We _could_ ignore parse failures for this, but that might be confusing
175        let discriminator = match u.discriminator {
176            Some(d) => Some(
177                d.parse()
178                    .map_err(|_err| Error::InvalidField("discriminator"))?,
179            ),
180            None => None,
181        };
182        // We don't really do anything with this so it's allowed to fail
183        let avatar = match u.avatar {
184            Some(a) => Avatar::from_str(a),
185            None => None,
186        };
187
188        Ok(Self {
189            id,
190            username,
191            discriminator,
192            avatar,
193            is_bot: u.bot.unwrap_or(false),
194        })
195    }
196}