From a19ff188e9f4ea7019966dd7411754230b739abc Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 23 Dec 2017 15:09:24 -0500 Subject: [PATCH] Added support for animated emoji (#913) * Added support for animated emoji This was such a useful feature Discord, I'm glad you added this instead of fixing bugs. * Fix bugs in emote parser * Added unit tests for emotes --- src/Discord.Net.Core/CDN.cs | 4 +- src/Discord.Net.Core/Entities/Emotes/Emote.cs | 22 +++++++--- .../Entities/Emotes/GuildEmote.cs | 4 +- src/Discord.Net.Rest/API/Common/Emoji.cs | 2 + .../Entities/Messages/RestReaction.cs | 2 +- .../Extensions/EntityExtensions.cs | 2 +- .../Entities/Messages/SocketReaction.cs | 2 +- test/Discord.Net.Tests/Tests.Emotes.cs | 44 +++++++++++++++++++ 8 files changed, 68 insertions(+), 14 deletions(-) create mode 100644 test/Discord.Net.Tests/Tests.Emotes.cs diff --git a/src/Discord.Net.Core/CDN.cs b/src/Discord.Net.Core/CDN.cs index 415c0c30d8..070b965ee2 100644 --- a/src/Discord.Net.Core/CDN.cs +++ b/src/Discord.Net.Core/CDN.cs @@ -19,8 +19,8 @@ public static string GetGuildSplashUrl(ulong guildId, string splashId) => splashId != null ? $"{DiscordConfig.CDNUrl}splashes/{guildId}/{splashId}.jpg" : null; public static string GetChannelIconUrl(ulong channelId, string iconId) => iconId != null ? $"{DiscordConfig.CDNUrl}channel-icons/{channelId}/{iconId}.jpg" : null; - public static string GetEmojiUrl(ulong emojiId) - => $"{DiscordConfig.CDNUrl}emojis/{emojiId}.png"; + public static string GetEmojiUrl(ulong emojiId, bool animated) + => $"{DiscordConfig.CDNUrl}emojis/{emojiId}.{(animated ? "gif" : "png")}"; public static string GetRichAssetUrl(ulong appId, string assetId, ushort size, ImageFormat format) { diff --git a/src/Discord.Net.Core/Entities/Emotes/Emote.cs b/src/Discord.Net.Core/Entities/Emotes/Emote.cs index f498c818e0..e3a228c832 100644 --- a/src/Discord.Net.Core/Entities/Emotes/Emote.cs +++ b/src/Discord.Net.Core/Entities/Emotes/Emote.cs @@ -16,13 +16,18 @@ public class Emote : IEmote, ISnowflakeEntity /// The ID of this emote /// public ulong Id { get; } + /// + /// Is this emote animated? + /// + public bool Animated { get; } public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); - public string Url => CDN.GetEmojiUrl(Id); + public string Url => CDN.GetEmojiUrl(Id, Animated); - internal Emote(ulong id, string name) + internal Emote(ulong id, string name, bool animated) { Id = id; Name = name; + Animated = animated; } public override bool Equals(object other) @@ -59,17 +64,20 @@ public static Emote Parse(string text) public static bool TryParse(string text, out Emote result) { result = null; - if (text.Length >= 4 && text[0] == '<' && text[1] == ':' && text[text.Length - 1] == '>') + if (text.Length >= 4 && text[0] == '<' && (text[1] == ':' || (text[1] == 'a' && text[2] == ':')) && text[text.Length - 1] == '>') { - int splitIndex = text.IndexOf(':', 2); + bool animated = text[1] == 'a'; + int startIndex = animated ? 3 : 2; + + int splitIndex = text.IndexOf(':', startIndex); if (splitIndex == -1) return false; if (!ulong.TryParse(text.Substring(splitIndex + 1, text.Length - splitIndex - 2), NumberStyles.None, CultureInfo.InvariantCulture, out ulong id)) return false; - string name = text.Substring(2, splitIndex - 2); - result = new Emote(id, name); + string name = text.Substring(startIndex, splitIndex - startIndex); + result = new Emote(id, name, animated); return true; } return false; @@ -77,6 +85,6 @@ public static bool TryParse(string text, out Emote result) } private string DebuggerDisplay => $"{Name} ({Id})"; - public override string ToString() => $"<:{Name}:{Id}>"; + public override string ToString() => $"<{(Animated ? "a" : "")}:{Name}:{Id}>"; } } diff --git a/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs b/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs index 8d776a4cd8..95b062bd23 100644 --- a/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs +++ b/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs @@ -13,7 +13,7 @@ public class GuildEmote : Emote public bool RequireColons { get; } public IReadOnlyList RoleIds { get; } - internal GuildEmote(ulong id, string name, bool isManaged, bool requireColons, IReadOnlyList roleIds) : base(id, name) + internal GuildEmote(ulong id, string name, bool animated, bool isManaged, bool requireColons, IReadOnlyList roleIds) : base(id, name, animated) { IsManaged = isManaged; RequireColons = requireColons; @@ -21,6 +21,6 @@ internal GuildEmote(ulong id, string name, bool isManaged, bool requireColons, I } private string DebuggerDisplay => $"{Name} ({Id})"; - public override string ToString() => $"<:{Name}:{Id}>"; + public override string ToString() => $"<{(Animated ? "a" : "")}:{Name}:{Id}>"; } } diff --git a/src/Discord.Net.Rest/API/Common/Emoji.cs b/src/Discord.Net.Rest/API/Common/Emoji.cs index bd9c4d466b..2bdfdcc36e 100644 --- a/src/Discord.Net.Rest/API/Common/Emoji.cs +++ b/src/Discord.Net.Rest/API/Common/Emoji.cs @@ -9,6 +9,8 @@ internal class Emoji public ulong? Id { get; set; } [JsonProperty("name")] public string Name { get; set; } + [JsonProperty("animated")] + public bool? Animated { get; set; } [JsonProperty("roles")] public ulong[] Roles { get; set; } [JsonProperty("require_colons")] diff --git a/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs b/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs index 05c8179353..6d3f72419d 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs @@ -18,7 +18,7 @@ internal static RestReaction Create(Model model) { IEmote emote; if (model.Emoji.Id.HasValue) - emote = new Emote(model.Emoji.Id.Value, model.Emoji.Name); + emote = new Emote(model.Emoji.Id.Value, model.Emoji.Name, model.Emoji.Animated.GetValueOrDefault()); else emote = new Emoji(model.Emoji.Name); return new RestReaction(emote, model.Count, model.Me); diff --git a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs index b88a5b5159..74b05dacd1 100644 --- a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs @@ -7,7 +7,7 @@ internal static class EntityExtensions { public static GuildEmote ToEntity(this API.Emoji model) { - return new GuildEmote(model.Id.Value, model.Name, model.Managed, model.RequireColons, ImmutableArray.Create(model.Roles)); + return new GuildEmote(model.Id.Value, model.Name, model.Animated.GetValueOrDefault(), model.Managed, model.RequireColons, ImmutableArray.Create(model.Roles)); } public static Embed ToEntity(this API.Embed model) diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs index 35bee9e686..e8fa17a35b 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs @@ -24,7 +24,7 @@ internal static SocketReaction Create(Model model, ISocketMessageChannel channel { IEmote emote; if (model.Emoji.Id.HasValue) - emote = new Emote(model.Emoji.Id.Value, model.Emoji.Name); + emote = new Emote(model.Emoji.Id.Value, model.Emoji.Name, model.Emoji.Animated.GetValueOrDefault()); else emote = new Emoji(model.Emoji.Name); return new SocketReaction(channel, model.MessageId, message, model.UserId, user, emote); diff --git a/test/Discord.Net.Tests/Tests.Emotes.cs b/test/Discord.Net.Tests/Tests.Emotes.cs new file mode 100644 index 0000000000..334975ce4d --- /dev/null +++ b/test/Discord.Net.Tests/Tests.Emotes.cs @@ -0,0 +1,44 @@ +using System; +using Xunit; + +namespace Discord +{ + public class EmoteTests + { + [Fact] + public void Test_Emote_Parse() + { + Assert.True(Emote.TryParse("<:typingstatus:394207658351263745>", out Emote emote)); + Assert.NotNull(emote); + Assert.Equal("typingstatus", emote.Name); + Assert.Equal(394207658351263745UL, emote.Id); + Assert.False(emote.Animated); + Assert.Equal(DateTimeOffset.FromUnixTimeMilliseconds(1514056829775), emote.CreatedAt); + Assert.EndsWith("png", emote.Url); + } + [Fact] + public void Test_Invalid_Emote_Parse() + { + Assert.False(Emote.TryParse("invalid", out _)); + Assert.False(Emote.TryParse("<:typingstatus:not_a_number>", out _)); + Assert.Throws(() => Emote.Parse("invalid")); + } + [Fact] + public void Test_Animated_Emote_Parse() + { + Assert.True(Emote.TryParse("", out Emote emote)); + Assert.NotNull(emote); + Assert.Equal("typingstatus", emote.Name); + Assert.Equal(394207658351263745UL, emote.Id); + Assert.True(emote.Animated); + Assert.Equal(DateTimeOffset.FromUnixTimeMilliseconds(1514056829775), emote.CreatedAt); + Assert.EndsWith("gif", emote.Url); + } + public void Test_Invalid_Amimated_Emote_Parse() + { + Assert.False(Emote.TryParse("", out _)); + Assert.False(Emote.TryParse("", out _)); + Assert.False(Emote.TryParse("", out _)); + } + } +}