| @@ -11,4 +11,20 @@ | |||||
| <PackageReference Include="System.Collections.Immutable" Version="1.3.1" /> | <PackageReference Include="System.Collections.Immutable" Version="1.3.1" /> | ||||
| <PackageReference Include="System.Interactive.Async" Version="3.1.1" /> | <PackageReference Include="System.Interactive.Async" Version="3.1.1" /> | ||||
| </ItemGroup> | </ItemGroup> | ||||
| <ItemGroup> | |||||
| <None Update="Entities\Emotes\Emoji.Codepoints.tt"> | |||||
| <Generator>TextTemplatingFileGenerator</Generator> | |||||
| <LastGenOutput>Emoji.Codepoints.cs</LastGenOutput> | |||||
| </None> | |||||
| </ItemGroup> | |||||
| <ItemGroup> | |||||
| <Service Include="{508349b6-6b84-4df5-91f0-309beebad82d}" /> | |||||
| </ItemGroup> | |||||
| <ItemGroup> | |||||
| <Compile Update="Entities\Emotes\Emoji.Codepoints.cs"> | |||||
| <DesignTime>True</DesignTime> | |||||
| <AutoGen>True</AutoGen> | |||||
| <DependentUpon>Emoji.Codepoints.tt</DependentUpon> | |||||
| </Compile> | |||||
| </ItemGroup> | |||||
| </Project> | </Project> | ||||
| @@ -0,0 +1,59 @@ | |||||
| <#@ output extension=".cs" #> | |||||
| <#@ template language="C#" #> | |||||
| <#@ assembly name="System.Net.Http.dll" #> | |||||
| <#@ import namespace="System.Net.Http" #> | |||||
| <#@ assembly name="System.Core.dll" #> | |||||
| <#@ import namespace="System.Collections.Generic" #> | |||||
| <#@ import namespace="System.Linq" #> | |||||
| <# | |||||
| const string DataUrl = "http://www.unicode.org/Public/emoji/6.0/emoji-data.txt"; | |||||
| List<int> codepoints = new List<int>(); | |||||
| void FetchData() | |||||
| { | |||||
| var client = new HttpClient(); | |||||
| try | |||||
| { | |||||
| var response = client.GetAsync(DataUrl).GetAwaiter().GetResult(); | |||||
| response.EnsureSuccessStatusCode(); | |||||
| string body = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); | |||||
| foreach (var line in body.Split('\n')) | |||||
| { | |||||
| if (line.StartsWith("#")) continue; | |||||
| var split = line.Split(';'); | |||||
| if (split.Length < 1) continue; | |||||
| var code = split[0].Trim(); | |||||
| if (string.IsNullOrEmpty(code)) continue; | |||||
| var ranges = code.Split("..".ToCharArray()); | |||||
| if (ranges.Length == 3) | |||||
| { | |||||
| var lower = Convert.ToInt32(ranges[0], 16); | |||||
| var upper = Convert.ToInt32(ranges[2], 16); | |||||
| codepoints.AddRange(Enumerable.Range(lower, (upper-lower)+1)); | |||||
| } | |||||
| else | |||||
| { | |||||
| var point = Convert.ToInt32(code, 16); | |||||
| codepoints.Add(point); | |||||
| } | |||||
| } | |||||
| } | |||||
| catch | |||||
| { | |||||
| } | |||||
| } | |||||
| FetchData(); | |||||
| #> | |||||
| namespace Discord | |||||
| { | |||||
| public partial class Emoji | |||||
| { | |||||
| internal readonly int[] Codepoints = new int[] | |||||
| { | |||||
| <# foreach (var codepoint in codepoints) { #><#= codepoint #>, <# } #> | |||||
| }; | |||||
| } | |||||
| } | |||||
| @@ -1,9 +1,12 @@ | |||||
| namespace Discord | |||||
| using System; | |||||
| using System.Text; | |||||
| namespace Discord | |||||
| { | { | ||||
| /// <summary> | /// <summary> | ||||
| /// A unicode emoji | /// A unicode emoji | ||||
| /// </summary> | /// </summary> | ||||
| public class Emoji : IEmote | |||||
| public partial class Emoji : IEmote | |||||
| { | { | ||||
| // TODO: need to constrain this to unicode-only emojis somehow | // TODO: need to constrain this to unicode-only emojis somehow | ||||
| @@ -20,6 +23,31 @@ | |||||
| /// <param name="unicode">The pure UTF-8 encoding of an emoji</param> | /// <param name="unicode">The pure UTF-8 encoding of an emoji</param> | ||||
| public Emoji(string unicode) | public Emoji(string unicode) | ||||
| { | { | ||||
| byte[] utf32 = Encoding.Unicode.GetBytes(unicode); | |||||
| bool safe = false, any = false; | |||||
| int codepoint = 0; | |||||
| for (var i = 0; i < utf32.Length; i += 4) | |||||
| { | |||||
| codepoint = BitConverter.ToInt32(utf32, i); | |||||
| any = false; | |||||
| for (var j = 0; j < Codepoints.Length; j++) | |||||
| { | |||||
| if (Codepoints[j] == codepoint) | |||||
| { | |||||
| any = true; | |||||
| break; | |||||
| } | |||||
| } | |||||
| if (any) | |||||
| safe = true; | |||||
| else | |||||
| { | |||||
| safe = false; | |||||
| break; | |||||
| } | |||||
| } | |||||
| if (!safe) | |||||
| throw new ArgumentException("One or more characters was not a valid Emoji", nameof(unicode)); | |||||
| Name = unicode; | Name = unicode; | ||||
| } | } | ||||
| @@ -10,6 +10,9 @@ | |||||
| <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | ||||
| </Content> | </Content> | ||||
| </ItemGroup> | </ItemGroup> | ||||
| <ItemGroup> | |||||
| <None Include="Tests.Emotes.cs" /> | |||||
| </ItemGroup> | |||||
| <ItemGroup> | <ItemGroup> | ||||
| <ProjectReference Include="../../src/Discord.Net.Commands/Discord.Net.Commands.csproj" /> | <ProjectReference Include="../../src/Discord.Net.Commands/Discord.Net.Commands.csproj" /> | ||||
| <ProjectReference Include="../../src/Discord.Net.Core/Discord.Net.Core.csproj" /> | <ProjectReference Include="../../src/Discord.Net.Core/Discord.Net.Core.csproj" /> | ||||
| @@ -0,0 +1,33 @@ | |||||
| using System; | |||||
| using Xunit; | |||||
| namespace Discord | |||||
| { | |||||
| public class EmoteTests | |||||
| { | |||||
| [Fact] | |||||
| public void Emoji() | |||||
| { | |||||
| // Future: Validate emoji parsing | |||||
| Assert.Equal("🦅", new Emoji("🦅").Name); | |||||
| } | |||||
| [Fact] | |||||
| public void Emote() | |||||
| { | |||||
| Assert.Equal(true, Discord.Emote.TryParse("<:foxDab:280494667093508096>", out var emote)); | |||||
| Assert.NotNull(emote); | |||||
| Assert.Equal("foxDab", emote.Name); | |||||
| Assert.Equal(280494667093508096UL, emote.Id); | |||||
| Assert.Equal(DateTimeOffset.FromUnixTimeMilliseconds(1486945539974), emote.CreatedAt); | |||||
| } | |||||
| [Fact] | |||||
| public void Emote_Parse_Fail() | |||||
| { | |||||
| Assert.Equal(false, Discord.Emote.TryParse("", out _)); | |||||
| Assert.Equal(false, Discord.Emote.TryParse(":foxDab", out _)); | |||||
| Assert.Equal(false, Discord.Emote.TryParse(":foxDab:", out _)); | |||||
| Assert.Throws<ArgumentException>(() => Discord.Emote.Parse(":foxDab:")); | |||||
| } | |||||
| } | |||||
| } | |||||