discord-sdk 0.4.0

An open implementation of the Discord Game SDK
Documentation
//! Provides types and functionality around [Users](https://discord.com/developers/docs/game-sdk/users)

pub mod events;

use crate::Error;
use serde::Deserialize;
use std::{convert::TryFrom, fmt};

pub type UserId = crate::types::Snowflake;

/// The MD5 hash of a user's avatar
#[derive(Clone)]
pub struct Avatar(pub [u8; 16]);

impl Avatar {
    pub(crate) fn from_str(ava_str: &str) -> Option<Self> {
        let avatar = ava_str.strip_prefix("a_").unwrap_or(ava_str);

        if avatar.len() != 32 {
            None
        } else {
            let mut md5 = [0u8; 16];

            for (ind, exp) in avatar.as_bytes().chunks(2).enumerate() {
                let mut cur = match exp[0] {
                    b'A'..=b'F' => exp[0] - b'A' + 10,
                    b'a'..=b'f' => exp[0] - b'a' + 10,
                    b'0'..=b'9' => exp[0] - b'0',
                    c => {
                        tracing::debug!("invalid character '{}' found in avatar", c);
                        return None;
                    }
                };

                cur <<= 4;

                cur |= match exp[1] {
                    b'A'..=b'F' => exp[1] - b'A' + 10,
                    b'a'..=b'f' => exp[1] - b'a' + 10,
                    b'0'..=b'9' => exp[1] - b'0',
                    c => {
                        tracing::debug!("invalid character '{}' found in avatar", c);
                        return None;
                    }
                };

                md5[ind] = cur;
            }

            Some(Self(md5))
        }
    }
}

#[cfg(test)]
impl serde::Serialize for Avatar {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::ser::Serializer,
    {
        use std::fmt::Write;
        let mut hex_str = String::with_capacity(32);

        for byte in self.0 {
            let _ = write!(&mut hex_str, "{:02x}", byte);
        }

        serializer.serialize_str(&hex_str)
    }
}

/// A Discord user.
///
/// [API docs](https://discord.com/developers/docs/game-sdk/users#data-models-user-struct)
#[derive(Clone)]
pub struct User {
    /// The user's id
    pub id: UserId,
    /// The username
    pub username: String,
    /// The user's unique discriminator (ie. the #<number> after their name) to
    /// disambiguate between users with the same username
    pub discriminator: Option<u32>,
    /// The MD5 hash of the user's avatar
    pub avatar: Option<Avatar>,
    /// Whether the user belongs to an `OAuth2` application
    pub is_bot: bool,
}

impl<'de> Deserialize<'de> for User {
    fn deserialize<D>(d: D) -> Result<Self, D::Error>
    where
        D: serde::de::Deserializer<'de>,
    {
        let u: DeUser<'de> = serde::de::Deserialize::deserialize(d)?;
        Self::try_from(u).map_err(serde::de::Error::custom)
    }
}

#[cfg(test)]
impl serde::Serialize for User {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::ser::Serializer,
    {
        use serde::ser::SerializeStruct;

        let mut state = serializer.serialize_struct("User", 5)?;
        state.serialize_field("id", &self.id)?;
        state.serialize_field("username", &self.username)?;
        state.serialize_field(
            "discriminator",
            &self.discriminator.unwrap_or_default().to_string(),
        )?;
        state.serialize_field("avatar", &self.avatar)?;
        state.serialize_field("bot", &self.is_bot)?;
        state.end()
    }
}

impl fmt::Debug for User {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("User")
            .field("id", &self.id)
            .field("username", &self.username)
            .field("discriminator", &self.discriminator)
            .finish()
    }
}

/// Display the name of the user exactly as Discord does, eg `john.smith#1337`.
impl fmt::Display for User {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.username)?;

        if let Some(disc) = self.discriminator {
            write!(f, "#{}", disc)?;
        }

        Ok(())
    }
}

/// Inner type purely used for deserialization because writing it manually is
/// annoying.
///
/// [API docs](https://discord.com/developers/docs/game-sdk/activities#data-models-user-struct)
#[derive(Deserialize)]
struct DeUser<'u> {
    /// The i64 unique id of the user, but serialized as a string for I guess
    /// backwards compatiblity
    id: Option<UserId>,
    /// The user's username
    username: Option<&'u str>,
    /// A u32 discriminator (serialized as a string, again) to disambiguate
    /// between users with the same username
    discriminator: Option<&'u str>,
    /// A hex-encoded MD5 hash of the user's avatar
    avatar: Option<&'u str>,
    /// Whether the user belongs to an `OAuth2` application
    bot: Option<bool>,
}

impl<'de> TryFrom<DeUser<'de>> for User {
    type Error = Error;

    fn try_from(u: DeUser<'de>) -> Result<Self, Self::Error> {
        let id = u.id.ok_or(Error::MissingField("id"))?;
        let username = u
            .username
            .ok_or(Error::MissingField("username"))?
            .to_owned();

        // We _could_ ignore parse failures for this, but that might be confusing
        let discriminator = match u.discriminator {
            Some(d) => Some(
                d.parse()
                    .map_err(|_err| Error::InvalidField("discriminator"))?,
            ),
            None => None,
        };
        // We don't really do anything with this so it's allowed to fail
        let avatar = match u.avatar {
            Some(a) => Avatar::from_str(a),
            None => None,
        };

        Ok(Self {
            id,
            username,
            discriminator,
            avatar,
            is_bot: u.bot.unwrap_or(false),
        })
    }
}