diff --git a/DisCatSharp/Entities/User/DiscordUser.cs b/DisCatSharp/Entities/User/DiscordUser.cs index 97d758645..1affd4727 100644 --- a/DisCatSharp/Entities/User/DiscordUser.cs +++ b/DisCatSharp/Entities/User/DiscordUser.cs @@ -1,718 +1,727 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading.Tasks; using DisCatSharp.Attributes; using DisCatSharp.Entities.OAuth2; using DisCatSharp.Enums; using DisCatSharp.Exceptions; using DisCatSharp.Net; using DisCatSharp.Net.Abstractions; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents a Discord user. /// public class DiscordUser : SnowflakeObject, IEquatable { /// /// Initializes a new instance of the class. /// internal DiscordUser() : base(["display_name", "linked_users", "banner_color"]) { } /// /// Initializes a new instance of the class. /// /// The transport user. internal DiscordUser(TransportUser transport) { this.Id = transport.Id; this.Username = transport.Username; this.Discriminator = transport.Discriminator; this.AvatarHash = transport.AvatarHash; this.AvatarDecorationData = transport.AvatarDecorationData; this.BannerHash = transport.BannerHash; this.BannerColorInternal = transport.BannerColor; this.ThemeColorsInternal = [.. transport.ThemeColors ?? []]; this.IsBot = transport.IsBot; this.MfaEnabled = transport.MfaEnabled; this.Verified = transport.Verified; this.Email = transport.Email; this.PremiumType = transport.PremiumType; this.Locale = transport.Locale; this.Flags = transport.Flags; this.OAuthFlags = transport.OAuthFlags; this.Bio = transport.Bio; this.Pronouns = transport.Pronouns; this.GlobalName = transport.GlobalName; } /// /// Gets this user's username. /// [JsonProperty("username", NullValueHandling = NullValueHandling.Ignore)] public virtual string Username { get; internal set; } /// /// Gets this user's username with the discriminator. /// Example: Discord#0000 /// [JsonIgnore, DiscordDeprecated("We will internally use the GlobalName if a user is already migrated. This will be removed in future. Consider switching to UsernameWithGlobalName then.")] public virtual string UsernameWithDiscriminator => this.IsMigrated ? this.UsernameWithGlobalName : $"{this.Username}#{this.Discriminator}"; /// /// Gets the username with the global name. /// Example: @lulalaby (Lala Sabathil) /// [JsonIgnore, DiscordInExperiment] public virtual string UsernameWithGlobalName => this.GlobalName != null ? $"{this.Username} ({this.GlobalName})" : this.Username; /// /// Gets this user's global name. /// Only applicable if is . /// [JsonProperty("global_name", NullValueHandling = NullValueHandling.Ignore), DiscordInExperiment] public virtual string? GlobalName { get; internal set; } /// /// Whether this user account is migrated to the new username system. /// Learn more at dis.gd/usernames. /// [JsonIgnore] public virtual bool IsMigrated => this.Discriminator == "0"; /// /// Gets the user's 4-digit discriminator. /// [JsonProperty("discriminator", NullValueHandling = NullValueHandling.Ignore), DiscordDeprecated("Users are being migrated currently. Bots still have discrims")] public virtual string Discriminator { get; internal set; } /// /// Gets the discriminator integer. /// [JsonIgnore, Deprecated("Users are being migrated currently. Bots still have discrims")] internal int DiscriminatorInt => int.Parse(this.Discriminator, NumberStyles.Integer, CultureInfo.InvariantCulture); /// /// Gets the user's banner color, if set. Mutually exclusive with . /// [JsonIgnore] public virtual DiscordColor? BannerColor => !this.BannerColorInternal.HasValue ? null : new DiscordColor(this.BannerColorInternal.Value); /// /// Gets the user's theme colors, if set. /// [JsonIgnore] public virtual IReadOnlyList? ThemeColors => !(this.ThemeColorsInternal is not null && this.ThemeColorsInternal.Count != 0) ? null : this.ThemeColorsInternal.Select(x => new DiscordColor(x)).ToList(); /// /// Gets the user's banner color integer. /// [JsonProperty("accent_color")] internal int? BannerColorInternal; /// /// Gets the user's theme color integers. /// [JsonProperty("theme_colors", NullValueHandling = NullValueHandling.Ignore)] internal List? ThemeColorsInternal; /// /// Gets the user's banner url /// [JsonIgnore] public string? BannerUrl => string.IsNullOrWhiteSpace(this.BannerHash) ? null : $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.BANNERS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.BannerHash}.{(this.BannerHash.StartsWith("a_", StringComparison.Ordinal) ? "gif" : "png")}?size=4096"; /// /// Gets the user's profile banner hash. Mutually exclusive with . /// [JsonProperty("banner", NullValueHandling = NullValueHandling.Ignore)] public virtual string BannerHash { get; internal set; } /// /// Gets the users bio. /// This is not available to bots tho. /// [JsonProperty("bio", NullValueHandling = NullValueHandling.Ignore)] public virtual string Bio { get; internal set; } /// /// Gets the user's avatar hash. /// [JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)] public virtual string AvatarHash { get; internal set; } /// /// Gets the user's avatar decoration data. /// [JsonProperty("avatar_decoration_data", NullValueHandling = NullValueHandling.Ignore)] public virtual AvatarDecorationData AvatarDecorationData { get; internal set; } /// /// Returns a uri to this users profile. /// [JsonIgnore] public Uri ProfileUri => new($"{DiscordDomain.GetDomain(CoreDomain.Discord).Url}{Endpoints.USERS}/{this.Id}"); /// /// Returns a string representing the direct URL to this users profile. /// /// The URL of this users profile. [JsonIgnore] public string ProfileUrl => this.ProfileUri.AbsoluteUri; /// /// Gets the user's avatar url. /// [JsonIgnore] public string AvatarUrl => string.IsNullOrWhiteSpace(this.AvatarHash) ? this.DefaultAvatarUrl : $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.AVATARS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.AvatarHash}.{(this.AvatarHash.StartsWith("a_", StringComparison.Ordinal) ? "gif" : "png")}?size=1024"; /// /// Gets the user's avatar decoration url. /// [JsonIgnore] public string? AvatarDecorationUrl => this.AvatarDecorationData?.AssetUrl; /// /// Gets the URL of default avatar for this user. /// [JsonIgnore] public string DefaultAvatarUrl => $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.EMBED}{Endpoints.AVATARS}/{(this.IsMigrated ? (this.Id >> 22) % 6 : Convert.ToUInt64(this.DiscriminatorInt) % 5).ToString(CultureInfo.InvariantCulture)}.png?size=1024"; /// /// Gets whether the user is a bot. /// [JsonProperty("bot", NullValueHandling = NullValueHandling.Ignore)] public virtual bool IsBot { get; internal set; } /// /// Gets whether the user has multi-factor authentication enabled. /// [JsonProperty("mfa_enabled", NullValueHandling = NullValueHandling.Ignore)] public virtual bool? MfaEnabled { get; internal set; } /// /// Gets whether the user is an official Discord system user. /// [JsonProperty("system", NullValueHandling = NullValueHandling.Ignore)] public virtual bool? IsSystem { get; internal set; } /// /// Gets whether the user is verified. /// This is only present in OAuth. /// [JsonProperty("verified", NullValueHandling = NullValueHandling.Ignore)] public virtual bool? Verified { get; internal set; } /// /// Gets the user's email address. /// This is only present in OAuth. /// [JsonProperty("email", NullValueHandling = NullValueHandling.Ignore)] public virtual string? Email { get; internal set; } /// /// Gets the user's premium type. /// [JsonProperty("premium_type", NullValueHandling = NullValueHandling.Ignore)] public virtual PremiumType? PremiumType { get; internal set; } /// /// Gets the user's chosen language /// [JsonProperty("locale", NullValueHandling = NullValueHandling.Ignore)] public virtual string Locale { get; internal set; } /// /// Gets the user's flags for OAuth. /// [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] public virtual UserFlags? OAuthFlags { get; internal set; } /// /// Gets the user's flags. /// [JsonProperty("public_flags", NullValueHandling = NullValueHandling.Ignore)] public virtual UserFlags? Flags { get; internal set; } /// /// Gets the user's pronouns. /// [JsonProperty("pronouns", NullValueHandling = NullValueHandling.Ignore)] public virtual string? Pronouns { get; internal set; } /// /// Gets the user's mention string. /// [JsonIgnore] public string Mention => this.Mention(this is DiscordMember); /// /// Gets whether this user is the Client which created this object. /// [JsonIgnore] public bool IsCurrent => this.Id == this.Discord.CurrentUser.Id; /// /// Gets the user's access token. /// Can be used in combination with . /// You can generate a token object from json with , if needed. /// As alternative you can construct the object via new DiscordAccessToken(string accessToken, string tokenType, int expiresIn, string refreshToken, string scope). /// [JsonIgnore] - public DiscordAccessToken AccessToken { get; set; } + public DiscordAccessToken? AccessToken { get; set; } #region Extension of DiscordUser /// /// Whether this member is a /// /// [JsonIgnore] public bool IsMod => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.CertifiedModerator); /// /// Whether this member is a /// /// [JsonIgnore] public bool IsPartner => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.Partner); /// /// Whether this member is a /// /// [JsonIgnore] public bool IsVerifiedBot => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.VerifiedBot); /// /// Whether this member is a /// /// [JsonIgnore] public bool IsBotDev => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.VerifiedDeveloper); /// /// Whether this member is a /// /// [JsonIgnore] public bool IsActiveDeveloper => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.ActiveDeveloper); /// /// Whether this member is a /// /// [JsonIgnore] public bool IsStaff => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.Staff); #endregion #region OAuth2 Methods /// /// Gets the current user's connections. /// Requires a set in . /// /// The oauth2 client. + /// Thrown when is not present. public async Task> OAuth2GetConnectionsAsync(DiscordOAuth2Client oauth2Client) - => await oauth2Client.GetCurrentUserConnectionsAsync(this.AccessToken); + => this.AccessToken is not null ? await oauth2Client.GetCurrentUserConnectionsAsync(this.AccessToken) : throw new NullReferenceException("You need to specify the AccessToken on this DiscordUser entity."); /// /// Gets the current user's guilds. /// Requires a set in . /// /// The oauth2 client. + /// Thrown when is not present. public async Task> OAuth2GetGuildAsync(DiscordOAuth2Client oauth2Client) - => await oauth2Client.GetCurrentUserGuildsAsync(this.AccessToken); + => this.AccessToken is not null ? await oauth2Client.GetCurrentUserGuildsAsync(this.AccessToken) : throw new NullReferenceException("You need to specify the AccessToken on this DiscordUser entity."); /// /// Gets the current user's member object for given . /// Requires a set in . /// /// The oauth2 client. /// The guild to get the member object for. + /// Thrown when is not present. public async Task OAuth2GetGuildMemberAsync(DiscordOAuth2Client oauth2Client, DiscordGuild guild) - => await oauth2Client.GetCurrentUserGuildMemberAsync(this.AccessToken, guild.Id); + => this.AccessToken is not null ? await oauth2Client.GetCurrentUserGuildMemberAsync(this.AccessToken, guild.Id) : throw new NullReferenceException("You need to specify the AccessToken on this DiscordUser entity."); /// /// Gets the current user's member object for given . /// Requires a set in . /// /// The oauth2 client. /// The guild id to get the member object for. + /// Thrown when is not present. public async Task OAuth2GetGuildMemberAsync(DiscordOAuth2Client oauth2Client, ulong guildId) - => await oauth2Client.GetCurrentUserGuildMemberAsync(this.AccessToken, guildId); + => this.AccessToken is not null ? await oauth2Client.GetCurrentUserGuildMemberAsync(this.AccessToken, guildId) : throw new NullReferenceException("You need to specify the AccessToken on this DiscordUser entity."); /// /// Adds the user to the given . /// Some parameters might need additional permissions for the bot on the target guild. See https://discord.com/developers/docs/resources/guild#add-guild-member for details. /// /// The oauth2 client. /// The guild id to add the member to. /// The new nickname. /// The new roles. /// Whether this user has to be muted. /// Whether this user has to be deafened. + /// Thrown when is not present. public async Task OAuth2AddToGuildAsync(DiscordOAuth2Client oauth2Client, ulong guildId, string? nickname = null, IEnumerable? roles = null, bool? muted = null, bool? deafened = null) - => await oauth2Client.AddCurrentUserToGuildAsync(this.AccessToken, this.Id, guildId, nickname, roles, muted, deafened); + => this.AccessToken is not null ? await oauth2Client.AddCurrentUserToGuildAsync(this.AccessToken, this.Id, guildId, nickname, roles, muted, deafened) : throw new NullReferenceException("You need to specify the AccessToken on this DiscordUser entity."); /// /// Adds the user to the given . /// Some parameters might need additional permissions for the bot on the target guild. See https://discord.com/developers/docs/resources/guild#add-guild-member for details. /// /// The oauth2 client. /// The guild to add the member to. /// The new nickname. /// The new roles. /// Whether this user has to be muted. /// Whether this user has to be deafened. + /// Thrown when is not present. public async Task OAuth2AddToGuildAsync(DiscordOAuth2Client oauth2Client, DiscordGuild guild, string? nickname = null, IEnumerable? roles = null, bool? muted = null, bool? deafened = null) - => await oauth2Client.AddCurrentUserToGuildAsync(this.AccessToken, this.Id, guild.Id, nickname, roles, muted, deafened); + => this.AccessToken is not null ? await oauth2Client.AddCurrentUserToGuildAsync(this.AccessToken, this.Id, guild.Id, nickname, roles, muted, deafened) : throw new NullReferenceException("You need to specify the AccessToken on this DiscordUser entity."); /// /// Gets the current user's oauth2 object. /// Requires a set in . /// /// The oauth2 client. + /// Thrown when is not present. public async Task OAuth2GetAsync(DiscordOAuth2Client oauth2Client) - => await oauth2Client.GetCurrentUserAsync(this.AccessToken); + => this.AccessToken is not null ? await oauth2Client.GetCurrentUserAsync(this.AccessToken) : throw new NullReferenceException("You need to specify the AccessToken on this DiscordUser entity."); /// /// Gets the current user's authorization info. /// Requires a set in . /// /// The oauth2 client. + /// Thrown when is not present. public async Task OAuth2GetAuthorizationInfoAsync(DiscordOAuth2Client oauth2Client) - => await oauth2Client.GetCurrentAuthorizationInformationAsync(this.AccessToken); + => this.AccessToken is not null ? await oauth2Client.GetCurrentAuthorizationInformationAsync(this.AccessToken) : throw new NullReferenceException("You need to specify the AccessToken on this DiscordUser entity."); /// /// Gets the current user's application role connection. /// Requires a set in . /// /// The oauth2 client. + /// Thrown when is not present. public async Task OAuth2GetApplicationRoleConnectionAsync(DiscordOAuth2Client oauth2Client) - => await oauth2Client.GetCurrentUserApplicationRoleConnectionAsync(this.AccessToken); + => this.AccessToken is not null ? await oauth2Client.GetCurrentUserApplicationRoleConnectionAsync(this.AccessToken) : throw new NullReferenceException("You need to specify the AccessToken on this DiscordUser entity."); #endregion /// /// Fetches the user from the API. /// /// The user with fresh data from the API. public async Task GetFromApiAsync() => await this.Discord.ApiClient.GetUserAsync(this.Id).ConfigureAwait(false); /// /// Gets additional information about an application if the user is an bot. /// /// The rpc info or /// Thrown when the application does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task GetRpcInfoAsync() => this.IsBot ? await this.Discord.ApiClient.GetApplicationRpcInfoAsync(this.Id).ConfigureAwait(false) : await Task.FromResult(null).ConfigureAwait(false); /// /// Whether this user is in a /// /// /// /// DiscordGuild guild = await Client.GetGuildAsync(806675511555915806); /// DiscordUser user = await Client.GetUserAsync(469957180968271873); /// Console.WriteLine($"{user.Username} {(user.IsInGuild(guild) ? "is a" : "is not a")} member of {guild.Name}"); /// /// results to J_M_Lutra is a member of Project Nyaw~. /// /// /// public async Task IsInGuild(DiscordGuild guild) { try { var member = await guild.GetMemberAsync(this.Id).ConfigureAwait(false); return member is not null; } catch (NotFoundException) { return false; } } /// /// Whether this user is not in a /// /// /// public async Task IsNotInGuild(DiscordGuild guild) => !await this.IsInGuild(guild).ConfigureAwait(false); /// /// Returns the DiscordMember in the specified /// /// The to get this user on. /// The . /// Thrown when the user is not part of the guild. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task ConvertToMember(DiscordGuild guild) => await guild.GetMemberAsync(this.Id).ConfigureAwait(false); /// /// Unbans this user from a guild. /// /// Guild to unban this user from. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the user does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task UnbanAsync(DiscordGuild guild, string? reason = null) => guild.UnbanMemberAsync(this, reason); /// /// Gets this user's presence. /// [JsonIgnore] public DiscordPresence? Presence => this.Discord is DiscordClient dc && dc.Presences.TryGetValue(this.Id, out var presence) ? presence : null; /// /// Gets the user's avatar URL, in requested format and size. /// /// Format of the avatar to get. /// Maximum size of the avatar. Must be a power of two, minimum 16, maximum 2048. /// URL of the user's avatar. public string GetAvatarUrl(ImageFormat fmt, ushort size = 1024) { if (fmt is ImageFormat.Unknown) throw new ArgumentException("You must specify valid image format.", nameof(fmt)); if (size is < 16 or > 2048) throw new ArgumentOutOfRangeException(nameof(size)); var log = Math.Log(size, 2); if (log < 4 || log > 11 || log % 1 is not 0) throw new ArgumentOutOfRangeException(nameof(size)); var sfmt = fmt switch { ImageFormat.Gif => "gif", ImageFormat.Jpeg => "jpg", ImageFormat.Png => "png", ImageFormat.WebP => "webp", ImageFormat.Auto => !string.IsNullOrWhiteSpace(this.AvatarHash) ? this.AvatarHash.StartsWith("a_", StringComparison.Ordinal) ? "gif" : "png" : "png", _ => throw new ArgumentOutOfRangeException(nameof(fmt)) }; var ssize = size.ToString(CultureInfo.InvariantCulture); if (!string.IsNullOrWhiteSpace(this.AvatarHash)) { var id = this.Id.ToString(CultureInfo.InvariantCulture); return $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.AVATARS}/{id}/{this.AvatarHash}.{sfmt}?size={ssize}"; } var type = (this.DiscriminatorInt % 5).ToString(CultureInfo.InvariantCulture); return $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.EMBED}{Endpoints.AVATARS}/{type}.{sfmt}?size={ssize}"; } /// /// Creates a direct message channel to this user. /// /// Direct message channel to this user. /// Thrown when the user has the bot blocked, the member shares no guild with the bot, or if the member has Allow DM from server members off. /// Thrown when the user does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task CreateDmChannelAsync() => this.Discord.ApiClient.CreateDmAsync(this.Id); /// /// Sends a direct message to this user. Creates a direct message channel if one does not exist already. /// /// Content of the message to send. /// The sent message. /// Thrown when the user has the bot blocked, the member shares no guild with the bot, or if the member has Allow DM from server members off. /// Thrown when the user does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task SendMessageAsync(string content) { if (this.IsBot && this.Discord.CurrentUser.IsBot) throw new ArgumentException("Bots cannot DM each other."); var chn = await this.CreateDmChannelAsync().ConfigureAwait(false); return await chn.SendMessageAsync(content).ConfigureAwait(false); } /// /// Sends a direct message to this user. Creates a direct message channel if one does not exist already. /// /// Embed to attach to the message. /// The sent message. /// Thrown when the user has the bot blocked, the member shares no guild with the bot, or if the member has Allow DM from server members off. /// Thrown when the user does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task SendMessageAsync(DiscordEmbed embed) { if (this.IsBot && this.Discord.CurrentUser.IsBot) throw new ArgumentException("Bots cannot DM each other."); var chn = await this.CreateDmChannelAsync().ConfigureAwait(false); return await chn.SendMessageAsync(embed).ConfigureAwait(false); } /// /// Sends a direct message to this user. Creates a direct message channel if one does not exist already. /// /// Content of the message to send. /// Embed to attach to the message. /// The sent message. /// Thrown when the user has the bot blocked, the member shares no guild with the bot, or if the member has Allow DM from server members off. /// Thrown when the user does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task SendMessageAsync(string content, DiscordEmbed embed) { if (this.IsBot && this.Discord.CurrentUser.IsBot) throw new ArgumentException("Bots cannot DM each other."); var chn = await this.CreateDmChannelAsync().ConfigureAwait(false); return await chn.SendMessageAsync(content, embed).ConfigureAwait(false); } /// /// Sends a direct message to this user. Creates a direct message channel if one does not exist already. /// /// Builder to with the message. /// The sent message. /// Thrown when the user has the bot blocked, the member shares no guild with the bot, or if the member has Allow DM from server members off. /// Thrown when the user does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task SendMessageAsync(DiscordMessageBuilder message) { if (this.IsBot && this.Discord.CurrentUser.IsBot) throw new ArgumentException("Bots cannot DM each other."); var chn = await this.CreateDmChannelAsync().ConfigureAwait(false); return await chn.SendMessageAsync(message).ConfigureAwait(false); } /// /// Returns a string representation of this user. /// /// String representation of this user. public override string ToString() => this.IsMigrated ? $"User {this.Id}; {this.UsernameWithGlobalName}" : $"User {this.Id}; {this.UsernameWithDiscriminator}"; /// /// Checks whether this is equal to another object. /// /// Object to compare to. /// Whether the object is equal to this . public override bool Equals(object obj) => this.Equals(obj as DiscordUser); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordUser e) => e is not null && (ReferenceEquals(this, e) || this.Id == e.Id); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() => this.Id.GetHashCode(); /// /// Gets whether the two objects are equal. /// /// First user to compare. /// Second user to compare. /// Whether the two users are equal. public static bool operator ==(DiscordUser? e1, DiscordUser? e2) { var o1 = e1 as object; var o2 = e2 as object; return (o1 is null && o2 is null) || (o1 is not null && o2 is not null && e1.Id == e2.Id); } /// /// Gets whether the two objects are not equal. /// /// First user to compare. /// Second user to compare. /// Whether the two users are not equal. public static bool operator !=(DiscordUser? e1, DiscordUser? e2) => !(e1 == e2); } /// /// Represents a user's avatar decoration data. /// public class AvatarDecorationData { [JsonProperty("asset", NullValueHandling = NullValueHandling.Ignore)] public string Asset { get; internal set; } /// /// Gets the user's avatar decoration url. /// [JsonIgnore] public string? AssetUrl => string.IsNullOrWhiteSpace(this.Asset) ? null : $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.AVATARS_DECORATION_PRESETS}/{this.Asset}.png?size=1024"; [JsonProperty("sku_id", NullValueHandling = NullValueHandling.Ignore)] public ulong SkuId { get; internal set; } } /// /// Represents a user comparer. /// internal sealed class DiscordUserComparer : IEqualityComparer { /// /// Whether the users are equal. /// /// The first user /// The second user. public bool Equals(DiscordUser x, DiscordUser y) => x.Equals(y); /// /// Gets the hash code. /// /// The user. public int GetHashCode(DiscordUser obj) => obj.Id.GetHashCode(); }