diff --git a/Directory.Build.targets b/Directory.Build.targets index 6e7b0d1aa..180821c63 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -24,6 +24,8 @@ + + diff --git a/Discord.Net.sln b/Discord.Net.sln index 8e8cebaa2..04a83acce 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -7,6 +7,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{381B0F15-BA2 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Core", "src\Core\Discord.Net.Core.csproj", "{57A52C6A-337D-4165-A42D-94FAC87B2807}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{B960E106-DC21-4A28-9C28-6AA0B49346BB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Core.UnitTests", "test\Discord.Net.Core.UnitTests\Discord.Net.Core.UnitTests.csproj", "{1A1D1A6F-F9DD-4A14-9F93-9B4C88F0B534}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -32,8 +36,21 @@ Global {57A52C6A-337D-4165-A42D-94FAC87B2807}.Release|x64.Build.0 = Release|Any CPU {57A52C6A-337D-4165-A42D-94FAC87B2807}.Release|x86.ActiveCfg = Release|Any CPU {57A52C6A-337D-4165-A42D-94FAC87B2807}.Release|x86.Build.0 = Release|Any CPU + {1A1D1A6F-F9DD-4A14-9F93-9B4C88F0B534}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A1D1A6F-F9DD-4A14-9F93-9B4C88F0B534}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A1D1A6F-F9DD-4A14-9F93-9B4C88F0B534}.Debug|x64.ActiveCfg = Debug|Any CPU + {1A1D1A6F-F9DD-4A14-9F93-9B4C88F0B534}.Debug|x64.Build.0 = Debug|Any CPU + {1A1D1A6F-F9DD-4A14-9F93-9B4C88F0B534}.Debug|x86.ActiveCfg = Debug|Any CPU + {1A1D1A6F-F9DD-4A14-9F93-9B4C88F0B534}.Debug|x86.Build.0 = Debug|Any CPU + {1A1D1A6F-F9DD-4A14-9F93-9B4C88F0B534}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A1D1A6F-F9DD-4A14-9F93-9B4C88F0B534}.Release|Any CPU.Build.0 = Release|Any CPU + {1A1D1A6F-F9DD-4A14-9F93-9B4C88F0B534}.Release|x64.ActiveCfg = Release|Any CPU + {1A1D1A6F-F9DD-4A14-9F93-9B4C88F0B534}.Release|x64.Build.0 = Release|Any CPU + {1A1D1A6F-F9DD-4A14-9F93-9B4C88F0B534}.Release|x86.ActiveCfg = Release|Any CPU + {1A1D1A6F-F9DD-4A14-9F93-9B4C88F0B534}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {57A52C6A-337D-4165-A42D-94FAC87B2807} = {381B0F15-BA2C-4E23-BE68-015462861AF0} + {1A1D1A6F-F9DD-4A14-9F93-9B4C88F0B534} = {B960E106-DC21-4A28-9C28-6AA0B49346BB} EndGlobalSection EndGlobal diff --git a/src/Core/Snowflake.cs b/src/Core/Snowflake.cs new file mode 100644 index 000000000..34074dce4 --- /dev/null +++ b/src/Core/Snowflake.cs @@ -0,0 +1,70 @@ +using System; + +namespace Discord +{ + /// + /// Utilities for reading and writing Discord snowflakes. + /// + /// + public static class Snowflake + { + /// + /// The offset, in milliseconds, from the Unix epoch which represents + /// the Discord Epoch. + /// + public const ulong DiscordEpochOffset = 1420070400000UL; + + /// + /// Calculates the time a given snowflake was created. + /// + /// + /// The snowflake to calculate the creation time of. + /// + /// + /// A representing the creation time, in + /// UTC, of the snowflake. + /// + /// + /// This sample demonstrates how to identify when a Discord user was + /// created. + /// + /// IUser user = await GetUserAsync(); + /// var snowflake = user.Id; + /// var created = Snowflake.GetCreatedTime(snowflake); + /// Console.WriteLine($"The user {user.Name} was created at {created}"); + /// + /// + public static DateTimeOffset GetCreatedTime(ulong snowflake) + => DateTimeOffset.FromUnixTimeMilliseconds( + (long)((snowflake >> 22) + DiscordEpochOffset)); + + /// + /// Calculates the smallest possible snowflake for a given creation + /// time. + /// + /// + /// The time to generate a snowflake for. + /// + /// + /// A snowflake representing the smallest possible snowflake for the + /// given creation time. + /// + /// + /// This sample demonstrates how to check if a user was created before + /// a certain date. + /// + /// IUser user = await GetUserAsync(); + /// var desiredTime = DateTimeOffset.UtcNow.AddDays(-7); + /// var minimumSnowflake = Snowflake.GetSnowflake(desiredTime); + /// + /// if (user.Id <= minimumSnowflake) + /// Console.WriteLine($"The user {user.Name} was created at least 7 days ago"); + /// else + /// Console.WriteLine($"The user {user.Name} was created less than 7 days ago"); + /// + /// + public static ulong GetSnowflake(DateTimeOffset time) + => ((ulong)time.ToUnixTimeMilliseconds() + - DiscordEpochOffset) << 22; + } +} diff --git a/test/Directory.Build.props b/test/Directory.Build.props new file mode 100644 index 000000000..79e8a93f9 --- /dev/null +++ b/test/Directory.Build.props @@ -0,0 +1,34 @@ + + + + + + + + + $(MSBuildAllProjects);$(MSBuildThisFileDirectory)..\Directory.Build.props + test + + + + + + false + trx + $(BaseArtifactsPath)tst/$(Configuration)/ + + + + + + + + + diff --git a/test/Directory.Build.targets b/test/Directory.Build.targets new file mode 100644 index 000000000..cb93612f1 --- /dev/null +++ b/test/Directory.Build.targets @@ -0,0 +1,21 @@ + + + + + + + + + $(MSBuildAllProjects);$(MSBuildThisFileDirectory)..\Directory.Build.targets + + + + + diff --git a/test/Discord.Net.Core.UnitTests/Discord.Net.Core.UnitTests.csproj b/test/Discord.Net.Core.UnitTests/Discord.Net.Core.UnitTests.csproj new file mode 100644 index 000000000..c7d579df1 --- /dev/null +++ b/test/Discord.Net.Core.UnitTests/Discord.Net.Core.UnitTests.csproj @@ -0,0 +1,13 @@ + + + + netcoreapp3.1 + + false + + + + + + + diff --git a/test/Discord.Net.Core.UnitTests/SnowflakeTests.cs b/test/Discord.Net.Core.UnitTests/SnowflakeTests.cs new file mode 100644 index 000000000..380b9f3bb --- /dev/null +++ b/test/Discord.Net.Core.UnitTests/SnowflakeTests.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using Xunit; + +namespace Discord.UnitTests +{ + public class SnowflakeTests + { + private static IEnumerable GetTimestampTestData() + { + // N.B. snowflakes here should have the least significant 22 bits + // set to zero. + yield return new object[] + { + 81062087257751552UL, + new DateTimeOffset( + year: 2015, month: 08, day: 12, + hour: 16, minute: 31, second: 47, + millisecond: 663, + offset: TimeSpan.Zero), + }; + + yield return new object[] + { + 0UL, + new DateTimeOffset( + year: 2015, month: 1, day: 1, + hour: 0, minute: 0, second: 0, + millisecond: 0, + offset: TimeSpan.Zero) + }; + + yield return new object[] + { + (ulong.MaxValue >> 22) << 22, + new DateTimeOffset( + year: 2154, month: 05, day: 15, + hour: 07, minute: 35, second: 11, + millisecond: 103, + offset: TimeSpan.Zero) + }; + } + + private static IEnumerable GetRoundtrippableTestData() + { + // N.B. snowflakes here should have the least significant 22 bits + // set to zero. + + yield return new object[]{ 81062087257751552UL }; + yield return new object[]{ 0UL }; + yield return new object[]{ (ulong.MaxValue >> 22) << 22 }; + } + + [Theory] + [MemberData(nameof(GetTimestampTestData))] + public void SnowflakeExpectedTimestamp( + ulong snowflake, DateTimeOffset expected) + { + var time = Snowflake.GetCreatedTime(snowflake); + + Assert.Equal(time, expected); + } + + [Theory] + [MemberData(nameof(GetTimestampTestData))] + public void SnowflakeExpectedSnowflake( + ulong expected, DateTimeOffset time) + { + var snowflake = Snowflake.GetSnowflake(time); + + Assert.Equal(expected, snowflake); + } + + [Theory] + [MemberData(nameof(GetRoundtrippableTestData))] + public void SnowflakeIsRoundTrippable( + ulong expected) + { + var time = Snowflake.GetCreatedTime(expected); + var roundtripped = Snowflake.GetSnowflake(time); + + Assert.Equal(expected, roundtripped); + } + } +}