diff --git a/src/Discord.Net/Discord.Net.csproj b/src/Discord.Net/Discord.Net.csproj index a06f73582..a3d3b7168 100644 --- a/src/Discord.Net/Discord.Net.csproj +++ b/src/Discord.Net/Discord.Net.csproj @@ -2,7 +2,7 @@ netstandard2.0 - 7.1 + 7.3 diff --git a/src/Discord.Net/Entities/Emotes/IEmote.cs b/src/Discord.Net/Entities/Emotes/IEmote.cs index 12b7e283c..c1475b22b 100644 --- a/src/Discord.Net/Entities/Emotes/IEmote.cs +++ b/src/Discord.Net/Entities/Emotes/IEmote.cs @@ -1,7 +1,19 @@ namespace Discord { - public interface IEmote : IMentionable // TODO: Is `Mention` the correct verbage here? + /// + /// An emote which may be used as a reaction or embedded in chat. + /// + /// This includes Unicode emoji as well as unattached guild emotes. + /// + public interface IEmote : ITaggable { + /// + /// The display-name of the emote. + /// + /// + /// For Unicode emoji, this is the raw value of the character, not its + /// Unicode display name. + /// string Name { get; } } } diff --git a/src/Discord.Net/Entities/Emotes/IGuildEmote.cs b/src/Discord.Net/Entities/Emotes/IGuildEmote.cs index 11dc9eec3..dd3e6bc31 100644 --- a/src/Discord.Net/Entities/Emotes/IGuildEmote.cs +++ b/src/Discord.Net/Entities/Emotes/IGuildEmote.cs @@ -3,13 +3,17 @@ using System.Threading.Tasks; namespace Discord { + /// + /// An emote attached to a guild. This differs from an in that it contains + /// information relevant to the source guild. + /// public interface IGuildEmote : IEmote, ISnowflakeEntity, IDeletable { /// /// Gets whether this emoji is managed by an integration. /// /// - /// A boolean that determines whether or not this emote is managed by a Twitch integration. + /// A boolean that determines whether or not this emote is managed by an external integration, such as Twitch. /// bool IsManaged { get; } /// diff --git a/src/Discord.Net/Entities/IMentionable.cs b/src/Discord.Net/Entities/IMentionable.cs deleted file mode 100644 index 184c83ef3..000000000 --- a/src/Discord.Net/Entities/IMentionable.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Discord -{ - public interface IMentionable - { - string Mention { get; } - } -} diff --git a/src/Discord.Net/Entities/ITaggable.cs b/src/Discord.Net/Entities/ITaggable.cs new file mode 100644 index 000000000..39d779d5b --- /dev/null +++ b/src/Discord.Net/Entities/ITaggable.cs @@ -0,0 +1,7 @@ +namespace Discord +{ + public interface ITaggable + { + string Tag { get; } + } +} diff --git a/src/Discord.Net/Entities/Roles/IRole.cs b/src/Discord.Net/Entities/Roles/IRole.cs index f58fc79ee..346ffed48 100644 --- a/src/Discord.Net/Entities/Roles/IRole.cs +++ b/src/Discord.Net/Entities/Roles/IRole.cs @@ -8,7 +8,7 @@ namespace Discord /// /// Represents a generic role object to be given to a guild user. /// - public interface IRole : ISnowflakeEntity, IDeletable, IMentionable, IComparable + public interface IRole : ISnowflakeEntity, IDeletable, ITaggable, IComparable { /// /// Gets the guild that owns this role. diff --git a/src/Discord.Net/Models/Emotes/AttachedGuildEmote.cs b/src/Discord.Net/Models/Emotes/AttachedGuildEmote.cs index fc106578b..6043832b3 100644 --- a/src/Discord.Net/Models/Emotes/AttachedGuildEmote.cs +++ b/src/Discord.Net/Models/Emotes/AttachedGuildEmote.cs @@ -22,6 +22,7 @@ namespace Discord Guild = guild; } + // IGuildEmote public bool IsManaged { get; set; } public bool RequireColons { get; set; } public IReadOnlyList Roles { get; set; } @@ -29,12 +30,14 @@ namespace Discord public string Name { get; set; } public IGuild Guild { get; set; } - // IMentionable - public string Mention => EmoteUtilities.FormatGuildEmote(Id, Name); + // ITaggable + public string Tag => EmoteUtilities.FormatGuildEmote(Id, Name); + // IDeleteable public Task DeleteAsync() => Discord.Rest.DeleteGuildEmojiAsync(Guild.Id, Id); + // IGuildEmote public Task ModifyAsync() // TODO { throw new System.NotImplementedException(); diff --git a/src/Discord.Net/Models/Emotes/Emoji.cs b/src/Discord.Net/Models/Emotes/Emoji.cs index 8261e780a..4a9562c8d 100644 --- a/src/Discord.Net/Models/Emotes/Emoji.cs +++ b/src/Discord.Net/Models/Emotes/Emoji.cs @@ -9,6 +9,6 @@ namespace Discord } public string Name { get; set; } - public string Mention => Name; + public string Tag => Name; } } diff --git a/src/Discord.Net/Models/Emotes/Emote.cs b/src/Discord.Net/Models/Emotes/Emote.cs index 61575408b..7310b3206 100644 --- a/src/Discord.Net/Models/Emotes/Emote.cs +++ b/src/Discord.Net/Models/Emotes/Emote.cs @@ -13,6 +13,6 @@ namespace Discord public ulong Id { get; set; } public string Name { get; set; } - public string Mention => EmoteUtilities.FormatGuildEmote(Id, Name); + public string Tag => EmoteUtilities.FormatGuildEmote(Id, Name); } } diff --git a/src/Discord.Net/Models/Emotes/EmoteBuilder.cs b/src/Discord.Net/Models/Emotes/EmoteBuilder.cs index 86a1ce38b..35a600341 100644 --- a/src/Discord.Net/Models/Emotes/EmoteBuilder.cs +++ b/src/Discord.Net/Models/Emotes/EmoteBuilder.cs @@ -2,12 +2,56 @@ using System; namespace Discord { + /// + /// Methods to create an . + /// public static class EmoteBuilder { - public static IEmote FromEmoji(string emoji) + /// + /// Creates an emote from a raw unicode emoji. + /// + /// The unicode character this emoji should be created from. + public static IEmote FromUnicodeEmoji(string emoji) => new Emoji(emoji); - public static IEmote FromMention(string mention) - => throw new NotImplementedException(); // TODO: emoteutil + + /// + /// Creates an emote from an escaped tag. + /// + /// The escaped mention tag for an emote. + /// Throws if the passed tag was of an invalid format. + public static IEmote FromTag(string tag) + { + if (EmoteUtilities.TryParseGuildEmote(tag.AsSpan(), out var result)) + { + var (id, name) = result; + return new Emote(id, name); + } + throw new ArgumentException("Passed emote tag was of an invalid format", nameof(tag)); + } + + /// + /// Creates an emote from an escaped tag. + /// + /// The escaped mention tag for an emote. + /// Returns true if the emote could be created; returns false if it was of an invalid format. + public static bool TryFromTag(string tag, out IEmote result) + { + if (EmoteUtilities.TryParseGuildEmote(tag.AsSpan(), out var r)) + { + var (id, name) = r; + result = new Emote(id, name); + return true; + } + + result = default; + return false; + } + + /// + /// Creates an emote from a raw snowflake and name. + /// + /// The snowflake ID of the guild emote. + /// The name of the guild emote. public static IEmote FromID(ulong id, string name) => new Emote(id, name); } diff --git a/src/Discord.Net/Models/Roles/Role.cs b/src/Discord.Net/Models/Roles/Role.cs index 4179c95bd..ea079fdac 100644 --- a/src/Discord.Net/Models/Roles/Role.cs +++ b/src/Discord.Net/Models/Roles/Role.cs @@ -31,7 +31,7 @@ namespace Discord public string Name { get; set; } public GuildPermissions Permissions { get; set; } public int Position { get; set; } - public string Mention => throw new NotImplementedException(); // TODO: MentionUtils + public string Tag => throw new NotImplementedException(); // TODO: MentionUtils public Task DeleteAsync() => Discord.Rest.DeleteGuildRoleAsync(Guild.Id, Id); diff --git a/src/Discord.Net/Utilities/EmoteUtilities.cs b/src/Discord.Net/Utilities/EmoteUtilities.cs index 20819dc69..b1c46ed89 100644 --- a/src/Discord.Net/Utilities/EmoteUtilities.cs +++ b/src/Discord.Net/Utilities/EmoteUtilities.cs @@ -7,21 +7,28 @@ namespace Discord public static string FormatGuildEmote(ulong id, string name) => $"<:{name}:{id}>"; - public static (ulong, string) ParseGuildEmote(string formatted) + // TODO: perf: bench whether this should be passed by ref (in) + public static bool TryParseGuildEmote(ReadOnlySpan formatted, out (ulong, string) result) { - if (formatted.IndexOf('<') != 0 || formatted.IndexOf(':') != 1 || formatted.IndexOf('>') != formatted.Length-1) - throw new ArgumentException("passed string does not match a guild emote format", nameof(formatted)); // TODO: grammar + result = default; - int closingIndex = formatted.IndexOf(':', 2); + if (formatted.IndexOf('<') != 0 || formatted.IndexOf(':') != 1 || formatted.IndexOf('>') != formatted.Length - 1) + return false; + + int closingIndex = formatted.LastIndexOf(':'); if (closingIndex < 0) - throw new ArgumentException("passed string does not match a guild emote format", nameof(formatted)); + return false; + + ReadOnlySpan name = formatted.Slice(2, closingIndex-2); + ReadOnlySpan idStr = formatted.Slice(closingIndex + 1, formatted.Length - (name.Length + 4)); + idStr = idStr.Slice(0, idStr.Length - 1); // ignore closing > + + if (!ulong.TryParse(idStr.ToString(), out ulong id)) + return false; - string name = formatted.Substring(2, closingIndex-2); - string idStr = formatted.Substring(closingIndex + 1); - idStr = idStr.Substring(0, idStr.Length - 1); // ignore closing > - ulong id = ulong.Parse(idStr); // TODO: TryParse here? + result = (id, name.ToString()); - return (id, name); + return true; } } } diff --git a/test/Discord.Tests.Unit/Utilities/EmoteTests.cs b/test/Discord.Tests.Unit/Utilities/EmoteTests.cs index 93dfb7e7b..d708c0798 100644 --- a/test/Discord.Tests.Unit/Utilities/EmoteTests.cs +++ b/test/Discord.Tests.Unit/Utilities/EmoteTests.cs @@ -1,4 +1,3 @@ -using System; using Xunit; namespace Discord.Tests.Unit @@ -9,9 +8,10 @@ namespace Discord.Tests.Unit public void Parse() { string input = "<:gopher:243902586946715658>"; - var (resultId, resultName) = EmoteUtilities.ParseGuildEmote(input); - Assert.Equal(243902586946715658UL, resultId); - Assert.Equal("gopher", resultName); + var success = EmoteUtilities.TryParseGuildEmote(input, out var result); + var (id, name) = result; + Assert.Equal(243902586946715658UL, id); + Assert.Equal("gopher", name); } [Theory] @@ -21,7 +21,8 @@ namespace Discord.Tests.Unit [InlineData("<:foo>")] public void Parse_Fail(string data) { - Assert.Throws(() => EmoteUtilities.ParseGuildEmote(data)); + var success = EmoteUtilities.TryParseGuildEmote(data, out _); + Assert.False(success); } [Fact]