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]