| @@ -24,6 +24,8 @@ | |||||
| <PackageReference Update="Microsoft.Net.Compilers.Toolset" Version="3.6.0-2.final" /> | <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.NET.Test.Sdk" Version="16.5.0" /> | ||||
| <PackageReference Update="Microsoft.SourceLink.GitHub" Version="1.0.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> | </ItemGroup> | ||||
| </Project> | </Project> | ||||
| @@ -7,6 +7,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{381B0F15-BA2 | |||||
| EndProject | EndProject | ||||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Core", "src\Core\Discord.Net.Core.csproj", "{57A52C6A-337D-4165-A42D-94FAC87B2807}" | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Core", "src\Core\Discord.Net.Core.csproj", "{57A52C6A-337D-4165-A42D-94FAC87B2807}" | ||||
| EndProject | 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 | Global | ||||
| GlobalSection(SolutionConfigurationPlatforms) = preSolution | GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||||
| Debug|Any CPU = Debug|Any CPU | 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|x64.Build.0 = Release|Any CPU | ||||
| {57A52C6A-337D-4165-A42D-94FAC87B2807}.Release|x86.ActiveCfg = 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 | {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 | EndGlobalSection | ||||
| GlobalSection(NestedProjects) = preSolution | GlobalSection(NestedProjects) = preSolution | ||||
| {57A52C6A-337D-4165-A42D-94FAC87B2807} = {381B0F15-BA2C-4E23-BE68-015462861AF0} | {57A52C6A-337D-4165-A42D-94FAC87B2807} = {381B0F15-BA2C-4E23-BE68-015462861AF0} | ||||
| {1A1D1A6F-F9DD-4A14-9F93-9B4C88F0B534} = {B960E106-DC21-4A28-9C28-6AA0B49346BB} | |||||
| EndGlobalSection | EndGlobalSection | ||||
| EndGlobal | 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); | |||||
| } | |||||
| } | |||||
| } | |||||