@@ -2,7 +2,7 @@ | |||||
<PropertyGroup> | <PropertyGroup> | ||||
<TargetFramework>netstandard2.0</TargetFramework> | <TargetFramework>netstandard2.0</TargetFramework> | ||||
<LangVersion>7.1</LangVersion> | |||||
<LangVersion>7.3</LangVersion> | |||||
</PropertyGroup> | </PropertyGroup> | ||||
<ItemGroup> | <ItemGroup> | ||||
@@ -1,7 +1,19 @@ | |||||
namespace Discord | namespace Discord | ||||
{ | { | ||||
public interface IEmote : IMentionable // TODO: Is `Mention` the correct verbage here? | |||||
/// <summary> | |||||
/// An emote which may be used as a reaction or embedded in chat. | |||||
/// | |||||
/// This includes Unicode emoji as well as unattached guild emotes. | |||||
/// </summary> | |||||
public interface IEmote : ITaggable | |||||
{ | { | ||||
/// <summary> | |||||
/// The display-name of the emote. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// For Unicode emoji, this is the raw value of the character, not its | |||||
/// Unicode display name. | |||||
/// </remarks> | |||||
string Name { get; } | string Name { get; } | ||||
} | } | ||||
} | } |
@@ -3,13 +3,17 @@ using System.Threading.Tasks; | |||||
namespace Discord | namespace Discord | ||||
{ | { | ||||
/// <summary> | |||||
/// An emote attached to a guild. This differs from an <see cref="IEmote"/> in that it contains | |||||
/// information relevant to the source guild. | |||||
/// </summary> | |||||
public interface IGuildEmote : IEmote, ISnowflakeEntity, IDeletable | public interface IGuildEmote : IEmote, ISnowflakeEntity, IDeletable | ||||
{ | { | ||||
/// <summary> | /// <summary> | ||||
/// Gets whether this emoji is managed by an integration. | /// Gets whether this emoji is managed by an integration. | ||||
/// </summary> | /// </summary> | ||||
/// <returns> | /// <returns> | ||||
/// 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. | |||||
/// </returns> | /// </returns> | ||||
bool IsManaged { get; } | bool IsManaged { get; } | ||||
/// <summary> | /// <summary> | ||||
@@ -1,7 +0,0 @@ | |||||
namespace Discord | |||||
{ | |||||
public interface IMentionable | |||||
{ | |||||
string Mention { get; } | |||||
} | |||||
} |
@@ -0,0 +1,7 @@ | |||||
namespace Discord | |||||
{ | |||||
public interface ITaggable | |||||
{ | |||||
string Tag { get; } | |||||
} | |||||
} |
@@ -8,7 +8,7 @@ namespace Discord | |||||
/// <summary> | /// <summary> | ||||
/// Represents a generic role object to be given to a guild user. | /// Represents a generic role object to be given to a guild user. | ||||
/// </summary> | /// </summary> | ||||
public interface IRole : ISnowflakeEntity, IDeletable, IMentionable, IComparable<IRole> | |||||
public interface IRole : ISnowflakeEntity, IDeletable, ITaggable, IComparable<IRole> | |||||
{ | { | ||||
/// <summary> | /// <summary> | ||||
/// Gets the guild that owns this role. | /// Gets the guild that owns this role. | ||||
@@ -22,6 +22,7 @@ namespace Discord | |||||
Guild = guild; | Guild = guild; | ||||
} | } | ||||
// IGuildEmote | |||||
public bool IsManaged { get; set; } | public bool IsManaged { get; set; } | ||||
public bool RequireColons { get; set; } | public bool RequireColons { get; set; } | ||||
public IReadOnlyList<IRole> Roles { get; set; } | public IReadOnlyList<IRole> Roles { get; set; } | ||||
@@ -29,12 +30,14 @@ namespace Discord | |||||
public string Name { get; set; } | public string Name { get; set; } | ||||
public IGuild Guild { 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() | public Task DeleteAsync() | ||||
=> Discord.Rest.DeleteGuildEmojiAsync(Guild.Id, Id); | => Discord.Rest.DeleteGuildEmojiAsync(Guild.Id, Id); | ||||
// IGuildEmote | |||||
public Task ModifyAsync() // TODO | public Task ModifyAsync() // TODO | ||||
{ | { | ||||
throw new System.NotImplementedException(); | throw new System.NotImplementedException(); | ||||
@@ -9,6 +9,6 @@ namespace Discord | |||||
} | } | ||||
public string Name { get; set; } | public string Name { get; set; } | ||||
public string Mention => Name; | |||||
public string Tag => Name; | |||||
} | } | ||||
} | } |
@@ -13,6 +13,6 @@ namespace Discord | |||||
public ulong Id { get; set; } | public ulong Id { get; set; } | ||||
public string Name { get; set; } | public string Name { get; set; } | ||||
public string Mention => EmoteUtilities.FormatGuildEmote(Id, Name); | |||||
public string Tag => EmoteUtilities.FormatGuildEmote(Id, Name); | |||||
} | } | ||||
} | } |
@@ -2,12 +2,56 @@ using System; | |||||
namespace Discord | namespace Discord | ||||
{ | { | ||||
/// <summary> | |||||
/// Methods to create an <see cref="IEmote"/>. | |||||
/// </summary> | |||||
public static class EmoteBuilder | public static class EmoteBuilder | ||||
{ | { | ||||
public static IEmote FromEmoji(string emoji) | |||||
/// <summary> | |||||
/// Creates an emote from a raw unicode emoji. | |||||
/// </summary> | |||||
/// <param name="emoji">The unicode character this emoji should be created from.</param> | |||||
public static IEmote FromUnicodeEmoji(string emoji) | |||||
=> new Emoji(emoji); | => new Emoji(emoji); | ||||
public static IEmote FromMention(string mention) | |||||
=> throw new NotImplementedException(); // TODO: emoteutil | |||||
/// <summary> | |||||
/// Creates an emote from an escaped tag. | |||||
/// </summary> | |||||
/// <param name="mention">The escaped mention tag for an emote.</param> | |||||
/// <exception cref="ArgumentException">Throws if the passed tag was of an invalid format.</exception> | |||||
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)); | |||||
} | |||||
/// <summary> | |||||
/// Creates an emote from an escaped tag. | |||||
/// </summary> | |||||
/// <param name="tag">The escaped mention tag for an emote.</param> | |||||
/// <returns>Returns true if the emote could be created; returns false if it was of an invalid format.</returns> | |||||
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; | |||||
} | |||||
/// <summary> | |||||
/// Creates an emote from a raw snowflake and name. | |||||
/// </summary> | |||||
/// <param name="id">The snowflake ID of the guild emote.</param> | |||||
/// <param name="name">The name of the guild emote.</param> | |||||
public static IEmote FromID(ulong id, string name) | public static IEmote FromID(ulong id, string name) | ||||
=> new Emote(id, name); | => new Emote(id, name); | ||||
} | } | ||||
@@ -31,7 +31,7 @@ namespace Discord | |||||
public string Name { get; set; } | public string Name { get; set; } | ||||
public GuildPermissions Permissions { get; set; } | public GuildPermissions Permissions { get; set; } | ||||
public int Position { 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() | public Task DeleteAsync() | ||||
=> Discord.Rest.DeleteGuildRoleAsync(Guild.Id, Id); | => Discord.Rest.DeleteGuildRoleAsync(Guild.Id, Id); | ||||
@@ -7,21 +7,28 @@ namespace Discord | |||||
public static string FormatGuildEmote(ulong id, string name) | public static string FormatGuildEmote(ulong id, string name) | ||||
=> $"<:{name}:{id}>"; | => $"<:{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<char> 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) | if (closingIndex < 0) | ||||
throw new ArgumentException("passed string does not match a guild emote format", nameof(formatted)); | |||||
return false; | |||||
ReadOnlySpan<char> name = formatted.Slice(2, closingIndex-2); | |||||
ReadOnlySpan<char> 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; | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -1,4 +1,3 @@ | |||||
using System; | |||||
using Xunit; | using Xunit; | ||||
namespace Discord.Tests.Unit | namespace Discord.Tests.Unit | ||||
@@ -9,9 +8,10 @@ namespace Discord.Tests.Unit | |||||
public void Parse() | public void Parse() | ||||
{ | { | ||||
string input = "<:gopher:243902586946715658>"; | 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] | [Theory] | ||||
@@ -21,7 +21,8 @@ namespace Discord.Tests.Unit | |||||
[InlineData("<:foo>")] | [InlineData("<:foo>")] | ||||
public void Parse_Fail(string data) | public void Parse_Fail(string data) | ||||
{ | { | ||||
Assert.Throws<ArgumentException>(() => EmoteUtilities.ParseGuildEmote(data)); | |||||
var success = EmoteUtilities.TryParseGuildEmote(data, out _); | |||||
Assert.False(success); | |||||
} | } | ||||
[Fact] | [Fact] | ||||