diff --git a/DisCatSharp/Entities/Emoji/DiscordEmoji.cs b/DisCatSharp/Entities/Emoji/DiscordEmoji.cs index 37ab73e53..e0263773c 100644 --- a/DisCatSharp/Entities/Emoji/DiscordEmoji.cs +++ b/DisCatSharp/Entities/Emoji/DiscordEmoji.cs @@ -1,375 +1,375 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2023 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using DisCatSharp.Enums; using DisCatSharp.Net; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents a Discord emoji. /// public partial class DiscordEmoji : SnowflakeObject, IEquatable { /// /// Gets the name of this emoji. /// [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; internal set; } /// /// Gets IDs the roles this emoji is enabled for. /// [JsonIgnore] public IReadOnlyList Roles => this._rolesLazy.Value; [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] public List RolesInternal; private readonly Lazy> _rolesLazy; /// /// Gets whether this emoji requires colons to use. /// [JsonProperty("require_colons")] public bool RequiresColons { get; internal set; } /// /// Gets whether this emoji is managed by an integration. /// [JsonProperty("managed")] public bool IsManaged { get; internal set; } /// /// Gets whether this emoji is animated. /// [JsonProperty("animated")] public bool IsAnimated { get; internal set; } /// /// Gets whether the emoji is available for use. /// An emoji may not be available due to loss of server boost. /// [JsonProperty("available", NullValueHandling = NullValueHandling.Ignore)] public bool IsAvailable { get; internal set; } /// /// Gets the image URL of this emoji. /// [JsonIgnore] public string Url => this.Id == 0 ? throw new InvalidOperationException("Cannot get URL of unicode emojis.") : this.IsAnimated ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.EMOJIS}/{this.Id.ToString(CultureInfo.InvariantCulture)}.gif" : $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.EMOJIS}/{this.Id.ToString(CultureInfo.InvariantCulture)}.png"; /// /// Initializes a new instance of the class. /// internal DiscordEmoji() { this._rolesLazy = new Lazy>(() => new ReadOnlyCollection(this.RolesInternal)); } /// /// Gets emoji's name in non-Unicode format (eg. :thinking: instead of the Unicode representation of the emoji). /// public string GetDiscordName() { s_discordNameLookup.TryGetValue(this.Name, out var name); return name ?? $":{this.Name}:"; } /// /// Returns a string representation of this emoji. /// /// String representation of this emoji. public override string ToString() => this.Id != 0 ? this.IsAnimated ? $"" : $"<:{this.Name}:{this.Id.ToString(CultureInfo.InvariantCulture)}>" : this.Name; /// /// 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 DiscordEmoji); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordEmoji e) => e is not null && (ReferenceEquals(this, e) || (this.Id == e.Id && this.Name == e.Name)); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() { var hash = 13; hash = (hash * 7) + this.Id.GetHashCode(); hash = (hash * 7) + this.Name.GetHashCode(); return hash; } /// /// Gets the reactions string. /// internal string ToReactionString() => this.Id != 0 ? $"{this.Name}:{this.Id.ToString(CultureInfo.InvariantCulture)}" : this.Name; /// /// Gets whether the two objects are equal. /// /// First emoji to compare. /// Second emoji to compare. /// Whether the two emoji are equal. public static bool operator ==(DiscordEmoji e1, DiscordEmoji e2) { var o1 = e1 as object; var o2 = e2 as object; - return (o1 != null || o2 == null) && (o1 == null || o2 != null) && ((o1 == null && o2 == null) || e1.Id == e2.Id); + return (o1 != null || o2 == null) && (o1 == null || o2 != null) && ((o1 == null && o2 == null) || (e1.Id == e2.Id && e1.Name == e2.Name)); } /// /// Gets whether the two objects are not equal. /// /// First emoji to compare. /// Second emoji to compare. /// Whether the two emoji are not equal. public static bool operator !=(DiscordEmoji e1, DiscordEmoji e2) => !(e1 == e2); /// /// Implicitly converts this emoji to its string representation. /// /// Emoji to convert. public static implicit operator string(DiscordEmoji e1) => e1.ToString(); /// /// Checks whether specified unicode entity is a valid unicode emoji. /// /// Entity to check. /// Whether it's a valid emoji. public static bool IsValidUnicode(string unicodeEntity) => s_discordNameLookup.ContainsKey(unicodeEntity); /// /// Creates an emoji object from a unicode entity. /// /// to attach to the object. /// Unicode entity to create the object from. /// Create object. public static DiscordEmoji FromUnicode(BaseDiscordClient client, string unicodeEntity) => !IsValidUnicode(unicodeEntity) ? throw new ArgumentException("Specified unicode entity is not a valid unicode emoji.", nameof(unicodeEntity)) : new DiscordEmoji { Name = unicodeEntity, Discord = client }; /// /// Creates an emoji object from a unicode entity. /// /// Unicode entity to create the object from. /// Create object. public static DiscordEmoji FromUnicode(string unicodeEntity) => FromUnicode(null, unicodeEntity); /// /// Attempts to create an emoji object from a unicode entity. /// /// to attach to the object. /// Unicode entity to create the object from. /// Resulting object. /// Whether the operation was successful. public static bool TryFromUnicode(BaseDiscordClient client, string unicodeEntity, out DiscordEmoji emoji) { // this is a round-trip operation because of FE0F inconsistencies. // through this, the inconsistency is normalized. emoji = null; if (!s_discordNameLookup.TryGetValue(unicodeEntity, out var discordName)) return false; if (!s_unicodeEmojis.TryGetValue(discordName, out unicodeEntity)) return false; emoji = new DiscordEmoji { Name = unicodeEntity, Discord = client }; return true; } /// /// Attempts to create an emoji object from a unicode entity. /// /// Unicode entity to create the object from. /// Resulting object. /// Whether the operation was successful. public static bool TryFromUnicode(string unicodeEntity, out DiscordEmoji emoji) => TryFromUnicode(null, unicodeEntity, out emoji); /// /// Creates an emoji object from a guild emote. /// /// to attach to the object. /// Id of the emote. /// Create object. public static DiscordEmoji FromGuildEmote(BaseDiscordClient client, ulong id) { if (client == null) throw new ArgumentNullException(nameof(client), "Client cannot be null."); foreach (var guild in client.Guilds.Values) { if (guild.Emojis.TryGetValue(id, out var found)) return found; } throw new KeyNotFoundException("Given emote was not found."); } /// /// Attempts to create an emoji object from a guild emote. /// /// to attach to the object. /// Id of the emote. /// Resulting object. /// Whether the operation was successful. public static bool TryFromGuildEmote(BaseDiscordClient client, ulong id, out DiscordEmoji emoji) { if (client == null) throw new ArgumentNullException(nameof(client), "Client cannot be null."); foreach (var guild in client.Guilds.Values) { if (guild.Emojis.TryGetValue(id, out emoji)) return true; } emoji = null; return false; } /// /// Creates an emoji object from emote name that includes colons (eg. :thinking:). This method also supports /// skin tone variations (eg. :ok_hand::skin-tone-2:), standard emoticons (eg. :D), as well as guild emoji /// (still specified by :name:). /// /// to attach to the object. /// Name of the emote to find, including colons (eg. :thinking:). /// Should guild emojis be included in the search. /// Create object. public static DiscordEmoji FromName(BaseDiscordClient client, string name, bool includeGuilds = true) { if (client == null) throw new ArgumentNullException(nameof(client), "Client cannot be null."); if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name), "Name cannot be empty or null."); if (s_unicodeEmojis.TryGetValue(name, out var unicodeEntity)) return new DiscordEmoji { Discord = client, Name = unicodeEntity }; if (includeGuilds) { var allEmojis = client.Guilds.Values .SelectMany(xg => xg.Emojis.Values); // save cycles - don't order var ek = name.AsSpan().Slice(1, name.Length - 2); foreach (var emoji in allEmojis) if (emoji.Name.AsSpan().SequenceEqual(ek)) return emoji; } throw new ArgumentException("Invalid emoji name specified.", nameof(name)); } /// /// Attempts to create an emoji object from emote name that includes colons (eg. :thinking:). This method also /// supports skin tone variations (eg. :ok_hand::skin-tone-2:), standard emoticons (eg. :D), as well as guild /// emoji (still specified by :name:). /// /// to attach to the object. /// Name of the emote to find, including colons (eg. :thinking:). /// Resulting object. /// Whether the operation was successful. public static bool TryFromName(BaseDiscordClient client, string name, out DiscordEmoji emoji) => TryFromName(client, name, true, out emoji); /// /// Attempts to create an emoji object from emote name that includes colons (eg. :thinking:). This method also /// supports skin tone variations (eg. :ok_hand::skin-tone-2:), standard emoticons (eg. :D), as well as guild /// emoji (still specified by :name:). /// /// to attach to the object. /// Name of the emote to find, including colons (eg. :thinking:). /// Should guild emojis be included in the search. /// Resulting object. /// Whether the operation was successful. public static bool TryFromName(BaseDiscordClient client, string name, bool includeGuilds, out DiscordEmoji emoji) { if (client == null) throw new ArgumentNullException(nameof(client), "Client cannot be null."); if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name), "Name cannot be empty or null."); if (s_unicodeEmojis.TryGetValue(name, out var unicodeEntity)) { emoji = new DiscordEmoji { Discord = client, Name = unicodeEntity }; return true; } if (includeGuilds) { var allEmojis = client.Guilds.Values .SelectMany(xg => xg.Emojis.Values); // save cycles - don't order var ek = name.AsSpan().Slice(1, name.Length - 2); foreach (var xemoji in allEmojis) if (xemoji.Name.AsSpan().SequenceEqual(ek)) { emoji = xemoji; return true; } } emoji = null; return false; } }