* 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 emotespull/915/head
@@ -19,8 +19,8 @@ namespace Discord | |||||
=> splashId != null ? $"{DiscordConfig.CDNUrl}splashes/{guildId}/{splashId}.jpg" : null; | => splashId != null ? $"{DiscordConfig.CDNUrl}splashes/{guildId}/{splashId}.jpg" : null; | ||||
public static string GetChannelIconUrl(ulong channelId, string iconId) | public static string GetChannelIconUrl(ulong channelId, string iconId) | ||||
=> iconId != null ? $"{DiscordConfig.CDNUrl}channel-icons/{channelId}/{iconId}.jpg" : null; | => 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) | public static string GetRichAssetUrl(ulong appId, string assetId, ushort size, ImageFormat format) | ||||
{ | { | ||||
@@ -16,13 +16,18 @@ namespace Discord | |||||
/// The ID of this emote | /// The ID of this emote | ||||
/// </summary> | /// </summary> | ||||
public ulong Id { get; } | public ulong Id { get; } | ||||
/// <summary> | |||||
/// Is this emote animated? | |||||
/// </summary> | |||||
public bool Animated { get; } | |||||
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | 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; | Id = id; | ||||
Name = name; | Name = name; | ||||
Animated = animated; | |||||
} | } | ||||
public override bool Equals(object other) | public override bool Equals(object other) | ||||
@@ -59,17 +64,20 @@ namespace Discord | |||||
public static bool TryParse(string text, out Emote result) | public static bool TryParse(string text, out Emote result) | ||||
{ | { | ||||
result = null; | 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) | if (splitIndex == -1) | ||||
return false; | return false; | ||||
if (!ulong.TryParse(text.Substring(splitIndex + 1, text.Length - splitIndex - 2), NumberStyles.None, CultureInfo.InvariantCulture, out ulong id)) | if (!ulong.TryParse(text.Substring(splitIndex + 1, text.Length - splitIndex - 2), NumberStyles.None, CultureInfo.InvariantCulture, out ulong id)) | ||||
return false; | 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 true; | ||||
} | } | ||||
return false; | return false; | ||||
@@ -77,6 +85,6 @@ namespace Discord | |||||
} | } | ||||
private string DebuggerDisplay => $"{Name} ({Id})"; | private string DebuggerDisplay => $"{Name} ({Id})"; | ||||
public override string ToString() => $"<:{Name}:{Id}>"; | |||||
public override string ToString() => $"<{(Animated ? "a" : "")}:{Name}:{Id}>"; | |||||
} | } | ||||
} | } |
@@ -13,7 +13,7 @@ namespace Discord | |||||
public bool RequireColons { get; } | public bool RequireColons { get; } | ||||
public IReadOnlyList<ulong> RoleIds { get; } | public IReadOnlyList<ulong> RoleIds { get; } | ||||
internal GuildEmote(ulong id, string name, bool isManaged, bool requireColons, IReadOnlyList<ulong> roleIds) : base(id, name) | |||||
internal GuildEmote(ulong id, string name, bool animated, bool isManaged, bool requireColons, IReadOnlyList<ulong> roleIds) : base(id, name, animated) | |||||
{ | { | ||||
IsManaged = isManaged; | IsManaged = isManaged; | ||||
RequireColons = requireColons; | RequireColons = requireColons; | ||||
@@ -21,6 +21,6 @@ namespace Discord | |||||
} | } | ||||
private string DebuggerDisplay => $"{Name} ({Id})"; | private string DebuggerDisplay => $"{Name} ({Id})"; | ||||
public override string ToString() => $"<:{Name}:{Id}>"; | |||||
public override string ToString() => $"<{(Animated ? "a" : "")}:{Name}:{Id}>"; | |||||
} | } | ||||
} | } |
@@ -9,6 +9,8 @@ namespace Discord.API | |||||
public ulong? Id { get; set; } | public ulong? Id { get; set; } | ||||
[JsonProperty("name")] | [JsonProperty("name")] | ||||
public string Name { get; set; } | public string Name { get; set; } | ||||
[JsonProperty("animated")] | |||||
public bool? Animated { get; set; } | |||||
[JsonProperty("roles")] | [JsonProperty("roles")] | ||||
public ulong[] Roles { get; set; } | public ulong[] Roles { get; set; } | ||||
[JsonProperty("require_colons")] | [JsonProperty("require_colons")] | ||||
@@ -18,7 +18,7 @@ namespace Discord.Rest | |||||
{ | { | ||||
IEmote emote; | IEmote emote; | ||||
if (model.Emoji.Id.HasValue) | 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 | else | ||||
emote = new Emoji(model.Emoji.Name); | emote = new Emoji(model.Emoji.Name); | ||||
return new RestReaction(emote, model.Count, model.Me); | return new RestReaction(emote, model.Count, model.Me); | ||||
@@ -7,7 +7,7 @@ namespace Discord.Rest | |||||
{ | { | ||||
public static GuildEmote ToEntity(this API.Emoji model) | 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) | public static Embed ToEntity(this API.Embed model) | ||||
@@ -24,7 +24,7 @@ namespace Discord.WebSocket | |||||
{ | { | ||||
IEmote emote; | IEmote emote; | ||||
if (model.Emoji.Id.HasValue) | 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 | else | ||||
emote = new Emoji(model.Emoji.Name); | emote = new Emoji(model.Emoji.Name); | ||||
return new SocketReaction(channel, model.MessageId, message, model.UserId, user, emote); | return new SocketReaction(channel, model.MessageId, message, model.UserId, user, emote); | ||||
@@ -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<ArgumentException>(() => Emote.Parse("invalid")); | |||||
} | |||||
[Fact] | |||||
public void Test_Animated_Emote_Parse() | |||||
{ | |||||
Assert.True(Emote.TryParse("<a:typingstatus:394207658351263745>", 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("<x:typingstatus:394207658351263745>", out _)); | |||||
Assert.False(Emote.TryParse("<a:typingstatus>", out _)); | |||||
Assert.False(Emote.TryParse("<a:typingstatus:not_a_number>", out _)); | |||||
} | |||||
} | |||||
} |