| @@ -24,6 +24,8 @@ | |||
| <PackageReference Update="Microsoft.Net.Compilers.Toolset" Version="3.6.0-2.final" /> | |||
| <PackageReference Update="Microsoft.NET.Test.Sdk" Version="16.5.0" /> | |||
| <PackageReference Update="Microsoft.SourceLink.GitHub" Version="1.0.0" /> | |||
| <PackageReference Update="xunit" Version="2.4.1" /> | |||
| <PackageReference Update="xunit.runner.visualstudio" Version="2.4.1" /> | |||
| </ItemGroup> | |||
| </Project> | |||
| @@ -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 | |||
| @@ -0,0 +1,70 @@ | |||
| using System; | |||
| namespace Discord | |||
| { | |||
| /// <summary> | |||
| /// Utilities for reading and writing Discord snowflakes. | |||
| /// <seealso href="https://discordapp.com/developers/docs/reference#snowflakes"/> | |||
| /// </summary> | |||
| public static class Snowflake | |||
| { | |||
| /// <summary> | |||
| /// The offset, in milliseconds, from the Unix epoch which represents | |||
| /// the Discord Epoch. | |||
| /// </summary> | |||
| public const ulong DiscordEpochOffset = 1420070400000UL; | |||
| /// <summary> | |||
| /// Calculates the time a given snowflake was created. | |||
| /// </summary> | |||
| /// <param name="snowflake"> | |||
| /// The snowflake to calculate the creation time of. | |||
| /// </param> | |||
| /// <returns> | |||
| /// A <see cref="DateTimeOffset"/> representing the creation time, in | |||
| /// UTC, of the snowflake. | |||
| /// </returns> | |||
| /// <example> | |||
| /// This sample demonstrates how to identify when a Discord user was | |||
| /// created. | |||
| /// <code> | |||
| /// IUser user = await GetUserAsync(); | |||
| /// var snowflake = user.Id; | |||
| /// var created = Snowflake.GetCreatedTime(snowflake); | |||
| /// Console.WriteLine($"The user {user.Name} was created at {created}"); | |||
| /// </code> | |||
| /// </example> | |||
| public static DateTimeOffset GetCreatedTime(ulong snowflake) | |||
| => DateTimeOffset.FromUnixTimeMilliseconds( | |||
| (long)((snowflake >> 22) + DiscordEpochOffset)); | |||
| /// <summary> | |||
| /// Calculates the smallest possible snowflake for a given creation | |||
| /// time. | |||
| /// </summary> | |||
| /// <param name="time"> | |||
| /// The time to generate a snowflake for. | |||
| /// </param> | |||
| /// <returns> | |||
| /// A snowflake representing the smallest possible snowflake for the | |||
| /// given creation time. | |||
| /// </returns> | |||
| /// <example> | |||
| /// This sample demonstrates how to check if a user was created before | |||
| /// a certain date. | |||
| /// <code> | |||
| /// 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"); | |||
| /// </code> | |||
| /// </example> | |||
| public static ulong GetSnowflake(DateTimeOffset time) | |||
| => ((ulong)time.ToUnixTimeMilliseconds() | |||
| - DiscordEpochOffset) << 22; | |||
| } | |||
| } | |||
| @@ -0,0 +1,34 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | |||
| <!-- Based on https://github.com/terrafx/terrafx/blob/master/Directory.Build.props --> | |||
| <!-- Copyright © Tanner Gooding and Contributors --> | |||
| <Project> | |||
| <!-- | |||
| Directory.Build.props is automatically picked up and imported by | |||
| Microsoft.Common.props. This file needs to exist, even if empty so that | |||
| files in the parent directory tree, with the same name, are not imported | |||
| instead. The import fairly early and only Sdk.props will have been | |||
| imported beforehand. We also don't need to add ourselves to | |||
| MSBuildAllProjects, as that is done by the file that imports us. | |||
| --> | |||
| <PropertyGroup> | |||
| <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileDirectory)..\Directory.Build.props</MSBuildAllProjects> | |||
| <DiscordNetProjectCategory>test</DiscordNetProjectCategory> | |||
| </PropertyGroup> | |||
| <Import Project="$(MSBuildThisFileDirectory)..\Directory.Build.props" /> | |||
| <PropertyGroup> | |||
| <GenerateDocumentationFile>false</GenerateDocumentationFile> | |||
| <VSTestLogger>trx</VSTestLogger> | |||
| <VSTestResultsDirectory>$(BaseArtifactsPath)tst/$(Configuration)/</VSTestResultsDirectory> | |||
| </PropertyGroup> | |||
| <ItemGroup> | |||
| <PackageReference Include="Microsoft.NET.Test.Sdk" IsImplicitlyDefined="true" PrivateAssets="all" /> | |||
| <PackageReference Include="xunit" IsImplicitlyDefined="true" PrivateAssets="all" /> | |||
| <PackageReference Include="xunit.runner.visualstudio" IsImplicitlyDefined="true" PrivateAssets="all" /> | |||
| </ItemGroup> | |||
| </Project> | |||
| @@ -0,0 +1,21 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | |||
| <!-- Based on https://github.com/terrafx/terrafx/blob/master/Directory.Build.props --> | |||
| <!-- Copyright © Tanner Gooding and Contributors --> | |||
| <Project> | |||
| <!-- | |||
| Directory.Build.targets is automatically picked up and imported by | |||
| Microsoft.Common.targets. This file needs to exist, even if empty so that | |||
| files in the parent directory tree, with the same name, are not imported | |||
| instead. The import fairly late and most other props/targets will have | |||
| been imported beforehand. We also don't need to add ourselves to | |||
| MSBuildAllProjects, as that is done by the file that imports us. | |||
| --> | |||
| <PropertyGroup> | |||
| <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileDirectory)..\Directory.Build.targets</MSBuildAllProjects> | |||
| </PropertyGroup> | |||
| <Import Project="$(MSBuildThisFileDirectory)..\Directory.Build.targets" /> | |||
| </Project> | |||
| @@ -0,0 +1,13 @@ | |||
| <Project Sdk="Microsoft.NET.Sdk"> | |||
| <PropertyGroup> | |||
| <TargetFramework>netcoreapp3.1</TargetFramework> | |||
| <IsPackable>false</IsPackable> | |||
| </PropertyGroup> | |||
| <ItemGroup> | |||
| <ProjectReference Include="..\..\src\Core\Discord.Net.Core.csproj" /> | |||
| </ItemGroup> | |||
| </Project> | |||
| @@ -0,0 +1,85 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using Xunit; | |||
| namespace Discord.UnitTests | |||
| { | |||
| public class SnowflakeTests | |||
| { | |||
| private static IEnumerable<object[]> 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<object[]> 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); | |||
| } | |||
| } | |||
| } | |||