use std::fmt;
#[cfg(feature = "model")]
use std::fmt::Write;
use std::num::NonZeroU16;
use std::ops::{Deref, DerefMut};
use serde::{Deserialize, Serialize};
use super::prelude::*;
#[cfg(feature = "model")]
use crate::builder::{Builder, CreateMessage, EditProfile};
#[cfg(all(feature = "cache", feature = "model"))]
use crate::cache::{Cache, UserRef};
#[cfg(feature = "collector")]
use crate::collector::{MessageCollector, ReactionCollector};
#[cfg(feature = "collector")]
use crate::gateway::ShardMessenger;
#[cfg(feature = "model")]
use crate::http::CacheHttp;
#[cfg(feature = "model")]
use crate::internal::prelude::*;
#[cfg(feature = "model")]
use crate::json::json;
#[cfg(feature = "model")]
use crate::model::utils::{avatar_url, user_banner_url};
pub(crate) mod discriminator {
use std::fmt;
use serde::de::{Error, Visitor};
struct DiscriminatorVisitor;
impl Visitor<'_> for DiscriminatorVisitor {
type Value = u16;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("string or integer discriminator")
}
fn visit_u64<E: Error>(self, value: u64) -> Result<Self::Value, E> {
u16::try_from(value).map_err(Error::custom)
}
fn visit_str<E: Error>(self, s: &str) -> Result<Self::Value, E> {
s.parse().map_err(Error::custom)
}
}
use std::num::NonZeroU16;
use serde::{Deserializer, Serializer};
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<NonZeroU16>, D::Error> {
deserializer.deserialize_option(OptionalDiscriminatorVisitor)
}
#[allow(clippy::trivially_copy_pass_by_ref, clippy::ref_option)]
pub fn serialize<S: Serializer>(
value: &Option<NonZeroU16>,
serializer: S,
) -> Result<S::Ok, S::Error> {
match value {
Some(value) => serializer.serialize_some(&format_args!("{value:04}")),
None => serializer.serialize_none(),
}
}
struct OptionalDiscriminatorVisitor;
impl<'de> Visitor<'de> for OptionalDiscriminatorVisitor {
type Value = Option<NonZeroU16>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("optional string or integer discriminator")
}
fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_unit<E: Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_some<D: Deserializer<'de>>(
self,
deserializer: D,
) -> Result<Self::Value, D::Error> {
deserializer.deserialize_any(DiscriminatorVisitor).map(NonZeroU16::new)
}
}
}
#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(transparent)]
pub struct CurrentUser(User);
impl Deref for CurrentUser {
type Target = User;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for CurrentUser {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl From<CurrentUser> for User {
fn from(user: CurrentUser) -> Self {
user.0
}
}
#[cfg(feature = "model")]
impl CurrentUser {
pub async fn edit(&mut self, cache_http: impl CacheHttp, builder: EditProfile) -> Result<()> {
*self = builder.execute(cache_http, ()).await?;
Ok(())
}
}
#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
#[derive(
Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize,
)]
#[non_exhaustive]
pub enum OnlineStatus {
#[serde(rename = "dnd")]
DoNotDisturb,
#[serde(rename = "idle")]
Idle,
#[serde(rename = "invisible")]
Invisible,
#[serde(rename = "offline")]
Offline,
#[serde(rename = "online")]
#[default]
Online,
}
impl OnlineStatus {
#[must_use]
pub fn name(&self) -> &str {
match *self {
OnlineStatus::DoNotDisturb => "dnd",
OnlineStatus::Idle => "idle",
OnlineStatus::Invisible => "invisible",
OnlineStatus::Offline => "offline",
OnlineStatus::Online => "online",
}
}
}
#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[non_exhaustive]
pub struct User {
pub id: UserId,
#[serde(rename = "username")]
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none", with = "discriminator")]
pub discriminator: Option<NonZeroU16>,
pub global_name: Option<String>,
pub avatar: Option<ImageHash>,
#[serde(default)]
pub bot: bool,
#[serde(default)]
pub system: bool,
#[serde(default)]
pub mfa_enabled: bool,
pub banner: Option<ImageHash>,
#[serde(rename = "accent_color")]
pub accent_colour: Option<Colour>,
pub locale: Option<String>,
pub verified: Option<bool>,
pub email: Option<String>,
#[serde(default)]
pub flags: UserPublicFlags,
#[serde(default)]
pub premium_type: PremiumType,
pub public_flags: Option<UserPublicFlags>,
pub member: Option<Box<PartialMember>>,
pub primary_guild: Option<PrimaryGuild>,
pub avatar_decoration_data: Option<AvatarDecorationData>,
pub collectibles: Option<Collectibles>,
}
enum_number! {
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
#[serde(from = "u8", into = "u8")]
#[non_exhaustive]
pub enum PremiumType {
#[default]
None = 0,
NitroClassic = 1,
Nitro = 2,
NitroBasic = 3,
_ => Unknown(u8),
}
}
bitflags! {
#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
#[derive(Copy, Clone, Default, Debug, Eq, Hash, PartialEq)]
pub struct UserPublicFlags: u32 {
const DISCORD_EMPLOYEE = 1 << 0;
const PARTNERED_SERVER_OWNER = 1 << 1;
const HYPESQUAD_EVENTS = 1 << 2;
const BUG_HUNTER_LEVEL_1 = 1 << 3;
const HOUSE_BRAVERY = 1 << 6;
const HOUSE_BRILLIANCE = 1 << 7;
const HOUSE_BALANCE = 1 << 8;
const EARLY_SUPPORTER = 1 << 9;
const TEAM_USER = 1 << 10;
const SYSTEM = 1 << 12;
const BUG_HUNTER_LEVEL_2 = 1 << 14;
const VERIFIED_BOT = 1 << 16;
const EARLY_VERIFIED_BOT_DEVELOPER = 1 << 17;
const DISCORD_CERTIFIED_MODERATOR = 1 << 18;
const BOT_HTTP_INTERACTIONS = 1 << 19;
#[cfg(feature = "unstable_discord_api")]
const SPAMMER = 1 << 20;
const ACTIVE_DEVELOPER = 1 << 22;
}
}
#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[non_exhaustive]
pub struct PrimaryGuild {
pub identity_guild_id: Option<GuildId>,
pub identity_enabled: Option<bool>,
pub tag: Option<String>,
pub badge: Option<ImageHash>,
}
#[cfg(feature = "model")]
impl PrimaryGuild {
#[must_use]
pub fn badge_url(&self) -> Option<String> {
primary_guild_badge_url(self.identity_guild_id, self.badge.as_ref())
}
}
#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct AvatarDecorationData {
pub asset: ImageHash,
pub sku_id: SkuId,
}
#[cfg(feature = "model")]
impl AvatarDecorationData {
#[must_use]
pub fn decoration_url(&self) -> String {
avatar_decoration_url(&self.asset)
}
}
#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct Collectibles {
pub nameplate: Option<Nameplate>,
}
#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct Nameplate {
pub sku_id: SkuId,
pub asset: String,
pub label: String,
pub palette: String,
}
#[cfg(all(feature = "unstable_discord_api", feature = "model"))]
impl Nameplate {
#[must_use]
pub fn static_url(&self) -> String {
static_nameplate_url(&self.asset)
}
#[must_use]
pub fn url(&self) -> String {
nameplate_url(&self.asset)
}
}
use std::hash::{Hash, Hasher};
impl PartialEq for User {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for User {}
impl Hash for User {
fn hash<H: Hasher>(&self, hasher: &mut H) {
self.id.hash(hasher);
}
}
#[cfg(feature = "model")]
impl User {
#[inline]
#[must_use]
pub fn avatar_url(&self) -> Option<String> {
avatar_url(None, self.id, self.avatar.as_ref())
}
#[inline]
#[must_use]
pub fn banner_url(&self) -> Option<String> {
user_banner_url(None, self.id, self.banner.as_ref())
}
#[inline]
pub async fn create_dm_channel(&self, cache_http: impl CacheHttp) -> Result<PrivateChannel> {
if self.bot {
return Err(Error::Model(ModelError::MessagingBot));
}
self.id.create_dm_channel(cache_http).await
}
#[inline]
#[must_use]
pub fn created_at(&self) -> Timestamp {
self.id.created_at()
}
#[inline]
#[must_use]
pub fn default_avatar_url(&self) -> String {
default_avatar_url(self)
}
pub async fn direct_message(
&self,
cache_http: impl CacheHttp,
builder: CreateMessage,
) -> Result<Message> {
self.id.direct_message(cache_http, builder).await
}
#[inline]
#[must_use]
pub fn display_name(&self) -> &str {
self.global_name.as_deref().unwrap_or(&self.name)
}
#[allow(clippy::missing_errors_doc)]
#[inline]
pub async fn dm(&self, cache_http: impl CacheHttp, builder: CreateMessage) -> Result<Message> {
self.direct_message(cache_http, builder).await
}
#[must_use]
pub fn face(&self) -> String {
self.avatar_url().unwrap_or_else(|| self.default_avatar_url())
}
#[must_use]
pub fn static_face(&self) -> String {
self.static_avatar_url().unwrap_or_else(|| self.default_avatar_url())
}
#[inline]
pub async fn has_role(
&self,
cache_http: impl CacheHttp,
guild_id: impl Into<GuildId>,
role: impl Into<RoleId>,
) -> Result<bool> {
guild_id.into().member(cache_http, self).await.map(|m| m.roles.contains(&role.into()))
}
#[inline]
pub async fn refresh(&mut self, cache_http: impl CacheHttp) -> Result<()> {
*self = self.id.to_user(cache_http).await?;
Ok(())
}
#[inline]
#[must_use]
pub fn static_avatar_url(&self) -> Option<String> {
static_avatar_url(self.id, self.avatar.as_ref())
}
#[inline]
#[must_use]
pub fn tag(&self) -> String {
tag(&self.name, self.discriminator)
}
#[inline]
pub async fn nick_in(
&self,
cache_http: impl CacheHttp,
guild_id: impl Into<GuildId>,
) -> Option<String> {
let guild_id = guild_id.into();
#[cfg(feature = "cache")]
{
if let Some(cache) = cache_http.cache() {
if let Some(guild) = guild_id.to_guild_cached(cache) {
if let Some(member) = guild.members.get(&self.id) {
return member.nick.clone();
}
}
}
}
guild_id.member(cache_http, &self.id).await.ok().and_then(|member| member.nick)
}
#[cfg(feature = "collector")]
pub fn await_reply(&self, shard_messenger: impl AsRef<ShardMessenger>) -> MessageCollector {
MessageCollector::new(shard_messenger).author_id(self.id)
}
#[cfg(feature = "collector")]
pub fn await_replies(&self, shard_messenger: impl AsRef<ShardMessenger>) -> MessageCollector {
self.await_reply(shard_messenger)
}
#[cfg(feature = "collector")]
pub fn await_reaction(&self, shard_messenger: impl AsRef<ShardMessenger>) -> ReactionCollector {
ReactionCollector::new(shard_messenger).author_id(self.id)
}
#[cfg(feature = "collector")]
pub fn await_reactions(
&self,
shard_messenger: impl AsRef<ShardMessenger>,
) -> ReactionCollector {
self.await_reaction(shard_messenger)
}
}
impl fmt::Display for User {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.id.mention(), f)
}
}
#[cfg(feature = "model")]
impl UserId {
pub async fn create_dm_channel(self, cache_http: impl CacheHttp) -> Result<PrivateChannel> {
#[cfg(feature = "temp_cache")]
if let Some(cache) = cache_http.cache() {
if let Some(private_channel) = cache.temp_private_channels.get(&self) {
return Ok(PrivateChannel::clone(&private_channel));
}
}
let map = json!({
"recipient_id": self,
});
let channel = cache_http.http().create_private_channel(&map).await?;
#[cfg(feature = "temp_cache")]
if let Some(cache) = cache_http.cache() {
use crate::cache::MaybeOwnedArc;
let cached_channel = MaybeOwnedArc::new(channel.clone());
cache.temp_private_channels.insert(self, cached_channel);
}
Ok(channel)
}
pub async fn direct_message(
self,
cache_http: impl CacheHttp,
builder: CreateMessage,
) -> Result<Message> {
self.create_dm_channel(&cache_http).await?.send_message(cache_http, builder).await
}
#[allow(clippy::missing_errors_doc)]
#[inline]
pub async fn dm(self, cache_http: impl CacheHttp, builder: CreateMessage) -> Result<Message> {
self.direct_message(cache_http, builder).await
}
#[cfg(feature = "cache")]
#[inline]
pub fn to_user_cached(self, cache: &impl AsRef<Cache>) -> Option<UserRef<'_>> {
cache.as_ref().user(self)
}
#[inline]
pub async fn to_user(self, cache_http: impl CacheHttp) -> Result<User> {
#[cfg(feature = "cache")]
{
if let Some(cache) = cache_http.cache() {
if let Some(user) = cache.user(self) {
return Ok(user.clone());
}
}
}
let user = cache_http.http().get_user(self).await?;
#[cfg(all(feature = "cache", feature = "temp_cache"))]
{
if let Some(cache) = cache_http.cache() {
use crate::cache::MaybeOwnedArc;
let cached_user = MaybeOwnedArc::new(user.clone());
cache.temp_users.insert(cached_user.id, cached_user);
}
}
Ok(user)
}
}
impl From<Member> for UserId {
fn from(member: Member) -> UserId {
member.user.id
}
}
impl From<&Member> for UserId {
fn from(member: &Member) -> UserId {
member.user.id
}
}
impl From<User> for UserId {
fn from(user: User) -> UserId {
user.id
}
}
impl From<&User> for UserId {
fn from(user: &User) -> UserId {
user.id
}
}
#[cfg(feature = "model")]
fn default_avatar_url(user: &User) -> String {
let avatar_id = if let Some(discriminator) = user.discriminator {
discriminator.get() % 5 } else {
((user.id.get() >> 22) % 6) as u16 };
cdn!("/embed/avatars/{}.png", avatar_id)
}
#[cfg(feature = "model")]
fn static_avatar_url(user_id: UserId, hash: Option<&ImageHash>) -> Option<String> {
hash.map(|hash| cdn!("/avatars/{}/{}.webp?size=1024", user_id, hash))
}
#[cfg(feature = "model")]
fn tag(name: &str, discriminator: Option<NonZeroU16>) -> String {
let mut tag = String::with_capacity(37);
tag.push_str(name);
if let Some(discriminator) = discriminator {
tag.push('#');
write!(tag, "{discriminator:04}").expect("writing to a string should never fail");
}
tag
}
#[cfg(feature = "model")]
fn primary_guild_badge_url(guild_id: Option<GuildId>, hash: Option<&ImageHash>) -> Option<String> {
if let Some(guild_id) = guild_id {
return hash.map(|hash| cdn!("/guild-tag-badges/{}/{}.png?size=1024", guild_id, hash));
}
None
}
#[cfg(feature = "model")]
fn avatar_decoration_url(hash: &ImageHash) -> String {
cdn!("/avatar-decoration-presets/{}.png?size=1024", hash)
}
#[cfg(all(feature = "unstable_discord_api", feature = "model"))]
fn nameplate_url(path: &str) -> String {
cdn!("https://cdn.discordapp.com/assets/collectibles/{}/asset.webm", path)
}
#[cfg(all(feature = "unstable_discord_api", feature = "model"))]
#[cfg(feature = "model")]
fn static_nameplate_url(path: &str) -> String {
cdn!("https://cdn.discordapp.com/assets/collectibles/{}/static.png", path)
}
#[cfg(test)]
mod test {
use std::num::NonZeroU16;
#[test]
fn test_discriminator_serde() {
use serde::{Deserialize, Serialize};
use super::discriminator;
use crate::json::{assert_json, json};
#[derive(Debug, PartialEq, Deserialize, Serialize)]
struct User {
#[serde(default, skip_serializing_if = "Option::is_none", with = "discriminator")]
discriminator: Option<NonZeroU16>,
}
let user = User {
discriminator: NonZeroU16::new(123),
};
assert_json(&user, json!({"discriminator": "0123"}));
let user_no_discriminator = User {
discriminator: None,
};
assert_json(&user_no_discriminator, json!({}));
}
#[cfg(feature = "model")]
mod model {
use std::num::NonZeroU16;
use std::str::FromStr;
use crate::model::id::UserId;
use crate::model::misc::ImageHash;
use crate::model::user::User;
#[test]
fn test_core() {
let mut user = User {
id: UserId::new(210),
avatar: Some(ImageHash::from_str("fb211703bcc04ee612c88d494df0272f").unwrap()),
discriminator: NonZeroU16::new(1432),
name: "test".to_string(),
..Default::default()
};
let expected = "/avatars/210/fb211703bcc04ee612c88d494df0272f.webp?size=1024";
assert!(user.avatar_url().unwrap().ends_with(expected));
assert!(user.static_avatar_url().unwrap().ends_with(expected));
user.avatar = Some(ImageHash::from_str("a_fb211703bcc04ee612c88d494df0272f").unwrap());
let expected = "/avatars/210/a_fb211703bcc04ee612c88d494df0272f.gif?size=1024";
assert!(user.avatar_url().unwrap().ends_with(expected));
let expected = "/avatars/210/a_fb211703bcc04ee612c88d494df0272f.webp?size=1024";
assert!(user.static_avatar_url().unwrap().ends_with(expected));
user.avatar = None;
assert!(user.avatar_url().is_none());
assert_eq!(user.tag(), "test#1432");
}
#[test]
fn default_avatars() {
let mut user = User {
discriminator: None,
id: UserId::new(737323631117598811),
..Default::default()
};
assert!(user.default_avatar_url().ends_with("5.png"));
user.discriminator = NonZeroU16::new(1);
assert!(user.default_avatar_url().ends_with("1.png"));
user.discriminator = NonZeroU16::new(2);
assert!(user.default_avatar_url().ends_with("2.png"));
user.discriminator = NonZeroU16::new(3);
assert!(user.default_avatar_url().ends_with("3.png"));
user.discriminator = NonZeroU16::new(4);
assert!(user.default_avatar_url().ends_with("4.png"));
}
}
}