diff --git a/.gitignore b/.gitignore index ccd272109..45e5e009d 100644 --- a/.gitignore +++ b/.gitignore @@ -202,4 +202,7 @@ project.lock.json /docs/_build *.pyc /.editorconfig -.vscode/ \ No newline at end of file +.vscode/ +docs/api/\.manifest + +\.idea/ diff --git a/Discord.Net.sln b/Discord.Net.sln index 6308b4444..a63606787 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -1,7 +1,6 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26014.0 +VisualStudioVersion = 15.0.26228.4 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Core", "src\Discord.Net.Core\Discord.Net.Core.csproj", "{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}" EndProject @@ -19,15 +18,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Providers", "Providers", "{ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Providers.WS4Net", "src\Discord.Net.Providers.WS4Net\Discord.Net.Providers.WS4Net.csproj", "{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Providers.UdpClient", "src\Discord.Net.Providers.UdpClient\Discord.Net.Providers.UdpClient.csproj", "{ABC9F4B9-2452-4725-B522-754E0A02E282}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Tests", "test\Discord.Net.Tests\Discord.Net.Tests.csproj", "{C38E5BC1-11CB-4101-8A38-5B40A1BC6433}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F66D75C0-E304-46E0-9C3A-294F340DB37D}" -EndProject -Project("{13B669BE-BB05-4DDF-9536-439F39A36129}") = "Discord.Net.Relay", "src\Discord.Net.Relay\Discord.Net.Relay.csproj", "{2705FCB3-68C9-4CEB-89CC-01F8EC80512B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Webhook", "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj", "{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -41,16 +36,16 @@ Global GlobalSection(ProjectConfigurationPlatforms) = postSolution {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|x64.ActiveCfg = Debug|x64 - {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|x64.Build.0 = Debug|x64 - {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|x86.ActiveCfg = Debug|x86 - {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|x86.Build.0 = Debug|x86 + {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|x64.ActiveCfg = Debug|Any CPU + {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|x64.Build.0 = Debug|Any CPU + {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|x86.ActiveCfg = Debug|Any CPU + {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|x86.Build.0 = Debug|Any CPU {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|Any CPU.ActiveCfg = Release|Any CPU {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|Any CPU.Build.0 = Release|Any CPU - {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|x64.ActiveCfg = Release|x64 - {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|x64.Build.0 = Release|x64 - {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|x86.ActiveCfg = Release|x86 - {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|x86.Build.0 = Release|x86 + {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|x64.ActiveCfg = Release|Any CPU + {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|x64.Build.0 = Release|Any CPU + {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|x86.ActiveCfg = Release|Any CPU + {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|x86.Build.0 = Release|Any CPU {BFC6DC28-0351-4573-926A-D4124244C04F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BFC6DC28-0351-4573-926A-D4124244C04F}.Debug|Any CPU.Build.0 = Debug|Any CPU {BFC6DC28-0351-4573-926A-D4124244C04F}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -89,76 +84,52 @@ Global {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Release|x86.Build.0 = Debug|Any CPU {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|x64.ActiveCfg = Debug|x64 - {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|x64.Build.0 = Debug|x64 - {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|x86.ActiveCfg = Debug|x86 - {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|x86.Build.0 = Debug|x86 + {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|x64.ActiveCfg = Debug|Any CPU + {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|x64.Build.0 = Debug|Any CPU + {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|x86.ActiveCfg = Debug|Any CPU + {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|x86.Build.0 = Debug|Any CPU {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|Any CPU.ActiveCfg = Release|Any CPU {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|Any CPU.Build.0 = Release|Any CPU - {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x64.ActiveCfg = Release|x64 - {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x64.Build.0 = Release|x64 - {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x86.ActiveCfg = Release|x86 - {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x86.Build.0 = Release|x86 + {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x64.ActiveCfg = Release|Any CPU + {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x64.Build.0 = Release|Any CPU + {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x86.ActiveCfg = Release|Any CPU + {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x86.Build.0 = Release|Any CPU {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|x64.ActiveCfg = Debug|x64 - {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|x64.Build.0 = Debug|x64 - {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|x86.ActiveCfg = Debug|x86 - {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|x86.Build.0 = Debug|x86 + {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|x64.ActiveCfg = Debug|Any CPU + {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|x64.Build.0 = Debug|Any CPU + {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|x86.ActiveCfg = Debug|Any CPU + {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|x86.Build.0 = Debug|Any CPU {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|Any CPU.ActiveCfg = Release|Any CPU {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|Any CPU.Build.0 = Release|Any CPU - {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x64.ActiveCfg = Release|x64 - {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x64.Build.0 = Release|x64 - {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x86.ActiveCfg = Release|x86 - {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x86.Build.0 = Release|x86 - {547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Debug|Any CPU.Build.0 = Debug|Any CPU - {547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Debug|x64.ActiveCfg = Debug|x64 - {547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Debug|x64.Build.0 = Debug|x64 - {547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Debug|x86.ActiveCfg = Debug|x86 - {547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Debug|x86.Build.0 = Debug|x86 - {547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Release|Any CPU.ActiveCfg = Release|Any CPU - {547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Release|Any CPU.Build.0 = Release|Any CPU - {547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Release|x64.ActiveCfg = Release|x64 - {547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Release|x64.Build.0 = Release|x64 - {547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Release|x86.ActiveCfg = Release|x86 - {547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Release|x86.Build.0 = Release|x86 - {ABC9F4B9-2452-4725-B522-754E0A02E282}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {ABC9F4B9-2452-4725-B522-754E0A02E282}.Debug|Any CPU.Build.0 = Debug|Any CPU - {ABC9F4B9-2452-4725-B522-754E0A02E282}.Debug|x64.ActiveCfg = Debug|x64 - {ABC9F4B9-2452-4725-B522-754E0A02E282}.Debug|x64.Build.0 = Debug|x64 - {ABC9F4B9-2452-4725-B522-754E0A02E282}.Debug|x86.ActiveCfg = Debug|x86 - {ABC9F4B9-2452-4725-B522-754E0A02E282}.Debug|x86.Build.0 = Debug|x86 - {ABC9F4B9-2452-4725-B522-754E0A02E282}.Release|Any CPU.ActiveCfg = Release|Any CPU - {ABC9F4B9-2452-4725-B522-754E0A02E282}.Release|Any CPU.Build.0 = Release|Any CPU - {ABC9F4B9-2452-4725-B522-754E0A02E282}.Release|x64.ActiveCfg = Release|x64 - {ABC9F4B9-2452-4725-B522-754E0A02E282}.Release|x64.Build.0 = Release|x64 - {ABC9F4B9-2452-4725-B522-754E0A02E282}.Release|x86.ActiveCfg = Release|x86 - {ABC9F4B9-2452-4725-B522-754E0A02E282}.Release|x86.Build.0 = Release|x86 + {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x64.ActiveCfg = Release|Any CPU + {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x64.Build.0 = Release|Any CPU + {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x86.ActiveCfg = Release|Any CPU + {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x86.Build.0 = Release|Any CPU {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|x64.ActiveCfg = Debug|x64 - {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|x64.Build.0 = Debug|x64 - {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|x86.ActiveCfg = Debug|x86 - {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|x86.Build.0 = Debug|x86 + {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|x64.ActiveCfg = Debug|Any CPU + {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|x64.Build.0 = Debug|Any CPU + {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|x86.ActiveCfg = Debug|Any CPU + {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|x86.Build.0 = Debug|Any CPU {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|Any CPU.ActiveCfg = Release|Any CPU {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|Any CPU.Build.0 = Release|Any CPU - {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x64.ActiveCfg = Release|x64 - {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x64.Build.0 = Release|x64 - {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x86.ActiveCfg = Release|x86 - {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x86.Build.0 = Release|x86 - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|x64.ActiveCfg = Debug|x64 - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|x64.Build.0 = Debug|x64 - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|x86.ActiveCfg = Debug|x86 - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|x86.Build.0 = Debug|x86 - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|Any CPU.Build.0 = Release|Any CPU - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x64.ActiveCfg = Release|x64 - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x64.Build.0 = Release|x64 - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x86.ActiveCfg = Release|x86 - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x86.Build.0 = Release|x86 + {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x64.ActiveCfg = Release|Any CPU + {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x64.Build.0 = Release|Any CPU + {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x86.ActiveCfg = Release|Any CPU + {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x86.Build.0 = Release|Any CPU + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|x64.ActiveCfg = Debug|Any CPU + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|x64.Build.0 = Debug|Any CPU + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|x86.ActiveCfg = Debug|Any CPU + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|x86.Build.0 = Debug|Any CPU + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|Any CPU.Build.0 = Release|Any CPU + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x64.ActiveCfg = Release|Any CPU + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x64.Build.0 = Release|Any CPU + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x86.ActiveCfg = Release|Any CPU + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -169,7 +140,6 @@ Global {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A} {688FD1D8-7F01-4539-B2E9-F473C5D699C7} = {288C363D-A636-4EAE-9AC1-4698B641B26E} {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012} - {ABC9F4B9-2452-4725-B522-754E0A02E282} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012} - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B} = {F66D75C0-E304-46E0-9C3A-294F340DB37D} + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A} EndGlobalSection EndGlobal diff --git a/Discord.Net.targets b/Discord.Net.targets new file mode 100644 index 000000000..6dc4bb140 --- /dev/null +++ b/Discord.Net.targets @@ -0,0 +1,31 @@ + + + 1.0.1 + + RogueException + discord;discordapp + https://github.com/RogueException/Discord.Net + http://opensource.org/licenses/MIT + git + git://github.com/RogueException/Discord.Net + + + $(VersionSuffix)-dev + dev + + + $(VersionSuffix)-$(BuildNumber) + build-$(BuildNumber) + + + $(DefineConstants);FILESYSTEM;DEFAULTUDPCLIENT;DEFAULTWEBSOCKET + + + $(DefineConstants);FORMATSTR;UNIXTIME;MSTRYBUFFER;UDPDISPOSE + + + $(NoWarn);CS1573;CS1591 + true + true + + \ No newline at end of file diff --git a/README.md b/README.md index 903fa76c1..2b58d4579 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -# Discord.Net v1.0.0-rc +# Discord.Net +[![NuGet](https://img.shields.io/nuget/vpre/Discord.Net.svg?maxAge=2592000?style=plastic)](https://www.nuget.org/packages/Discord.Net) [![MyGet](https://img.shields.io/myget/discord-net/vpre/Discord.Net.svg)](https://www.myget.org/feed/Packages/discord-net) [![Build status](https://ci.appveyor.com/api/projects/status/5sb7n8a09w9clute/branch/dev?svg=true)](https://ci.appveyor.com/project/RogueException/discord-net/branch/dev) [![Discord](https://discordapp.com/api/guilds/81384788765712384/widget.png)](https://discord.gg/0SBTUU1wZTVjAMPx) @@ -13,13 +14,13 @@ Our stable builds available from NuGet through the Discord.Net metapackage: - [Discord.Net](https://www.nuget.org/packages/Discord.Net/) The individual components may also be installed from NuGet: +- [Discord.Net.Commands](https://www.nuget.org/packages/Discord.Net.Commands/) - [Discord.Net.Rest](https://www.nuget.org/packages/Discord.Net.Rest/) - [Discord.Net.Rpc](https://www.nuget.org/packages/Discord.Net.Rpc/) - [Discord.Net.WebSocket](https://www.nuget.org/packages/Discord.Net.WebSocket/) -- [Discord.Net.Commands](https://www.nuget.org/packages/Discord.Net.Commands/) +- [Discord.Net.Webhook](https://www.nuget.org/packages/Discord.Net.Webhook/) -The following providers are available for platforms not supporting .NET Standard 1.3: -- [Discord.Net.Providers.UdpClient](https://www.nuget.org/packages/Discord.Net.Providers.UdpClient/) +The following provider is available for platforms not supporting .NET Standard 1.3: - [Discord.Net.Providers.WS4Net](https://www.nuget.org/packages/Discord.Net.Providers.WS4Net/) ### Unstable (MyGet) @@ -29,13 +30,13 @@ Nightly builds are available through our MyGet feed (`https://www.myget.org/F/di In order to compile Discord.Net, you require the following: ### Using Visual Studio -- [Visual Studio 2017 RC](https://www.microsoft.com/net/core#windowsvs2017) -- [.NET Core SDK 1.0 RC3](https://github.com/dotnet/core/blob/master/release-notes/rc3-download.md) +- [Visual Studio 2017](https://www.microsoft.com/net/core#windowsvs2017) +- [.NET Core SDK](https://www.microsoft.com/net/download/core) -The .NET Core and Docker (Preview) workload is required during Visual Studio installation. +The .NET Core workload must be selected during Visual Studio installation. ### Using Command Line -- [.NET Core SDK 1.0 RC3](https://github.com/dotnet/core/blob/master/release-notes/rc3-download.md) +- [.NET Core SDK](https://www.microsoft.com/net/download/core) ## Known Issues diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000..d94e2ad68 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,61 @@ +version: build-{build} +branches: + only: + - dev +image: Visual Studio 2017 + +nuget: + disable_publish_on_pr: true +pull_requests: + do_not_increment_build_number: true +clone_folder: C:\Projects\Discord.Net +cache: test/Discord.Net.Tests/cache.db + +environment: + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + DNET_TEST_TOKEN: + secure: l7h5e7UE7yRd70hAB97kjPiQpPOShwqoBbOzEAYQ+XBd/Pre5OA33IXa3uisdUeQJP/nPFhcOsI+yn7WpuFaoQ== + DNET_TEST_GUILDID: 273160668180381696 +init: +- ps: $Env:BUILD = "$($Env:APPVEYOR_BUILD_NUMBER.PadLeft(5, "0"))" + +build_script: +- ps: appveyor-retry dotnet restore Discord.Net.sln -v Minimal /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" +- ps: dotnet build Discord.Net.sln -c "Release" /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" +after_build: +- ps: dotnet pack "src\Discord.Net.Core\Discord.Net.Core.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" +- ps: dotnet pack "src\Discord.Net.Rest\Discord.Net.Rest.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" +- ps: dotnet pack "src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" +- ps: dotnet pack "src\Discord.Net.Rpc\Discord.Net.Rpc.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" +- ps: dotnet pack "src\Discord.Net.Commands\Discord.Net.Commands.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" +- ps: dotnet pack "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" +- ps: dotnet pack "src\Discord.Net.Providers.WS4Net\Discord.Net.Providers.WS4Net.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" +- ps: >- + if ($Env:APPVEYOR_REPO_TAG -eq "true") { + nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="" + } else { + nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="-build-$Env:BUILD" + } +- ps: Get-ChildItem artifacts\*.nupkg | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } + +test_script: +- ps: >- + if ($APPVEYOR_PULL_REQUEST_NUMBER -eq "") { + dotnet test test/Discord.Net.Tests/Discord.Net.Tests.csproj -c "Release" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" + } + +deploy: +- provider: NuGet + server: https://www.myget.org/F/discord-net/api/v2/package + api_key: + secure: Jl7BXeUjRnkVHDMBuUWSXcEOkrli1PBleW2IiLyUs5j63UNUNp1hcjaUJRujx9lz + symbol_server: https://www.myget.org/F/discord-net/symbols/api/v2/package + on: + branch: dev +- provider: NuGet + server: https://www.myget.org/F/rogueexception/api/v2/package + api_key: + secure: D+vW2O2LBf/iJb4f+q8fkyIW2VdIYIGxSYLWNrOD4BHlDBZQlJipDbNarWjUr2Kn + symbol_server: https://www.myget.org/F/rogueexception/symbols/api/v2/package + on: + branch: dev \ No newline at end of file diff --git a/build.ps1 b/build.ps1 deleted file mode 100644 index 08508bbcf..000000000 --- a/build.ps1 +++ /dev/null @@ -1,4 +0,0 @@ -appveyor-retry dotnet restore Discord.Net.sln -v Minimal /p:BuildNumber="$Env:BUILD" -if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } -dotnet build Discord.Net.sln -c "Release" /p:BuildNumber="$Env:BUILD" -if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } \ No newline at end of file diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index f19e7c297..296b6d1cb 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,15 +1,46 @@ # Contributing to Docs -I don't really have any strict conditions for writing documentation, but just keep these few guidelines in mind: +I don't really have any strict conditions for writing documentation, +but just keep these few guidelines in mind: * Keep code samples in the `guides/samples` folder -* When referencing an object in the API, link to it's page in the API documentation. +* When referencing an object in the API, link to it's page in the +API documentation. * Documentation should be written in clear and proper English* -\* If anyone is interested in translating documentation into other languages, please open an issue or contact me on Discord (`foxbot#0282`). +\* If anyone is interested in translating documentation into other +languages, please open an issue or contact me on +Discord (`foxbot#0282`). + +### Layout + +Documentation should be written in a FAQ/Wiki style format. + +Recommended reads: + +* http://docs.microsoft.com +* http://flask.pocoo.org/docs/0.12/ + +Style consistencies: + +* Use a ruler set at 70 characters +* Links should use long syntax +* Pages should be short and concise, not broad and long + +Example of long link syntax: + +``` +Please consult the [API Documentation] for more information. + +[API Documentation]: xref:System.String +``` ### Compiling -Documentation is compiled into a static site using [DocFx](https://dotnet.github.io/docfx/). We currently use version 2.8 +Documentation is compiled into a static site using [DocFx]. +We currently use the most recent build off the dev branch. + +After making changes, compile your changes into the static site with +`docfx`. You can also view your changes live with `docfx --serve`. -After making changes, compile your changes into the static site with `docfx`. You can also view your changes live with `docfx --serve`. \ No newline at end of file +[DocFx]: https://dotnet.github.io/docfx/ \ No newline at end of file diff --git a/docs/api/.manifest b/docs/api/.manifest deleted file mode 100644 index 0a47304c4..000000000 --- a/docs/api/.manifest +++ /dev/null @@ -1 +0,0 @@ -{"Discord.Rpc":"Discord.Rpc.yml","Discord.Rpc.DiscordRpcClient":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.ConnectionState":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.Scopes":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.TokenExpiresAt":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.CurrentUser":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.ApplicationInfo":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.#ctor(System.String,System.String)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.#ctor(System.String,System.String,Discord.Rpc.DiscordRpcConfig)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.ConnectAsync":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.DisconnectAsync":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.AuthorizeAsync(System.String[],System.String,RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.SubscribeGlobal(Discord.Rpc.RpcGlobalEvent,RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.UnsubscribeGlobal(Discord.Rpc.RpcGlobalEvent,RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.SubscribeGuild(System.UInt64,Discord.Rpc.RpcChannelEvent,RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.UnsubscribeGuild(System.UInt64,Discord.Rpc.RpcChannelEvent,RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.SubscribeChannel(System.UInt64,Discord.Rpc.RpcChannelEvent,RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.UnsubscribeChannel(System.UInt64,Discord.Rpc.RpcChannelEvent,RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.GetRpcGuildAsync(System.UInt64,RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.GetRpcGuildsAsync(RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.GetRpcChannelAsync(System.UInt64,RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.GetRpcChannelsAsync(System.UInt64,RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.SelectTextChannelAsync(IChannel,RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.SelectTextChannelAsync(Discord.Rpc.RpcChannelSummary,RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.SelectTextChannelAsync(System.UInt64,RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.SelectVoiceChannelAsync(IChannel,System.Boolean,RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.SelectVoiceChannelAsync(Discord.Rpc.RpcChannelSummary,System.Boolean,RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.SelectVoiceChannelAsync(System.UInt64,System.Boolean,RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.GetVoiceSettingsAsync(RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.SetVoiceSettingsAsync(Action{Discord.Rpc.VoiceProperties},RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.SetUserVoiceSettingsAsync(System.UInt64,Action{Discord.Rpc.UserVoiceProperties},RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.Connected":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.Disconnected":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.Ready":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.ChannelCreated":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.GuildCreated":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.GuildStatusUpdated":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.VoiceStateCreated":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.VoiceStateUpdated":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.VoiceStateDeleted":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.SpeakingStarted":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.SpeakingStopped":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.VoiceSettingsUpdated":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.MessageReceived":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.MessageUpdated":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.MessageDeleted":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcConfig":"Discord.Rpc.DiscordRpcConfig.yml","Discord.Rpc.DiscordRpcConfig.RpcAPIVersion":"Discord.Rpc.DiscordRpcConfig.yml","Discord.Rpc.DiscordRpcConfig.PortRangeStart":"Discord.Rpc.DiscordRpcConfig.yml","Discord.Rpc.DiscordRpcConfig.PortRangeEnd":"Discord.Rpc.DiscordRpcConfig.yml","Discord.Rpc.DiscordRpcConfig.ConnectionTimeout":"Discord.Rpc.DiscordRpcConfig.yml","Discord.Rpc.DiscordRpcConfig.WebSocketProvider":"Discord.Rpc.DiscordRpcConfig.yml","Discord.Rpc.DiscordRpcConfig.#ctor":"Discord.Rpc.DiscordRpcConfig.yml","Discord.Rpc.RpcChannelEvent":"Discord.Rpc.RpcChannelEvent.yml","Discord.Rpc.RpcChannelEvent.VoiceStateCreate":"Discord.Rpc.RpcChannelEvent.yml","Discord.Rpc.RpcChannelEvent.VoiceStateUpdate":"Discord.Rpc.RpcChannelEvent.yml","Discord.Rpc.RpcChannelEvent.VoiceStateDelete":"Discord.Rpc.RpcChannelEvent.yml","Discord.Rpc.RpcChannelEvent.SpeakingStart":"Discord.Rpc.RpcChannelEvent.yml","Discord.Rpc.RpcChannelEvent.SpeakingStop":"Discord.Rpc.RpcChannelEvent.yml","Discord.Rpc.RpcChannelEvent.MessageCreate":"Discord.Rpc.RpcChannelEvent.yml","Discord.Rpc.RpcChannelEvent.MessageUpdate":"Discord.Rpc.RpcChannelEvent.yml","Discord.Rpc.RpcChannelEvent.MessageDelete":"Discord.Rpc.RpcChannelEvent.yml","Discord.Rpc.RpcGlobalEvent":"Discord.Rpc.RpcGlobalEvent.yml","Discord.Rpc.RpcGlobalEvent.ChannelCreated":"Discord.Rpc.RpcGlobalEvent.yml","Discord.Rpc.RpcGlobalEvent.GuildCreated":"Discord.Rpc.RpcGlobalEvent.yml","Discord.Rpc.RpcGlobalEvent.VoiceSettingsUpdated":"Discord.Rpc.RpcGlobalEvent.yml","Discord.Rpc.RpcGuildEvent":"Discord.Rpc.RpcGuildEvent.yml","Discord.Rpc.RpcGuildEvent.GuildStatus":"Discord.Rpc.RpcGuildEvent.yml","Discord.Rpc.RpcEntity`1":"Discord.Rpc.RpcEntity-1.yml","Discord.Rpc.RpcEntity`1.Discord":"Discord.Rpc.RpcEntity-1.yml","Discord.Rpc.RpcEntity`1.Id":"Discord.Rpc.RpcEntity-1.yml","Discord.Rpc.UserVoiceProperties":"Discord.Rpc.UserVoiceProperties.yml","Discord.Rpc.UserVoiceProperties.Pan":"Discord.Rpc.UserVoiceProperties.yml","Discord.Rpc.UserVoiceProperties.Volume":"Discord.Rpc.UserVoiceProperties.yml","Discord.Rpc.UserVoiceProperties.Mute":"Discord.Rpc.UserVoiceProperties.yml","Discord.Rpc.VoiceDevice":"Discord.Rpc.VoiceDevice.yml","Discord.Rpc.VoiceDevice.Id":"Discord.Rpc.VoiceDevice.yml","Discord.Rpc.VoiceDevice.Name":"Discord.Rpc.VoiceDevice.yml","Discord.Rpc.VoiceDevice.ToString":"Discord.Rpc.VoiceDevice.yml","Discord.Rpc.VoiceDeviceProperties":"Discord.Rpc.VoiceDeviceProperties.yml","Discord.Rpc.VoiceDeviceProperties.DeviceId":"Discord.Rpc.VoiceDeviceProperties.yml","Discord.Rpc.VoiceDeviceProperties.Volume":"Discord.Rpc.VoiceDeviceProperties.yml","Discord.Rpc.VoiceDeviceProperties.AvailableDevices":"Discord.Rpc.VoiceDeviceProperties.yml","Discord.Rpc.VoiceModeProperties":"Discord.Rpc.VoiceModeProperties.yml","Discord.Rpc.VoiceModeProperties.Type":"Discord.Rpc.VoiceModeProperties.yml","Discord.Rpc.VoiceModeProperties.AutoThreshold":"Discord.Rpc.VoiceModeProperties.yml","Discord.Rpc.VoiceModeProperties.Threshold":"Discord.Rpc.VoiceModeProperties.yml","Discord.Rpc.VoiceModeProperties.Shortcut":"Discord.Rpc.VoiceModeProperties.yml","Discord.Rpc.VoiceModeProperties.Delay":"Discord.Rpc.VoiceModeProperties.yml","Discord.Rpc.VoiceProperties":"Discord.Rpc.VoiceProperties.yml","Discord.Rpc.VoiceProperties.Input":"Discord.Rpc.VoiceProperties.yml","Discord.Rpc.VoiceProperties.Output":"Discord.Rpc.VoiceProperties.yml","Discord.Rpc.VoiceProperties.Mode":"Discord.Rpc.VoiceProperties.yml","Discord.Rpc.VoiceProperties.AutomaticGainControl":"Discord.Rpc.VoiceProperties.yml","Discord.Rpc.VoiceProperties.EchoCancellation":"Discord.Rpc.VoiceProperties.yml","Discord.Rpc.VoiceProperties.NoiseSuppression":"Discord.Rpc.VoiceProperties.yml","Discord.Rpc.VoiceProperties.QualityOfService":"Discord.Rpc.VoiceProperties.yml","Discord.Rpc.VoiceProperties.SilenceWarning":"Discord.Rpc.VoiceProperties.yml","Discord.Rpc.VoiceSettings":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceSettings.InputDeviceId":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceSettings.InputVolume":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceSettings.AvailableInputDevices":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceSettings.OutputDeviceId":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceSettings.OutputVolume":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceSettings.AvailableOutputDevices":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceSettings.AutomaticGainControl":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceSettings.EchoCancellation":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceSettings.NoiseSuppression":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceSettings.QualityOfService":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceSettings.SilenceWarning":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceSettings.ActivationMode":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceSettings.AutoThreshold":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceSettings.Threshold":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceSettings.Shortcuts":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceSettings.Delay":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceShortcut":"Discord.Rpc.VoiceShortcut.yml","Discord.Rpc.VoiceShortcut.Type":"Discord.Rpc.VoiceShortcut.yml","Discord.Rpc.VoiceShortcut.Code":"Discord.Rpc.VoiceShortcut.yml","Discord.Rpc.VoiceShortcut.Name":"Discord.Rpc.VoiceShortcut.yml","Discord.Rpc.VoiceShortcut.ToString":"Discord.Rpc.VoiceShortcut.yml","Discord.Rpc.VoiceShortcutType":"Discord.Rpc.VoiceShortcutType.yml","Discord.Rpc.VoiceShortcutType.KeyboardKey":"Discord.Rpc.VoiceShortcutType.yml","Discord.Rpc.VoiceShortcutType.MouseButton":"Discord.Rpc.VoiceShortcutType.yml","Discord.Rpc.VoiceShortcutType.KeyboardModifierKey":"Discord.Rpc.VoiceShortcutType.yml","Discord.Rpc.VoiceShortcutType.GamepadButton":"Discord.Rpc.VoiceShortcutType.yml","Discord.Rpc.IRpcAudioChannel":"Discord.Rpc.IRpcAudioChannel.yml","Discord.Rpc.IRpcAudioChannel.VoiceStates":"Discord.Rpc.IRpcAudioChannel.yml","Discord.Rpc.IRpcMessageChannel":"Discord.Rpc.IRpcMessageChannel.yml","Discord.Rpc.IRpcMessageChannel.CachedMessages":"Discord.Rpc.IRpcMessageChannel.yml","Discord.Rpc.IRpcPrivateChannel":"Discord.Rpc.IRpcPrivateChannel.yml","Discord.Rpc.RpcChannel":"Discord.Rpc.RpcChannel.yml","Discord.Rpc.RpcChannel.Name":"Discord.Rpc.RpcChannel.yml","Discord.Rpc.RpcChannel.CreatedAt":"Discord.Rpc.RpcChannel.yml","Discord.Rpc.RpcChannelSummary":"Discord.Rpc.RpcChannelSummary.yml","Discord.Rpc.RpcChannelSummary.Id":"Discord.Rpc.RpcChannelSummary.yml","Discord.Rpc.RpcChannelSummary.Name":"Discord.Rpc.RpcChannelSummary.yml","Discord.Rpc.RpcChannelSummary.Type":"Discord.Rpc.RpcChannelSummary.yml","Discord.Rpc.RpcChannelSummary.ToString":"Discord.Rpc.RpcChannelSummary.yml","Discord.Rpc.RpcDMChannel":"Discord.Rpc.RpcDMChannel.yml","Discord.Rpc.RpcDMChannel.CachedMessages":"Discord.Rpc.RpcDMChannel.yml","Discord.Rpc.RpcDMChannel.CloseAsync(RequestOptions)":"Discord.Rpc.RpcDMChannel.yml","Discord.Rpc.RpcDMChannel.GetMessageAsync(System.UInt64,RequestOptions)":"Discord.Rpc.RpcDMChannel.yml","Discord.Rpc.RpcDMChannel.GetMessagesAsync(System.Int32,RequestOptions)":"Discord.Rpc.RpcDMChannel.yml","Discord.Rpc.RpcDMChannel.GetMessagesAsync(System.UInt64,Direction,System.Int32,RequestOptions)":"Discord.Rpc.RpcDMChannel.yml","Discord.Rpc.RpcDMChannel.GetMessagesAsync(IMessage,Direction,System.Int32,RequestOptions)":"Discord.Rpc.RpcDMChannel.yml","Discord.Rpc.RpcDMChannel.GetPinnedMessagesAsync(RequestOptions)":"Discord.Rpc.RpcDMChannel.yml","Discord.Rpc.RpcDMChannel.SendMessageAsync(System.String,System.Boolean,Embed,RequestOptions)":"Discord.Rpc.RpcDMChannel.yml","Discord.Rpc.RpcDMChannel.SendFileAsync(Stream,System.String,System.String,System.Boolean,RequestOptions)":"Discord.Rpc.RpcDMChannel.yml","Discord.Rpc.RpcDMChannel.DeleteMessagesAsync(IEnumerable{IMessage},RequestOptions)":"Discord.Rpc.RpcDMChannel.yml","Discord.Rpc.RpcDMChannel.TriggerTypingAsync(RequestOptions)":"Discord.Rpc.RpcDMChannel.yml","Discord.Rpc.RpcDMChannel.EnterTypingState(RequestOptions)":"Discord.Rpc.RpcDMChannel.yml","Discord.Rpc.RpcDMChannel.ToString":"Discord.Rpc.RpcDMChannel.yml","Discord.Rpc.RpcGroupChannel":"Discord.Rpc.RpcGroupChannel.yml","Discord.Rpc.RpcGroupChannel.CachedMessages":"Discord.Rpc.RpcGroupChannel.yml","Discord.Rpc.RpcGroupChannel.VoiceStates":"Discord.Rpc.RpcGroupChannel.yml","Discord.Rpc.RpcGroupChannel.LeaveAsync(RequestOptions)":"Discord.Rpc.RpcGroupChannel.yml","Discord.Rpc.RpcGroupChannel.GetMessageAsync(System.UInt64,RequestOptions)":"Discord.Rpc.RpcGroupChannel.yml","Discord.Rpc.RpcGroupChannel.GetMessagesAsync(System.Int32,RequestOptions)":"Discord.Rpc.RpcGroupChannel.yml","Discord.Rpc.RpcGroupChannel.GetMessagesAsync(System.UInt64,Direction,System.Int32,RequestOptions)":"Discord.Rpc.RpcGroupChannel.yml","Discord.Rpc.RpcGroupChannel.GetMessagesAsync(IMessage,Direction,System.Int32,RequestOptions)":"Discord.Rpc.RpcGroupChannel.yml","Discord.Rpc.RpcGroupChannel.GetPinnedMessagesAsync(RequestOptions)":"Discord.Rpc.RpcGroupChannel.yml","Discord.Rpc.RpcGroupChannel.SendMessageAsync(System.String,System.Boolean,Embed,RequestOptions)":"Discord.Rpc.RpcGroupChannel.yml","Discord.Rpc.RpcGroupChannel.SendFileAsync(Stream,System.String,System.String,System.Boolean,RequestOptions)":"Discord.Rpc.RpcGroupChannel.yml","Discord.Rpc.RpcGroupChannel.DeleteMessagesAsync(IEnumerable{IMessage},RequestOptions)":"Discord.Rpc.RpcGroupChannel.yml","Discord.Rpc.RpcGroupChannel.TriggerTypingAsync(RequestOptions)":"Discord.Rpc.RpcGroupChannel.yml","Discord.Rpc.RpcGroupChannel.EnterTypingState(RequestOptions)":"Discord.Rpc.RpcGroupChannel.yml","Discord.Rpc.RpcGroupChannel.ToString":"Discord.Rpc.RpcGroupChannel.yml","Discord.Rpc.RpcGuildChannel":"Discord.Rpc.RpcGuildChannel.yml","Discord.Rpc.RpcGuildChannel.GuildId":"Discord.Rpc.RpcGuildChannel.yml","Discord.Rpc.RpcGuildChannel.Position":"Discord.Rpc.RpcGuildChannel.yml","Discord.Rpc.RpcGuildChannel.ModifyAsync(Action{GuildChannelProperties},RequestOptions)":"Discord.Rpc.RpcGuildChannel.yml","Discord.Rpc.RpcGuildChannel.DeleteAsync(RequestOptions)":"Discord.Rpc.RpcGuildChannel.yml","Discord.Rpc.RpcGuildChannel.AddPermissionOverwriteAsync(IUser,OverwritePermissions,RequestOptions)":"Discord.Rpc.RpcGuildChannel.yml","Discord.Rpc.RpcGuildChannel.AddPermissionOverwriteAsync(IRole,OverwritePermissions,RequestOptions)":"Discord.Rpc.RpcGuildChannel.yml","Discord.Rpc.RpcGuildChannel.RemovePermissionOverwriteAsync(IUser,RequestOptions)":"Discord.Rpc.RpcGuildChannel.yml","Discord.Rpc.RpcGuildChannel.RemovePermissionOverwriteAsync(IRole,RequestOptions)":"Discord.Rpc.RpcGuildChannel.yml","Discord.Rpc.RpcGuildChannel.GetInvitesAsync(RequestOptions)":"Discord.Rpc.RpcGuildChannel.yml","Discord.Rpc.RpcGuildChannel.CreateInviteAsync(System.Nullable{System.Int32},System.Nullable{System.Int32},System.Boolean,RequestOptions)":"Discord.Rpc.RpcGuildChannel.yml","Discord.Rpc.RpcGuildChannel.ToString":"Discord.Rpc.RpcGuildChannel.yml","Discord.Rpc.RpcTextChannel":"Discord.Rpc.RpcTextChannel.yml","Discord.Rpc.RpcTextChannel.CachedMessages":"Discord.Rpc.RpcTextChannel.yml","Discord.Rpc.RpcTextChannel.Mention":"Discord.Rpc.RpcTextChannel.yml","Discord.Rpc.RpcTextChannel.ModifyAsync(Action{TextChannelProperties},RequestOptions)":"Discord.Rpc.RpcTextChannel.yml","Discord.Rpc.RpcTextChannel.GetMessageAsync(System.UInt64,RequestOptions)":"Discord.Rpc.RpcTextChannel.yml","Discord.Rpc.RpcTextChannel.GetMessagesAsync(System.Int32,RequestOptions)":"Discord.Rpc.RpcTextChannel.yml","Discord.Rpc.RpcTextChannel.GetMessagesAsync(System.UInt64,Direction,System.Int32,RequestOptions)":"Discord.Rpc.RpcTextChannel.yml","Discord.Rpc.RpcTextChannel.GetMessagesAsync(IMessage,Direction,System.Int32,RequestOptions)":"Discord.Rpc.RpcTextChannel.yml","Discord.Rpc.RpcTextChannel.GetPinnedMessagesAsync(RequestOptions)":"Discord.Rpc.RpcTextChannel.yml","Discord.Rpc.RpcTextChannel.SendMessageAsync(System.String,System.Boolean,Embed,RequestOptions)":"Discord.Rpc.RpcTextChannel.yml","Discord.Rpc.RpcTextChannel.SendFileAsync(Stream,System.String,System.String,System.Boolean,RequestOptions)":"Discord.Rpc.RpcTextChannel.yml","Discord.Rpc.RpcTextChannel.DeleteMessagesAsync(IEnumerable{IMessage},RequestOptions)":"Discord.Rpc.RpcTextChannel.yml","Discord.Rpc.RpcTextChannel.TriggerTypingAsync(RequestOptions)":"Discord.Rpc.RpcTextChannel.yml","Discord.Rpc.RpcTextChannel.EnterTypingState(RequestOptions)":"Discord.Rpc.RpcTextChannel.yml","Discord.Rpc.RpcVoiceChannel":"Discord.Rpc.RpcVoiceChannel.yml","Discord.Rpc.RpcVoiceChannel.UserLimit":"Discord.Rpc.RpcVoiceChannel.yml","Discord.Rpc.RpcVoiceChannel.Bitrate":"Discord.Rpc.RpcVoiceChannel.yml","Discord.Rpc.RpcVoiceChannel.VoiceStates":"Discord.Rpc.RpcVoiceChannel.yml","Discord.Rpc.RpcVoiceChannel.ModifyAsync(Action{VoiceChannelProperties},RequestOptions)":"Discord.Rpc.RpcVoiceChannel.yml","Discord.Rpc.RpcGuild":"Discord.Rpc.RpcGuild.yml","Discord.Rpc.RpcGuild.Name":"Discord.Rpc.RpcGuild.yml","Discord.Rpc.RpcGuild.IconUrl":"Discord.Rpc.RpcGuild.yml","Discord.Rpc.RpcGuild.Users":"Discord.Rpc.RpcGuild.yml","Discord.Rpc.RpcGuild.ToString":"Discord.Rpc.RpcGuild.yml","Discord.Rpc.RpcGuildStatus":"Discord.Rpc.RpcGuildStatus.yml","Discord.Rpc.RpcGuildStatus.Guild":"Discord.Rpc.RpcGuildStatus.yml","Discord.Rpc.RpcGuildStatus.Online":"Discord.Rpc.RpcGuildStatus.yml","Discord.Rpc.RpcGuildStatus.ToString":"Discord.Rpc.RpcGuildStatus.yml","Discord.Rpc.RpcGuildSummary":"Discord.Rpc.RpcGuildSummary.yml","Discord.Rpc.RpcGuildSummary.Id":"Discord.Rpc.RpcGuildSummary.yml","Discord.Rpc.RpcGuildSummary.Name":"Discord.Rpc.RpcGuildSummary.yml","Discord.Rpc.RpcGuildSummary.ToString":"Discord.Rpc.RpcGuildSummary.yml","Discord.Rpc.RpcMessage":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.Channel":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.Author":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.Content":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.AuthorColor":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.CreatedAt":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.IsTTS":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.IsPinned":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.IsBlocked":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.EditedTimestamp":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.Attachments":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.Embeds":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.MentionedChannelIds":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.MentionedRoleIds":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.MentionedUserIds":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.Tags":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.WebhookId":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.IsWebhook":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.Timestamp":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.DeleteAsync(RequestOptions)":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.ToString":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcSystemMessage":"Discord.Rpc.RpcSystemMessage.yml","Discord.Rpc.RpcSystemMessage.Type":"Discord.Rpc.RpcSystemMessage.yml","Discord.Rpc.RpcUserMessage":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.IsTTS":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.IsPinned":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.IsBlocked":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.WebhookId":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.EditedTimestamp":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.Attachments":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.Embeds":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.MentionedChannelIds":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.MentionedRoleIds":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.MentionedUserIds":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.Tags":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.Reactions":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.ModifyAsync(Action{MessageProperties},RequestOptions)":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.AddReactionAsync(Emoji,RequestOptions)":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.AddReactionAsync(System.String,RequestOptions)":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.RemoveReactionAsync(Emoji,IUser,RequestOptions)":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.RemoveReactionAsync(System.String,IUser,RequestOptions)":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.RemoveAllReactionsAsync(RequestOptions)":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.GetReactionUsersAsync(System.String,System.Int32,System.Nullable{System.UInt64},RequestOptions)":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.PinAsync(RequestOptions)":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.UnpinAsync(RequestOptions)":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.Resolve(System.Int32,TagHandling,TagHandling,TagHandling,TagHandling,TagHandling)":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.Resolve(TagHandling,TagHandling,TagHandling,TagHandling,TagHandling)":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.Pan":"Discord.Rpc.Pan.yml","Discord.Rpc.Pan.Left":"Discord.Rpc.Pan.yml","Discord.Rpc.Pan.Right":"Discord.Rpc.Pan.yml","Discord.Rpc.Pan.#ctor(System.Single,System.Single)":"Discord.Rpc.Pan.yml","Discord.Rpc.Pan.ToString":"Discord.Rpc.Pan.yml","Discord.Rpc.RpcGuildUser":"Discord.Rpc.RpcGuildUser.yml","Discord.Rpc.RpcGuildUser.Status":"Discord.Rpc.RpcGuildUser.yml","Discord.Rpc.RpcUser":"Discord.Rpc.RpcUser.yml","Discord.Rpc.RpcUser.IsBot":"Discord.Rpc.RpcUser.yml","Discord.Rpc.RpcUser.Username":"Discord.Rpc.RpcUser.yml","Discord.Rpc.RpcUser.DiscriminatorValue":"Discord.Rpc.RpcUser.yml","Discord.Rpc.RpcUser.AvatarId":"Discord.Rpc.RpcUser.yml","Discord.Rpc.RpcUser.AvatarUrl":"Discord.Rpc.RpcUser.yml","Discord.Rpc.RpcUser.CreatedAt":"Discord.Rpc.RpcUser.yml","Discord.Rpc.RpcUser.Discriminator":"Discord.Rpc.RpcUser.yml","Discord.Rpc.RpcUser.Mention":"Discord.Rpc.RpcUser.yml","Discord.Rpc.RpcUser.Game":"Discord.Rpc.RpcUser.yml","Discord.Rpc.RpcUser.Status":"Discord.Rpc.RpcUser.yml","Discord.Rpc.RpcUser.CreateDMChannelAsync(RequestOptions)":"Discord.Rpc.RpcUser.yml","Discord.Rpc.RpcUser.ToString":"Discord.Rpc.RpcUser.yml","Discord.Rpc.RpcVoiceState":"Discord.Rpc.RpcVoiceState.yml","Discord.Rpc.RpcVoiceState.User":"Discord.Rpc.RpcVoiceState.yml","Discord.Rpc.RpcVoiceState.Nickname":"Discord.Rpc.RpcVoiceState.yml","Discord.Rpc.RpcVoiceState.Volume":"Discord.Rpc.RpcVoiceState.yml","Discord.Rpc.RpcVoiceState.IsMuted2":"Discord.Rpc.RpcVoiceState.yml","Discord.Rpc.RpcVoiceState.Pan":"Discord.Rpc.RpcVoiceState.yml","Discord.Rpc.RpcVoiceState.IsMuted":"Discord.Rpc.RpcVoiceState.yml","Discord.Rpc.RpcVoiceState.IsDeafened":"Discord.Rpc.RpcVoiceState.yml","Discord.Rpc.RpcVoiceState.IsSuppressed":"Discord.Rpc.RpcVoiceState.yml","Discord.Rpc.RpcVoiceState.IsSelfMuted":"Discord.Rpc.RpcVoiceState.yml","Discord.Rpc.RpcVoiceState.IsSelfDeafened":"Discord.Rpc.RpcVoiceState.yml","Discord.Rpc.RpcVoiceState.ToString":"Discord.Rpc.RpcVoiceState.yml","Discord.Commands":"Discord.Commands.yml","Discord.Commands.RpcCommandContext":"Discord.Commands.RpcCommandContext.yml","Discord.Commands.RpcCommandContext.Client":"Discord.Commands.RpcCommandContext.yml","Discord.Commands.RpcCommandContext.Channel":"Discord.Commands.RpcCommandContext.yml","Discord.Commands.RpcCommandContext.User":"Discord.Commands.RpcCommandContext.yml","Discord.Commands.RpcCommandContext.Message":"Discord.Commands.RpcCommandContext.yml","Discord.Commands.RpcCommandContext.IsPrivate":"Discord.Commands.RpcCommandContext.yml","Discord.Commands.RpcCommandContext.#ctor(Discord.Rpc.DiscordRpcClient,Discord.Rpc.RpcUserMessage)":"Discord.Commands.RpcCommandContext.yml","Discord.Commands.ShardedCommandContext":"Discord.Commands.ShardedCommandContext.yml","Discord.Commands.ShardedCommandContext.Client":"Discord.Commands.ShardedCommandContext.yml","Discord.Commands.ShardedCommandContext.Guild":"Discord.Commands.ShardedCommandContext.yml","Discord.Commands.ShardedCommandContext.Channel":"Discord.Commands.ShardedCommandContext.yml","Discord.Commands.ShardedCommandContext.User":"Discord.Commands.ShardedCommandContext.yml","Discord.Commands.ShardedCommandContext.Message":"Discord.Commands.ShardedCommandContext.yml","Discord.Commands.ShardedCommandContext.IsPrivate":"Discord.Commands.ShardedCommandContext.yml","Discord.Commands.ShardedCommandContext.#ctor(Discord.WebSocket.DiscordShardedClient,Discord.WebSocket.SocketUserMessage)":"Discord.Commands.ShardedCommandContext.yml","Discord.Commands.SocketCommandContext":"Discord.Commands.SocketCommandContext.yml","Discord.Commands.SocketCommandContext.Client":"Discord.Commands.SocketCommandContext.yml","Discord.Commands.SocketCommandContext.Guild":"Discord.Commands.SocketCommandContext.yml","Discord.Commands.SocketCommandContext.Channel":"Discord.Commands.SocketCommandContext.yml","Discord.Commands.SocketCommandContext.User":"Discord.Commands.SocketCommandContext.yml","Discord.Commands.SocketCommandContext.Message":"Discord.Commands.SocketCommandContext.yml","Discord.Commands.SocketCommandContext.IsPrivate":"Discord.Commands.SocketCommandContext.yml","Discord.Commands.SocketCommandContext.#ctor(Discord.WebSocket.DiscordSocketClient,Discord.WebSocket.SocketUserMessage)":"Discord.Commands.SocketCommandContext.yml","Discord.Commands.CommandContext":"Discord.Commands.CommandContext.yml","Discord.Commands.CommandContext.Client":"Discord.Commands.CommandContext.yml","Discord.Commands.CommandContext.Guild":"Discord.Commands.CommandContext.yml","Discord.Commands.CommandContext.Channel":"Discord.Commands.CommandContext.yml","Discord.Commands.CommandContext.User":"Discord.Commands.CommandContext.yml","Discord.Commands.CommandContext.Message":"Discord.Commands.CommandContext.yml","Discord.Commands.CommandContext.IsPrivate":"Discord.Commands.CommandContext.yml","Discord.Commands.CommandContext.#ctor(IDiscordClient,IUserMessage)":"Discord.Commands.CommandContext.yml","Discord.Commands.CommandError":"Discord.Commands.CommandError.yml","Discord.Commands.CommandError.UnknownCommand":"Discord.Commands.CommandError.yml","Discord.Commands.CommandError.ParseFailed":"Discord.Commands.CommandError.yml","Discord.Commands.CommandError.BadArgCount":"Discord.Commands.CommandError.yml","Discord.Commands.CommandError.ObjectNotFound":"Discord.Commands.CommandError.yml","Discord.Commands.CommandError.MultipleMatches":"Discord.Commands.CommandError.yml","Discord.Commands.CommandError.UnmetPrecondition":"Discord.Commands.CommandError.yml","Discord.Commands.CommandError.Exception":"Discord.Commands.CommandError.yml","Discord.Commands.CommandMatch":"Discord.Commands.CommandMatch.yml","Discord.Commands.CommandMatch.Command":"Discord.Commands.CommandMatch.yml","Discord.Commands.CommandMatch.Alias":"Discord.Commands.CommandMatch.yml","Discord.Commands.CommandMatch.#ctor(Discord.Commands.CommandInfo,System.String)":"Discord.Commands.CommandMatch.yml","Discord.Commands.CommandMatch.CheckPreconditionsAsync(ICommandContext,Discord.Commands.IDependencyMap)":"Discord.Commands.CommandMatch.yml","Discord.Commands.CommandMatch.ParseAsync(ICommandContext,Discord.Commands.SearchResult,System.Nullable{Discord.Commands.PreconditionResult})":"Discord.Commands.CommandMatch.yml","Discord.Commands.CommandMatch.ExecuteAsync(ICommandContext,IEnumerable{System.Object},IEnumerable{System.Object},Discord.Commands.IDependencyMap)":"Discord.Commands.CommandMatch.yml","Discord.Commands.CommandMatch.ExecuteAsync(ICommandContext,Discord.Commands.ParseResult,Discord.Commands.IDependencyMap)":"Discord.Commands.CommandMatch.yml","Discord.Commands.CommandService":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.Modules":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.Commands":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.TypeReaders":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.#ctor":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.#ctor(Discord.Commands.CommandServiceConfig)":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.CreateModuleAsync(System.String,Action{Discord.Commands.Builders.ModuleBuilder})":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.AddModuleAsync``1":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.AddModulesAsync(Assembly)":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.RemoveModuleAsync(Discord.Commands.ModuleInfo)":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.RemoveModuleAsync``1":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.AddTypeReader``1(Discord.Commands.TypeReader)":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.AddTypeReader(Type,Discord.Commands.TypeReader)":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.Search(ICommandContext,System.Int32)":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.Search(ICommandContext,System.String)":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.ExecuteAsync(ICommandContext,System.Int32,Discord.Commands.IDependencyMap,Discord.Commands.MultiMatchHandling)":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.ExecuteAsync(ICommandContext,System.String,Discord.Commands.IDependencyMap,Discord.Commands.MultiMatchHandling)":"Discord.Commands.CommandService.yml","Discord.Commands.CommandServiceConfig":"Discord.Commands.CommandServiceConfig.yml","Discord.Commands.CommandServiceConfig.DefaultRunMode":"Discord.Commands.CommandServiceConfig.yml","Discord.Commands.CommandServiceConfig.SeparatorChar":"Discord.Commands.CommandServiceConfig.yml","Discord.Commands.CommandServiceConfig.CaseSensitiveCommands":"Discord.Commands.CommandServiceConfig.yml","Discord.Commands.ModuleBase":"Discord.Commands.ModuleBase.yml","Discord.Commands.ModuleBase`1":"Discord.Commands.ModuleBase-1.yml","Discord.Commands.ModuleBase`1.Context":"Discord.Commands.ModuleBase-1.yml","Discord.Commands.ModuleBase`1.ReplyAsync(System.String,System.Boolean,Embed,RequestOptions)":"Discord.Commands.ModuleBase-1.yml","Discord.Commands.MultiMatchHandling":"Discord.Commands.MultiMatchHandling.yml","Discord.Commands.MultiMatchHandling.Exception":"Discord.Commands.MultiMatchHandling.yml","Discord.Commands.MultiMatchHandling.Best":"Discord.Commands.MultiMatchHandling.yml","Discord.Commands.RunMode":"Discord.Commands.RunMode.yml","Discord.Commands.RunMode.Default":"Discord.Commands.RunMode.yml","Discord.Commands.RunMode.Sync":"Discord.Commands.RunMode.yml","Discord.Commands.RunMode.Mixed":"Discord.Commands.RunMode.yml","Discord.Commands.RunMode.Async":"Discord.Commands.RunMode.yml","Discord.Commands.AliasAttribute":"Discord.Commands.AliasAttribute.yml","Discord.Commands.AliasAttribute.Aliases":"Discord.Commands.AliasAttribute.yml","Discord.Commands.AliasAttribute.#ctor(System.String[])":"Discord.Commands.AliasAttribute.yml","Discord.Commands.CommandAttribute":"Discord.Commands.CommandAttribute.yml","Discord.Commands.CommandAttribute.Text":"Discord.Commands.CommandAttribute.yml","Discord.Commands.CommandAttribute.RunMode":"Discord.Commands.CommandAttribute.yml","Discord.Commands.CommandAttribute.#ctor":"Discord.Commands.CommandAttribute.yml","Discord.Commands.CommandAttribute.#ctor(System.String)":"Discord.Commands.CommandAttribute.yml","Discord.Commands.DontAutoLoadAttribute":"Discord.Commands.DontAutoLoadAttribute.yml","Discord.Commands.GroupAttribute":"Discord.Commands.GroupAttribute.yml","Discord.Commands.GroupAttribute.Prefix":"Discord.Commands.GroupAttribute.yml","Discord.Commands.GroupAttribute.#ctor":"Discord.Commands.GroupAttribute.yml","Discord.Commands.GroupAttribute.#ctor(System.String)":"Discord.Commands.GroupAttribute.yml","Discord.Commands.NameAttribute":"Discord.Commands.NameAttribute.yml","Discord.Commands.NameAttribute.Text":"Discord.Commands.NameAttribute.yml","Discord.Commands.NameAttribute.#ctor(System.String)":"Discord.Commands.NameAttribute.yml","Discord.Commands.OverrideTypeReaderAttribute":"Discord.Commands.OverrideTypeReaderAttribute.yml","Discord.Commands.OverrideTypeReaderAttribute.TypeReader":"Discord.Commands.OverrideTypeReaderAttribute.yml","Discord.Commands.OverrideTypeReaderAttribute.#ctor(Type)":"Discord.Commands.OverrideTypeReaderAttribute.yml","Discord.Commands.ParameterPreconditionAttribute":"Discord.Commands.ParameterPreconditionAttribute.yml","Discord.Commands.ParameterPreconditionAttribute.CheckPermissions(ICommandContext,Discord.Commands.ParameterInfo,System.Object,Discord.Commands.IDependencyMap)":"Discord.Commands.ParameterPreconditionAttribute.yml","Discord.Commands.PreconditionAttribute":"Discord.Commands.PreconditionAttribute.yml","Discord.Commands.PreconditionAttribute.CheckPermissions(ICommandContext,Discord.Commands.CommandInfo,Discord.Commands.IDependencyMap)":"Discord.Commands.PreconditionAttribute.yml","Discord.Commands.PriorityAttribute":"Discord.Commands.PriorityAttribute.yml","Discord.Commands.PriorityAttribute.Priority":"Discord.Commands.PriorityAttribute.yml","Discord.Commands.PriorityAttribute.#ctor(System.Int32)":"Discord.Commands.PriorityAttribute.yml","Discord.Commands.RemainderAttribute":"Discord.Commands.RemainderAttribute.yml","Discord.Commands.RemarksAttribute":"Discord.Commands.RemarksAttribute.yml","Discord.Commands.RemarksAttribute.Text":"Discord.Commands.RemarksAttribute.yml","Discord.Commands.RemarksAttribute.#ctor(System.String)":"Discord.Commands.RemarksAttribute.yml","Discord.Commands.SummaryAttribute":"Discord.Commands.SummaryAttribute.yml","Discord.Commands.SummaryAttribute.Text":"Discord.Commands.SummaryAttribute.yml","Discord.Commands.SummaryAttribute.#ctor(System.String)":"Discord.Commands.SummaryAttribute.yml","Discord.Commands.RequireBotPermissionAttribute":"Discord.Commands.RequireBotPermissionAttribute.yml","Discord.Commands.RequireBotPermissionAttribute.GuildPermission":"Discord.Commands.RequireBotPermissionAttribute.yml","Discord.Commands.RequireBotPermissionAttribute.ChannelPermission":"Discord.Commands.RequireBotPermissionAttribute.yml","Discord.Commands.RequireBotPermissionAttribute.#ctor(GuildPermission)":"Discord.Commands.RequireBotPermissionAttribute.yml","Discord.Commands.RequireBotPermissionAttribute.#ctor(ChannelPermission)":"Discord.Commands.RequireBotPermissionAttribute.yml","Discord.Commands.RequireBotPermissionAttribute.CheckPermissions(ICommandContext,Discord.Commands.CommandInfo,Discord.Commands.IDependencyMap)":"Discord.Commands.RequireBotPermissionAttribute.yml","Discord.Commands.ContextType":"Discord.Commands.ContextType.yml","Discord.Commands.ContextType.Guild":"Discord.Commands.ContextType.yml","Discord.Commands.ContextType.DM":"Discord.Commands.ContextType.yml","Discord.Commands.ContextType.Group":"Discord.Commands.ContextType.yml","Discord.Commands.RequireContextAttribute":"Discord.Commands.RequireContextAttribute.yml","Discord.Commands.RequireContextAttribute.Contexts":"Discord.Commands.RequireContextAttribute.yml","Discord.Commands.RequireContextAttribute.#ctor(Discord.Commands.ContextType)":"Discord.Commands.RequireContextAttribute.yml","Discord.Commands.RequireContextAttribute.CheckPermissions(ICommandContext,Discord.Commands.CommandInfo,Discord.Commands.IDependencyMap)":"Discord.Commands.RequireContextAttribute.yml","Discord.Commands.RequireOwnerAttribute":"Discord.Commands.RequireOwnerAttribute.yml","Discord.Commands.RequireOwnerAttribute.CheckPermissions(ICommandContext,Discord.Commands.CommandInfo,Discord.Commands.IDependencyMap)":"Discord.Commands.RequireOwnerAttribute.yml","Discord.Commands.RequireUserPermissionAttribute":"Discord.Commands.RequireUserPermissionAttribute.yml","Discord.Commands.RequireUserPermissionAttribute.GuildPermission":"Discord.Commands.RequireUserPermissionAttribute.yml","Discord.Commands.RequireUserPermissionAttribute.ChannelPermission":"Discord.Commands.RequireUserPermissionAttribute.yml","Discord.Commands.RequireUserPermissionAttribute.#ctor(GuildPermission)":"Discord.Commands.RequireUserPermissionAttribute.yml","Discord.Commands.RequireUserPermissionAttribute.#ctor(ChannelPermission)":"Discord.Commands.RequireUserPermissionAttribute.yml","Discord.Commands.RequireUserPermissionAttribute.CheckPermissions(ICommandContext,Discord.Commands.CommandInfo,Discord.Commands.IDependencyMap)":"Discord.Commands.RequireUserPermissionAttribute.yml","Discord.Commands.DependencyMap":"Discord.Commands.DependencyMap.yml","Discord.Commands.DependencyMap.Empty":"Discord.Commands.DependencyMap.yml","Discord.Commands.DependencyMap.#ctor":"Discord.Commands.DependencyMap.yml","Discord.Commands.DependencyMap.Add``1(``0)":"Discord.Commands.DependencyMap.yml","Discord.Commands.DependencyMap.Get``1":"Discord.Commands.DependencyMap.yml","Discord.Commands.DependencyMap.Get(Type)":"Discord.Commands.DependencyMap.yml","Discord.Commands.DependencyMap.TryGet``1(``0@)":"Discord.Commands.DependencyMap.yml","Discord.Commands.DependencyMap.TryGet(Type,System.Object@)":"Discord.Commands.DependencyMap.yml","Discord.Commands.IDependencyMap":"Discord.Commands.IDependencyMap.yml","Discord.Commands.IDependencyMap.Add``1(``0)":"Discord.Commands.IDependencyMap.yml","Discord.Commands.IDependencyMap.Get``1":"Discord.Commands.IDependencyMap.yml","Discord.Commands.IDependencyMap.TryGet``1(``0@)":"Discord.Commands.IDependencyMap.yml","Discord.Commands.IDependencyMap.Get(Type)":"Discord.Commands.IDependencyMap.yml","Discord.Commands.IDependencyMap.TryGet(Type,System.Object@)":"Discord.Commands.IDependencyMap.yml","Discord.Commands.IEnumerableExtensions":"Discord.Commands.IEnumerableExtensions.yml","Discord.Commands.IEnumerableExtensions.Permutate``3(IEnumerable{``0},IEnumerable{``1},Func{``0,``1,``2})":"Discord.Commands.IEnumerableExtensions.yml","Discord.Commands.MessageExtensions":"Discord.Commands.MessageExtensions.yml","Discord.Commands.MessageExtensions.HasCharPrefix(IUserMessage,System.Char,System.Int32@)":"Discord.Commands.MessageExtensions.yml","Discord.Commands.MessageExtensions.HasStringPrefix(IUserMessage,System.String,System.Int32@)":"Discord.Commands.MessageExtensions.yml","Discord.Commands.MessageExtensions.HasMentionPrefix(IUserMessage,IUser,System.Int32@)":"Discord.Commands.MessageExtensions.yml","Discord.Commands.CommandInfo":"Discord.Commands.CommandInfo.yml","Discord.Commands.CommandInfo.Module":"Discord.Commands.CommandInfo.yml","Discord.Commands.CommandInfo.Name":"Discord.Commands.CommandInfo.yml","Discord.Commands.CommandInfo.Summary":"Discord.Commands.CommandInfo.yml","Discord.Commands.CommandInfo.Remarks":"Discord.Commands.CommandInfo.yml","Discord.Commands.CommandInfo.Priority":"Discord.Commands.CommandInfo.yml","Discord.Commands.CommandInfo.HasVarArgs":"Discord.Commands.CommandInfo.yml","Discord.Commands.CommandInfo.RunMode":"Discord.Commands.CommandInfo.yml","Discord.Commands.CommandInfo.Aliases":"Discord.Commands.CommandInfo.yml","Discord.Commands.CommandInfo.Parameters":"Discord.Commands.CommandInfo.yml","Discord.Commands.CommandInfo.Preconditions":"Discord.Commands.CommandInfo.yml","Discord.Commands.CommandInfo.CheckPreconditionsAsync(ICommandContext,Discord.Commands.IDependencyMap)":"Discord.Commands.CommandInfo.yml","Discord.Commands.CommandInfo.ParseAsync(ICommandContext,System.Int32,Discord.Commands.SearchResult,System.Nullable{Discord.Commands.PreconditionResult})":"Discord.Commands.CommandInfo.yml","Discord.Commands.CommandInfo.ExecuteAsync(ICommandContext,Discord.Commands.ParseResult,Discord.Commands.IDependencyMap)":"Discord.Commands.CommandInfo.yml","Discord.Commands.CommandInfo.ExecuteAsync(ICommandContext,IEnumerable{System.Object},IEnumerable{System.Object},Discord.Commands.IDependencyMap)":"Discord.Commands.CommandInfo.yml","Discord.Commands.ModuleInfo":"Discord.Commands.ModuleInfo.yml","Discord.Commands.ModuleInfo.Service":"Discord.Commands.ModuleInfo.yml","Discord.Commands.ModuleInfo.Name":"Discord.Commands.ModuleInfo.yml","Discord.Commands.ModuleInfo.Summary":"Discord.Commands.ModuleInfo.yml","Discord.Commands.ModuleInfo.Remarks":"Discord.Commands.ModuleInfo.yml","Discord.Commands.ModuleInfo.Aliases":"Discord.Commands.ModuleInfo.yml","Discord.Commands.ModuleInfo.Commands":"Discord.Commands.ModuleInfo.yml","Discord.Commands.ModuleInfo.Preconditions":"Discord.Commands.ModuleInfo.yml","Discord.Commands.ModuleInfo.Submodules":"Discord.Commands.ModuleInfo.yml","Discord.Commands.ModuleInfo.Parent":"Discord.Commands.ModuleInfo.yml","Discord.Commands.ModuleInfo.IsSubmodule":"Discord.Commands.ModuleInfo.yml","Discord.Commands.ParameterInfo":"Discord.Commands.ParameterInfo.yml","Discord.Commands.ParameterInfo.Command":"Discord.Commands.ParameterInfo.yml","Discord.Commands.ParameterInfo.Name":"Discord.Commands.ParameterInfo.yml","Discord.Commands.ParameterInfo.Summary":"Discord.Commands.ParameterInfo.yml","Discord.Commands.ParameterInfo.IsOptional":"Discord.Commands.ParameterInfo.yml","Discord.Commands.ParameterInfo.IsRemainder":"Discord.Commands.ParameterInfo.yml","Discord.Commands.ParameterInfo.IsMultiple":"Discord.Commands.ParameterInfo.yml","Discord.Commands.ParameterInfo.Type":"Discord.Commands.ParameterInfo.yml","Discord.Commands.ParameterInfo.DefaultValue":"Discord.Commands.ParameterInfo.yml","Discord.Commands.ParameterInfo.Preconditions":"Discord.Commands.ParameterInfo.yml","Discord.Commands.ParameterInfo.CheckPreconditionsAsync(ICommandContext,System.Object[],Discord.Commands.IDependencyMap)":"Discord.Commands.ParameterInfo.yml","Discord.Commands.ParameterInfo.Parse(ICommandContext,System.String)":"Discord.Commands.ParameterInfo.yml","Discord.Commands.ParameterInfo.ToString":"Discord.Commands.ParameterInfo.yml","Discord.Commands.TypeReader":"Discord.Commands.TypeReader.yml","Discord.Commands.TypeReader.Read(ICommandContext,System.String)":"Discord.Commands.TypeReader.yml","Discord.Commands.ExecuteResult":"Discord.Commands.ExecuteResult.yml","Discord.Commands.ExecuteResult.Exception":"Discord.Commands.ExecuteResult.yml","Discord.Commands.ExecuteResult.Error":"Discord.Commands.ExecuteResult.yml","Discord.Commands.ExecuteResult.ErrorReason":"Discord.Commands.ExecuteResult.yml","Discord.Commands.ExecuteResult.IsSuccess":"Discord.Commands.ExecuteResult.yml","Discord.Commands.ExecuteResult.FromSuccess":"Discord.Commands.ExecuteResult.yml","Discord.Commands.ExecuteResult.FromError(Discord.Commands.CommandError,System.String)":"Discord.Commands.ExecuteResult.yml","Discord.Commands.ExecuteResult.FromError(Exception)":"Discord.Commands.ExecuteResult.yml","Discord.Commands.ExecuteResult.FromError(Discord.Commands.IResult)":"Discord.Commands.ExecuteResult.yml","Discord.Commands.ExecuteResult.ToString":"Discord.Commands.ExecuteResult.yml","Discord.Commands.IResult":"Discord.Commands.IResult.yml","Discord.Commands.IResult.Error":"Discord.Commands.IResult.yml","Discord.Commands.IResult.ErrorReason":"Discord.Commands.IResult.yml","Discord.Commands.IResult.IsSuccess":"Discord.Commands.IResult.yml","Discord.Commands.ParseResult":"Discord.Commands.ParseResult.yml","Discord.Commands.ParseResult.ArgValues":"Discord.Commands.ParseResult.yml","Discord.Commands.ParseResult.ParamValues":"Discord.Commands.ParseResult.yml","Discord.Commands.ParseResult.Error":"Discord.Commands.ParseResult.yml","Discord.Commands.ParseResult.ErrorReason":"Discord.Commands.ParseResult.yml","Discord.Commands.ParseResult.IsSuccess":"Discord.Commands.ParseResult.yml","Discord.Commands.ParseResult.FromSuccess(IReadOnlyList{Discord.Commands.TypeReaderResult},IReadOnlyList{Discord.Commands.TypeReaderResult})":"Discord.Commands.ParseResult.yml","Discord.Commands.ParseResult.FromSuccess(IReadOnlyList{Discord.Commands.TypeReaderValue},IReadOnlyList{Discord.Commands.TypeReaderValue})":"Discord.Commands.ParseResult.yml","Discord.Commands.ParseResult.FromError(Discord.Commands.CommandError,System.String)":"Discord.Commands.ParseResult.yml","Discord.Commands.ParseResult.FromError(Discord.Commands.IResult)":"Discord.Commands.ParseResult.yml","Discord.Commands.ParseResult.ToString":"Discord.Commands.ParseResult.yml","Discord.Commands.PreconditionResult":"Discord.Commands.PreconditionResult.yml","Discord.Commands.PreconditionResult.Error":"Discord.Commands.PreconditionResult.yml","Discord.Commands.PreconditionResult.ErrorReason":"Discord.Commands.PreconditionResult.yml","Discord.Commands.PreconditionResult.IsSuccess":"Discord.Commands.PreconditionResult.yml","Discord.Commands.PreconditionResult.FromSuccess":"Discord.Commands.PreconditionResult.yml","Discord.Commands.PreconditionResult.FromError(System.String)":"Discord.Commands.PreconditionResult.yml","Discord.Commands.PreconditionResult.FromError(Discord.Commands.IResult)":"Discord.Commands.PreconditionResult.yml","Discord.Commands.PreconditionResult.ToString":"Discord.Commands.PreconditionResult.yml","Discord.Commands.SearchResult":"Discord.Commands.SearchResult.yml","Discord.Commands.SearchResult.Text":"Discord.Commands.SearchResult.yml","Discord.Commands.SearchResult.Commands":"Discord.Commands.SearchResult.yml","Discord.Commands.SearchResult.Error":"Discord.Commands.SearchResult.yml","Discord.Commands.SearchResult.ErrorReason":"Discord.Commands.SearchResult.yml","Discord.Commands.SearchResult.IsSuccess":"Discord.Commands.SearchResult.yml","Discord.Commands.SearchResult.FromSuccess(System.String,IReadOnlyList{Discord.Commands.CommandMatch})":"Discord.Commands.SearchResult.yml","Discord.Commands.SearchResult.FromError(Discord.Commands.CommandError,System.String)":"Discord.Commands.SearchResult.yml","Discord.Commands.SearchResult.FromError(Discord.Commands.IResult)":"Discord.Commands.SearchResult.yml","Discord.Commands.SearchResult.ToString":"Discord.Commands.SearchResult.yml","Discord.Commands.TypeReaderValue":"Discord.Commands.TypeReaderValue.yml","Discord.Commands.TypeReaderValue.Value":"Discord.Commands.TypeReaderValue.yml","Discord.Commands.TypeReaderValue.Score":"Discord.Commands.TypeReaderValue.yml","Discord.Commands.TypeReaderValue.#ctor(System.Object,System.Single)":"Discord.Commands.TypeReaderValue.yml","Discord.Commands.TypeReaderValue.ToString":"Discord.Commands.TypeReaderValue.yml","Discord.Commands.TypeReaderResult":"Discord.Commands.TypeReaderResult.yml","Discord.Commands.TypeReaderResult.Values":"Discord.Commands.TypeReaderResult.yml","Discord.Commands.TypeReaderResult.Error":"Discord.Commands.TypeReaderResult.yml","Discord.Commands.TypeReaderResult.ErrorReason":"Discord.Commands.TypeReaderResult.yml","Discord.Commands.TypeReaderResult.IsSuccess":"Discord.Commands.TypeReaderResult.yml","Discord.Commands.TypeReaderResult.FromSuccess(System.Object)":"Discord.Commands.TypeReaderResult.yml","Discord.Commands.TypeReaderResult.FromSuccess(Discord.Commands.TypeReaderValue)":"Discord.Commands.TypeReaderResult.yml","Discord.Commands.TypeReaderResult.FromSuccess(IReadOnlyCollection{Discord.Commands.TypeReaderValue})":"Discord.Commands.TypeReaderResult.yml","Discord.Commands.TypeReaderResult.FromError(Discord.Commands.CommandError,System.String)":"Discord.Commands.TypeReaderResult.yml","Discord.Commands.TypeReaderResult.FromError(Discord.Commands.IResult)":"Discord.Commands.TypeReaderResult.yml","Discord.Commands.TypeReaderResult.ToString":"Discord.Commands.TypeReaderResult.yml","Discord.Commands.ICommandContext":"Discord.Commands.ICommandContext.yml","Discord.Commands.ICommandContext.Client":"Discord.Commands.ICommandContext.yml","Discord.Commands.ICommandContext.Guild":"Discord.Commands.ICommandContext.yml","Discord.Commands.ICommandContext.Channel":"Discord.Commands.ICommandContext.yml","Discord.Commands.ICommandContext.User":"Discord.Commands.ICommandContext.yml","Discord.Commands.ICommandContext.Message":"Discord.Commands.ICommandContext.yml","Discord.WebSocket":"Discord.WebSocket.yml","Discord.WebSocket.DiscordShardedClient":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.Latency":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.CurrentUser":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.Guilds":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.PrivateChannels":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.Shards":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.VoiceRegions":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.#ctor":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.#ctor(Discord.WebSocket.DiscordSocketConfig)":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.#ctor(System.Int32[])":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.#ctor(System.Int32[],Discord.WebSocket.DiscordSocketConfig)":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.OnLoginAsync(TokenType,System.String)":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.OnLogoutAsync":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.ConnectAsync(System.Boolean)":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.DisconnectAsync":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.GetShard(System.Int32)":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.GetApplicationInfoAsync":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.GetGuild(System.UInt64)":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.CreateGuildAsync(System.String,IVoiceRegion,Stream)":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.GetChannel(System.UInt64)":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.GetConnectionsAsync":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.GetInviteAsync(System.String)":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.GetUser(System.UInt64)":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.GetUser(System.String,System.String)":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.GetVoiceRegion(System.String)":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.DownloadAllUsersAsync":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.DownloadUsersAsync(IEnumerable{Discord.WebSocket.SocketGuild})":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.SetStatusAsync(UserStatus)":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.SetGameAsync(System.String,System.String,StreamType)":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.ChannelCreated":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.ChannelDestroyed":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.ChannelUpdated":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.MessageReceived":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.MessageDeleted":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.MessageUpdated":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.ReactionAdded":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.ReactionRemoved":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.ReactionsCleared":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.RoleCreated":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.RoleDeleted":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.RoleUpdated":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.JoinedGuild":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.LeftGuild":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.GuildAvailable":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.GuildUnavailable":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.GuildMembersDownloaded":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.GuildUpdated":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.UserJoined":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.UserLeft":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.UserBanned":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.UserUnbanned":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.UserUpdated":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.GuildMemberUpdated":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.UserPresenceUpdated":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.UserVoiceStateUpdated":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.CurrentUserUpdated":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.UserIsTyping":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.RecipientAdded":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.RecipientRemoved":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordSocketClient":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.ShardId":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.ConnectionState":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.Latency":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.CurrentUser":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.Guilds":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.PrivateChannels":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.VoiceRegions":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.#ctor":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.#ctor(Discord.WebSocket.DiscordSocketConfig)":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.OnLoginAsync(TokenType,System.String)":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.OnLogoutAsync":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.ConnectAsync(System.Boolean)":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.DisconnectAsync":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GetApplicationInfoAsync":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GetGuild(System.UInt64)":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.CreateGuildAsync(System.String,IVoiceRegion,Stream)":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GetChannel(System.UInt64)":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GetConnectionsAsync":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GetInviteAsync(System.String)":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GetUser(System.UInt64)":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GetUser(System.String,System.String)":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GetVoiceRegion(System.String)":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.DownloadAllUsersAsync":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.DownloadUsersAsync(IEnumerable{Discord.WebSocket.SocketGuild})":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.SetStatusAsync(UserStatus)":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.SetGameAsync(System.String,System.String,StreamType)":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.Connected":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.Disconnected":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.Ready":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.LatencyUpdated":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.ChannelCreated":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.ChannelDestroyed":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.ChannelUpdated":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.MessageReceived":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.MessageDeleted":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.MessageUpdated":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.ReactionAdded":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.ReactionRemoved":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.ReactionsCleared":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.RoleCreated":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.RoleDeleted":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.RoleUpdated":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.JoinedGuild":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.LeftGuild":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GuildAvailable":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GuildUnavailable":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GuildMembersDownloaded":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GuildUpdated":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.UserJoined":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.UserLeft":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.UserBanned":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.UserUnbanned":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.UserUpdated":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GuildMemberUpdated":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.UserPresenceUpdated":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.UserVoiceStateUpdated":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.CurrentUserUpdated":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.UserIsTyping":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.RecipientAdded":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.RecipientRemoved":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketConfig":"Discord.WebSocket.DiscordSocketConfig.yml","Discord.WebSocket.DiscordSocketConfig.GatewayEncoding":"Discord.WebSocket.DiscordSocketConfig.yml","Discord.WebSocket.DiscordSocketConfig.ConnectionTimeout":"Discord.WebSocket.DiscordSocketConfig.yml","Discord.WebSocket.DiscordSocketConfig.ShardId":"Discord.WebSocket.DiscordSocketConfig.yml","Discord.WebSocket.DiscordSocketConfig.TotalShards":"Discord.WebSocket.DiscordSocketConfig.yml","Discord.WebSocket.DiscordSocketConfig.MessageCacheSize":"Discord.WebSocket.DiscordSocketConfig.yml","Discord.WebSocket.DiscordSocketConfig.LargeThreshold":"Discord.WebSocket.DiscordSocketConfig.yml","Discord.WebSocket.DiscordSocketConfig.AudioMode":"Discord.WebSocket.DiscordSocketConfig.yml","Discord.WebSocket.DiscordSocketConfig.WebSocketProvider":"Discord.WebSocket.DiscordSocketConfig.yml","Discord.WebSocket.DiscordSocketConfig.UdpSocketProvider":"Discord.WebSocket.DiscordSocketConfig.yml","Discord.WebSocket.DiscordSocketConfig.DownloadUsersOnGuildAvailable":"Discord.WebSocket.DiscordSocketConfig.yml","Discord.WebSocket.DiscordSocketConfig.#ctor":"Discord.WebSocket.DiscordSocketConfig.yml","Discord.WebSocket.SocketEntity`1":"Discord.WebSocket.SocketEntity-1.yml","Discord.WebSocket.SocketEntity`1.Discord":"Discord.WebSocket.SocketEntity-1.yml","Discord.WebSocket.SocketEntity`1.Id":"Discord.WebSocket.SocketEntity-1.yml","Discord.WebSocket.ISocketAudioChannel":"Discord.WebSocket.ISocketAudioChannel.yml","Discord.WebSocket.ISocketAudioChannel.ConnectAsync":"Discord.WebSocket.ISocketAudioChannel.yml","Discord.WebSocket.ISocketMessageChannel":"Discord.WebSocket.ISocketMessageChannel.yml","Discord.WebSocket.ISocketMessageChannel.CachedMessages":"Discord.WebSocket.ISocketMessageChannel.yml","Discord.WebSocket.ISocketMessageChannel.SendMessageAsync(System.String,System.Boolean,Embed,RequestOptions)":"Discord.WebSocket.ISocketMessageChannel.yml","Discord.WebSocket.ISocketMessageChannel.SendFileAsync(Stream,System.String,System.String,System.Boolean,RequestOptions)":"Discord.WebSocket.ISocketMessageChannel.yml","Discord.WebSocket.ISocketMessageChannel.GetCachedMessage(System.UInt64)":"Discord.WebSocket.ISocketMessageChannel.yml","Discord.WebSocket.ISocketMessageChannel.GetCachedMessages(System.Int32)":"Discord.WebSocket.ISocketMessageChannel.yml","Discord.WebSocket.ISocketMessageChannel.GetCachedMessages(System.UInt64,Direction,System.Int32)":"Discord.WebSocket.ISocketMessageChannel.yml","Discord.WebSocket.ISocketMessageChannel.GetCachedMessages(IMessage,Direction,System.Int32)":"Discord.WebSocket.ISocketMessageChannel.yml","Discord.WebSocket.ISocketMessageChannel.GetPinnedMessagesAsync(RequestOptions)":"Discord.WebSocket.ISocketMessageChannel.yml","Discord.WebSocket.ISocketPrivateChannel":"Discord.WebSocket.ISocketPrivateChannel.yml","Discord.WebSocket.ISocketPrivateChannel.Recipients":"Discord.WebSocket.ISocketPrivateChannel.yml","Discord.WebSocket.SocketChannel":"Discord.WebSocket.SocketChannel.yml","Discord.WebSocket.SocketChannel.CreatedAt":"Discord.WebSocket.SocketChannel.yml","Discord.WebSocket.SocketChannel.Users":"Discord.WebSocket.SocketChannel.yml","Discord.WebSocket.SocketChannel.GetUser(System.UInt64)":"Discord.WebSocket.SocketChannel.yml","Discord.WebSocket.SocketDMChannel":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.Recipient":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.CachedMessages":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.Users":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.CloseAsync(RequestOptions)":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.GetCachedMessage(System.UInt64)":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.GetMessageAsync(System.UInt64,RequestOptions)":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.GetMessagesAsync(System.Int32,RequestOptions)":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.GetMessagesAsync(System.UInt64,Direction,System.Int32,RequestOptions)":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.GetMessagesAsync(IMessage,Direction,System.Int32,RequestOptions)":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.GetCachedMessages(System.Int32)":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.GetCachedMessages(System.UInt64,Direction,System.Int32)":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.GetCachedMessages(IMessage,Direction,System.Int32)":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.GetPinnedMessagesAsync(RequestOptions)":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.SendMessageAsync(System.String,System.Boolean,Embed,RequestOptions)":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.SendFileAsync(Stream,System.String,System.String,System.Boolean,RequestOptions)":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.DeleteMessagesAsync(IEnumerable{IMessage},RequestOptions)":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.TriggerTypingAsync(RequestOptions)":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.EnterTypingState(RequestOptions)":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.GetUser(System.UInt64)":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.ToString":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.Discord#WebSocket#ISocketPrivateChannel#Recipients":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketGroupChannel":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.Name":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.CachedMessages":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.Users":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.Recipients":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.LeaveAsync(RequestOptions)":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.ConnectAsync":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.GetCachedMessage(System.UInt64)":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.GetMessageAsync(System.UInt64,RequestOptions)":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.GetMessagesAsync(System.Int32,RequestOptions)":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.GetMessagesAsync(System.UInt64,Direction,System.Int32,RequestOptions)":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.GetMessagesAsync(IMessage,Direction,System.Int32,RequestOptions)":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.GetCachedMessages(System.Int32)":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.GetCachedMessages(System.UInt64,Direction,System.Int32)":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.GetCachedMessages(IMessage,Direction,System.Int32)":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.GetPinnedMessagesAsync(RequestOptions)":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.SendMessageAsync(System.String,System.Boolean,Embed,RequestOptions)":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.SendFileAsync(Stream,System.String,System.String,System.Boolean,RequestOptions)":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.DeleteMessagesAsync(IEnumerable{IMessage},RequestOptions)":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.TriggerTypingAsync(RequestOptions)":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.EnterTypingState(RequestOptions)":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.GetUser(System.UInt64)":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.ToString":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.Discord#WebSocket#ISocketPrivateChannel#Recipients":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGuildChannel":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.Guild":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.Name":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.Position":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.PermissionOverwrites":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.Users":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.ModifyAsync(Action{GuildChannelProperties},RequestOptions)":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.DeleteAsync(RequestOptions)":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.GetPermissionOverwrite(IUser)":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.GetPermissionOverwrite(IRole)":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.AddPermissionOverwriteAsync(IUser,OverwritePermissions,RequestOptions)":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.AddPermissionOverwriteAsync(IRole,OverwritePermissions,RequestOptions)":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.RemovePermissionOverwriteAsync(IUser,RequestOptions)":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.RemovePermissionOverwriteAsync(IRole,RequestOptions)":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.GetInvitesAsync(RequestOptions)":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.CreateInviteAsync(System.Nullable{System.Int32},System.Nullable{System.Int32},System.Boolean,RequestOptions)":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.GetUser(System.UInt64)":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.ToString":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketTextChannel":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.Topic":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.Mention":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.CachedMessages":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.Users":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.ModifyAsync(Action{TextChannelProperties},RequestOptions)":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.GetCachedMessage(System.UInt64)":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.GetMessageAsync(System.UInt64,RequestOptions)":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.GetMessagesAsync(System.Int32,RequestOptions)":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.GetMessagesAsync(System.UInt64,Direction,System.Int32,RequestOptions)":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.GetMessagesAsync(IMessage,Direction,System.Int32,RequestOptions)":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.GetCachedMessages(System.Int32)":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.GetCachedMessages(System.UInt64,Direction,System.Int32)":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.GetCachedMessages(IMessage,Direction,System.Int32)":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.GetPinnedMessagesAsync(RequestOptions)":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.SendMessageAsync(System.String,System.Boolean,Embed,RequestOptions)":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.SendFileAsync(Stream,System.String,System.String,System.Boolean,RequestOptions)":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.DeleteMessagesAsync(IEnumerable{IMessage},RequestOptions)":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.TriggerTypingAsync(RequestOptions)":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.EnterTypingState(RequestOptions)":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.GetUser(System.UInt64)":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketVoiceChannel":"Discord.WebSocket.SocketVoiceChannel.yml","Discord.WebSocket.SocketVoiceChannel.Bitrate":"Discord.WebSocket.SocketVoiceChannel.yml","Discord.WebSocket.SocketVoiceChannel.UserLimit":"Discord.WebSocket.SocketVoiceChannel.yml","Discord.WebSocket.SocketVoiceChannel.Users":"Discord.WebSocket.SocketVoiceChannel.yml","Discord.WebSocket.SocketVoiceChannel.ModifyAsync(Action{VoiceChannelProperties},RequestOptions)":"Discord.WebSocket.SocketVoiceChannel.yml","Discord.WebSocket.SocketVoiceChannel.ConnectAsync":"Discord.WebSocket.SocketVoiceChannel.yml","Discord.WebSocket.SocketVoiceChannel.GetUser(System.UInt64)":"Discord.WebSocket.SocketVoiceChannel.yml","Discord.WebSocket.SocketGuild":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.Name":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.AFKTimeout":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.IsEmbeddable":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.VerificationLevel":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.MfaLevel":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.DefaultMessageNotifications":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.MemberCount":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.DownloadedMemberCount":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.AFKChannelId":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.EmbedChannelId":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.OwnerId":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.VoiceRegionId":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.IconId":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.SplashId":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.CreatedAt":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.DefaultChannelId":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.IconUrl":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.SplashUrl":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.HasAllMembers":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.IsSynced":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.SyncPromise":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.DownloaderPromise":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.AudioClient":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.CurrentUser":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.EveryoneRole":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.Channels":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.Emojis":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.Features":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.Users":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.Roles":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.DeleteAsync(RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.ModifyAsync(Action{GuildProperties},RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.ModifyEmbedAsync(Action{GuildEmbedProperties},RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.ModifyChannelsAsync(IEnumerable{BulkGuildChannelProperties},RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.ModifyRolesAsync(IEnumerable{BulkRoleProperties},RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.LeaveAsync(RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.GetBansAsync(RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.AddBanAsync(IUser,System.Int32,RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.AddBanAsync(System.UInt64,System.Int32,RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.RemoveBanAsync(IUser,RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.RemoveBanAsync(System.UInt64,RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.GetChannel(System.UInt64)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.CreateTextChannelAsync(System.String,RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.CreateVoiceChannelAsync(System.String,RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.GetIntegrationsAsync(RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.CreateIntegrationAsync(System.UInt64,System.String,RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.GetInvitesAsync(RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.GetRole(System.UInt64)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.CreateRoleAsync(System.String,System.Nullable{GuildPermissions},System.Nullable{Color},System.Boolean,RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.GetUser(System.UInt64)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.PruneUsersAsync(System.Int32,System.Boolean,RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.DownloadUsersAsync":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.ToString":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketMessage":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.Author":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.Channel":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.Content":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.CreatedAt":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.IsTTS":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.IsPinned":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.EditedTimestamp":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.Attachments":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.Embeds":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.MentionedChannels":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.MentionedRoles":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.MentionedUsers":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.Tags":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.WebhookId":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.IsWebhook":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.Timestamp":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.DeleteAsync(RequestOptions)":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.ToString":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketReaction":"Discord.WebSocket.SocketReaction.yml","Discord.WebSocket.SocketReaction.UserId":"Discord.WebSocket.SocketReaction.yml","Discord.WebSocket.SocketReaction.User":"Discord.WebSocket.SocketReaction.yml","Discord.WebSocket.SocketReaction.MessageId":"Discord.WebSocket.SocketReaction.yml","Discord.WebSocket.SocketReaction.Message":"Discord.WebSocket.SocketReaction.yml","Discord.WebSocket.SocketReaction.Channel":"Discord.WebSocket.SocketReaction.yml","Discord.WebSocket.SocketReaction.Emoji":"Discord.WebSocket.SocketReaction.yml","Discord.WebSocket.SocketUserMessage":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.IsTTS":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.IsPinned":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.WebhookId":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.EditedTimestamp":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.Attachments":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.Embeds":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.Tags":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.MentionedChannels":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.MentionedRoles":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.MentionedUsers":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.Reactions":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.ModifyAsync(Action{MessageProperties},RequestOptions)":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.AddReactionAsync(Emoji,RequestOptions)":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.AddReactionAsync(System.String,RequestOptions)":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.RemoveReactionAsync(Emoji,IUser,RequestOptions)":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.RemoveReactionAsync(System.String,IUser,RequestOptions)":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.RemoveAllReactionsAsync(RequestOptions)":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.GetReactionUsersAsync(System.String,System.Int32,System.Nullable{System.UInt64},RequestOptions)":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.PinAsync(RequestOptions)":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.UnpinAsync(RequestOptions)":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.Resolve(System.Int32,TagHandling,TagHandling,TagHandling,TagHandling,TagHandling)":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.Resolve(TagHandling,TagHandling,TagHandling,TagHandling,TagHandling)":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketRole":"Discord.WebSocket.SocketRole.yml","Discord.WebSocket.SocketRole.Guild":"Discord.WebSocket.SocketRole.yml","Discord.WebSocket.SocketRole.Color":"Discord.WebSocket.SocketRole.yml","Discord.WebSocket.SocketRole.IsHoisted":"Discord.WebSocket.SocketRole.yml","Discord.WebSocket.SocketRole.IsManaged":"Discord.WebSocket.SocketRole.yml","Discord.WebSocket.SocketRole.IsMentionable":"Discord.WebSocket.SocketRole.yml","Discord.WebSocket.SocketRole.Name":"Discord.WebSocket.SocketRole.yml","Discord.WebSocket.SocketRole.Permissions":"Discord.WebSocket.SocketRole.yml","Discord.WebSocket.SocketRole.Position":"Discord.WebSocket.SocketRole.yml","Discord.WebSocket.SocketRole.CreatedAt":"Discord.WebSocket.SocketRole.yml","Discord.WebSocket.SocketRole.IsEveryone":"Discord.WebSocket.SocketRole.yml","Discord.WebSocket.SocketRole.Mention":"Discord.WebSocket.SocketRole.yml","Discord.WebSocket.SocketRole.ModifyAsync(Action{RoleProperties},RequestOptions)":"Discord.WebSocket.SocketRole.yml","Discord.WebSocket.SocketRole.DeleteAsync(RequestOptions)":"Discord.WebSocket.SocketRole.yml","Discord.WebSocket.SocketRole.ToString":"Discord.WebSocket.SocketRole.yml","Discord.WebSocket.SocketRole.CompareTo(IRole)":"Discord.WebSocket.SocketRole.yml","Discord.WebSocket.SocketGroupUser":"Discord.WebSocket.SocketGroupUser.yml","Discord.WebSocket.SocketGroupUser.Channel":"Discord.WebSocket.SocketGroupUser.yml","Discord.WebSocket.SocketGroupUser.IsBot":"Discord.WebSocket.SocketGroupUser.yml","Discord.WebSocket.SocketGroupUser.Username":"Discord.WebSocket.SocketGroupUser.yml","Discord.WebSocket.SocketGroupUser.DiscriminatorValue":"Discord.WebSocket.SocketGroupUser.yml","Discord.WebSocket.SocketGroupUser.AvatarId":"Discord.WebSocket.SocketGroupUser.yml","Discord.WebSocket.SocketGuildUser":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.Guild":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.Nickname":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.IsBot":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.Username":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.DiscriminatorValue":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.AvatarId":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.GuildPermissions":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.IsSelfDeafened":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.IsSelfMuted":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.IsSuppressed":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.IsDeafened":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.IsMuted":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.JoinedAt":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.RoleIds":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.VoiceChannel":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.VoiceSessionId":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.VoiceState":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.Hierarchy":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.ModifyAsync(Action{GuildUserProperties},RequestOptions)":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.KickAsync(RequestOptions)":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.GetPermissions(IGuildChannel)":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketPresence":"Discord.WebSocket.SocketPresence.yml","Discord.WebSocket.SocketPresence.Status":"Discord.WebSocket.SocketPresence.yml","Discord.WebSocket.SocketPresence.Game":"Discord.WebSocket.SocketPresence.yml","Discord.WebSocket.SocketPresence.ToString":"Discord.WebSocket.SocketPresence.yml","Discord.WebSocket.SocketSelfUser":"Discord.WebSocket.SocketSelfUser.yml","Discord.WebSocket.SocketSelfUser.Email":"Discord.WebSocket.SocketSelfUser.yml","Discord.WebSocket.SocketSelfUser.IsVerified":"Discord.WebSocket.SocketSelfUser.yml","Discord.WebSocket.SocketSelfUser.IsMfaEnabled":"Discord.WebSocket.SocketSelfUser.yml","Discord.WebSocket.SocketSelfUser.IsBot":"Discord.WebSocket.SocketSelfUser.yml","Discord.WebSocket.SocketSelfUser.Username":"Discord.WebSocket.SocketSelfUser.yml","Discord.WebSocket.SocketSelfUser.DiscriminatorValue":"Discord.WebSocket.SocketSelfUser.yml","Discord.WebSocket.SocketSelfUser.AvatarId":"Discord.WebSocket.SocketSelfUser.yml","Discord.WebSocket.SocketSelfUser.ModifyAsync(Action{SelfUserProperties},RequestOptions)":"Discord.WebSocket.SocketSelfUser.yml","Discord.WebSocket.SocketSimpleUser":"Discord.WebSocket.SocketSimpleUser.yml","Discord.WebSocket.SocketSimpleUser.IsBot":"Discord.WebSocket.SocketSimpleUser.yml","Discord.WebSocket.SocketSimpleUser.Username":"Discord.WebSocket.SocketSimpleUser.yml","Discord.WebSocket.SocketSimpleUser.DiscriminatorValue":"Discord.WebSocket.SocketSimpleUser.yml","Discord.WebSocket.SocketSimpleUser.AvatarId":"Discord.WebSocket.SocketSimpleUser.yml","Discord.WebSocket.SocketUser":"Discord.WebSocket.SocketUser.yml","Discord.WebSocket.SocketUser.IsBot":"Discord.WebSocket.SocketUser.yml","Discord.WebSocket.SocketUser.Username":"Discord.WebSocket.SocketUser.yml","Discord.WebSocket.SocketUser.DiscriminatorValue":"Discord.WebSocket.SocketUser.yml","Discord.WebSocket.SocketUser.AvatarId":"Discord.WebSocket.SocketUser.yml","Discord.WebSocket.SocketUser.AvatarUrl":"Discord.WebSocket.SocketUser.yml","Discord.WebSocket.SocketUser.CreatedAt":"Discord.WebSocket.SocketUser.yml","Discord.WebSocket.SocketUser.Discriminator":"Discord.WebSocket.SocketUser.yml","Discord.WebSocket.SocketUser.Mention":"Discord.WebSocket.SocketUser.yml","Discord.WebSocket.SocketUser.Game":"Discord.WebSocket.SocketUser.yml","Discord.WebSocket.SocketUser.Status":"Discord.WebSocket.SocketUser.yml","Discord.WebSocket.SocketUser.CreateDMChannelAsync(RequestOptions)":"Discord.WebSocket.SocketUser.yml","Discord.WebSocket.SocketUser.ToString":"Discord.WebSocket.SocketUser.yml","Discord.WebSocket.SocketVoiceState":"Discord.WebSocket.SocketVoiceState.yml","Discord.WebSocket.SocketVoiceState.Default":"Discord.WebSocket.SocketVoiceState.yml","Discord.WebSocket.SocketVoiceState.VoiceChannel":"Discord.WebSocket.SocketVoiceState.yml","Discord.WebSocket.SocketVoiceState.VoiceSessionId":"Discord.WebSocket.SocketVoiceState.yml","Discord.WebSocket.SocketVoiceState.IsMuted":"Discord.WebSocket.SocketVoiceState.yml","Discord.WebSocket.SocketVoiceState.IsDeafened":"Discord.WebSocket.SocketVoiceState.yml","Discord.WebSocket.SocketVoiceState.IsSuppressed":"Discord.WebSocket.SocketVoiceState.yml","Discord.WebSocket.SocketVoiceState.IsSelfMuted":"Discord.WebSocket.SocketVoiceState.yml","Discord.WebSocket.SocketVoiceState.IsSelfDeafened":"Discord.WebSocket.SocketVoiceState.yml","Discord.WebSocket.SocketVoiceState.ToString":"Discord.WebSocket.SocketVoiceState.yml","Discord.Audio":"Discord.Audio.yml","Discord.Audio.AudioMode":"Discord.Audio.AudioMode.yml","Discord.Audio.AudioMode.Disabled":"Discord.Audio.AudioMode.yml","Discord.Audio.AudioMode.Outgoing":"Discord.Audio.AudioMode.yml","Discord.Audio.AudioMode.Incoming":"Discord.Audio.AudioMode.yml","Discord.Audio.AudioMode.Both":"Discord.Audio.AudioMode.yml","Discord.Audio.OpusApplication":"Discord.Audio.OpusApplication.yml","Discord.Audio.OpusApplication.Voice":"Discord.Audio.OpusApplication.yml","Discord.Audio.OpusApplication.MusicOrMixed":"Discord.Audio.OpusApplication.yml","Discord.Audio.OpusApplication.LowLatency":"Discord.Audio.OpusApplication.yml","Discord.Audio.SecretBox":"Discord.Audio.SecretBox.yml","Discord.Audio.SecretBox.Encrypt(System.Byte[],System.Int32,System.Int32,System.Byte[],System.Int32,System.Byte[],System.Byte[])":"Discord.Audio.SecretBox.yml","Discord.Audio.SecretBox.Decrypt(System.Byte[],System.Int32,System.Int32,System.Byte[],System.Int32,System.Byte[],System.Byte[])":"Discord.Audio.SecretBox.yml","Discord.Audio.AudioInStream":"Discord.Audio.AudioInStream.yml","Discord.Audio.AudioOutStream":"Discord.Audio.AudioOutStream.yml","Discord.Audio.AudioOutStream.CanRead":"Discord.Audio.AudioOutStream.yml","Discord.Audio.AudioOutStream.CanSeek":"Discord.Audio.AudioOutStream.yml","Discord.Audio.AudioOutStream.CanWrite":"Discord.Audio.AudioOutStream.yml","Discord.Audio.AudioOutStream.Clear":"Discord.Audio.AudioOutStream.yml","Discord.Audio.AudioOutStream.ClearAsync(CancellationToken)":"Discord.Audio.AudioOutStream.yml","Discord.Audio.IAudioClient":"Discord.Audio.IAudioClient.yml","Discord.Audio.IAudioClient.Connected":"Discord.Audio.IAudioClient.yml","Discord.Audio.IAudioClient.Disconnected":"Discord.Audio.IAudioClient.yml","Discord.Audio.IAudioClient.LatencyUpdated":"Discord.Audio.IAudioClient.yml","Discord.Audio.IAudioClient.ConnectionState":"Discord.Audio.IAudioClient.yml","Discord.Audio.IAudioClient.Latency":"Discord.Audio.IAudioClient.yml","Discord.Audio.IAudioClient.DisconnectAsync":"Discord.Audio.IAudioClient.yml","Discord.Audio.IAudioClient.CreateOpusStream(System.Int32,System.Int32)":"Discord.Audio.IAudioClient.yml","Discord.Audio.IAudioClient.CreateDirectOpusStream(System.Int32)":"Discord.Audio.IAudioClient.yml","Discord.Audio.IAudioClient.CreatePCMStream(System.Int32,System.Int32,System.Nullable{System.Int32},System.Int32)":"Discord.Audio.IAudioClient.yml","Discord.Audio.IAudioClient.CreateDirectPCMStream(System.Int32,System.Int32,System.Nullable{System.Int32})":"Discord.Audio.IAudioClient.yml","Discord.Commands.Builders":"Discord.Commands.Builders.yml","Discord.Commands.Builders.CommandBuilder":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.Module":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.Name":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.Summary":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.Remarks":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.RunMode":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.Priority":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.Preconditions":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.Parameters":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.Aliases":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.WithName(System.String)":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.WithSummary(System.String)":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.WithRemarks(System.String)":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.WithRunMode(Discord.Commands.RunMode)":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.WithPriority(System.Int32)":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.AddAliases(System.String[])":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.AddPrecondition(Discord.Commands.PreconditionAttribute)":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.AddParameter``1(System.String,Action{Discord.Commands.Builders.ParameterBuilder})":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.AddParameter(System.String,Type,Action{Discord.Commands.Builders.ParameterBuilder})":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.ModuleBuilder":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.Service":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.Parent":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.Name":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.Summary":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.Remarks":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.Commands":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.Modules":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.Preconditions":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.Aliases":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.WithName(System.String)":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.WithSummary(System.String)":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.WithRemarks(System.String)":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.AddAliases(System.String[])":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.AddPrecondition(Discord.Commands.PreconditionAttribute)":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.AddCommand(System.String,Func{ICommandContext,System.Object[],Discord.Commands.IDependencyMap,Task},Action{Discord.Commands.Builders.CommandBuilder})":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.AddModule(System.String,Action{Discord.Commands.Builders.ModuleBuilder})":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.Build(Discord.Commands.CommandService)":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ParameterBuilder":"Discord.Commands.Builders.ParameterBuilder.yml","Discord.Commands.Builders.ParameterBuilder.Command":"Discord.Commands.Builders.ParameterBuilder.yml","Discord.Commands.Builders.ParameterBuilder.Name":"Discord.Commands.Builders.ParameterBuilder.yml","Discord.Commands.Builders.ParameterBuilder.ParameterType":"Discord.Commands.Builders.ParameterBuilder.yml","Discord.Commands.Builders.ParameterBuilder.TypeReader":"Discord.Commands.Builders.ParameterBuilder.yml","Discord.Commands.Builders.ParameterBuilder.IsOptional":"Discord.Commands.Builders.ParameterBuilder.yml","Discord.Commands.Builders.ParameterBuilder.IsRemainder":"Discord.Commands.Builders.ParameterBuilder.yml","Discord.Commands.Builders.ParameterBuilder.IsMultiple":"Discord.Commands.Builders.ParameterBuilder.yml","Discord.Commands.Builders.ParameterBuilder.DefaultValue":"Discord.Commands.Builders.ParameterBuilder.yml","Discord.Commands.Builders.ParameterBuilder.Summary":"Discord.Commands.Builders.ParameterBuilder.yml","Discord.Commands.Builders.ParameterBuilder.Preconditions":"Discord.Commands.Builders.ParameterBuilder.yml","Discord.Commands.Builders.ParameterBuilder.WithSummary(System.String)":"Discord.Commands.Builders.ParameterBuilder.yml","Discord.Commands.Builders.ParameterBuilder.WithDefault(System.Object)":"Discord.Commands.Builders.ParameterBuilder.yml","Discord.Commands.Builders.ParameterBuilder.WithIsOptional(System.Boolean)":"Discord.Commands.Builders.ParameterBuilder.yml","Discord.Commands.Builders.ParameterBuilder.WithIsRemainder(System.Boolean)":"Discord.Commands.Builders.ParameterBuilder.yml","Discord.Commands.Builders.ParameterBuilder.WithIsMultiple(System.Boolean)":"Discord.Commands.Builders.ParameterBuilder.yml","Discord.Commands.Builders.ParameterBuilder.AddPrecondition(Discord.Commands.ParameterPreconditionAttribute)":"Discord.Commands.Builders.ParameterBuilder.yml","Discord":"Discord.yml","Discord.CDN":"Discord.CDN.yml","Discord.CDN.GetApplicationIconUrl(System.UInt64,System.String)":"Discord.CDN.yml","Discord.CDN.GetUserAvatarUrl(System.UInt64,System.String)":"Discord.CDN.yml","Discord.CDN.GetGuildIconUrl(System.UInt64,System.String)":"Discord.CDN.yml","Discord.CDN.GetGuildSplashUrl(System.UInt64,System.String)":"Discord.CDN.yml","Discord.CDN.GetChannelIconUrl(System.UInt64,System.String)":"Discord.CDN.yml","Discord.CDN.GetEmojiUrl(System.UInt64)":"Discord.CDN.yml","Discord.ConnectionState":"Discord.ConnectionState.yml","Discord.ConnectionState.Disconnected":"Discord.ConnectionState.yml","Discord.ConnectionState.Connecting":"Discord.ConnectionState.yml","Discord.ConnectionState.Connected":"Discord.ConnectionState.yml","Discord.ConnectionState.Disconnecting":"Discord.ConnectionState.yml","Discord.DiscordConfig":"Discord.DiscordConfig.yml","Discord.DiscordConfig.APIVersion":"Discord.DiscordConfig.yml","Discord.DiscordConfig.Version":"Discord.DiscordConfig.yml","Discord.DiscordConfig.ClientAPIUrl":"Discord.DiscordConfig.yml","Discord.DiscordConfig.CDNUrl":"Discord.DiscordConfig.yml","Discord.DiscordConfig.InviteUrl":"Discord.DiscordConfig.yml","Discord.DiscordConfig.DefaultRequestTimeout":"Discord.DiscordConfig.yml","Discord.DiscordConfig.MaxMessageSize":"Discord.DiscordConfig.yml","Discord.DiscordConfig.MaxMessagesPerBatch":"Discord.DiscordConfig.yml","Discord.DiscordConfig.MaxUsersPerBatch":"Discord.DiscordConfig.yml","Discord.DiscordConfig.DefaultRetryMode":"Discord.DiscordConfig.yml","Discord.DiscordConfig.LogLevel":"Discord.DiscordConfig.yml","Discord.Format":"Discord.Format.yml","Discord.Format.Bold(System.String)":"Discord.Format.yml","Discord.Format.Italics(System.String)":"Discord.Format.yml","Discord.Format.Underline(System.String)":"Discord.Format.yml","Discord.Format.Strikethrough(System.String)":"Discord.Format.yml","Discord.Format.Code(System.String,System.String)":"Discord.Format.yml","Discord.Format.Sanitize(System.String)":"Discord.Format.yml","Discord.IDiscordClient":"Discord.IDiscordClient.yml","Discord.IDiscordClient.ConnectionState":"Discord.IDiscordClient.yml","Discord.IDiscordClient.CurrentUser":"Discord.IDiscordClient.yml","Discord.IDiscordClient.ConnectAsync":"Discord.IDiscordClient.yml","Discord.IDiscordClient.DisconnectAsync":"Discord.IDiscordClient.yml","Discord.IDiscordClient.GetApplicationInfoAsync":"Discord.IDiscordClient.yml","Discord.IDiscordClient.GetChannelAsync(System.UInt64,Discord.CacheMode)":"Discord.IDiscordClient.yml","Discord.IDiscordClient.GetPrivateChannelsAsync(Discord.CacheMode)":"Discord.IDiscordClient.yml","Discord.IDiscordClient.GetConnectionsAsync":"Discord.IDiscordClient.yml","Discord.IDiscordClient.GetGuildAsync(System.UInt64,Discord.CacheMode)":"Discord.IDiscordClient.yml","Discord.IDiscordClient.GetGuildsAsync(Discord.CacheMode)":"Discord.IDiscordClient.yml","Discord.IDiscordClient.CreateGuildAsync(System.String,Discord.IVoiceRegion,Stream)":"Discord.IDiscordClient.yml","Discord.IDiscordClient.GetInviteAsync(System.String)":"Discord.IDiscordClient.yml","Discord.IDiscordClient.GetUserAsync(System.UInt64,Discord.CacheMode)":"Discord.IDiscordClient.yml","Discord.IDiscordClient.GetUserAsync(System.String,System.String)":"Discord.IDiscordClient.yml","Discord.IDiscordClient.GetVoiceRegionsAsync":"Discord.IDiscordClient.yml","Discord.IDiscordClient.GetVoiceRegionAsync(System.String)":"Discord.IDiscordClient.yml","Discord.LoginState":"Discord.LoginState.yml","Discord.LoginState.LoggedOut":"Discord.LoginState.yml","Discord.LoginState.LoggingIn":"Discord.LoginState.yml","Discord.LoginState.LoggedIn":"Discord.LoginState.yml","Discord.LoginState.LoggingOut":"Discord.LoginState.yml","Discord.RequestOptions":"Discord.RequestOptions.yml","Discord.RequestOptions.Default":"Discord.RequestOptions.yml","Discord.RequestOptions.Timeout":"Discord.RequestOptions.yml","Discord.RequestOptions.CancelToken":"Discord.RequestOptions.yml","Discord.RequestOptions.RetryMode":"Discord.RequestOptions.yml","Discord.RequestOptions.HeaderOnly":"Discord.RequestOptions.yml","Discord.RequestOptions.#ctor":"Discord.RequestOptions.yml","Discord.RequestOptions.Clone":"Discord.RequestOptions.yml","Discord.RetryMode":"Discord.RetryMode.yml","Discord.RetryMode.AlwaysFail":"Discord.RetryMode.yml","Discord.RetryMode.RetryTimeouts":"Discord.RetryMode.yml","Discord.RetryMode.RetryRatelimit":"Discord.RetryMode.yml","Discord.RetryMode.Retry502":"Discord.RetryMode.yml","Discord.RetryMode.AlwaysRetry":"Discord.RetryMode.yml","Discord.TokenType":"Discord.TokenType.yml","Discord.TokenType.User":"Discord.TokenType.yml","Discord.TokenType.Bearer":"Discord.TokenType.yml","Discord.TokenType.Bot":"Discord.TokenType.yml","Discord.CacheMode":"Discord.CacheMode.yml","Discord.CacheMode.AllowDownload":"Discord.CacheMode.yml","Discord.CacheMode.CacheOnly":"Discord.CacheMode.yml","Discord.IApplication":"Discord.IApplication.yml","Discord.IApplication.Name":"Discord.IApplication.yml","Discord.IApplication.Description":"Discord.IApplication.yml","Discord.IApplication.RPCOrigins":"Discord.IApplication.yml","Discord.IApplication.Flags":"Discord.IApplication.yml","Discord.IApplication.IconUrl":"Discord.IApplication.yml","Discord.IApplication.Owner":"Discord.IApplication.yml","Discord.IDeletable":"Discord.IDeletable.yml","Discord.IDeletable.DeleteAsync(Discord.RequestOptions)":"Discord.IDeletable.yml","Discord.IEntity`1":"Discord.IEntity-1.yml","Discord.IEntity`1.Id":"Discord.IEntity-1.yml","Discord.Image":"Discord.Image.yml","Discord.Image.Stream":"Discord.Image.yml","Discord.Image.#ctor(Stream)":"Discord.Image.yml","Discord.IMentionable":"Discord.IMentionable.yml","Discord.IMentionable.Mention":"Discord.IMentionable.yml","Discord.ISnowflakeEntity":"Discord.ISnowflakeEntity.yml","Discord.ISnowflakeEntity.CreatedAt":"Discord.ISnowflakeEntity.yml","Discord.IUpdateable":"Discord.IUpdateable.yml","Discord.IUpdateable.UpdateAsync(Discord.RequestOptions)":"Discord.IUpdateable.yml","Discord.BulkGuildChannelProperties":"Discord.BulkGuildChannelProperties.yml","Discord.BulkGuildChannelProperties.Id":"Discord.BulkGuildChannelProperties.yml","Discord.BulkGuildChannelProperties.Position":"Discord.BulkGuildChannelProperties.yml","Discord.BulkGuildChannelProperties.#ctor(System.UInt64,System.Int32)":"Discord.BulkGuildChannelProperties.yml","Discord.Direction":"Discord.Direction.yml","Discord.Direction.Before":"Discord.Direction.yml","Discord.Direction.After":"Discord.Direction.yml","Discord.Direction.Around":"Discord.Direction.yml","Discord.GuildChannelProperties":"Discord.GuildChannelProperties.yml","Discord.GuildChannelProperties.Name":"Discord.GuildChannelProperties.yml","Discord.GuildChannelProperties.Position":"Discord.GuildChannelProperties.yml","Discord.IAudioChannel":"Discord.IAudioChannel.yml","Discord.IChannel":"Discord.IChannel.yml","Discord.IChannel.Name":"Discord.IChannel.yml","Discord.IChannel.GetUsersAsync(Discord.CacheMode,Discord.RequestOptions)":"Discord.IChannel.yml","Discord.IChannel.GetUserAsync(System.UInt64,Discord.CacheMode,Discord.RequestOptions)":"Discord.IChannel.yml","Discord.IDMChannel":"Discord.IDMChannel.yml","Discord.IDMChannel.Recipient":"Discord.IDMChannel.yml","Discord.IDMChannel.CloseAsync(Discord.RequestOptions)":"Discord.IDMChannel.yml","Discord.IGroupChannel":"Discord.IGroupChannel.yml","Discord.IGroupChannel.LeaveAsync(Discord.RequestOptions)":"Discord.IGroupChannel.yml","Discord.IGuildChannel":"Discord.IGuildChannel.yml","Discord.IGuildChannel.Position":"Discord.IGuildChannel.yml","Discord.IGuildChannel.Guild":"Discord.IGuildChannel.yml","Discord.IGuildChannel.GuildId":"Discord.IGuildChannel.yml","Discord.IGuildChannel.PermissionOverwrites":"Discord.IGuildChannel.yml","Discord.IGuildChannel.CreateInviteAsync(System.Nullable{System.Int32},System.Nullable{System.Int32},System.Boolean,Discord.RequestOptions)":"Discord.IGuildChannel.yml","Discord.IGuildChannel.GetInvitesAsync(Discord.RequestOptions)":"Discord.IGuildChannel.yml","Discord.IGuildChannel.ModifyAsync(Action{Discord.GuildChannelProperties},Discord.RequestOptions)":"Discord.IGuildChannel.yml","Discord.IGuildChannel.GetPermissionOverwrite(Discord.IRole)":"Discord.IGuildChannel.yml","Discord.IGuildChannel.GetPermissionOverwrite(Discord.IUser)":"Discord.IGuildChannel.yml","Discord.IGuildChannel.RemovePermissionOverwriteAsync(Discord.IRole,Discord.RequestOptions)":"Discord.IGuildChannel.yml","Discord.IGuildChannel.RemovePermissionOverwriteAsync(Discord.IUser,Discord.RequestOptions)":"Discord.IGuildChannel.yml","Discord.IGuildChannel.AddPermissionOverwriteAsync(Discord.IRole,Discord.OverwritePermissions,Discord.RequestOptions)":"Discord.IGuildChannel.yml","Discord.IGuildChannel.AddPermissionOverwriteAsync(Discord.IUser,Discord.OverwritePermissions,Discord.RequestOptions)":"Discord.IGuildChannel.yml","Discord.IGuildChannel.GetUsersAsync(Discord.CacheMode,Discord.RequestOptions)":"Discord.IGuildChannel.yml","Discord.IGuildChannel.GetUserAsync(System.UInt64,Discord.CacheMode,Discord.RequestOptions)":"Discord.IGuildChannel.yml","Discord.IMessageChannel":"Discord.IMessageChannel.yml","Discord.IMessageChannel.SendMessageAsync(System.String,System.Boolean,Discord.Embed,Discord.RequestOptions)":"Discord.IMessageChannel.yml","Discord.IMessageChannel.SendFileAsync(Stream,System.String,System.String,System.Boolean,Discord.RequestOptions)":"Discord.IMessageChannel.yml","Discord.IMessageChannel.GetMessageAsync(System.UInt64,Discord.CacheMode,Discord.RequestOptions)":"Discord.IMessageChannel.yml","Discord.IMessageChannel.GetMessagesAsync(System.Int32,Discord.CacheMode,Discord.RequestOptions)":"Discord.IMessageChannel.yml","Discord.IMessageChannel.GetMessagesAsync(System.UInt64,Discord.Direction,System.Int32,Discord.CacheMode,Discord.RequestOptions)":"Discord.IMessageChannel.yml","Discord.IMessageChannel.GetMessagesAsync(Discord.IMessage,Discord.Direction,System.Int32,Discord.CacheMode,Discord.RequestOptions)":"Discord.IMessageChannel.yml","Discord.IMessageChannel.GetPinnedMessagesAsync(Discord.RequestOptions)":"Discord.IMessageChannel.yml","Discord.IMessageChannel.DeleteMessagesAsync(IEnumerable{Discord.IMessage},Discord.RequestOptions)":"Discord.IMessageChannel.yml","Discord.IMessageChannel.TriggerTypingAsync(Discord.RequestOptions)":"Discord.IMessageChannel.yml","Discord.IMessageChannel.EnterTypingState(Discord.RequestOptions)":"Discord.IMessageChannel.yml","Discord.IPrivateChannel":"Discord.IPrivateChannel.yml","Discord.IPrivateChannel.Recipients":"Discord.IPrivateChannel.yml","Discord.ITextChannel":"Discord.ITextChannel.yml","Discord.ITextChannel.Topic":"Discord.ITextChannel.yml","Discord.ITextChannel.ModifyAsync(Action{Discord.TextChannelProperties},Discord.RequestOptions)":"Discord.ITextChannel.yml","Discord.IVoiceChannel":"Discord.IVoiceChannel.yml","Discord.IVoiceChannel.Bitrate":"Discord.IVoiceChannel.yml","Discord.IVoiceChannel.UserLimit":"Discord.IVoiceChannel.yml","Discord.IVoiceChannel.ModifyAsync(Action{Discord.VoiceChannelProperties},Discord.RequestOptions)":"Discord.IVoiceChannel.yml","Discord.IVoiceChannel.ConnectAsync":"Discord.IVoiceChannel.yml","Discord.TextChannelProperties":"Discord.TextChannelProperties.yml","Discord.TextChannelProperties.Topic":"Discord.TextChannelProperties.yml","Discord.VoiceChannelProperties":"Discord.VoiceChannelProperties.yml","Discord.VoiceChannelProperties.Bitrate":"Discord.VoiceChannelProperties.yml","Discord.VoiceChannelProperties.UserLimit":"Discord.VoiceChannelProperties.yml","Discord.DefaultMessageNotifications":"Discord.DefaultMessageNotifications.yml","Discord.DefaultMessageNotifications.AllMessages":"Discord.DefaultMessageNotifications.yml","Discord.DefaultMessageNotifications.MentionsOnly":"Discord.DefaultMessageNotifications.yml","Discord.GuildEmbedProperties":"Discord.GuildEmbedProperties.yml","Discord.GuildEmbedProperties.Enabled":"Discord.GuildEmbedProperties.yml","Discord.GuildEmbedProperties.Channel":"Discord.GuildEmbedProperties.yml","Discord.GuildEmbedProperties.ChannelId":"Discord.GuildEmbedProperties.yml","Discord.GuildEmoji":"Discord.GuildEmoji.yml","Discord.GuildEmoji.Id":"Discord.GuildEmoji.yml","Discord.GuildEmoji.Name":"Discord.GuildEmoji.yml","Discord.GuildEmoji.IsManaged":"Discord.GuildEmoji.yml","Discord.GuildEmoji.RequireColons":"Discord.GuildEmoji.yml","Discord.GuildEmoji.RoleIds":"Discord.GuildEmoji.yml","Discord.GuildEmoji.ToString":"Discord.GuildEmoji.yml","Discord.GuildIntegrationProperties":"Discord.GuildIntegrationProperties.yml","Discord.GuildIntegrationProperties.ExpireBehavior":"Discord.GuildIntegrationProperties.yml","Discord.GuildIntegrationProperties.ExpireGracePeriod":"Discord.GuildIntegrationProperties.yml","Discord.GuildIntegrationProperties.EnableEmoticons":"Discord.GuildIntegrationProperties.yml","Discord.GuildProperties":"Discord.GuildProperties.yml","Discord.GuildProperties.Username":"Discord.GuildProperties.yml","Discord.GuildProperties.Name":"Discord.GuildProperties.yml","Discord.GuildProperties.Region":"Discord.GuildProperties.yml","Discord.GuildProperties.RegionId":"Discord.GuildProperties.yml","Discord.GuildProperties.VerificationLevel":"Discord.GuildProperties.yml","Discord.GuildProperties.DefaultMessageNotifications":"Discord.GuildProperties.yml","Discord.GuildProperties.AfkTimeout":"Discord.GuildProperties.yml","Discord.GuildProperties.Icon":"Discord.GuildProperties.yml","Discord.GuildProperties.Splash":"Discord.GuildProperties.yml","Discord.GuildProperties.AfkChannel":"Discord.GuildProperties.yml","Discord.GuildProperties.AfkChannelId":"Discord.GuildProperties.yml","Discord.GuildProperties.Owner":"Discord.GuildProperties.yml","Discord.GuildProperties.OwnerId":"Discord.GuildProperties.yml","Discord.IBan":"Discord.IBan.yml","Discord.IBan.User":"Discord.IBan.yml","Discord.IBan.Reason":"Discord.IBan.yml","Discord.IGuild":"Discord.IGuild.yml","Discord.IGuild.Name":"Discord.IGuild.yml","Discord.IGuild.AFKTimeout":"Discord.IGuild.yml","Discord.IGuild.IsEmbeddable":"Discord.IGuild.yml","Discord.IGuild.DefaultMessageNotifications":"Discord.IGuild.yml","Discord.IGuild.MfaLevel":"Discord.IGuild.yml","Discord.IGuild.VerificationLevel":"Discord.IGuild.yml","Discord.IGuild.IconId":"Discord.IGuild.yml","Discord.IGuild.IconUrl":"Discord.IGuild.yml","Discord.IGuild.SplashId":"Discord.IGuild.yml","Discord.IGuild.SplashUrl":"Discord.IGuild.yml","Discord.IGuild.Available":"Discord.IGuild.yml","Discord.IGuild.AFKChannelId":"Discord.IGuild.yml","Discord.IGuild.DefaultChannelId":"Discord.IGuild.yml","Discord.IGuild.EmbedChannelId":"Discord.IGuild.yml","Discord.IGuild.OwnerId":"Discord.IGuild.yml","Discord.IGuild.VoiceRegionId":"Discord.IGuild.yml","Discord.IGuild.AudioClient":"Discord.IGuild.yml","Discord.IGuild.EveryoneRole":"Discord.IGuild.yml","Discord.IGuild.Emojis":"Discord.IGuild.yml","Discord.IGuild.Features":"Discord.IGuild.yml","Discord.IGuild.Roles":"Discord.IGuild.yml","Discord.IGuild.ModifyAsync(Action{Discord.GuildProperties},Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.ModifyEmbedAsync(Action{Discord.GuildEmbedProperties},Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.ModifyChannelsAsync(IEnumerable{Discord.BulkGuildChannelProperties},Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.ModifyRolesAsync(IEnumerable{Discord.BulkRoleProperties},Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.LeaveAsync(Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.GetBansAsync(Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.AddBanAsync(Discord.IUser,System.Int32,Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.AddBanAsync(System.UInt64,System.Int32,Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.RemoveBanAsync(Discord.IUser,Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.RemoveBanAsync(System.UInt64,Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.GetChannelsAsync(Discord.CacheMode,Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.GetChannelAsync(System.UInt64,Discord.CacheMode,Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.CreateTextChannelAsync(System.String,Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.CreateVoiceChannelAsync(System.String,Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.GetIntegrationsAsync(Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.CreateIntegrationAsync(System.UInt64,System.String,Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.GetInvitesAsync(Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.GetRole(System.UInt64)":"Discord.IGuild.yml","Discord.IGuild.CreateRoleAsync(System.String,System.Nullable{Discord.GuildPermissions},System.Nullable{Discord.Color},System.Boolean,Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.GetUsersAsync(Discord.CacheMode,Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.GetUserAsync(System.UInt64,Discord.CacheMode,Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.GetCurrentUserAsync(Discord.CacheMode,Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.DownloadUsersAsync":"Discord.IGuild.yml","Discord.IGuild.PruneUsersAsync(System.Int32,System.Boolean,Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuildIntegration":"Discord.IGuildIntegration.yml","Discord.IGuildIntegration.Id":"Discord.IGuildIntegration.yml","Discord.IGuildIntegration.Name":"Discord.IGuildIntegration.yml","Discord.IGuildIntegration.Type":"Discord.IGuildIntegration.yml","Discord.IGuildIntegration.IsEnabled":"Discord.IGuildIntegration.yml","Discord.IGuildIntegration.IsSyncing":"Discord.IGuildIntegration.yml","Discord.IGuildIntegration.ExpireBehavior":"Discord.IGuildIntegration.yml","Discord.IGuildIntegration.ExpireGracePeriod":"Discord.IGuildIntegration.yml","Discord.IGuildIntegration.SyncedAt":"Discord.IGuildIntegration.yml","Discord.IGuildIntegration.Account":"Discord.IGuildIntegration.yml","Discord.IGuildIntegration.Guild":"Discord.IGuildIntegration.yml","Discord.IGuildIntegration.GuildId":"Discord.IGuildIntegration.yml","Discord.IGuildIntegration.RoleId":"Discord.IGuildIntegration.yml","Discord.IGuildIntegration.User":"Discord.IGuildIntegration.yml","Discord.IntegrationAccount":"Discord.IntegrationAccount.yml","Discord.IntegrationAccount.Id":"Discord.IntegrationAccount.yml","Discord.IntegrationAccount.Name":"Discord.IntegrationAccount.yml","Discord.IntegrationAccount.ToString":"Discord.IntegrationAccount.yml","Discord.IUserGuild":"Discord.IUserGuild.yml","Discord.IUserGuild.Name":"Discord.IUserGuild.yml","Discord.IUserGuild.IconUrl":"Discord.IUserGuild.yml","Discord.IUserGuild.IsOwner":"Discord.IUserGuild.yml","Discord.IUserGuild.Permissions":"Discord.IUserGuild.yml","Discord.IVoiceRegion":"Discord.IVoiceRegion.yml","Discord.IVoiceRegion.Id":"Discord.IVoiceRegion.yml","Discord.IVoiceRegion.Name":"Discord.IVoiceRegion.yml","Discord.IVoiceRegion.IsVip":"Discord.IVoiceRegion.yml","Discord.IVoiceRegion.IsOptimal":"Discord.IVoiceRegion.yml","Discord.IVoiceRegion.SampleHostname":"Discord.IVoiceRegion.yml","Discord.IVoiceRegion.SamplePort":"Discord.IVoiceRegion.yml","Discord.MfaLevel":"Discord.MfaLevel.yml","Discord.MfaLevel.Disabled":"Discord.MfaLevel.yml","Discord.MfaLevel.Enabled":"Discord.MfaLevel.yml","Discord.PermissionTarget":"Discord.PermissionTarget.yml","Discord.PermissionTarget.Role":"Discord.PermissionTarget.yml","Discord.PermissionTarget.User":"Discord.PermissionTarget.yml","Discord.VerificationLevel":"Discord.VerificationLevel.yml","Discord.VerificationLevel.None":"Discord.VerificationLevel.yml","Discord.VerificationLevel.Low":"Discord.VerificationLevel.yml","Discord.VerificationLevel.Medium":"Discord.VerificationLevel.yml","Discord.VerificationLevel.High":"Discord.VerificationLevel.yml","Discord.IInvite":"Discord.IInvite.yml","Discord.IInvite.Code":"Discord.IInvite.yml","Discord.IInvite.Url":"Discord.IInvite.yml","Discord.IInvite.Channel":"Discord.IInvite.yml","Discord.IInvite.ChannelId":"Discord.IInvite.yml","Discord.IInvite.Guild":"Discord.IInvite.yml","Discord.IInvite.GuildId":"Discord.IInvite.yml","Discord.IInvite.AcceptAsync(Discord.RequestOptions)":"Discord.IInvite.yml","Discord.IInviteMetadata":"Discord.IInviteMetadata.yml","Discord.IInviteMetadata.Inviter":"Discord.IInviteMetadata.yml","Discord.IInviteMetadata.IsRevoked":"Discord.IInviteMetadata.yml","Discord.IInviteMetadata.IsTemporary":"Discord.IInviteMetadata.yml","Discord.IInviteMetadata.MaxAge":"Discord.IInviteMetadata.yml","Discord.IInviteMetadata.MaxUses":"Discord.IInviteMetadata.yml","Discord.IInviteMetadata.Uses":"Discord.IInviteMetadata.yml","Discord.IInviteMetadata.CreatedAt":"Discord.IInviteMetadata.yml","Discord.Embed":"Discord.Embed.yml","Discord.Embed.Type":"Discord.Embed.yml","Discord.Embed.Description":"Discord.Embed.yml","Discord.Embed.Url":"Discord.Embed.yml","Discord.Embed.Title":"Discord.Embed.yml","Discord.Embed.Timestamp":"Discord.Embed.yml","Discord.Embed.Color":"Discord.Embed.yml","Discord.Embed.Image":"Discord.Embed.yml","Discord.Embed.Video":"Discord.Embed.yml","Discord.Embed.Author":"Discord.Embed.yml","Discord.Embed.Footer":"Discord.Embed.yml","Discord.Embed.Provider":"Discord.Embed.yml","Discord.Embed.Thumbnail":"Discord.Embed.yml","Discord.Embed.Fields":"Discord.Embed.yml","Discord.Embed.ToString":"Discord.Embed.yml","Discord.EmbedAuthor":"Discord.EmbedAuthor.yml","Discord.EmbedAuthor.Name":"Discord.EmbedAuthor.yml","Discord.EmbedAuthor.Url":"Discord.EmbedAuthor.yml","Discord.EmbedAuthor.IconUrl":"Discord.EmbedAuthor.yml","Discord.EmbedAuthor.ProxyIconUrl":"Discord.EmbedAuthor.yml","Discord.EmbedAuthor.ToString":"Discord.EmbedAuthor.yml","Discord.EmbedField":"Discord.EmbedField.yml","Discord.EmbedField.Name":"Discord.EmbedField.yml","Discord.EmbedField.Value":"Discord.EmbedField.yml","Discord.EmbedField.Inline":"Discord.EmbedField.yml","Discord.EmbedField.ToString":"Discord.EmbedField.yml","Discord.EmbedFooter":"Discord.EmbedFooter.yml","Discord.EmbedFooter.Text":"Discord.EmbedFooter.yml","Discord.EmbedFooter.IconUrl":"Discord.EmbedFooter.yml","Discord.EmbedFooter.ProxyUrl":"Discord.EmbedFooter.yml","Discord.EmbedFooter.ToString":"Discord.EmbedFooter.yml","Discord.EmbedImage":"Discord.EmbedImage.yml","Discord.EmbedImage.Url":"Discord.EmbedImage.yml","Discord.EmbedImage.ProxyUrl":"Discord.EmbedImage.yml","Discord.EmbedImage.Height":"Discord.EmbedImage.yml","Discord.EmbedImage.Width":"Discord.EmbedImage.yml","Discord.EmbedImage.ToString":"Discord.EmbedImage.yml","Discord.EmbedProvider":"Discord.EmbedProvider.yml","Discord.EmbedProvider.Name":"Discord.EmbedProvider.yml","Discord.EmbedProvider.Url":"Discord.EmbedProvider.yml","Discord.EmbedProvider.ToString":"Discord.EmbedProvider.yml","Discord.EmbedThumbnail":"Discord.EmbedThumbnail.yml","Discord.EmbedThumbnail.Url":"Discord.EmbedThumbnail.yml","Discord.EmbedThumbnail.ProxyUrl":"Discord.EmbedThumbnail.yml","Discord.EmbedThumbnail.Height":"Discord.EmbedThumbnail.yml","Discord.EmbedThumbnail.Width":"Discord.EmbedThumbnail.yml","Discord.EmbedThumbnail.ToString":"Discord.EmbedThumbnail.yml","Discord.EmbedVideo":"Discord.EmbedVideo.yml","Discord.EmbedVideo.Url":"Discord.EmbedVideo.yml","Discord.EmbedVideo.Height":"Discord.EmbedVideo.yml","Discord.EmbedVideo.Width":"Discord.EmbedVideo.yml","Discord.EmbedVideo.ToString":"Discord.EmbedVideo.yml","Discord.Emoji":"Discord.Emoji.yml","Discord.Emoji.Id":"Discord.Emoji.yml","Discord.Emoji.Name":"Discord.Emoji.yml","Discord.Emoji.Url":"Discord.Emoji.yml","Discord.Emoji.Parse(System.String)":"Discord.Emoji.yml","Discord.Emoji.TryParse(System.String,Discord.Emoji@)":"Discord.Emoji.yml","Discord.Emoji.ToString":"Discord.Emoji.yml","Discord.IAttachment":"Discord.IAttachment.yml","Discord.IAttachment.Id":"Discord.IAttachment.yml","Discord.IAttachment.Filename":"Discord.IAttachment.yml","Discord.IAttachment.Url":"Discord.IAttachment.yml","Discord.IAttachment.ProxyUrl":"Discord.IAttachment.yml","Discord.IAttachment.Size":"Discord.IAttachment.yml","Discord.IAttachment.Height":"Discord.IAttachment.yml","Discord.IAttachment.Width":"Discord.IAttachment.yml","Discord.IEmbed":"Discord.IEmbed.yml","Discord.IEmbed.Url":"Discord.IEmbed.yml","Discord.IEmbed.Type":"Discord.IEmbed.yml","Discord.IEmbed.Title":"Discord.IEmbed.yml","Discord.IEmbed.Description":"Discord.IEmbed.yml","Discord.IEmbed.Timestamp":"Discord.IEmbed.yml","Discord.IEmbed.Color":"Discord.IEmbed.yml","Discord.IEmbed.Image":"Discord.IEmbed.yml","Discord.IEmbed.Video":"Discord.IEmbed.yml","Discord.IEmbed.Author":"Discord.IEmbed.yml","Discord.IEmbed.Footer":"Discord.IEmbed.yml","Discord.IEmbed.Provider":"Discord.IEmbed.yml","Discord.IEmbed.Thumbnail":"Discord.IEmbed.yml","Discord.IEmbed.Fields":"Discord.IEmbed.yml","Discord.IMessage":"Discord.IMessage.yml","Discord.IMessage.Type":"Discord.IMessage.yml","Discord.IMessage.IsTTS":"Discord.IMessage.yml","Discord.IMessage.IsPinned":"Discord.IMessage.yml","Discord.IMessage.IsWebhook":"Discord.IMessage.yml","Discord.IMessage.Content":"Discord.IMessage.yml","Discord.IMessage.Timestamp":"Discord.IMessage.yml","Discord.IMessage.EditedTimestamp":"Discord.IMessage.yml","Discord.IMessage.Channel":"Discord.IMessage.yml","Discord.IMessage.Author":"Discord.IMessage.yml","Discord.IMessage.WebhookId":"Discord.IMessage.yml","Discord.IMessage.Attachments":"Discord.IMessage.yml","Discord.IMessage.Embeds":"Discord.IMessage.yml","Discord.IMessage.Tags":"Discord.IMessage.yml","Discord.IMessage.MentionedChannelIds":"Discord.IMessage.yml","Discord.IMessage.MentionedRoleIds":"Discord.IMessage.yml","Discord.IMessage.MentionedUserIds":"Discord.IMessage.yml","Discord.IReaction":"Discord.IReaction.yml","Discord.IReaction.Emoji":"Discord.IReaction.yml","Discord.ISystemMessage":"Discord.ISystemMessage.yml","Discord.ITag":"Discord.ITag.yml","Discord.ITag.Index":"Discord.ITag.yml","Discord.ITag.Length":"Discord.ITag.yml","Discord.ITag.Type":"Discord.ITag.yml","Discord.ITag.Key":"Discord.ITag.yml","Discord.ITag.Value":"Discord.ITag.yml","Discord.IUserMessage":"Discord.IUserMessage.yml","Discord.IUserMessage.ModifyAsync(Action{Discord.MessageProperties},Discord.RequestOptions)":"Discord.IUserMessage.yml","Discord.IUserMessage.PinAsync(Discord.RequestOptions)":"Discord.IUserMessage.yml","Discord.IUserMessage.UnpinAsync(Discord.RequestOptions)":"Discord.IUserMessage.yml","Discord.IUserMessage.Reactions":"Discord.IUserMessage.yml","Discord.IUserMessage.AddReactionAsync(Discord.Emoji,Discord.RequestOptions)":"Discord.IUserMessage.yml","Discord.IUserMessage.AddReactionAsync(System.String,Discord.RequestOptions)":"Discord.IUserMessage.yml","Discord.IUserMessage.RemoveReactionAsync(Discord.Emoji,Discord.IUser,Discord.RequestOptions)":"Discord.IUserMessage.yml","Discord.IUserMessage.RemoveReactionAsync(System.String,Discord.IUser,Discord.RequestOptions)":"Discord.IUserMessage.yml","Discord.IUserMessage.RemoveAllReactionsAsync(Discord.RequestOptions)":"Discord.IUserMessage.yml","Discord.IUserMessage.GetReactionUsersAsync(System.String,System.Int32,System.Nullable{System.UInt64},Discord.RequestOptions)":"Discord.IUserMessage.yml","Discord.IUserMessage.Resolve(Discord.TagHandling,Discord.TagHandling,Discord.TagHandling,Discord.TagHandling,Discord.TagHandling)":"Discord.IUserMessage.yml","Discord.MessageProperties":"Discord.MessageProperties.yml","Discord.MessageProperties.Content":"Discord.MessageProperties.yml","Discord.MessageProperties.Embed":"Discord.MessageProperties.yml","Discord.MessageType":"Discord.MessageType.yml","Discord.MessageType.Default":"Discord.MessageType.yml","Discord.MessageType.RecipientAdd":"Discord.MessageType.yml","Discord.MessageType.RecipientRemove":"Discord.MessageType.yml","Discord.MessageType.Call":"Discord.MessageType.yml","Discord.MessageType.ChannelNameChange":"Discord.MessageType.yml","Discord.MessageType.ChannelIconChange":"Discord.MessageType.yml","Discord.MessageType.ChannelPinnedMessage":"Discord.MessageType.yml","Discord.Tag`1":"Discord.Tag-1.yml","Discord.Tag`1.Type":"Discord.Tag-1.yml","Discord.Tag`1.Index":"Discord.Tag-1.yml","Discord.Tag`1.Length":"Discord.Tag-1.yml","Discord.Tag`1.Key":"Discord.Tag-1.yml","Discord.Tag`1.Value":"Discord.Tag-1.yml","Discord.Tag`1.ToString":"Discord.Tag-1.yml","Discord.Tag`1.Discord#ITag#Value":"Discord.Tag-1.yml","Discord.TagHandling":"Discord.TagHandling.yml","Discord.TagHandling.Ignore":"Discord.TagHandling.yml","Discord.TagHandling.Remove":"Discord.TagHandling.yml","Discord.TagHandling.Name":"Discord.TagHandling.yml","Discord.TagHandling.NameNoPrefix":"Discord.TagHandling.yml","Discord.TagHandling.FullName":"Discord.TagHandling.yml","Discord.TagHandling.FullNameNoPrefix":"Discord.TagHandling.yml","Discord.TagHandling.Sanitize":"Discord.TagHandling.yml","Discord.TagType":"Discord.TagType.yml","Discord.TagType.UserMention":"Discord.TagType.yml","Discord.TagType.ChannelMention":"Discord.TagType.yml","Discord.TagType.RoleMention":"Discord.TagType.yml","Discord.TagType.EveryoneMention":"Discord.TagType.yml","Discord.TagType.HereMention":"Discord.TagType.yml","Discord.TagType.Emoji":"Discord.TagType.yml","Discord.ChannelPermission":"Discord.ChannelPermission.yml","Discord.ChannelPermission.CreateInstantInvite":"Discord.ChannelPermission.yml","Discord.ChannelPermission.ManageChannel":"Discord.ChannelPermission.yml","Discord.ChannelPermission.AddReactions":"Discord.ChannelPermission.yml","Discord.ChannelPermission.ReadMessages":"Discord.ChannelPermission.yml","Discord.ChannelPermission.SendMessages":"Discord.ChannelPermission.yml","Discord.ChannelPermission.SendTTSMessages":"Discord.ChannelPermission.yml","Discord.ChannelPermission.ManageMessages":"Discord.ChannelPermission.yml","Discord.ChannelPermission.EmbedLinks":"Discord.ChannelPermission.yml","Discord.ChannelPermission.AttachFiles":"Discord.ChannelPermission.yml","Discord.ChannelPermission.ReadMessageHistory":"Discord.ChannelPermission.yml","Discord.ChannelPermission.MentionEveryone":"Discord.ChannelPermission.yml","Discord.ChannelPermission.UseExternalEmojis":"Discord.ChannelPermission.yml","Discord.ChannelPermission.Connect":"Discord.ChannelPermission.yml","Discord.ChannelPermission.Speak":"Discord.ChannelPermission.yml","Discord.ChannelPermission.MuteMembers":"Discord.ChannelPermission.yml","Discord.ChannelPermission.DeafenMembers":"Discord.ChannelPermission.yml","Discord.ChannelPermission.MoveMembers":"Discord.ChannelPermission.yml","Discord.ChannelPermission.UseVAD":"Discord.ChannelPermission.yml","Discord.ChannelPermission.ManagePermissions":"Discord.ChannelPermission.yml","Discord.ChannelPermission.ManageWebhooks":"Discord.ChannelPermission.yml","Discord.ChannelPermissions":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.None":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.All(Discord.IChannel)":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.RawValue":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.CreateInstantInvite":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.ManageChannel":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.AddReactions":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.ReadMessages":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.SendMessages":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.SendTTSMessages":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.ManageMessages":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.EmbedLinks":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.AttachFiles":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.ReadMessageHistory":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.MentionEveryone":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.UseExternalEmojis":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.Connect":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.Speak":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.MuteMembers":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.DeafenMembers":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.MoveMembers":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.UseVAD":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.ManagePermissions":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.ManageWebhooks":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.#ctor(System.UInt64)":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.#ctor(System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean)":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.Modify(System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Boolean,System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean})":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.Has(Discord.ChannelPermission)":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.ToList":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.ToString":"Discord.ChannelPermissions.yml","Discord.GuildPermission":"Discord.GuildPermission.yml","Discord.GuildPermission.CreateInstantInvite":"Discord.GuildPermission.yml","Discord.GuildPermission.KickMembers":"Discord.GuildPermission.yml","Discord.GuildPermission.BanMembers":"Discord.GuildPermission.yml","Discord.GuildPermission.Administrator":"Discord.GuildPermission.yml","Discord.GuildPermission.ManageChannels":"Discord.GuildPermission.yml","Discord.GuildPermission.ManageGuild":"Discord.GuildPermission.yml","Discord.GuildPermission.AddReactions":"Discord.GuildPermission.yml","Discord.GuildPermission.ReadMessages":"Discord.GuildPermission.yml","Discord.GuildPermission.SendMessages":"Discord.GuildPermission.yml","Discord.GuildPermission.SendTTSMessages":"Discord.GuildPermission.yml","Discord.GuildPermission.ManageMessages":"Discord.GuildPermission.yml","Discord.GuildPermission.EmbedLinks":"Discord.GuildPermission.yml","Discord.GuildPermission.AttachFiles":"Discord.GuildPermission.yml","Discord.GuildPermission.ReadMessageHistory":"Discord.GuildPermission.yml","Discord.GuildPermission.MentionEveryone":"Discord.GuildPermission.yml","Discord.GuildPermission.UseExternalEmojis":"Discord.GuildPermission.yml","Discord.GuildPermission.Connect":"Discord.GuildPermission.yml","Discord.GuildPermission.Speak":"Discord.GuildPermission.yml","Discord.GuildPermission.MuteMembers":"Discord.GuildPermission.yml","Discord.GuildPermission.DeafenMembers":"Discord.GuildPermission.yml","Discord.GuildPermission.MoveMembers":"Discord.GuildPermission.yml","Discord.GuildPermission.UseVAD":"Discord.GuildPermission.yml","Discord.GuildPermission.ChangeNickname":"Discord.GuildPermission.yml","Discord.GuildPermission.ManageNicknames":"Discord.GuildPermission.yml","Discord.GuildPermission.ManageRoles":"Discord.GuildPermission.yml","Discord.GuildPermission.ManageWebhooks":"Discord.GuildPermission.yml","Discord.GuildPermission.ManageEmojis":"Discord.GuildPermission.yml","Discord.GuildPermissions":"Discord.GuildPermissions.yml","Discord.GuildPermissions.None":"Discord.GuildPermissions.yml","Discord.GuildPermissions.All":"Discord.GuildPermissions.yml","Discord.GuildPermissions.RawValue":"Discord.GuildPermissions.yml","Discord.GuildPermissions.CreateInstantInvite":"Discord.GuildPermissions.yml","Discord.GuildPermissions.BanMembers":"Discord.GuildPermissions.yml","Discord.GuildPermissions.KickMembers":"Discord.GuildPermissions.yml","Discord.GuildPermissions.Administrator":"Discord.GuildPermissions.yml","Discord.GuildPermissions.ManageChannels":"Discord.GuildPermissions.yml","Discord.GuildPermissions.ManageGuild":"Discord.GuildPermissions.yml","Discord.GuildPermissions.AddReactions":"Discord.GuildPermissions.yml","Discord.GuildPermissions.ReadMessages":"Discord.GuildPermissions.yml","Discord.GuildPermissions.SendMessages":"Discord.GuildPermissions.yml","Discord.GuildPermissions.SendTTSMessages":"Discord.GuildPermissions.yml","Discord.GuildPermissions.ManageMessages":"Discord.GuildPermissions.yml","Discord.GuildPermissions.EmbedLinks":"Discord.GuildPermissions.yml","Discord.GuildPermissions.AttachFiles":"Discord.GuildPermissions.yml","Discord.GuildPermissions.ReadMessageHistory":"Discord.GuildPermissions.yml","Discord.GuildPermissions.MentionEveryone":"Discord.GuildPermissions.yml","Discord.GuildPermissions.UseExternalEmojis":"Discord.GuildPermissions.yml","Discord.GuildPermissions.Connect":"Discord.GuildPermissions.yml","Discord.GuildPermissions.Speak":"Discord.GuildPermissions.yml","Discord.GuildPermissions.MuteMembers":"Discord.GuildPermissions.yml","Discord.GuildPermissions.DeafenMembers":"Discord.GuildPermissions.yml","Discord.GuildPermissions.MoveMembers":"Discord.GuildPermissions.yml","Discord.GuildPermissions.UseVAD":"Discord.GuildPermissions.yml","Discord.GuildPermissions.ChangeNickname":"Discord.GuildPermissions.yml","Discord.GuildPermissions.ManageNicknames":"Discord.GuildPermissions.yml","Discord.GuildPermissions.ManageRoles":"Discord.GuildPermissions.yml","Discord.GuildPermissions.ManageWebhooks":"Discord.GuildPermissions.yml","Discord.GuildPermissions.ManageEmojis":"Discord.GuildPermissions.yml","Discord.GuildPermissions.#ctor(System.UInt64)":"Discord.GuildPermissions.yml","Discord.GuildPermissions.#ctor(System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Boolean,System.Boolean,System.Boolean)":"Discord.GuildPermissions.yml","Discord.GuildPermissions.Modify(System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean})":"Discord.GuildPermissions.yml","Discord.GuildPermissions.Has(Discord.GuildPermission)":"Discord.GuildPermissions.yml","Discord.GuildPermissions.ToList":"Discord.GuildPermissions.yml","Discord.GuildPermissions.ToString":"Discord.GuildPermissions.yml","Discord.Overwrite":"Discord.Overwrite.yml","Discord.Overwrite.TargetId":"Discord.Overwrite.yml","Discord.Overwrite.TargetType":"Discord.Overwrite.yml","Discord.Overwrite.Permissions":"Discord.Overwrite.yml","Discord.Overwrite.#ctor(System.UInt64,Discord.PermissionTarget,Discord.OverwritePermissions)":"Discord.Overwrite.yml","Discord.OverwritePermissions":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.InheritAll":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.AllowAll(Discord.IChannel)":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.DenyAll(Discord.IChannel)":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.AllowValue":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.DenyValue":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.CreateInstantInvite":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.ManageChannel":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.AddReactions":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.ReadMessages":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.SendMessages":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.SendTTSMessages":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.ManageMessages":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.EmbedLinks":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.AttachFiles":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.ReadMessageHistory":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.MentionEveryone":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.UseExternalEmojis":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.Connect":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.Speak":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.MuteMembers":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.DeafenMembers":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.MoveMembers":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.UseVAD":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.ManagePermissions":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.ManageWebhooks":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.#ctor(System.UInt64,System.UInt64)":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.#ctor(Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue)":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.Modify(System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue})":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.ToAllowList":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.ToDenyList":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.ToString":"Discord.OverwritePermissions.yml","Discord.PermValue":"Discord.PermValue.yml","Discord.PermValue.Allow":"Discord.PermValue.yml","Discord.PermValue.Deny":"Discord.PermValue.yml","Discord.PermValue.Inherit":"Discord.PermValue.yml","Discord.BulkRoleProperties":"Discord.BulkRoleProperties.yml","Discord.BulkRoleProperties.Id":"Discord.BulkRoleProperties.yml","Discord.BulkRoleProperties.#ctor(System.UInt64)":"Discord.BulkRoleProperties.yml","Discord.Color":"Discord.Color.yml","Discord.Color.Default":"Discord.Color.yml","Discord.Color.RawValue":"Discord.Color.yml","Discord.Color.R":"Discord.Color.yml","Discord.Color.G":"Discord.Color.yml","Discord.Color.B":"Discord.Color.yml","Discord.Color.#ctor(System.UInt32)":"Discord.Color.yml","Discord.Color.#ctor(System.Byte,System.Byte,System.Byte)":"Discord.Color.yml","Discord.Color.#ctor(System.Single,System.Single,System.Single)":"Discord.Color.yml","Discord.Color.ToString":"Discord.Color.yml","Discord.IRole":"Discord.IRole.yml","Discord.IRole.Guild":"Discord.IRole.yml","Discord.IRole.Color":"Discord.IRole.yml","Discord.IRole.IsHoisted":"Discord.IRole.yml","Discord.IRole.IsManaged":"Discord.IRole.yml","Discord.IRole.IsMentionable":"Discord.IRole.yml","Discord.IRole.Name":"Discord.IRole.yml","Discord.IRole.Permissions":"Discord.IRole.yml","Discord.IRole.Position":"Discord.IRole.yml","Discord.IRole.ModifyAsync(Action{Discord.RoleProperties},Discord.RequestOptions)":"Discord.IRole.yml","Discord.RoleProperties":"Discord.RoleProperties.yml","Discord.RoleProperties.Name":"Discord.RoleProperties.yml","Discord.RoleProperties.Permissions":"Discord.RoleProperties.yml","Discord.RoleProperties.Position":"Discord.RoleProperties.yml","Discord.RoleProperties.Color":"Discord.RoleProperties.yml","Discord.RoleProperties.Hoist":"Discord.RoleProperties.yml","Discord.RoleProperties.Mentionable":"Discord.RoleProperties.yml","Discord.Game":"Discord.Game.yml","Discord.Game.Name":"Discord.Game.yml","Discord.Game.StreamUrl":"Discord.Game.yml","Discord.Game.StreamType":"Discord.Game.yml","Discord.Game.#ctor(System.String,System.String,Discord.StreamType)":"Discord.Game.yml","Discord.Game.ToString":"Discord.Game.yml","Discord.GuildUserProperties":"Discord.GuildUserProperties.yml","Discord.GuildUserProperties.Mute":"Discord.GuildUserProperties.yml","Discord.GuildUserProperties.Deaf":"Discord.GuildUserProperties.yml","Discord.GuildUserProperties.Nickname":"Discord.GuildUserProperties.yml","Discord.GuildUserProperties.Roles":"Discord.GuildUserProperties.yml","Discord.GuildUserProperties.RoleIds":"Discord.GuildUserProperties.yml","Discord.GuildUserProperties.Channel":"Discord.GuildUserProperties.yml","Discord.GuildUserProperties.ChannelId":"Discord.GuildUserProperties.yml","Discord.IConnection":"Discord.IConnection.yml","Discord.IConnection.Id":"Discord.IConnection.yml","Discord.IConnection.Type":"Discord.IConnection.yml","Discord.IConnection.Name":"Discord.IConnection.yml","Discord.IConnection.IsRevoked":"Discord.IConnection.yml","Discord.IConnection.IntegrationIds":"Discord.IConnection.yml","Discord.IGroupUser":"Discord.IGroupUser.yml","Discord.IGuildUser":"Discord.IGuildUser.yml","Discord.IGuildUser.JoinedAt":"Discord.IGuildUser.yml","Discord.IGuildUser.Nickname":"Discord.IGuildUser.yml","Discord.IGuildUser.GuildPermissions":"Discord.IGuildUser.yml","Discord.IGuildUser.Guild":"Discord.IGuildUser.yml","Discord.IGuildUser.GuildId":"Discord.IGuildUser.yml","Discord.IGuildUser.RoleIds":"Discord.IGuildUser.yml","Discord.IGuildUser.GetPermissions(Discord.IGuildChannel)":"Discord.IGuildUser.yml","Discord.IGuildUser.KickAsync(Discord.RequestOptions)":"Discord.IGuildUser.yml","Discord.IGuildUser.ModifyAsync(Action{Discord.GuildUserProperties},Discord.RequestOptions)":"Discord.IGuildUser.yml","Discord.IPresence":"Discord.IPresence.yml","Discord.IPresence.Game":"Discord.IPresence.yml","Discord.IPresence.Status":"Discord.IPresence.yml","Discord.ISelfUser":"Discord.ISelfUser.yml","Discord.ISelfUser.Email":"Discord.ISelfUser.yml","Discord.ISelfUser.IsVerified":"Discord.ISelfUser.yml","Discord.ISelfUser.IsMfaEnabled":"Discord.ISelfUser.yml","Discord.ISelfUser.ModifyAsync(Action{Discord.SelfUserProperties},Discord.RequestOptions)":"Discord.ISelfUser.yml","Discord.IUser":"Discord.IUser.yml","Discord.IUser.AvatarId":"Discord.IUser.yml","Discord.IUser.AvatarUrl":"Discord.IUser.yml","Discord.IUser.Discriminator":"Discord.IUser.yml","Discord.IUser.DiscriminatorValue":"Discord.IUser.yml","Discord.IUser.IsBot":"Discord.IUser.yml","Discord.IUser.Username":"Discord.IUser.yml","Discord.IUser.GetDMChannelAsync(Discord.CacheMode,Discord.RequestOptions)":"Discord.IUser.yml","Discord.IUser.CreateDMChannelAsync(Discord.RequestOptions)":"Discord.IUser.yml","Discord.IVoiceState":"Discord.IVoiceState.yml","Discord.IVoiceState.IsDeafened":"Discord.IVoiceState.yml","Discord.IVoiceState.IsMuted":"Discord.IVoiceState.yml","Discord.IVoiceState.IsSelfDeafened":"Discord.IVoiceState.yml","Discord.IVoiceState.IsSelfMuted":"Discord.IVoiceState.yml","Discord.IVoiceState.IsSuppressed":"Discord.IVoiceState.yml","Discord.IVoiceState.VoiceChannel":"Discord.IVoiceState.yml","Discord.IVoiceState.VoiceSessionId":"Discord.IVoiceState.yml","Discord.SelfUserProperties":"Discord.SelfUserProperties.yml","Discord.SelfUserProperties.Username":"Discord.SelfUserProperties.yml","Discord.SelfUserProperties.Avatar":"Discord.SelfUserProperties.yml","Discord.StreamType":"Discord.StreamType.yml","Discord.StreamType.NotStreaming":"Discord.StreamType.yml","Discord.StreamType.Twitch":"Discord.StreamType.yml","Discord.UserStatus":"Discord.UserStatus.yml","Discord.UserStatus.Unknown":"Discord.UserStatus.yml","Discord.UserStatus.Online":"Discord.UserStatus.yml","Discord.UserStatus.Idle":"Discord.UserStatus.yml","Discord.UserStatus.AFK":"Discord.UserStatus.yml","Discord.UserStatus.DoNotDisturb":"Discord.UserStatus.yml","Discord.UserStatus.Invisible":"Discord.UserStatus.yml","Discord.UserStatus.Offline":"Discord.UserStatus.yml","Discord.AsyncEnumerableExtensions":"Discord.AsyncEnumerableExtensions.yml","Discord.AsyncEnumerableExtensions.Flatten``1(IAsyncEnumerable{IReadOnlyCollection{``0}})":"Discord.AsyncEnumerableExtensions.yml","Discord.DiscordClientExtensions":"Discord.DiscordClientExtensions.yml","Discord.DiscordClientExtensions.GetPrivateChannelAsync(Discord.IDiscordClient,System.UInt64)":"Discord.DiscordClientExtensions.yml","Discord.DiscordClientExtensions.GetDMChannelAsync(Discord.IDiscordClient,System.UInt64)":"Discord.DiscordClientExtensions.yml","Discord.DiscordClientExtensions.GetDMChannelsAsync(Discord.IDiscordClient)":"Discord.DiscordClientExtensions.yml","Discord.DiscordClientExtensions.GetGroupChannelAsync(Discord.IDiscordClient,System.UInt64)":"Discord.DiscordClientExtensions.yml","Discord.DiscordClientExtensions.GetGroupChannelsAsync(Discord.IDiscordClient)":"Discord.DiscordClientExtensions.yml","Discord.DiscordClientExtensions.GetOptimalVoiceRegionAsync(Discord.IDiscordClient)":"Discord.DiscordClientExtensions.yml","Discord.GuildExtensions":"Discord.GuildExtensions.yml","Discord.GuildExtensions.GetTextChannelAsync(Discord.IGuild,System.UInt64)":"Discord.GuildExtensions.yml","Discord.GuildExtensions.GetTextChannelsAsync(Discord.IGuild)":"Discord.GuildExtensions.yml","Discord.GuildExtensions.GetVoiceChannelAsync(Discord.IGuild,System.UInt64)":"Discord.GuildExtensions.yml","Discord.GuildExtensions.GetVoiceChannelsAsync(Discord.IGuild)":"Discord.GuildExtensions.yml","Discord.GuildExtensions.GetAFKChannelAsync(Discord.IGuild)":"Discord.GuildExtensions.yml","Discord.GuildExtensions.GetDefaultChannelAsync(Discord.IGuild)":"Discord.GuildExtensions.yml","Discord.GuildExtensions.GetEmbedChannelAsync(Discord.IGuild)":"Discord.GuildExtensions.yml","Discord.GuildExtensions.GetOwnerAsync(Discord.IGuild)":"Discord.GuildExtensions.yml","Discord.GuildUserExtensions":"Discord.GuildUserExtensions.yml","Discord.GuildUserExtensions.AddRolesAsync(Discord.IGuildUser,Discord.IRole[])":"Discord.GuildUserExtensions.yml","Discord.GuildUserExtensions.AddRolesAsync(Discord.IGuildUser,IEnumerable{Discord.IRole})":"Discord.GuildUserExtensions.yml","Discord.GuildUserExtensions.RemoveRolesAsync(Discord.IGuildUser,Discord.IRole[])":"Discord.GuildUserExtensions.yml","Discord.GuildUserExtensions.RemoveRolesAsync(Discord.IGuildUser,IEnumerable{Discord.IRole})":"Discord.GuildUserExtensions.yml","Discord.GuildUserExtensions.ChangeRolesAsync(Discord.IGuildUser,IEnumerable{Discord.IRole},IEnumerable{Discord.IRole})":"Discord.GuildUserExtensions.yml","Discord.LogMessage":"Discord.LogMessage.yml","Discord.LogMessage.Severity":"Discord.LogMessage.yml","Discord.LogMessage.Source":"Discord.LogMessage.yml","Discord.LogMessage.Message":"Discord.LogMessage.yml","Discord.LogMessage.Exception":"Discord.LogMessage.yml","Discord.LogMessage.#ctor(Discord.LogSeverity,System.String,System.String,Exception)":"Discord.LogMessage.yml","Discord.LogMessage.ToString":"Discord.LogMessage.yml","Discord.LogMessage.ToString(StringBuilder,System.Boolean,System.Boolean,DateTimeKind,System.Nullable{System.Int32})":"Discord.LogMessage.yml","Discord.LogSeverity":"Discord.LogSeverity.yml","Discord.LogSeverity.Critical":"Discord.LogSeverity.yml","Discord.LogSeverity.Error":"Discord.LogSeverity.yml","Discord.LogSeverity.Warning":"Discord.LogSeverity.yml","Discord.LogSeverity.Info":"Discord.LogSeverity.yml","Discord.LogSeverity.Verbose":"Discord.LogSeverity.yml","Discord.LogSeverity.Debug":"Discord.LogSeverity.yml","Discord.RpcException":"Discord.RpcException.yml","Discord.RpcException.ErrorCode":"Discord.RpcException.yml","Discord.RpcException.Reason":"Discord.RpcException.yml","Discord.RpcException.#ctor(System.Int32,System.String)":"Discord.RpcException.yml","Discord.MentionUtils":"Discord.MentionUtils.yml","Discord.MentionUtils.MentionUser(System.UInt64)":"Discord.MentionUtils.yml","Discord.MentionUtils.MentionChannel(System.UInt64)":"Discord.MentionUtils.yml","Discord.MentionUtils.MentionRole(System.UInt64)":"Discord.MentionUtils.yml","Discord.MentionUtils.ParseUser(System.String)":"Discord.MentionUtils.yml","Discord.MentionUtils.TryParseUser(System.String,System.UInt64@)":"Discord.MentionUtils.yml","Discord.MentionUtils.ParseChannel(System.String)":"Discord.MentionUtils.yml","Discord.MentionUtils.TryParseChannel(System.String,System.UInt64@)":"Discord.MentionUtils.yml","Discord.MentionUtils.ParseRole(System.String)":"Discord.MentionUtils.yml","Discord.MentionUtils.TryParseRole(System.String,System.UInt64@)":"Discord.MentionUtils.yml","Discord.Optional`1":"Discord.Optional-1.yml","Discord.Optional`1.Unspecified":"Discord.Optional-1.yml","Discord.Optional`1.Value":"Discord.Optional-1.yml","Discord.Optional`1.IsSpecified":"Discord.Optional-1.yml","Discord.Optional`1.#ctor(`0)":"Discord.Optional-1.yml","Discord.Optional`1.GetValueOrDefault":"Discord.Optional-1.yml","Discord.Optional`1.GetValueOrDefault(`0)":"Discord.Optional-1.yml","Discord.Optional`1.Equals(System.Object)":"Discord.Optional-1.yml","Discord.Optional`1.GetHashCode":"Discord.Optional-1.yml","Discord.Optional`1.ToString":"Discord.Optional-1.yml","Discord.Optional`1.op_Implicit(`0)~Discord.Optional{`0}":"Discord.Optional-1.yml","Discord.Optional`1.op_Explicit(Discord.Optional{`0})~`0":"Discord.Optional-1.yml","Discord.Optional":"Discord.Optional.yml","Discord.Optional.Create``1":"Discord.Optional.yml","Discord.Optional.Create``1(``0)":"Discord.Optional.yml","Discord.ChannelType":"Discord.ChannelType.yml","Discord.ChannelType.Text":"Discord.ChannelType.yml","Discord.ChannelType.DM":"Discord.ChannelType.yml","Discord.ChannelType.Voice":"Discord.ChannelType.yml","Discord.ChannelType.Group":"Discord.ChannelType.yml","Discord.RestGuildEmbed":"Discord.RestGuildEmbed.yml","Discord.RestGuildEmbed.IsEnabled":"Discord.RestGuildEmbed.yml","Discord.RestGuildEmbed.ChannelId":"Discord.RestGuildEmbed.yml","Discord.RestGuildEmbed.ToString":"Discord.RestGuildEmbed.yml","Discord.RestVoiceRegion":"Discord.RestVoiceRegion.yml","Discord.RestVoiceRegion.Name":"Discord.RestVoiceRegion.yml","Discord.RestVoiceRegion.IsVip":"Discord.RestVoiceRegion.yml","Discord.RestVoiceRegion.IsOptimal":"Discord.RestVoiceRegion.yml","Discord.RestVoiceRegion.SampleHostname":"Discord.RestVoiceRegion.yml","Discord.RestVoiceRegion.SamplePort":"Discord.RestVoiceRegion.yml","Discord.RestVoiceRegion.ToString":"Discord.RestVoiceRegion.yml","Discord.Attachment":"Discord.Attachment.yml","Discord.Attachment.Id":"Discord.Attachment.yml","Discord.Attachment.Filename":"Discord.Attachment.yml","Discord.Attachment.Url":"Discord.Attachment.yml","Discord.Attachment.ProxyUrl":"Discord.Attachment.yml","Discord.Attachment.Size":"Discord.Attachment.yml","Discord.Attachment.Height":"Discord.Attachment.yml","Discord.Attachment.Width":"Discord.Attachment.yml","Discord.Attachment.ToString":"Discord.Attachment.yml","Discord.EmbedBuilder":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.#ctor":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.Title":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.Description":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.Url":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.ThumbnailUrl":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.ImageUrl":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.Timestamp":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.Color":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.Author":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.Footer":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.WithTitle(System.String)":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.WithDescription(System.String)":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.WithUrl(System.String)":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.WithThumbnailUrl(System.String)":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.WithImageUrl(System.String)":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.WithCurrentTimestamp":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.WithTimestamp(DateTimeOffset)":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.WithColor(Color)":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.WithAuthor(Discord.EmbedAuthorBuilder)":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.WithAuthor(Action{Discord.EmbedAuthorBuilder})":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.WithFooter(Discord.EmbedFooterBuilder)":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.WithFooter(Action{Discord.EmbedFooterBuilder})":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.AddField(Action{Discord.EmbedFieldBuilder})":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.Build":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.op_Implicit(Discord.EmbedBuilder)~Embed":"Discord.EmbedBuilder.yml","Discord.EmbedFieldBuilder":"Discord.EmbedFieldBuilder.yml","Discord.EmbedFieldBuilder.Name":"Discord.EmbedFieldBuilder.yml","Discord.EmbedFieldBuilder.Value":"Discord.EmbedFieldBuilder.yml","Discord.EmbedFieldBuilder.IsInline":"Discord.EmbedFieldBuilder.yml","Discord.EmbedFieldBuilder.#ctor":"Discord.EmbedFieldBuilder.yml","Discord.EmbedFieldBuilder.WithName(System.String)":"Discord.EmbedFieldBuilder.yml","Discord.EmbedFieldBuilder.WithValue(System.String)":"Discord.EmbedFieldBuilder.yml","Discord.EmbedFieldBuilder.WithIsInline(System.Boolean)":"Discord.EmbedFieldBuilder.yml","Discord.EmbedFieldBuilder.Build":"Discord.EmbedFieldBuilder.yml","Discord.EmbedAuthorBuilder":"Discord.EmbedAuthorBuilder.yml","Discord.EmbedAuthorBuilder.Name":"Discord.EmbedAuthorBuilder.yml","Discord.EmbedAuthorBuilder.Url":"Discord.EmbedAuthorBuilder.yml","Discord.EmbedAuthorBuilder.IconUrl":"Discord.EmbedAuthorBuilder.yml","Discord.EmbedAuthorBuilder.#ctor":"Discord.EmbedAuthorBuilder.yml","Discord.EmbedAuthorBuilder.WithName(System.String)":"Discord.EmbedAuthorBuilder.yml","Discord.EmbedAuthorBuilder.WithUrl(System.String)":"Discord.EmbedAuthorBuilder.yml","Discord.EmbedAuthorBuilder.WithIconUrl(System.String)":"Discord.EmbedAuthorBuilder.yml","Discord.EmbedAuthorBuilder.Build":"Discord.EmbedAuthorBuilder.yml","Discord.EmbedFooterBuilder":"Discord.EmbedFooterBuilder.yml","Discord.EmbedFooterBuilder.Text":"Discord.EmbedFooterBuilder.yml","Discord.EmbedFooterBuilder.IconUrl":"Discord.EmbedFooterBuilder.yml","Discord.EmbedFooterBuilder.#ctor":"Discord.EmbedFooterBuilder.yml","Discord.EmbedFooterBuilder.WithText(System.String)":"Discord.EmbedFooterBuilder.yml","Discord.EmbedFooterBuilder.WithIconUrl(System.String)":"Discord.EmbedFooterBuilder.yml","Discord.EmbedFooterBuilder.Build":"Discord.EmbedFooterBuilder.yml","Discord.RestConnection":"Discord.RestConnection.yml","Discord.RestConnection.Id":"Discord.RestConnection.yml","Discord.RestConnection.Type":"Discord.RestConnection.yml","Discord.RestConnection.Name":"Discord.RestConnection.yml","Discord.RestConnection.IsRevoked":"Discord.RestConnection.yml","Discord.RestConnection.IntegrationIds":"Discord.RestConnection.yml","Discord.RestConnection.ToString":"Discord.RestConnection.yml","Discord.Rest":"Discord.Rest.yml","Discord.Rest.BaseDiscordClient":"Discord.Rest.BaseDiscordClient.yml","Discord.Rest.BaseDiscordClient.Log":"Discord.Rest.BaseDiscordClient.yml","Discord.Rest.BaseDiscordClient.LoggedIn":"Discord.Rest.BaseDiscordClient.yml","Discord.Rest.BaseDiscordClient.LoggedOut":"Discord.Rest.BaseDiscordClient.yml","Discord.Rest.BaseDiscordClient.LoginState":"Discord.Rest.BaseDiscordClient.yml","Discord.Rest.BaseDiscordClient.CurrentUser":"Discord.Rest.BaseDiscordClient.yml","Discord.Rest.BaseDiscordClient.LoginAsync(TokenType,System.String,System.Boolean)":"Discord.Rest.BaseDiscordClient.yml","Discord.Rest.BaseDiscordClient.OnLoginAsync(TokenType,System.String)":"Discord.Rest.BaseDiscordClient.yml","Discord.Rest.BaseDiscordClient.LogoutAsync":"Discord.Rest.BaseDiscordClient.yml","Discord.Rest.BaseDiscordClient.OnLogoutAsync":"Discord.Rest.BaseDiscordClient.yml","Discord.Rest.BaseDiscordClient.Dispose":"Discord.Rest.BaseDiscordClient.yml","Discord.Rest.DiscordRestClient":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.CurrentUser":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.#ctor":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.#ctor(Discord.Rest.DiscordRestConfig)":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.OnLoginAsync(TokenType,System.String)":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.OnLogoutAsync":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.GetApplicationInfoAsync":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.GetChannelAsync(System.UInt64)":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.GetPrivateChannelsAsync":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.GetConnectionsAsync":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.GetInviteAsync(System.String)":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.GetGuildAsync(System.UInt64)":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.GetGuildEmbedAsync(System.UInt64)":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.GetGuildSummariesAsync":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.GetGuildsAsync":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.CreateGuildAsync(System.String,IVoiceRegion,Stream)":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.GetUserAsync(System.UInt64)":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.GetGuildUserAsync(System.UInt64,System.UInt64)":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.GetVoiceRegionsAsync":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.GetVoiceRegionAsync(System.String)":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestConfig":"Discord.Rest.DiscordRestConfig.yml","Discord.Rest.DiscordRestConfig.UserAgent":"Discord.Rest.DiscordRestConfig.yml","Discord.Rest.DiscordRestConfig.RestClientProvider":"Discord.Rest.DiscordRestConfig.yml","Discord.Rest.RestApplication":"Discord.Rest.RestApplication.yml","Discord.Rest.RestApplication._iconId":"Discord.Rest.RestApplication.yml","Discord.Rest.RestApplication.Name":"Discord.Rest.RestApplication.yml","Discord.Rest.RestApplication.Description":"Discord.Rest.RestApplication.yml","Discord.Rest.RestApplication.RPCOrigins":"Discord.Rest.RestApplication.yml","Discord.Rest.RestApplication.Flags":"Discord.Rest.RestApplication.yml","Discord.Rest.RestApplication.Owner":"Discord.Rest.RestApplication.yml","Discord.Rest.RestApplication.CreatedAt":"Discord.Rest.RestApplication.yml","Discord.Rest.RestApplication.IconUrl":"Discord.Rest.RestApplication.yml","Discord.Rest.RestApplication.UpdateAsync":"Discord.Rest.RestApplication.yml","Discord.Rest.RestApplication.ToString":"Discord.Rest.RestApplication.yml","Discord.Rest.RestEntity`1":"Discord.Rest.RestEntity-1.yml","Discord.Rest.RestEntity`1.Discord":"Discord.Rest.RestEntity-1.yml","Discord.Rest.RestEntity`1.Id":"Discord.Rest.RestEntity-1.yml","Discord.Rest.IRestAudioChannel":"Discord.Rest.IRestAudioChannel.yml","Discord.Rest.IRestMessageChannel":"Discord.Rest.IRestMessageChannel.yml","Discord.Rest.IRestMessageChannel.SendMessageAsync(System.String,System.Boolean,Embed,RequestOptions)":"Discord.Rest.IRestMessageChannel.yml","Discord.Rest.IRestMessageChannel.SendFileAsync(Stream,System.String,System.String,System.Boolean,RequestOptions)":"Discord.Rest.IRestMessageChannel.yml","Discord.Rest.IRestMessageChannel.GetMessageAsync(System.UInt64,RequestOptions)":"Discord.Rest.IRestMessageChannel.yml","Discord.Rest.IRestMessageChannel.GetMessagesAsync(System.Int32,RequestOptions)":"Discord.Rest.IRestMessageChannel.yml","Discord.Rest.IRestMessageChannel.GetMessagesAsync(System.UInt64,Direction,System.Int32,RequestOptions)":"Discord.Rest.IRestMessageChannel.yml","Discord.Rest.IRestMessageChannel.GetMessagesAsync(IMessage,Direction,System.Int32,RequestOptions)":"Discord.Rest.IRestMessageChannel.yml","Discord.Rest.IRestMessageChannel.GetPinnedMessagesAsync(RequestOptions)":"Discord.Rest.IRestMessageChannel.yml","Discord.Rest.IRestPrivateChannel":"Discord.Rest.IRestPrivateChannel.yml","Discord.Rest.IRestPrivateChannel.Recipients":"Discord.Rest.IRestPrivateChannel.yml","Discord.Rest.RestChannel":"Discord.Rest.RestChannel.yml","Discord.Rest.RestChannel.CreatedAt":"Discord.Rest.RestChannel.yml","Discord.Rest.RestChannel.UpdateAsync(RequestOptions)":"Discord.Rest.RestChannel.yml","Discord.Rest.RestDMChannel":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.CurrentUser":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.Recipient":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.Users":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.UpdateAsync(RequestOptions)":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.CloseAsync(RequestOptions)":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.GetUser(System.UInt64)":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.GetMessageAsync(System.UInt64,RequestOptions)":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.GetMessagesAsync(System.Int32,RequestOptions)":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.GetMessagesAsync(System.UInt64,Direction,System.Int32,RequestOptions)":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.GetMessagesAsync(IMessage,Direction,System.Int32,RequestOptions)":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.GetPinnedMessagesAsync(RequestOptions)":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.SendMessageAsync(System.String,System.Boolean,Embed,RequestOptions)":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.SendFileAsync(Stream,System.String,System.String,System.Boolean,RequestOptions)":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.DeleteMessagesAsync(IEnumerable{IMessage},RequestOptions)":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.TriggerTypingAsync(RequestOptions)":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.EnterTypingState(RequestOptions)":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.ToString":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.Discord#Rest#IRestPrivateChannel#Recipients":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestGroupChannel":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.Name":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.Users":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.Recipients":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.UpdateAsync(RequestOptions)":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.LeaveAsync(RequestOptions)":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.GetUser(System.UInt64)":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.GetMessageAsync(System.UInt64,RequestOptions)":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.GetMessagesAsync(System.Int32,RequestOptions)":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.GetMessagesAsync(System.UInt64,Direction,System.Int32,RequestOptions)":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.GetMessagesAsync(IMessage,Direction,System.Int32,RequestOptions)":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.GetPinnedMessagesAsync(RequestOptions)":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.SendMessageAsync(System.String,System.Boolean,Embed,RequestOptions)":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.SendFileAsync(Stream,System.String,System.String,System.Boolean,RequestOptions)":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.DeleteMessagesAsync(IEnumerable{IMessage},RequestOptions)":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.TriggerTypingAsync(RequestOptions)":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.EnterTypingState(RequestOptions)":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.ToString":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.Discord#Rest#IRestPrivateChannel#Recipients":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGuildChannel":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestGuildChannel.PermissionOverwrites":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestGuildChannel.Name":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestGuildChannel.Position":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestGuildChannel.GuildId":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestGuildChannel.UpdateAsync(RequestOptions)":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestGuildChannel.ModifyAsync(Action{GuildChannelProperties},RequestOptions)":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestGuildChannel.DeleteAsync(RequestOptions)":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestGuildChannel.GetPermissionOverwrite(IUser)":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestGuildChannel.GetPermissionOverwrite(IRole)":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestGuildChannel.AddPermissionOverwriteAsync(IUser,OverwritePermissions,RequestOptions)":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestGuildChannel.AddPermissionOverwriteAsync(IRole,OverwritePermissions,RequestOptions)":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestGuildChannel.RemovePermissionOverwriteAsync(IUser,RequestOptions)":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestGuildChannel.RemovePermissionOverwriteAsync(IRole,RequestOptions)":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestGuildChannel.GetInvitesAsync(RequestOptions)":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestGuildChannel.CreateInviteAsync(System.Nullable{System.Int32},System.Nullable{System.Int32},System.Boolean,RequestOptions)":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestGuildChannel.ToString":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestTextChannel":"Discord.Rest.RestTextChannel.yml","Discord.Rest.RestTextChannel.Topic":"Discord.Rest.RestTextChannel.yml","Discord.Rest.RestTextChannel.Mention":"Discord.Rest.RestTextChannel.yml","Discord.Rest.RestTextChannel.ModifyAsync(Action{TextChannelProperties},RequestOptions)":"Discord.Rest.RestTextChannel.yml","Discord.Rest.RestTextChannel.GetUserAsync(System.UInt64,RequestOptions)":"Discord.Rest.RestTextChannel.yml","Discord.Rest.RestTextChannel.GetUsersAsync(RequestOptions)":"Discord.Rest.RestTextChannel.yml","Discord.Rest.RestTextChannel.GetMessageAsync(System.UInt64,RequestOptions)":"Discord.Rest.RestTextChannel.yml","Discord.Rest.RestTextChannel.GetMessagesAsync(System.Int32,RequestOptions)":"Discord.Rest.RestTextChannel.yml","Discord.Rest.RestTextChannel.GetMessagesAsync(System.UInt64,Direction,System.Int32,RequestOptions)":"Discord.Rest.RestTextChannel.yml","Discord.Rest.RestTextChannel.GetMessagesAsync(IMessage,Direction,System.Int32,RequestOptions)":"Discord.Rest.RestTextChannel.yml","Discord.Rest.RestTextChannel.GetPinnedMessagesAsync(RequestOptions)":"Discord.Rest.RestTextChannel.yml","Discord.Rest.RestTextChannel.SendMessageAsync(System.String,System.Boolean,Embed,RequestOptions)":"Discord.Rest.RestTextChannel.yml","Discord.Rest.RestTextChannel.SendFileAsync(Stream,System.String,System.String,System.Boolean,RequestOptions)":"Discord.Rest.RestTextChannel.yml","Discord.Rest.RestTextChannel.DeleteMessagesAsync(IEnumerable{IMessage},RequestOptions)":"Discord.Rest.RestTextChannel.yml","Discord.Rest.RestTextChannel.TriggerTypingAsync(RequestOptions)":"Discord.Rest.RestTextChannel.yml","Discord.Rest.RestTextChannel.EnterTypingState(RequestOptions)":"Discord.Rest.RestTextChannel.yml","Discord.Rest.RestVoiceChannel":"Discord.Rest.RestVoiceChannel.yml","Discord.Rest.RestVoiceChannel.Bitrate":"Discord.Rest.RestVoiceChannel.yml","Discord.Rest.RestVoiceChannel.UserLimit":"Discord.Rest.RestVoiceChannel.yml","Discord.Rest.RestVoiceChannel.ModifyAsync(Action{VoiceChannelProperties},RequestOptions)":"Discord.Rest.RestVoiceChannel.yml","Discord.Rest.RestBan":"Discord.Rest.RestBan.yml","Discord.Rest.RestBan.User":"Discord.Rest.RestBan.yml","Discord.Rest.RestBan.Reason":"Discord.Rest.RestBan.yml","Discord.Rest.RestBan.ToString":"Discord.Rest.RestBan.yml","Discord.Rest.RestGuild":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.Name":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.AFKTimeout":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.IsEmbeddable":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.VerificationLevel":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.MfaLevel":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.DefaultMessageNotifications":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.AFKChannelId":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.EmbedChannelId":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.OwnerId":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.VoiceRegionId":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.IconId":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.SplashId":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.CreatedAt":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.DefaultChannelId":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.IconUrl":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.SplashUrl":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.EveryoneRole":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.Roles":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.Emojis":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.Features":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.UpdateAsync(RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.DeleteAsync(RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.ModifyAsync(Action{GuildProperties},RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.ModifyEmbedAsync(Action{GuildEmbedProperties},RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.ModifyChannelsAsync(IEnumerable{BulkGuildChannelProperties},RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.ModifyRolesAsync(IEnumerable{BulkRoleProperties},RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.LeaveAsync(RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.GetBansAsync(RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.AddBanAsync(IUser,System.Int32,RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.AddBanAsync(System.UInt64,System.Int32,RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.RemoveBanAsync(IUser,RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.RemoveBanAsync(System.UInt64,RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.GetChannelsAsync(RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.GetChannelAsync(System.UInt64,RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.CreateTextChannelAsync(System.String,RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.CreateVoiceChannelAsync(System.String,RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.GetIntegrationsAsync(RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.CreateIntegrationAsync(System.UInt64,System.String,RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.GetInvitesAsync(RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.GetRole(System.UInt64)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.CreateRoleAsync(System.String,System.Nullable{GuildPermissions},System.Nullable{Color},System.Boolean,RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.GetUsersAsync(RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.GetUserAsync(System.UInt64,RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.GetCurrentUserAsync(RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.PruneUsersAsync(System.Int32,System.Boolean,RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.ToString":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuildIntegration":"Discord.Rest.RestGuildIntegration.yml","Discord.Rest.RestGuildIntegration.Name":"Discord.Rest.RestGuildIntegration.yml","Discord.Rest.RestGuildIntegration.Type":"Discord.Rest.RestGuildIntegration.yml","Discord.Rest.RestGuildIntegration.IsEnabled":"Discord.Rest.RestGuildIntegration.yml","Discord.Rest.RestGuildIntegration.IsSyncing":"Discord.Rest.RestGuildIntegration.yml","Discord.Rest.RestGuildIntegration.ExpireBehavior":"Discord.Rest.RestGuildIntegration.yml","Discord.Rest.RestGuildIntegration.ExpireGracePeriod":"Discord.Rest.RestGuildIntegration.yml","Discord.Rest.RestGuildIntegration.GuildId":"Discord.Rest.RestGuildIntegration.yml","Discord.Rest.RestGuildIntegration.RoleId":"Discord.Rest.RestGuildIntegration.yml","Discord.Rest.RestGuildIntegration.User":"Discord.Rest.RestGuildIntegration.yml","Discord.Rest.RestGuildIntegration.Account":"Discord.Rest.RestGuildIntegration.yml","Discord.Rest.RestGuildIntegration.SyncedAt":"Discord.Rest.RestGuildIntegration.yml","Discord.Rest.RestGuildIntegration.DeleteAsync":"Discord.Rest.RestGuildIntegration.yml","Discord.Rest.RestGuildIntegration.ModifyAsync(Action{GuildIntegrationProperties})":"Discord.Rest.RestGuildIntegration.yml","Discord.Rest.RestGuildIntegration.SyncAsync":"Discord.Rest.RestGuildIntegration.yml","Discord.Rest.RestGuildIntegration.ToString":"Discord.Rest.RestGuildIntegration.yml","Discord.Rest.RestUserGuild":"Discord.Rest.RestUserGuild.yml","Discord.Rest.RestUserGuild.Name":"Discord.Rest.RestUserGuild.yml","Discord.Rest.RestUserGuild.IsOwner":"Discord.Rest.RestUserGuild.yml","Discord.Rest.RestUserGuild.Permissions":"Discord.Rest.RestUserGuild.yml","Discord.Rest.RestUserGuild.CreatedAt":"Discord.Rest.RestUserGuild.yml","Discord.Rest.RestUserGuild.IconUrl":"Discord.Rest.RestUserGuild.yml","Discord.Rest.RestUserGuild.LeaveAsync(RequestOptions)":"Discord.Rest.RestUserGuild.yml","Discord.Rest.RestUserGuild.DeleteAsync(RequestOptions)":"Discord.Rest.RestUserGuild.yml","Discord.Rest.RestUserGuild.ToString":"Discord.Rest.RestUserGuild.yml","Discord.Rest.RestInvite":"Discord.Rest.RestInvite.yml","Discord.Rest.RestInvite.ChannelName":"Discord.Rest.RestInvite.yml","Discord.Rest.RestInvite.GuildName":"Discord.Rest.RestInvite.yml","Discord.Rest.RestInvite.ChannelId":"Discord.Rest.RestInvite.yml","Discord.Rest.RestInvite.GuildId":"Discord.Rest.RestInvite.yml","Discord.Rest.RestInvite.Code":"Discord.Rest.RestInvite.yml","Discord.Rest.RestInvite.Url":"Discord.Rest.RestInvite.yml","Discord.Rest.RestInvite.UpdateAsync(RequestOptions)":"Discord.Rest.RestInvite.yml","Discord.Rest.RestInvite.DeleteAsync(RequestOptions)":"Discord.Rest.RestInvite.yml","Discord.Rest.RestInvite.AcceptAsync(RequestOptions)":"Discord.Rest.RestInvite.yml","Discord.Rest.RestInvite.ToString":"Discord.Rest.RestInvite.yml","Discord.Rest.RestInviteMetadata":"Discord.Rest.RestInviteMetadata.yml","Discord.Rest.RestInviteMetadata.IsRevoked":"Discord.Rest.RestInviteMetadata.yml","Discord.Rest.RestInviteMetadata.IsTemporary":"Discord.Rest.RestInviteMetadata.yml","Discord.Rest.RestInviteMetadata.MaxAge":"Discord.Rest.RestInviteMetadata.yml","Discord.Rest.RestInviteMetadata.MaxUses":"Discord.Rest.RestInviteMetadata.yml","Discord.Rest.RestInviteMetadata.Uses":"Discord.Rest.RestInviteMetadata.yml","Discord.Rest.RestInviteMetadata.Inviter":"Discord.Rest.RestInviteMetadata.yml","Discord.Rest.RestInviteMetadata.CreatedAt":"Discord.Rest.RestInviteMetadata.yml","Discord.Rest.RestMessage":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.Channel":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.Author":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.Content":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.CreatedAt":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.IsTTS":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.IsPinned":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.EditedTimestamp":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.Attachments":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.Embeds":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.MentionedChannelIds":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.MentionedRoleIds":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.MentionedUsers":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.Tags":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.WebhookId":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.IsWebhook":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.Timestamp":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.UpdateAsync(RequestOptions)":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.DeleteAsync(RequestOptions)":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.ToString":"Discord.Rest.RestMessage.yml","Discord.Rest.RestReaction":"Discord.Rest.RestReaction.yml","Discord.Rest.RestReaction.Emoji":"Discord.Rest.RestReaction.yml","Discord.Rest.RestReaction.Count":"Discord.Rest.RestReaction.yml","Discord.Rest.RestReaction.Me":"Discord.Rest.RestReaction.yml","Discord.Rest.RestSystemMessage":"Discord.Rest.RestSystemMessage.yml","Discord.Rest.RestSystemMessage.Type":"Discord.Rest.RestSystemMessage.yml","Discord.Rest.RestUserMessage":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.IsTTS":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.IsPinned":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.WebhookId":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.EditedTimestamp":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.Attachments":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.Embeds":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.MentionedChannelIds":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.MentionedRoleIds":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.MentionedUsers":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.Tags":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.Reactions":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.ModifyAsync(Action{MessageProperties},RequestOptions)":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.AddReactionAsync(Emoji,RequestOptions)":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.AddReactionAsync(System.String,RequestOptions)":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.RemoveReactionAsync(Emoji,IUser,RequestOptions)":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.RemoveReactionAsync(System.String,IUser,RequestOptions)":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.RemoveAllReactionsAsync(RequestOptions)":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.GetReactionUsersAsync(System.String,System.Int32,System.Nullable{System.UInt64},RequestOptions)":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.PinAsync(RequestOptions)":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.UnpinAsync(RequestOptions)":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.Resolve(System.Int32,TagHandling,TagHandling,TagHandling,TagHandling,TagHandling)":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.Resolve(TagHandling,TagHandling,TagHandling,TagHandling,TagHandling)":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestRole":"Discord.Rest.RestRole.yml","Discord.Rest.RestRole.Color":"Discord.Rest.RestRole.yml","Discord.Rest.RestRole.IsHoisted":"Discord.Rest.RestRole.yml","Discord.Rest.RestRole.IsManaged":"Discord.Rest.RestRole.yml","Discord.Rest.RestRole.IsMentionable":"Discord.Rest.RestRole.yml","Discord.Rest.RestRole.Name":"Discord.Rest.RestRole.yml","Discord.Rest.RestRole.Permissions":"Discord.Rest.RestRole.yml","Discord.Rest.RestRole.Position":"Discord.Rest.RestRole.yml","Discord.Rest.RestRole.CreatedAt":"Discord.Rest.RestRole.yml","Discord.Rest.RestRole.IsEveryone":"Discord.Rest.RestRole.yml","Discord.Rest.RestRole.Mention":"Discord.Rest.RestRole.yml","Discord.Rest.RestRole.ModifyAsync(Action{RoleProperties},RequestOptions)":"Discord.Rest.RestRole.yml","Discord.Rest.RestRole.DeleteAsync(RequestOptions)":"Discord.Rest.RestRole.yml","Discord.Rest.RestRole.CompareTo(IRole)":"Discord.Rest.RestRole.yml","Discord.Rest.RestRole.ToString":"Discord.Rest.RestRole.yml","Discord.Rest.RestGroupUser":"Discord.Rest.RestGroupUser.yml","Discord.Rest.RestGuildUser":"Discord.Rest.RestGuildUser.yml","Discord.Rest.RestGuildUser.Nickname":"Discord.Rest.RestGuildUser.yml","Discord.Rest.RestGuildUser.IsDeafened":"Discord.Rest.RestGuildUser.yml","Discord.Rest.RestGuildUser.IsMuted":"Discord.Rest.RestGuildUser.yml","Discord.Rest.RestGuildUser.GuildId":"Discord.Rest.RestGuildUser.yml","Discord.Rest.RestGuildUser.GuildPermissions":"Discord.Rest.RestGuildUser.yml","Discord.Rest.RestGuildUser.RoleIds":"Discord.Rest.RestGuildUser.yml","Discord.Rest.RestGuildUser.JoinedAt":"Discord.Rest.RestGuildUser.yml","Discord.Rest.RestGuildUser.UpdateAsync(RequestOptions)":"Discord.Rest.RestGuildUser.yml","Discord.Rest.RestGuildUser.ModifyAsync(Action{GuildUserProperties},RequestOptions)":"Discord.Rest.RestGuildUser.yml","Discord.Rest.RestGuildUser.KickAsync(RequestOptions)":"Discord.Rest.RestGuildUser.yml","Discord.Rest.RestGuildUser.GetPermissions(IGuildChannel)":"Discord.Rest.RestGuildUser.yml","Discord.Rest.RestSelfUser":"Discord.Rest.RestSelfUser.yml","Discord.Rest.RestSelfUser.Email":"Discord.Rest.RestSelfUser.yml","Discord.Rest.RestSelfUser.IsVerified":"Discord.Rest.RestSelfUser.yml","Discord.Rest.RestSelfUser.IsMfaEnabled":"Discord.Rest.RestSelfUser.yml","Discord.Rest.RestSelfUser.UpdateAsync(RequestOptions)":"Discord.Rest.RestSelfUser.yml","Discord.Rest.RestSelfUser.ModifyAsync(Action{SelfUserProperties},RequestOptions)":"Discord.Rest.RestSelfUser.yml","Discord.Rest.RestUser":"Discord.Rest.RestUser.yml","Discord.Rest.RestUser.IsBot":"Discord.Rest.RestUser.yml","Discord.Rest.RestUser.Username":"Discord.Rest.RestUser.yml","Discord.Rest.RestUser.DiscriminatorValue":"Discord.Rest.RestUser.yml","Discord.Rest.RestUser.AvatarId":"Discord.Rest.RestUser.yml","Discord.Rest.RestUser.AvatarUrl":"Discord.Rest.RestUser.yml","Discord.Rest.RestUser.CreatedAt":"Discord.Rest.RestUser.yml","Discord.Rest.RestUser.Discriminator":"Discord.Rest.RestUser.yml","Discord.Rest.RestUser.Mention":"Discord.Rest.RestUser.yml","Discord.Rest.RestUser.Game":"Discord.Rest.RestUser.yml","Discord.Rest.RestUser.Status":"Discord.Rest.RestUser.yml","Discord.Rest.RestUser.UpdateAsync(RequestOptions)":"Discord.Rest.RestUser.yml","Discord.Rest.RestUser.CreateDMChannelAsync(RequestOptions)":"Discord.Rest.RestUser.yml","Discord.Rest.RestUser.ToString":"Discord.Rest.RestUser.yml"} \ No newline at end of file diff --git a/docs/docfx.json b/docs/docfx.json index e0b5514cd..3c0b0611e 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -5,7 +5,7 @@ { "src": "..", "files": [ - "src/**project.json" + "src/**/*.cs" ], "exclude": [ "**/obj/**", @@ -42,7 +42,8 @@ "resource": [ { "files": [ - "images/**" + "**/images/**", + "**/samples/**" ], "exclude": [ "obj/**", @@ -66,7 +67,7 @@ "default" ], "globalMetadata": { - "_appFooter": "Discord.Net (c) 2015-2016" + "_appFooter": "Discord.Net (c) 2015-2017" }, "noLangKeyword": false } diff --git a/docs/filterConfig.yml b/docs/filterConfig.yml index 79ea54ae0..715b39606 100644 --- a/docs/filterConfig.yml +++ b/docs/filterConfig.yml @@ -6,4 +6,6 @@ apiRules: - exclude: uidRegex: ^Discord\.Net\.Converters$ - exclude: - uidRegex: ^Discord\.Net.*$ \ No newline at end of file + uidRegex: ^Discord\.Net.*$ +- exclude: + uidRegex: ^RegexAnalyzer$ \ No newline at end of file diff --git a/docs/guides/commands.md b/docs/guides/commands/commands.md similarity index 93% rename from docs/guides/commands.md rename to docs/guides/commands/commands.md index 8f1a34db9..e021b1eb3 100644 --- a/docs/guides/commands.md +++ b/docs/guides/commands/commands.md @@ -1,5 +1,9 @@ # The Command Service +>[!WARNING] +>This article is out of date, and has not been rewritten yet. +Information is not guaranteed to be accurate. + [Discord.Commands](xref:Discord.Commands) provides an Attribute-based Command Parser. @@ -41,7 +45,7 @@ Discord.Net's implementation of Modules is influenced heavily from ASP.Net Core's Controller pattern. This means that the lifetime of a module instance is only as long as the command being invoked. -**Avoid using long-running code** in your modules whereever possible. +**Avoid using long-running code** in your modules wherever possible. You should **not** be implementing very much logic into your modules; outsource to a service for that. @@ -163,8 +167,8 @@ a dependency map. Modules are constructed using Dependency Injection. Any parameters that are placed in the constructor must be injected into an -@Discord.Commands.IDependencyMap. Alternatively, you may accept an -IDependencyMap as an argument and extract services yourself. +@System.IServiceProvider. Alternatively, you may accept an +IServiceProvider as an argument and extract services yourself. ### Module Properties @@ -201,21 +205,20 @@ you use DI when writing your modules. ### Setup -First, you need to create an @Discord.Commands.IDependencyMap. -The library includes @Discord.Commands.DependencyMap to help with -this, however you may create your own IDependencyMap if you wish. +First, you need to create an @System.IServiceProvider +You may create your own IServiceProvider if you wish. Next, add the dependencies your modules will use to the map. Finally, pass the map into the `LoadAssembly` method. Your modules will automatically be loaded with this dependency map. -[!code-csharp[DependencyMap Setup](samples/dependency_map_setup.cs)] +[!code-csharp[IServiceProvider Setup](samples/dependency_map_setup.cs)] ### Usage in Modules In the constructor of your module, any parameters will be filled in by -the @Discord.Commands.IDependencyMap you pass into `LoadAssembly`. +the @System.IServiceProvider you pass into `LoadAssembly`. Any publicly settable properties will also be filled in the same manner. @@ -224,12 +227,12 @@ Any publicly settable properties will also be filled in the same manner. being injected. >[!NOTE] ->If you accept `CommandService` or `IDependencyMap` as a parameter in +>If you accept `CommandService` or `IServiceProvider` as a parameter in your constructor or as an injectable property, these entries will be filled -by the CommandService the module was loaded from, and the DependencyMap passed +by the CommandService the module was loaded from, and the ServiceProvider passed into it, respectively. -[!code-csharp[DependencyMap in Modules](samples/dependency_module.cs)] +[!code-csharp[ServiceProvider in Modules](samples/dependency_module.cs)] # Preconditions diff --git a/docs/guides/samples/command_handler.cs b/docs/guides/commands/samples/command_handler.cs similarity index 69% rename from docs/guides/samples/command_handler.cs rename to docs/guides/commands/samples/command_handler.cs index 71869415b..6b5d4ad2b 100644 --- a/docs/guides/samples/command_handler.cs +++ b/docs/guides/commands/samples/command_handler.cs @@ -1,14 +1,16 @@ +using System; using System.Threading.Tasks; using System.Reflection; using Discord; using Discord.WebSocket; using Discord.Commands; +using Microsoft.Extensions.DependencyInjection; public class Program { private CommandService commands; private DiscordSocketClient client; - private DependencyMap map; + private IServiceProvider services; static void Main(string[] args) => new Program().Start().GetAwaiter().GetResult(); @@ -19,38 +21,40 @@ public class Program string token = "bot token here"; - map = new DependencyMap(); + services = new ServiceCollection() + .BuildServiceProvider(); await InstallCommands(); await client.LoginAsync(TokenType.Bot, token); - await client.ConnectAsync(); + await client.StartAsync(); await Task.Delay(-1); } + public async Task InstallCommands() { // Hook the MessageReceived Event into our Command Handler client.MessageReceived += HandleCommand; - // Discover all of the commands in this assembly and load them. + // Discover all of the commands in this assembly and load them. await commands.AddModulesAsync(Assembly.GetEntryAssembly()); } + public async Task HandleCommand(SocketMessage messageParam) - { + { // Don't process the command if it was a System Message var message = messageParam as SocketUserMessage; if (message == null) return; - // Create a number to track where the prefix ends and the command begins - int argPos = 0; - // Determine if the message is a command, based on if it starts with '!' or a mention prefix - if (!(message.HasCharPrefix('!', ref argPos) || message.HasMentionPrefix(client.CurrentUser, ref argPos))) return; + // Create a number to track where the prefix ends and the command begins + int argPos = 0; + // Determine if the message is a command, based on if it starts with '!' or a mention prefix + if (!(message.HasCharPrefix('!', ref argPos) || message.HasMentionPrefix(client.CurrentUser, ref argPos))) return; // Create a Command Context var context = new CommandContext(client, message); // Execute the command. (result does not indicate a return value, - // rather an object stating if the command executed succesfully) - var result = await commands.ExecuteAsync(context, argPos, map); + // rather an object stating if the command executed successfully) + var result = await commands.ExecuteAsync(context, argPos, service); if (!result.IsSuccess) await context.Channel.SendMessageAsync(result.ErrorReason); - } - + } } diff --git a/docs/guides/samples/dependency_map_setup.cs b/docs/guides/commands/samples/dependency_map_setup.cs similarity index 57% rename from docs/guides/samples/dependency_map_setup.cs rename to docs/guides/commands/samples/dependency_map_setup.cs index aa39150e7..e205d891d 100644 --- a/docs/guides/samples/dependency_map_setup.cs +++ b/docs/guides/commands/samples/dependency_map_setup.cs @@ -7,12 +7,11 @@ public class Commands { public async Task Install(DiscordSocketClient client) { - // Here, we will inject the Dependency Map with + // Here, we will inject the ServiceProvider with // all of the services our client will use. - _map.Add(client); - _map.Add(commands); - _map.Add(new NotificationService(_map)); - _map.Add(new DatabaseService(_map)); + _serviceCollection.AddSingleton(client) + _serviceCollection.AddSingleton(new NotificationService()) + _serviceCollection.AddSingleton(new DatabaseService()) // ... await _commands.AddModulesAsync(Assembly.GetEntryAssembly()); } diff --git a/docs/guides/samples/dependency_module.cs b/docs/guides/commands/samples/dependency_module.cs similarity index 100% rename from docs/guides/samples/dependency_module.cs rename to docs/guides/commands/samples/dependency_module.cs diff --git a/docs/guides/samples/empty-module.cs b/docs/guides/commands/samples/empty-module.cs similarity index 100% rename from docs/guides/samples/empty-module.cs rename to docs/guides/commands/samples/empty-module.cs diff --git a/docs/guides/samples/groups.cs b/docs/guides/commands/samples/groups.cs similarity index 100% rename from docs/guides/samples/groups.cs rename to docs/guides/commands/samples/groups.cs diff --git a/docs/guides/samples/module.cs b/docs/guides/commands/samples/module.cs similarity index 100% rename from docs/guides/samples/module.cs rename to docs/guides/commands/samples/module.cs diff --git a/docs/guides/samples/require_owner.cs b/docs/guides/commands/samples/require_owner.cs similarity index 68% rename from docs/guides/samples/require_owner.cs rename to docs/guides/commands/samples/require_owner.cs index 567b3d2af..3611afab8 100644 --- a/docs/guides/samples/require_owner.cs +++ b/docs/guides/commands/samples/require_owner.cs @@ -1,13 +1,19 @@ // (Note: This precondition is obsolete, it is recommended to use the RequireOwnerAttribute that is bundled with Discord.Commands) +using Discord.Commands; +using Discord.WebSocket; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Threading.Tasks; + // Inherit from PreconditionAttribute public class RequireOwnerAttribute : PreconditionAttribute { // Override the CheckPermissions method - public async override Task CheckPermissions(CommandContext context, CommandInfo command, IDependencyMap map) + public async override Task CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services) { // Get the ID of the bot's owner - var ownerId = (await map.Get().GetApplicationInfoAsync()).Owner.Id; + var ownerId = (await services.GetService().GetApplicationInfoAsync()).Owner.Id; // If this command was executed by that user, return a success if (context.User.Id == ownerId) return PreconditionResult.FromSuccess(); diff --git a/docs/guides/samples/typereader.cs b/docs/guides/commands/samples/typereader.cs similarity index 100% rename from docs/guides/samples/typereader.cs rename to docs/guides/commands/samples/typereader.cs diff --git a/docs/guides/concepts/connections.md b/docs/guides/concepts/connections.md new file mode 100644 index 000000000..30e5e55cd --- /dev/null +++ b/docs/guides/concepts/connections.md @@ -0,0 +1,58 @@ +--- +title: Managing Connections +--- + +In Discord.Net, once a client has been started, it will automatically +maintain a connection to Discord's gateway, until it is manually +stopped. + +### Usage + +To start a connection, invoke the `StartAsync` method on a client that +supports a WebSocket connection. + +These clients include the [DiscordSocketClient] and +[DiscordRpcClient], as well as Audio clients. + +To end a connection, invoke the `StopAsync` method. This will +gracefully close any open WebSocket or UdpSocket connections. + +Since the Start/Stop methods only signal to an underlying connection +manager that a connection needs to be started, **they return before a +connection is actually made.** + +As a result, you will need to hook into one of the connection-state +based events to have an accurate representation of when a client is +ready for use. + +All clients provide a `Connected` and `Disconnected` event, which is +raised respectively when a connection opens or closes. In the case of +the DiscordSocketClient, this does **not** mean that the client is +ready to be used. + +A separate event, `Ready`, is provided on DiscordSocketClient, which +is raised only when the client has finished guild stream or guild +sync, and has a complete guild cache. + +[DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient +[DiscordRpcClient]: xref:Discord.Rpc.DiscordRpcClient + +### Samples + +[!code-csharp[Connection Sample](samples/events.cs)] + +### Tips + +Avoid running long-running code on the gateway! If you deadlock the +gateway (as explained in [events]), the connection manager will be +unable to recover and reconnect. + +Assuming the client disconnected because of a fault on Discord's end, +and not a deadlock on your end, we will always attempt to reconnect +and resume a connection. + +Don't worry about trying to maintain your own connections, the +connection manager is designed to be bulletproof and never fail - if +your client doesn't manage to reconnect, you've found a bug! + +[events]: events.md \ No newline at end of file diff --git a/docs/guides/concepts/entities.md b/docs/guides/concepts/entities.md new file mode 100644 index 000000000..a38651829 --- /dev/null +++ b/docs/guides/concepts/entities.md @@ -0,0 +1,68 @@ +--- +title: Entities +--- + +>[!NOTE] +This article is written with the Socket variants of entities in mind, +not the general interfaces or Rest/Rpc entities. + +Discord.Net provides a versatile entity system for navigating the +Discord API. + +### Inheritance + +Due to the nature of the Discord API, some entities are designed with +multiple variants, for example, `SocketUser` and `SocketGuildUser`. + +All models will contain the most detailed version of an entity +possible, even if the type is less detailed. + +For example, in the case of the `MessageReceived` event, a +`SocketMessage` is passed in with a channel property of type +`SocketMessageChannel`. All messages come from channels capable of +messaging, so this is the only variant of a channel that can cover +every single case. + +But that doesn't mean a message _can't_ come from a +`SocketTextChannel`, which is a message channel in a guild. To +retrieve information about a guild from a message entity, you will +need to cast its channel object to a `SocketTextChannel`. + +### Navigation + +All socket entities have navigation properties on them, which allow +you to easily navigate to an entity's parent or children. As explained +above, you will sometimes need to cast to a more detailed version of +an entity to navigate to its parent. + +### Accessing Entities + +The most basic forms of entities, `SocketGuild`, `SocketUser`, and +`SocketChannel` can be pulled from the DiscordSocketClient's global +cache, and can be retrieved using the respective `GetXXX` method on +DiscordSocketClient. + +>[!TIP] +It is **vital** that you use the proper IDs for an entity when using +a GetXXX method. It is recommended that you enable Discord's +_developer mode_ to allow easy access to entity IDs, found in +Settings > Appearance > Advanced + +More detailed versions of entities can be pulled from the basic +entities, e.g. `SocketGuild.GetUser`, which returns a +`SocketGuildUser`, or `SocketGuild.GetChannel`, which returns a +`SocketGuildChannel`. Again, you may need to cast these objects to get +a variant of the type that you need. + +### Samples + +[!code-csharp[Entity Sample](samples/entities.cs)] + +### Tips + +Avoid using boxing-casts to coerce entities into a variant, use the +`as` keyword, and a null-conditional operator. + +This allows you to write safer code, and avoid InvalidCastExceptions. + +For example, `(message.Author as SocketGuildUser)?.Nickname`. \ No newline at end of file diff --git a/docs/guides/concepts/events.md b/docs/guides/concepts/events.md new file mode 100644 index 000000000..f2dfb00f0 --- /dev/null +++ b/docs/guides/concepts/events.md @@ -0,0 +1,84 @@ +--- +title: Working with Events +--- + +Events in Discord.Net are consumed in a similar manner to the standard +convention, with the exception that every event must be of the type +`System.Threading.Tasks.Task`, and instead of using EventArgs, the +event's parameters are passed directly into the handler. + +This allows for events to be handled in an async context directly, +instead of relying on async void. + +### Usage + +To receive data from an event, hook into it using C#'s delegate +event pattern. + +You may opt either to hook an event to an anonymous function (lambda) +or a named function. + +### Safety + +All events are designed to be thread-safe, in that events are executed +synchronously off the gateway task, in the same context as the gateway +task. + +As a side effect, this makes it possible to deadlock the gateway task, +and kill a connection. As a general rule of thumb, any task that takes +longer than three seconds should **not** be awaited directly in the +context of an event, but should be wrapped in a `Task.Run` or +offloaded to another task. + +This also means that you should not await a task that requests data +from Discord's gateway in the same context of an event. Since the +gateway will wait on all invoked event handlers to finish before +processing any additional data from the gateway, this will create +a deadlock that will be impossible to recover from. + +Exceptions in commands will be swallowed by the gateway and logged out +through the client's log method. + +### Common Patterns + +As you may know, events in Discord.Net are only given a signature of +`Func`. There is no room for predefined argument names, +so you must either consult IntelliSense, or view the API documentation +directly. + +That being said, there are a variety of common patterns that allow you +to infer what the parameters in an event mean. + +#### Entity, Entity + +An event handler with a signature of `Func` +typically means that the first object will be a clone of the entity +_before_ a change was made, and the latter object will be an attached +model of the entity _after_ the change was made. + +This pattern is typically only found on `EntityUpdated` events. + +#### Cacheable + +An event handler with a signature of `Func` +means that the `before` state of the entity was not provided by the +API, so it can either be pulled from the client's cache, or +downloaded from the API. + +See the documentation for [Cacheable] for more information on this +object. + +[Cacheable]: xref:Discord.Cacheable`2 + +### Samples + +[!code-csharp[Event Sample](samples/events.cs)] + +### Tips + +Many events relating to a Message entity, e.g. `MessageUpdated` +and `ReactionAdded` rely on the client's message cache, which is +**not** enabled by default. Set the `MessageCacheSize` flag in +[DiscordSocketConfig] to enable it. + +[DiscordSocketConfig]: xref:Discord.WebSocket.DiscordSocketConfig \ No newline at end of file diff --git a/docs/guides/concepts/logging.md b/docs/guides/concepts/logging.md new file mode 100644 index 000000000..1592dfc72 --- /dev/null +++ b/docs/guides/concepts/logging.md @@ -0,0 +1,42 @@ +--- +title: Logging +--- + +Discord.Net's clients provide a [Log] event that all messages will be +disbatched over. + +For more information about events in Discord.Net, see the [Events] +section. + +[Log]: xref:Discord.Rest.BaseDiscordClient#Discord_Rest_BaseDiscordClient_Log +[Events]: events.md + +### Usage + +To receive log events, simply hook the discord client's log method +to a Task with a single parameter of type [LogMessage] + +It is recommended that you use an established function instead of a +lambda for handling logs, because most [addons] accept a reference +to a logging function to write their own messages. + +### Usage in Commands + +Discord.Net's [CommandService] also provides a log event, identical +in signature to other log events. + +Data logged through this event is typically coupled with a +[CommandException], where information about the command's context +and error can be found and handled. + +#### Samples + +[!code-csharp[Logging Sample](samples/logging.cs)] + +#### Tips + +Due to the nature of Discord.Net's event system, all log event +handlers will be executed synchronously on the gateway thread. If your +log output will be dumped to a Web API (e.g. Sentry), you are advised +to wrap your output in a `Task.Run` so the gateway thread does not +become blocked while waiting for logging data to be written. \ No newline at end of file diff --git a/docs/guides/concepts/samples/connections.cs b/docs/guides/concepts/samples/connections.cs new file mode 100644 index 000000000..f96251a39 --- /dev/null +++ b/docs/guides/concepts/samples/connections.cs @@ -0,0 +1,23 @@ +using Discord; +using Discord.WebSocket; + +public class Program +{ + private DiscordSocketClient _client; + static void Main(string[] args) => new Program().MainAsync().GetAwaiter().GetResult(); + + public async Task MainAsync() + { + _client = new DiscordSocketClient(); + + await _client.LoginAsync(TokenType.Bot, "bot token"); + await _client.StartAsync(); + + Console.WriteLine("Press any key to exit..."); + Console.ReadKey(); + + await _client.StopAsync(); + // Wait a little for the client to finish disconnecting before allowing the program to return + await Task.Delay(500); + } +} \ No newline at end of file diff --git a/docs/guides/concepts/samples/entities.cs b/docs/guides/concepts/samples/entities.cs new file mode 100644 index 000000000..7655c44e9 --- /dev/null +++ b/docs/guides/concepts/samples/entities.cs @@ -0,0 +1,13 @@ +public string GetChannelTopic(ulong id) +{ + var channel = client.GetChannel(81384956881809408) as SocketTextChannel; + if (channel == null) return ""; + return channel.Topic; +} + +public string GuildOwner(SocketChannel channel) +{ + var guild = (channel as SocketGuildChannel)?.Guild; + if (guild == null) return ""; + return Context.Guild.Owner.Username; +} \ No newline at end of file diff --git a/docs/guides/concepts/samples/events.cs b/docs/guides/concepts/samples/events.cs new file mode 100644 index 000000000..cf0492cb5 --- /dev/null +++ b/docs/guides/concepts/samples/events.cs @@ -0,0 +1,36 @@ +using Discord; +using Discord.WebSocket; + +public class Program +{ + private DiscordSocketClient _client; + static void Main(string[] args) => new Program().MainAsync().GetAwaiter().GetResult(); + + public async Task MainAsync() + { + // When working with events that have Cacheable parameters, + // you must enable the message cache in your config settings if you plan to + // use the cached message entity. + var _config = new DiscordSocketConfig { MessageCacheSize = 100 }; + _client = new DiscordSocketClient(_config); + + await _client.LoginAsync(TokenType.Bot, "bot token"); + await _client.StartAsync(); + + _client.MessageUpdated += MessageUpdated; + _client.Ready += () => + { + Console.WriteLine("Bot is connected!"); + return Task.CompletedTask; + } + + await Task.Delay(-1); + } + + private async Task MessageUpdated(Cacheable before, SocketMessage after, ISocketMessageChannel channel) + { + // If the message was not in the cache, downloading it will result in getting a copy of `after`. + var message = await before.GetOrDownloadAsync(); + Console.WriteLine($"{message} -> {after}"); + } +} diff --git a/docs/guides/concepts/samples/logging.cs b/docs/guides/concepts/samples/logging.cs new file mode 100644 index 000000000..a2ddf7b90 --- /dev/null +++ b/docs/guides/concepts/samples/logging.cs @@ -0,0 +1,29 @@ +using Discord; +using Discord.WebSocket; + +public class Program +{ + private DiscordSocketClient _client; + static void Main(string[] args) => new Program().MainAsync().GetAwaiter().GetResult(); + + public async Task MainAsync() + { + _client = new DiscordSocketClient(new DiscordSocketConfig + { + LogLevel = LogSeverity.Info + }); + + _client.Log += Log; + + await _client.LoginAsync(TokenType.Bot, "bot token"); + await _client.StartAsync(); + + await Task.Delay(-1); + } + + private Task Log(LogMessage message) + { + Console.WriteLine(message.ToString()); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/docs/guides/events.md b/docs/guides/events.md deleted file mode 100644 index b10dc7648..000000000 --- a/docs/guides/events.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: Events ---- - -# Events - -Messages from Discord are exposed via events, and follow a pattern of `Func<[event params], Task>`, which allows you to easily create either async or sync event handlers. - -To hook into events, you must be using the @Discord.WebSocket.DiscordSocketClient, which provides WebSocket capabilities, necessary for receiving events. - ->[!NOTE] ->The gateway will wait for all registered handlers of an event to finish before raising the next event. As a result of this, it is reccomended that if you need to perform any heavy work in an event handler, it is done on its own thread or Task. - -**For further documentation of all events**, it is reccomended to look at the [Events Section](xref:Discord.WebSocket.DiscordSocketClient#events) on the API documentation of @Discord.WebSocket.DiscordSocketClient - -## Connection State - -Connection Events will be raised when the Connection State of your client changes. - -[DiscordSocketClient.Connected](xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_Connected) and [Disconnected](xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_Disconnected) are raised when the Gateway Socket connects or disconnects, respectively. - ->[!WARNING] ->You should not use DiscordClient.Connected to run code when your client first connects to Discord. The client has not received and parsed the READY event and guild stream yet, and will have an incomplete or empty cache. - -[DiscordSocketClient.Ready](xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_Ready) is raised when the `READY` packet is parsed and received from Discord. - ->[!NOTE] ->The [DiscordSocketClient.ConnectAsync](xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_ConnectAsync_System_Boolean_) method will not return until the READY packet has been processed. By default, it also will not return until the guild stream has finished. This means it is safe to run bot code directly after awaiting the ConnectAsync method. \ No newline at end of file diff --git a/docs/guides/getting_started/images/install-rider-add.png b/docs/guides/getting_started/images/install-rider-add.png new file mode 100644 index 000000000..3f0dc729e Binary files /dev/null and b/docs/guides/getting_started/images/install-rider-add.png differ diff --git a/docs/guides/getting_started/images/install-rider-nuget-manager.png b/docs/guides/getting_started/images/install-rider-nuget-manager.png new file mode 100644 index 000000000..884b32d6b Binary files /dev/null and b/docs/guides/getting_started/images/install-rider-nuget-manager.png differ diff --git a/docs/guides/getting_started/images/install-rider-search.png b/docs/guides/getting_started/images/install-rider-search.png new file mode 100644 index 000000000..32f2f9722 Binary files /dev/null and b/docs/guides/getting_started/images/install-rider-search.png differ diff --git a/docs/guides/getting_started/images/install-vs-deps.png b/docs/guides/getting_started/images/install-vs-deps.png new file mode 100644 index 000000000..ab10bd1bd Binary files /dev/null and b/docs/guides/getting_started/images/install-vs-deps.png differ diff --git a/docs/guides/getting_started/images/install-vs-nuget.png b/docs/guides/getting_started/images/install-vs-nuget.png new file mode 100644 index 000000000..64da79a9f Binary files /dev/null and b/docs/guides/getting_started/images/install-vs-nuget.png differ diff --git a/docs/guides/getting_started/images/intro-add-bot.png b/docs/guides/getting_started/images/intro-add-bot.png new file mode 100644 index 000000000..e40997ed3 Binary files /dev/null and b/docs/guides/getting_started/images/intro-add-bot.png differ diff --git a/docs/guides/getting_started/images/intro-client-id.png b/docs/guides/getting_started/images/intro-client-id.png new file mode 100644 index 000000000..e370aa2ec Binary files /dev/null and b/docs/guides/getting_started/images/intro-client-id.png differ diff --git a/docs/guides/getting_started/images/intro-create-app.png b/docs/guides/getting_started/images/intro-create-app.png new file mode 100644 index 000000000..7aceb84b4 Binary files /dev/null and b/docs/guides/getting_started/images/intro-create-app.png differ diff --git a/docs/guides/getting_started/images/intro-create-bot.png b/docs/guides/getting_started/images/intro-create-bot.png new file mode 100644 index 000000000..0522358cf Binary files /dev/null and b/docs/guides/getting_started/images/intro-create-bot.png differ diff --git a/docs/guides/getting_started/images/intro-token.png b/docs/guides/getting_started/images/intro-token.png new file mode 100644 index 000000000..8617cb76f Binary files /dev/null and b/docs/guides/getting_started/images/intro-token.png differ diff --git a/docs/guides/getting_started/installing.md b/docs/guides/getting_started/installing.md new file mode 100644 index 000000000..82d242647 --- /dev/null +++ b/docs/guides/getting_started/installing.md @@ -0,0 +1,141 @@ +--- +title: Installing Discord.Net +--- + +Discord.Net is distributed through the NuGet package manager, and it is +recommended to use NuGet to get started. + +Optionally, you may compile from source and install yourself. + +# Supported Platforms + +Currently, Discord.Net targets [.NET Standard] 1.3, and offers support for +.NET Standard 1.1. If your application will be targeting .NET Standard 1.1, +please see the [additional steps](#installing-on-net-standard-11). + +Since Discord.Net is built on the .NET Standard, it is also recommended to +create applications using [.NET Core], though you are not required to. When +using .NET Framework, it is suggested to target `.NET 4.6.1` or higher. + +[.NET Standard]: https://docs.microsoft.com/en-us/dotnet/articles/standard/library +[.NET Core]: https://docs.microsoft.com/en-us/dotnet/articles/core/ + +# Installing with NuGet + +Release builds of Discord.Net 1.0 will be published to the +[official NuGet feed]. + +Development builds of Discord.Net 1.0, as well as [addons](TODO) are published +to our development [MyGet feed]. + +Direct feed link: `https://www.myget.org/F/discord-net/api/v3/index.json` + +Not sure how to add a direct feed? See how [with Visual Studio] +or [without Visual Studio](#configuring-nuget-without-visual-studio) + +[official NuGet feed]: https://nuget.org +[MyGet feed]: https://www.myget.org/feed/Packages/discord-net +[with Visual Studio]: https://docs.microsoft.com/en-us/nuget/tools/package-manager-ui#package-sources + + +## Using Visual Studio + +1. Create a solution for your bot +2. In Solution Explorer, find the 'Dependencies' element under your bot's +project +3. Right click on 'Dependencies', and select 'Manage NuGet packages' +![Step 3](images/install-vs-deps.png) +4. In the 'browse' tab, search for 'Discord.Net' + +> [!TIP] +Don't forget to change your package source if you're installing from the +developer feed. +Also make sure to check 'Enable Prereleases' if installing a dev build! + +5. Install the 'Discord.Net' package + +![Step 5](images/install-vs-nuget.png) + +## Using JetBrains Rider + +1. Create a new solution for your bot +2. Open the NuGet window (Tools > NuGet > Manage NuGet packages for Solution) +![Step 2](images/install-rider-nuget-manager.png) +3. In the 'Packages' tab, search for 'Discord.Net' +![Step 3](images/install-rider-search.png) + +> [!TIP] +Make sure to check the 'Prerelease' box if installing a dev build! + +4. Install by adding the package to your project +![Step 4](images/install-rider-add.png) + +## Using Visual Studio Code + +1. Create a new project for your bot +2. Add Discord.Net to your .csproj + +[!code-xml[Sample .csproj](samples/project.csproj)] + +> [!TIP] +Don't forget to add the package source to a [NuGet.Config file](#configuring-nuget-without-visual-studio) if you're installing from the +developer feed. + +# Compiling from Source + +In order to compile Discord.Net, you require the following: + +### Using Visual Studio + +- [Visual Studio 2017](https://www.visualstudio.com/) +- [.NET Core SDK 1.0](https://www.microsoft.com/net/download/core#/sdk) + +The .NET Core and Docker (Preview) workload is required during Visual Studio +installation. + +### Using Command Line + +- [.NET Core SDK 1.0](https://www.microsoft.com/net/download/core#/sdk) + +# Additional Information + +## Installing on .NET Standard 1.1 + +For applications targeting a runtime corresponding with .NET Standard 1.1 or 1.2, +the builtin WebSocket and UDP provider will not work. For applications which +utilize a WebSocket connection to Discord (WebSocket or RPC), third-party +provider packages will need to be installed and configured. + +First, install the following packages through NuGet, or compile yourself, if +you prefer: + +- Discord.Net.Providers.WS4Net +- Discord.Net.Providers.UDPClient + +Note that `Discord.Net.Providers.UDPClient` is _only_ required if your bot will +be utilizing voice chat. + +Next, you will need to configure your [DiscordSocketClient] to use these custom +providers over the default ones. + +To do this, set the `WebSocketProvider` and optionally `UdpSocketProvider` +properties on the [DiscordSocketConfig] that you are passing into your +client. + +[!code-csharp[NET Standard 1.1 Example](samples/netstd11.cs)] + +[DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient +[DiscordSocketConfig]: xref:Discord.WebSocket.DiscordSocketConfig + +## Configuring NuGet without Visual Studio + +If you plan on deploying your bot or developing outside of Visual Studio, you +will need to create a local NuGet configuration file for your project. + +To do this, create a file named `nuget.config` alongside the root of your +application, where the project solution is located. + +Paste the following snippets into this configuration file, adding any additional +feeds as necessary. + +[!code-xml[NuGet Configuration](samples/nuget.config)] diff --git a/docs/guides/getting_started/intro.md b/docs/guides/getting_started/intro.md new file mode 100644 index 000000000..837814511 --- /dev/null +++ b/docs/guides/getting_started/intro.md @@ -0,0 +1,227 @@ +--- +title: Getting Started +--- + +# Making a Ping-Pong bot + +One of the first steps to getting started with the Discord API is to +write a basic ping-pong bot. We will expand on this to create more +diverse commands later, but for now, it is a good starting point. + +## Creating a Discord Bot + +Before you can begin writing your bot, it is necessary to create a bot +account on Discord. + +1. Visit the [Discord Applications Portal] +2. Create a New Application +3. Give the application a name (this will be the bot's initial +username). +4. Create the Application +![Step 4](images/intro-create-app.png) +5. In the application review page, click **Create a Bot User** +![Step 5](images/intro-create-bot.png) +6. Confirm the popup +7. If this bot will be public, check 'Public Bot'. +**Do not tick any other options!** + +[Discord Applications Portal]: https://discordapp.com/developers/applications/me + +## Adding your bot to a server + +Bots **can not** use invite links, they must be explicitly invited +through the OAuth2 flow. + +1. Open your bot's application on the [Discord Applications Portal] +2. Retrieve the app's **Client ID**. + +![Step 2](images/intro-client-id.png) + +3. Create an OAuth2 authorization URL +`https://discordapp.com/oauth2/authorize?client_id=&scope=bot` +4. Open the authorization URL in your browser +5. Select a server + +>[!NOTE] +Only servers where you have the `MANAGE_SERVER` permission will be +present in this list. + +6. Click authorize + +![Step 6](images/intro-add-bot.png) + +## Connecting to Discord + +If you have not already created a project and installed Discord.Net, +do that now. (see the [Installing](installing.md) section) + +### Async + +Discord.Net uses .NET's Task-based Asynchronous Pattern ([TAP]) +extensively - nearly every operation is asynchronous. + +It is highly recommended that these operations be awaited in a +properly established async context whenever possible. Establishing an +async context can be problematic, but not hard. + +To do so, we will be creating an async main in your console +application, and rewriting the static main method to invoke the new +async main. + +[!code-csharp[Async Context](samples/intro/async-context.cs)] + +As a result of this, your program will now start, and immidiately +jump into an async context. This will allow us later on to create a +connection to Discord, without needing to worry about setting up the +correct async implementation. + +>[!TIP] +If your application throws any exceptions within an async context, +they will be thrown all the way back up to the first non-async method. +Since our first non-async method is the program's Main method, this +means that **all** unhandled exceptions will be thrown up there, which +will crash your application. Discord.Net will prevent exceptions in +event handlers from crashing your program, but any exceptions in your +async main **will** cause the application to crash. + +### Creating a logging method + +Before we create and configure a Discord client, we will add a method +to handle Discord.Net's log events. + +To allow agnostic support of as many log providers as possible, we +log information through a Log event, with a proprietary LogMessage +parameter. See the [API Documentation] for this event. + +If you are using your own logging framework, this is where you would +invoke it. For the sake of simplicity, we will only be logging to +the Console. + +[!code-csharp[Async Context](samples/intro/logging.cs)] + +### Creating a Discord Client + +Finally, we can create a connection to Discord. Since we are writing +a bot, we will be using a [DiscordSocketClient], along with socket +entities. See the [terminology](terminology.md) if you're unsure of +the differences. + +To do so, create an instance of [DiscordSocketClient] in your async +main, passing in a configuration object only if necessary. For most +users, the default will work fine. + +Before connecting, we should hook the client's log event to the +log handler that was just created. Events in Discord.Net work +similarly to other events in C#, so hook this event the way that +you typically would. + +Next, you will need to 'login to Discord' with the `LoginAsync` method. + +You may create a variable to hold your bot's token (this can be found +on your bot's application page on the [Discord Applications Portal]). +![Token](images/intro-token.png) + +>[!IMPORTANT] +Your bot's token can be used to gain total access to your bot, so +**do __NOT__ share this token with anyone!** It may behoove you to +store this token in an external file if you plan on distributing the +source code for your bot. + +We may now invoke the client's `StartAsync` method, which will +start connection/reconnection logic. It is important to note that +**this method returns as soon as connection logic has been started!** + +Any methods that rely on the client's state should go in an event +handler. + +>[!NOTE] +Connection logic is incomplete as of the current build. Events will +soon be added to indicate when the client's state is ready for use; +(rewrite this section when possible) + +Finally, we will want to block the async main method from returning +until after the application is exited. To do this, we can await an +infinite delay, or any other blocking method, such as reading from +the console. + +The following lines can now be added: + +[!code-csharp[Create client](samples/intro/client.cs)] + +At this point, feel free to start your program and see your bot come +online in Discord. + +>[!TIP] +Encountering a `PlatformNotSupportedException` when starting your bot? +This means that you are targeting a platform where .NET's default +WebSocket client is not supported. Refer to the [installing guide] +for how to fix this. + +[TAP]: https://docs.microsoft.com/en-us/dotnet/articles/csharp/async +[API Documentation]: xref:Discord.Rest.BaseDiscordClient#Discord_Rest_BaseDiscordClient_Log +[DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient +[installing guide]: installing.md#installing-on-net-standard-11 + +### Handling a 'ping' + +Now that we have learned how to open a connection to Discord, we can +begin handling messages that users are sending. + +To start out, our bot will listen for any message where the content +is equal to `!ping`, and respond back with `Pong!`. + +Since we want to listen for new messages, the event to hook in to +is [MessageReceived]. + +In your program, add a method that matches the signature of the +MessageReceived event - it must be a method (`Func`) that returns the +type `Task`, and takes a single parameter, a [SocketMessage]. Also, +since we will be sending data to Discord in this method, we will flag +it as `async`. + +In this method, we will add an `if` block, to determine if the message +content fits the rules of our scenario - recall that it must be equal +to `!ping`. + +Inside the branch of this condition, we will want to send a message +back to the channel from which the message came - `Pong!`. To find the +channel, look for the `Channel` property on the message parameter. + +Next, we will want to send a message to this channel. Since the +channel object is of type [SocketMessageChannel], we can invoke the +`SendMessageAsync` instance method. For the message content, send back +a string containing 'Pong!'. + +You should have now added the following lines: + +[!code-csharp[Message](samples/intro/message.cs)] + +Now, your first bot is complete. You may continue to add on to this +if you desire, but for any bot that will be carrying out multiple +commands, it is strongly encouraged to use the command framework, as +shown below. + +For your reference, you may view the [completed program]. + +[MessageReceived]: xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_MessageReceived +[SocketMessage]: xref:Discord.WebSocket.SocketMessage +[SocketMessageChannel]: xref:Discord.WebSocket.ISocketMessageChannel +[completed program]: samples/intro/complete.cs + +# Building a bot with commands + +This section will show you how to write a program that is ready for +[commands](commands/commands.md). Note that this will not be explaining _how_ +to write commands or services, it will only be covering the general +structure. + +For reference, view an [annotated example] of this structure. + +[annotated example]: samples/intro/structure.cs + +It is important to know that the recommended design pattern of bots +should be to separate the program (initialization and command handler), +the modules (handle commands), and the services (persistent storage, +pure functions, data manipulation). + +**todo:** diagram of bot structure diff --git a/docs/guides/getting_started/samples/intro/async-context.cs b/docs/guides/getting_started/samples/intro/async-context.cs new file mode 100644 index 000000000..c01ddec55 --- /dev/null +++ b/docs/guides/getting_started/samples/intro/async-context.cs @@ -0,0 +1,15 @@ +using System; +using System.Threading.Tasks; + +namespace MyBot +{ + public class Program + { + public static void Main(string[] args) + => new Program().MainAsync().GetAwaiter().GetResult(); + + public async Task MainAsync() + { + } + } +} \ No newline at end of file diff --git a/docs/guides/getting_started/samples/intro/client.cs b/docs/guides/getting_started/samples/intro/client.cs new file mode 100644 index 000000000..ea7c91932 --- /dev/null +++ b/docs/guides/getting_started/samples/intro/client.cs @@ -0,0 +1,16 @@ +// Program.cs +using Discord.WebSocket; +// ... +public async Task MainAsync() +{ + var client = new DiscordSocketClient(); + + client.Log += Log; + + string token = "abcdefg..."; // Remember to keep this private! + await client.LoginAsync(TokenType.Bot, token); + await client.StartAsync(); + + // Block this task until the program is closed. + await Task.Delay(-1); +} \ No newline at end of file diff --git a/docs/guides/getting_started/samples/intro/complete.cs b/docs/guides/getting_started/samples/intro/complete.cs new file mode 100644 index 000000000..b59b6b4d9 --- /dev/null +++ b/docs/guides/getting_started/samples/intro/complete.cs @@ -0,0 +1,42 @@ +using Discord; +using Discord.WebSocket; +using System; +using System.Threading.Tasks; + +namespace MyBot +{ + public class Program + { + public static void Main(string[] args) + => new Program().MainAsync().GetAwaiter().GetResult(); + + public async Task MainAsync() + { + var client = new DiscordSocketClient(); + + client.Log += Log; + client.MessageReceived += MessageReceived; + + string token = "abcdefg..."; // Remember to keep this private! + await client.LoginAsync(TokenType.Bot, token); + await client.StartAsync(); + + // Block this task until the program is closed. + await Task.Delay(-1); + } + + private async Task MessageReceived(SocketMessage message) + { + if (message.Content == "!ping") + { + await message.Channel.SendMessageAsync("Pong!"); + } + } + + private Task Log(LogMessage msg) + { + Console.WriteLine(msg.ToString()); + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/docs/guides/getting_started/samples/intro/logging.cs b/docs/guides/getting_started/samples/intro/logging.cs new file mode 100644 index 000000000..4fb85a063 --- /dev/null +++ b/docs/guides/getting_started/samples/intro/logging.cs @@ -0,0 +1,22 @@ +using Discord; +using System; +using System.Threading.Tasks; + +namespace MyBot +{ + public class Program + { + public static void Main(string[] args) + => new Program().MainAsync().GetAwaiter().GetResult(); + + public async Task MainAsync() + { + } + + private Task Log(LogMessage msg) + { + Console.WriteLine(msg.ToString()); + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/docs/guides/getting_started/samples/intro/message.cs b/docs/guides/getting_started/samples/intro/message.cs new file mode 100644 index 000000000..d3cda46e5 --- /dev/null +++ b/docs/guides/getting_started/samples/intro/message.cs @@ -0,0 +1,14 @@ +public async Task MainAsync() +{ + // client.Log ... + client.MessageReceived += MessageReceived; + // ... +} + +private async Task MessageReceived(SocketMessage message) +{ + if (message.Content == "!ping") + { + await message.Channel.SendMessageAsync("Pong!"); + } +} \ No newline at end of file diff --git a/docs/guides/samples/first-steps.cs b/docs/guides/getting_started/samples/intro/structure.cs similarity index 71% rename from docs/guides/samples/first-steps.cs rename to docs/guides/getting_started/samples/intro/structure.cs index 3f1377ed7..706d0a38d 100644 --- a/docs/guides/samples/first-steps.cs +++ b/docs/guides/getting_started/samples/intro/structure.cs @@ -1,6 +1,7 @@ using System; using System.Reflection; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Discord; using Discord.Commands; using Discord.WebSocket; @@ -9,8 +10,8 @@ class Program { private readonly DiscordSocketClient _client; - // Keep the CommandService and IDependencyMap around for use with commands. - private readonly IDependencyMap _map = new DependencyMap(); + // Keep the CommandService and IServiceCollection around for use with commands. + private readonly IServiceCollection _map = new ServiceCollection(); private readonly CommandService _commands = new CommandService(); // Program entry point @@ -29,8 +30,8 @@ class Program LogLevel = LogSeverity.Info, // If you or another service needs to do anything with messages - // (eg. checking Reactions), you should probably - // set the MessageCacheSize here. + // (eg. checking Reactions, checking the content of edited/deleted messages), + // you must set the MessageCacheSize. You may adjust the number as needed. //MessageCacheSize = 50, // If your platform doesn't have native websockets, @@ -40,7 +41,7 @@ class Program }); } - // Create a named logging handler, so it can be re-used by addons + // Example of a logging handler. This can be re-used by addons // that ask for a Func. private static Task Logger(LogMessage message) { @@ -64,6 +65,13 @@ class Program } Console.WriteLine($"{DateTime.Now,-19} [{message.Severity,8}] {message.Source}: {message.Message}"); Console.ForegroundColor = cc; + + // If you get an error saying 'CompletedTask' doesn't exist, + // your project is targeting .NET 4.5.2 or lower. You'll need + // to adjust your project's target framework to 4.6 or higher + // (instructions for this are easily Googled). + // If you *need* to run on .NET 4.5 for compat/other reasons, + // the alternative is to 'return Task.Delay(0);' instead. return Task.CompletedTask; } @@ -77,28 +85,36 @@ class Program // Login and connect. await _client.LoginAsync(TokenType.Bot, /* */); - await _client.ConnectAsync(); - + await _client.StartAsync(); + // Wait infinitely so your bot actually stays connected. await Task.Delay(-1); } + private IServiceProvider _services; + private async Task InitCommands() { // Repeat this for all the service classes // and other dependencies that your commands might need. - _map.Add(new SomeServiceClass()); + _map.AddSingleton(new SomeServiceClass()); + + // When all your required services are in the collection, build the container. + // Tip: There's an overload taking in a 'validateScopes' bool to make sure + // you haven't made any mistakes in your dependency graph. + _services = _map.BuildServiceProvider(); - // Either search the program and add all Module classes that can be found: + // Either search the program and add all Module classes that can be found. + // Module classes *must* be marked 'public' or they will be ignored. await _commands.AddModulesAsync(Assembly.GetEntryAssembly()); // Or add Modules manually if you prefer to be a little more explicit: await _commands.AddModuleAsync(); // Subscribe a handler to see if a message invokes a command. - _client.MessageReceived += CmdHandler; + _client.MessageReceived += HandleCommandAsync; } - private async Task CmdHandler(SocketMessage arg) + private async Task HandleCommandAsync(SocketMessage arg) { // Bail out if it's a System Message. var msg = arg as SocketUserMessage; @@ -110,14 +126,14 @@ class Program // you want to prefix your commands with. // Uncomment the second half if you also want // commands to be invoked by mentioning the bot instead. - if (msg.HasCharPrefix('!', ref pos) /* || msg.HasMentionPrefix(msg.Discord.CurrentUser, ref pos) */) + if (msg.HasCharPrefix('!', ref pos) /* || msg.HasMentionPrefix(_client.CurrentUser, ref pos) */) { - // Create a Command Context - var context = new SocketCommandContext(msg.Discord, msg); + // Create a Command Context. + var context = new SocketCommandContext(_client, msg); // Execute the command. (result does not indicate a return value, // rather an object stating if the command executed succesfully). - var result = await _commands.ExecuteAsync(context, pos, _map); + var result = await _commands.ExecuteAsync(context, pos, _services); // Uncomment the following lines if you want the bot // to send a message if it failed (not advised for most situations). @@ -125,4 +141,4 @@ class Program // await msg.Channel.SendMessageAsync(result.ErrorReason); } } -} \ No newline at end of file +} diff --git a/docs/guides/getting_started/samples/netstd11.cs b/docs/guides/getting_started/samples/netstd11.cs new file mode 100644 index 000000000..a8573696a --- /dev/null +++ b/docs/guides/getting_started/samples/netstd11.cs @@ -0,0 +1,9 @@ +using Discord.Providers.WS4Net; +using Discord.Providers.UDPClient; +using Discord.WebSocket; +// ... +var client = new DiscordSocketClient(new DiscordSocketConfig +{ + WebSocketProvider = WS4NetProvider.Instance, + UdpSocketProvider = UDPClientProvider.Instance, +}); \ No newline at end of file diff --git a/docs/guides/getting_started/samples/nuget.config b/docs/guides/getting_started/samples/nuget.config new file mode 100644 index 000000000..bf706a08b --- /dev/null +++ b/docs/guides/getting_started/samples/nuget.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/docs/guides/getting_started/samples/project.csproj b/docs/guides/getting_started/samples/project.csproj new file mode 100644 index 000000000..8daf71877 --- /dev/null +++ b/docs/guides/getting_started/samples/project.csproj @@ -0,0 +1,13 @@ + + + + Exe + netcoreapp1.1 + true + + + + + + + diff --git a/docs/guides/terminology.md b/docs/guides/getting_started/terminology.md similarity index 100% rename from docs/guides/terminology.md rename to docs/guides/getting_started/terminology.md diff --git a/docs/guides/intro.md b/docs/guides/intro.md deleted file mode 100644 index f16bc9883..000000000 --- a/docs/guides/intro.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: Getting Started ---- - -# Getting Started - -## Requirements - -Discord.Net supports logging in with all variations of Discord Accounts, however the Discord API reccomends using a `Bot Account`. - -You may [register a bot account here](https://discordapp.com/developers/applications/me). - -Bot accounts must be added to a server, you must use the [OAuth 2 Flow](https://discordapp.com/developers/docs/topics/oauth2#adding-bots-to-guilds) to add them to servers. - -## Installation - -You can install Discord.Net 1.0 from our [MyGet Feed](https://www.myget.org/feed/Packages/discord-net). - -**For most users writing bots, install only `Discord.Net.WebSocket`.** - -You may add the MyGet feed to Visual Studio directly from `https://www.myget.org/F/discord-net/api/v3/index.json`. - -You can also pull the latest source from [GitHub](https://github.com/RogueException/Discord.Net). - ->[!WARNING] ->The versions of Discord.Net on NuGet are behind the versions this ->documentation is written for. ->You MUST install from MyGet or Source! - -## Async - -Discord.Net uses C# tasks extensiely - nearly all operations return -one. - -It is highly reccomended these tasks be awaited whenever possible. -To do so requires the calling method to be marked as async, which -can be problematic in a console application. An example of how to -get around this is provided below. - -For more information, go to [MSDN's Async-Await section.](https://msdn.microsoft.com/en-us/library/hh191443.aspx) - -## First Steps - -[!code-csharp[Main](samples/first-steps.cs)] - ->[!NOTE] ->In previous versions of Discord.Net, you had to hook into the `Ready` and `GuildAvailable` events to determine when your client was ready for use. ->In 1.0, the [ConnectAsync] method will automatically wait for the Ready event, and for all guilds to stream. To avoid this, pass `false` into `ConnectAsync`. - -[ConnectAsync]: xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_ConnectAsync_System_Boolean_ \ No newline at end of file diff --git a/docs/guides/logging.md b/docs/guides/logging.md deleted file mode 100644 index 97fed3a52..000000000 --- a/docs/guides/logging.md +++ /dev/null @@ -1,11 +0,0 @@ -# Using the Logger - -Discord.Net will automatically output log messages through the [Log](xref:Discord.DiscordClient#Discord_DiscordClient_Log) event. - -## Usage - -To handle Log Messages through Discord.Net's Logger, hook into the [Log](xref:Discord.DiscordClient#Discord_DiscordClient_Log) event. - -The @Discord.LogMessage object has a custom `ToString` method attached to it, when outputting log messages, it is reccomended you use this, instead of building your own output message. - -[!code-csharp[](samples/logging.cs)] \ No newline at end of file diff --git a/docs/migrating.md b/docs/guides/migrating/migrating.md similarity index 94% rename from docs/migrating.md rename to docs/guides/migrating/migrating.md index 8f96dff98..bc628a5f8 100644 --- a/docs/migrating.md +++ b/docs/guides/migrating/migrating.md @@ -42,7 +42,7 @@ events are delegates, but are still registered the same. For example, let's look at [DiscordSocketClient.MessageReceived](xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_MessageReceived) To hook an event into MessageReceived, we now use the following code: -[!code-csharp[Event Registration](guides/samples/migrating/event.cs)] +[!code-csharp[Event Registration](samples/event.cs)] > **All Event Handlers in 1.0 MUST return Task!** @@ -50,7 +50,7 @@ If your event handler is marked as `async`, it will automatically return `Task`. if you do not need to execute asynchronus code, do _not_ mark your handler as `async`, and instead, stick a `return Task.CompletedTask` at the bottom. -[!code-csharp[Sync Event Registration](guides/samples/migrating/sync_event.cs)] +[!code-csharp[Sync Event Registration](samples/sync_event.cs)] **Event handlers no longer require a sender.** The only arguments your event handler needs to accept are the parameters used by the event. It is recommended to look at the event in IntelliSense or on the diff --git a/docs/guides/samples/migrating/event.cs b/docs/guides/migrating/samples/event.cs similarity index 100% rename from docs/guides/samples/migrating/event.cs rename to docs/guides/migrating/samples/event.cs diff --git a/docs/guides/samples/migrating/sync_event.cs b/docs/guides/migrating/samples/sync_event.cs similarity index 100% rename from docs/guides/samples/migrating/sync_event.cs rename to docs/guides/migrating/samples/sync_event.cs diff --git a/docs/guides/samples.md b/docs/guides/samples.md deleted file mode 100644 index 4406f2f1e..000000000 --- a/docs/guides/samples.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -title: Samples ---- - -# Samples - ->[!NOTE] ->All of these samples assume you have `_client` defined as a `DiscordSocketClient`. - -#### Changing the bot's avatar - -[!code-csharp[Bot Avatar](samples/faq/avatar.cs)] - -#### Changing the bot's status - -[!code-csharp[Bot Status](samples/faq/status.cs)] - -#### Sending a message to a channel - -[!code-csharp[Message to Channel](samples/faq/send_message.cs)] diff --git a/docs/guides/samples/faq/avatar.cs b/docs/guides/samples/faq/avatar.cs deleted file mode 100644 index d3995cf0c..000000000 --- a/docs/guides/samples/faq/avatar.cs +++ /dev/null @@ -1,5 +0,0 @@ -public async Task ChangeAvatar() -{ - var fileStream = new FileStream("./newAvatar.png", FileMode.Open); - await _client.CurrentUser.ModifyAsync(x => x.Avatar = fileStream); -} \ No newline at end of file diff --git a/docs/guides/samples/faq/send_message.cs b/docs/guides/samples/faq/send_message.cs deleted file mode 100644 index d7ecf5131..000000000 --- a/docs/guides/samples/faq/send_message.cs +++ /dev/null @@ -1,6 +0,0 @@ -public async Task SendMessageToChannel(ulong ChannelId) -{ - var channel = _client.GetChannel(ChannelId) as SocketMessageChannel; - await channel?.SendMessageAsync("aaaaaaaaahhh!!!") - /* ^ This question mark is used to indicate that 'channel' may sometimes be null, and in cases that it is null, we will do nothing here. */ -} \ No newline at end of file diff --git a/docs/guides/samples/faq/status.cs b/docs/guides/samples/faq/status.cs deleted file mode 100644 index 18906c53b..000000000 --- a/docs/guides/samples/faq/status.cs +++ /dev/null @@ -1,5 +0,0 @@ -public async Task ModifyStatus() -{ - await _client.SetStatusAsync(UserStatus.Idle); - await _client.SetGameAsync("Type !help for help"); -} diff --git a/docs/guides/samples/logging.cs b/docs/guides/samples/logging.cs deleted file mode 100644 index fd72daf2b..000000000 --- a/docs/guides/samples/logging.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Discord; -using Discord.Rest; - -public class Program -{ - private DiscordSocketClient _client; - static void Main(string[] args) => new Program().Start().GetAwaiter().GetResult(); - - public async Task Start() - { - _client = new DiscordSocketClient(new DiscordSocketConfig() { - LogLevel = LogSeverity.Info - }); - - _client.Log += Log; - - await _client.LoginAsync(TokenType.Bot, "bot token"); - await _client.ConnectAsync(); - - await Task.Delay(-1); - } - - private Task Log(LogMessage message) - { - Console.WriteLine(message.ToString()); - return Task.CompletedTask; - } -} diff --git a/docs/guides/toc.yml b/docs/guides/toc.yml index a420e4a1c..2e3a61e19 100644 --- a/docs/guides/toc.yml +++ b/docs/guides/toc.yml @@ -1,15 +1,27 @@ - - name: Getting Started - href: intro.md -- name: Terminology - href: terminology.md -- name: Logging - href: logging.md -- name: Commands - href: commands.md + items: + - name: Installation + href: getting_started/installing.md + - name: Your First Bot + href: getting_started/intro.md + - name: Terminology + href: getting_started/terminology.md +- name: Basic Concepts + items: + - name: Logging Data + href: concepts/logging.md + - name: Working with Events + href: concepts/events.md + - name: Managing Connections + href: concepts/connections.md + - name: Entities + href: concepts/entities.md +- name: The Command Service + items: + - name: Command Guide + href: commands/commands.md - name: Voice - href: voice.md -- name: Events - href: events.md -- name: Code Samples - href: samples.md \ No newline at end of file + items: + - name: Voice Guide + href: voice/sending-voice.md +- name: Migrating from 0.9 \ No newline at end of file diff --git a/docs/guides/samples/audio_create_ffmpeg.cs b/docs/guides/voice/samples/audio_create_ffmpeg.cs similarity index 100% rename from docs/guides/samples/audio_create_ffmpeg.cs rename to docs/guides/voice/samples/audio_create_ffmpeg.cs diff --git a/docs/guides/samples/audio_ffmpeg.cs b/docs/guides/voice/samples/audio_ffmpeg.cs similarity index 80% rename from docs/guides/samples/audio_ffmpeg.cs rename to docs/guides/voice/samples/audio_ffmpeg.cs index 877050caf..b9430ac11 100644 --- a/docs/guides/samples/audio_ffmpeg.cs +++ b/docs/guides/voice/samples/audio_ffmpeg.cs @@ -3,7 +3,7 @@ private async Task SendAsync(IAudioClient client, string path) // Create FFmpeg using the previous example var ffmpeg = CreateStream(path); var output = ffmpeg.StandardOutput.BaseStream; - var discord = client.CreatePCMStream(1920); + var discord = client.CreatePCMStream(AudioApplication.Mixed); await output.CopyToAsync(discord); await discord.FlushAsync(); -} \ No newline at end of file +} diff --git a/docs/guides/samples/joining_audio.cs b/docs/guides/voice/samples/joining_audio.cs similarity index 100% rename from docs/guides/samples/joining_audio.cs rename to docs/guides/voice/samples/joining_audio.cs diff --git a/docs/guides/voice.md b/docs/guides/voice/sending-voice.md similarity index 86% rename from docs/guides/voice.md rename to docs/guides/voice/sending-voice.md index 1f09069f5..c3ec8d9d7 100644 --- a/docs/guides/voice.md +++ b/docs/guides/voice/sending-voice.md @@ -1,24 +1,14 @@ -# Voice +--- +title: Sending Voice +--- **Information on this page is subject to change!** >[!WARNING] ->Audio in 1.0 is incomplete. Most of the below documentation is untested. +>This article is out of date, and has not been rewritten yet. +Information is not guaranteed to be accurate. -## Installation - -To use Audio, you must first configure your [DiscordSocketClient] -with Audio support. - -In your [DiscordSocketConfig], set `AudioMode` to the appropriate -[AudioMode] for your bot. For most bots, you will only need to use -`AudioMode.Outgoing`. - -[DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient -[DiscordSocketConfig]: xref:Discord.WebSocket.DiscordSocketConfig -[AudioMode]: xref:Discord.Audio.AudioMode - -### Dependencies +## Installing Audio requires two native libraries, `libsodium` and `opus`. Both of these libraries must be placed in the runtime directory of your diff --git a/docs/index.md b/docs/index.md index 3f0393d4f..ef9ecdfdd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,7 +3,7 @@ Discord.Net is an asynchronous, multiplatform .NET Library used to interface with the [Discord API](https://discordapp.com/). -If this is your first time using Discord.Net, you should refer to the [Intro](guides/intro.md) for tutorials. +If this is your first time using Discord.Net, you should refer to the [Intro](guides/getting_started/intro.md) for tutorials. More experienced users might refer to the [API Documentation](api/index.md) for a breakdown of the individuals objects in the library. For additional resources: diff --git a/docs/toc.yml b/docs/toc.yml index 47a8a22c1..c08e708bf 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -1,8 +1,6 @@ - name: Guides href: guides/ -- name: Migrating - href: migrating.md - name: API Documentation href: api/ homepage: api/index.md diff --git a/src/Discord.Net.Analyzers/AssemblyInfo.cs b/experiment/Discord.Net.Analyzers/AssemblyInfo.cs similarity index 100% rename from src/Discord.Net.Analyzers/AssemblyInfo.cs rename to experiment/Discord.Net.Analyzers/AssemblyInfo.cs diff --git a/src/Discord.Net.Analyzers/ConfigureAwaitAnalyzer.cs b/experiment/Discord.Net.Analyzers/ConfigureAwaitAnalyzer.cs similarity index 100% rename from src/Discord.Net.Analyzers/ConfigureAwaitAnalyzer.cs rename to experiment/Discord.Net.Analyzers/ConfigureAwaitAnalyzer.cs diff --git a/experiment/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj b/experiment/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj new file mode 100644 index 000000000..86541691d --- /dev/null +++ b/experiment/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj @@ -0,0 +1,18 @@ + + + + Discord.Net.Analyzers + Discord.Analyzers + A Discord.Net extension adding compile-time analysis. + netstandard1.3 + $(PackageTargetFallback);portable-net45+win81 + + + + + + + all + + + \ No newline at end of file diff --git a/src/Discord.Net.Relay/ApplicationBuilderExtensions.cs b/experiment/Discord.Net.Relay/ApplicationBuilderExtensions.cs similarity index 100% rename from src/Discord.Net.Relay/ApplicationBuilderExtensions.cs rename to experiment/Discord.Net.Relay/ApplicationBuilderExtensions.cs diff --git a/src/Discord.Net.Relay/AssemblyInfo.cs b/experiment/Discord.Net.Relay/AssemblyInfo.cs similarity index 100% rename from src/Discord.Net.Relay/AssemblyInfo.cs rename to experiment/Discord.Net.Relay/AssemblyInfo.cs diff --git a/experiment/Discord.Net.Relay/Discord.Net.Relay.csproj b/experiment/Discord.Net.Relay/Discord.Net.Relay.csproj new file mode 100644 index 000000000..2e9101600 --- /dev/null +++ b/experiment/Discord.Net.Relay/Discord.Net.Relay.csproj @@ -0,0 +1,18 @@ + + + + Discord.Net.Relay + Discord.Relay + A core Discord.Net library containing the Relay server. + netstandard1.3 + + + + + + + + + + + \ No newline at end of file diff --git a/src/Discord.Net.Relay/RelayConnection.cs b/experiment/Discord.Net.Relay/RelayConnection.cs similarity index 100% rename from src/Discord.Net.Relay/RelayConnection.cs rename to experiment/Discord.Net.Relay/RelayConnection.cs diff --git a/src/Discord.Net.Relay/RelayServer.cs b/experiment/Discord.Net.Relay/RelayServer.cs similarity index 100% rename from src/Discord.Net.Relay/RelayServer.cs rename to experiment/Discord.Net.Relay/RelayServer.cs diff --git a/pack.ps1 b/pack.ps1 deleted file mode 100644 index 0f84ea309..000000000 --- a/pack.ps1 +++ /dev/null @@ -1,17 +0,0 @@ -dotnet pack "src\Discord.Net.Core\Discord.Net.Core.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" -if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } -dotnet pack "src\Discord.Net.Rest\Discord.Net.Rest.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" -if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } -dotnet pack "src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" -if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } -dotnet pack "src\Discord.Net.Rpc\Discord.Net.Rpc.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" -if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } -dotnet pack "src\Discord.Net.Commands\Discord.Net.Commands.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" -if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } -dotnet pack "src\Discord.Net.Providers.WS4Net\Discord.Net.Providers.WS4Net.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" -if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } -dotnet pack "src\Discord.Net.Providers.UdpClient\Discord.Net.Providers.UdpClient.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" -if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } - -nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties build="$Env:BUILD" -if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } \ No newline at end of file diff --git a/src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj b/src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj deleted file mode 100644 index 0612e423f..000000000 --- a/src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj +++ /dev/null @@ -1,32 +0,0 @@ - - - 1.0.0 - rc-dev - rc-$(BuildNumber) - netstandard1.3 - Discord.Net.Analyzers - RogueException - A Discord.Net extension adding compile-time analysis. - discord;discordapp - https://github.com/RogueException/Discord.Net - http://opensource.org/licenses/MIT - git - git://github.com/RogueException/Discord.Net - Discord.Analyzers - portable-net45+win81 - true - - - - - - - all - - - - $(NoWarn);CS1573;CS1591 - true - true - - \ No newline at end of file diff --git a/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs b/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs index 168d15e5f..49dae6080 100644 --- a/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs @@ -1,11 +1,12 @@ using System; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true, Inherited = true)] public abstract class ParameterPreconditionAttribute : Attribute { - public abstract Task CheckPermissions(ICommandContext context, ParameterInfo parameter, object value, IDependencyMap map); + public abstract Task CheckPermissions(ICommandContext context, ParameterInfo parameter, object value, IServiceProvider services); } } \ No newline at end of file diff --git a/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs b/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs index 7755d459b..3727510d9 100644 --- a/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs @@ -6,6 +6,13 @@ namespace Discord.Commands [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = true)] public abstract class PreconditionAttribute : Attribute { - public abstract Task CheckPermissions(ICommandContext context, CommandInfo command, IDependencyMap map); + /// + /// Specify a group that this precondition belongs to. Preconditions of the same group require only one + /// of the preconditions to pass in order to be successful (A || B). Specifying = + /// or not at all will require *all* preconditions to pass, just like normal (A && B). + /// + public string Group { get; set; } = null; + + public abstract Task CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services); } } diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs index 520cfa6fd..0f865e864 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { @@ -41,16 +42,18 @@ namespace Discord.Commands GuildPermission = null; } - public override async Task CheckPermissions(ICommandContext context, CommandInfo command, IDependencyMap map) + public override async Task CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services) { - var guildUser = await context.Guild.GetCurrentUserAsync(); + IGuildUser guildUser = null; + if (context.Guild != null) + guildUser = await context.Guild.GetCurrentUserAsync().ConfigureAwait(false); if (GuildPermission.HasValue) { if (guildUser == null) return PreconditionResult.FromError("Command must be used in a guild channel"); if (!guildUser.GuildPermissions.Has(GuildPermission.Value)) - return PreconditionResult.FromError($"Command requires guild permission {GuildPermission.Value}"); + return PreconditionResult.FromError($"Bot requires guild permission {GuildPermission.Value}"); } if (ChannelPermission.HasValue) @@ -64,7 +67,7 @@ namespace Discord.Commands perms = ChannelPermissions.All(guildChannel); if (!perms.Has(ChannelPermission.Value)) - return PreconditionResult.FromError($"Command requires channel permission {ChannelPermission.Value}"); + return PreconditionResult.FromError($"Bot requires channel permission {ChannelPermission.Value}"); } return PreconditionResult.FromSuccess(); diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs index 42d835c30..a221eb4a9 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { @@ -37,7 +38,7 @@ namespace Discord.Commands Contexts = contexts; } - public override Task CheckPermissions(ICommandContext context, CommandInfo command, IDependencyMap map) + public override Task CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services) { bool isValid = false; diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs new file mode 100644 index 000000000..94235b1ae --- /dev/null +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs @@ -0,0 +1,20 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + /// + /// Require that the command is invoked in a channel marked NSFW + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class RequireNsfwAttribute : PreconditionAttribute + { + public override Task CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services) + { + if (context.Channel.IsNsfw) + return Task.FromResult(PreconditionResult.FromSuccess()); + else + return Task.FromResult(PreconditionResult.FromError("This command may only be invoked in an NSFW channel.")); + } + } +} diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs index cfedcad23..0852ce39c 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { @@ -10,11 +11,22 @@ namespace Discord.Commands [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class RequireOwnerAttribute : PreconditionAttribute { - public override async Task CheckPermissions(ICommandContext context, CommandInfo command, IDependencyMap map) + public override async Task CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services) { - var application = await context.Client.GetApplicationInfoAsync(); - if (context.User.Id == application.Owner.Id) return PreconditionResult.FromSuccess(); - return PreconditionResult.FromError("Command can only be run by the owner of the bot"); + switch (context.Client.TokenType) + { + case TokenType.Bot: + var application = await context.Client.GetApplicationInfoAsync(); + if (context.User.Id != application.Owner.Id) + return PreconditionResult.FromError("Command can only be run by the owner of the bot"); + return PreconditionResult.FromSuccess(); + case TokenType.User: + if (context.User.Id != context.Client.CurrentUser.Id) + return PreconditionResult.FromError("Command can only be run by the owner of the bot"); + return PreconditionResult.FromSuccess(); + default: + return PreconditionResult.FromError($"{nameof(RequireOwnerAttribute)} is not supported by this {nameof(TokenType)}."); + } } } } diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs index c5b79c5b9..b7729b0c8 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { @@ -42,7 +43,7 @@ namespace Discord.Commands GuildPermission = null; } - public override Task CheckPermissions(ICommandContext context, CommandInfo command, IDependencyMap map) + public override Task CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services) { var guildUser = context.User as IGuildUser; @@ -51,7 +52,7 @@ namespace Discord.Commands if (guildUser == null) return Task.FromResult(PreconditionResult.FromError("Command must be used in a guild channel")); if (!guildUser.GuildPermissions.Has(GuildPermission.Value)) - return Task.FromResult(PreconditionResult.FromError($"Command requires guild permission {GuildPermission.Value}")); + return Task.FromResult(PreconditionResult.FromError($"User requires guild permission {GuildPermission.Value}")); } if (ChannelPermission.HasValue) @@ -65,7 +66,7 @@ namespace Discord.Commands perms = ChannelPermissions.All(guildChannel); if (!perms.Has(ChannelPermission.Value)) - return Task.FromResult(PreconditionResult.FromError($"Command requires channel permission {ChannelPermission.Value}")); + return Task.FromResult(PreconditionResult.FromError($"User requires channel permission {ChannelPermission.Value}")); } return Task.FromResult(PreconditionResult.FromSuccess()); diff --git a/src/Discord.Net.Commands/Builders/CommandBuilder.cs b/src/Discord.Net.Commands/Builders/CommandBuilder.cs index 27f991b16..b6d002c70 100644 --- a/src/Discord.Net.Commands/Builders/CommandBuilder.cs +++ b/src/Discord.Net.Commands/Builders/CommandBuilder.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Threading.Tasks; using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands.Builders { @@ -9,19 +10,22 @@ namespace Discord.Commands.Builders { private readonly List _preconditions; private readonly List _parameters; + private readonly List _attributes; private readonly List _aliases; public ModuleBuilder Module { get; } - internal Func Callback { get; set; } + internal Func Callback { get; set; } public string Name { get; set; } public string Summary { get; set; } public string Remarks { get; set; } + public string PrimaryAlias { get; set; } public RunMode RunMode { get; set; } public int Priority { get; set; } public IReadOnlyList Preconditions => _preconditions; public IReadOnlyList Parameters => _parameters; + public IReadOnlyList Attributes => _attributes; public IReadOnlyList Aliases => _aliases; //Automatic @@ -31,16 +35,18 @@ namespace Discord.Commands.Builders _preconditions = new List(); _parameters = new List(); + _attributes = new List(); _aliases = new List(); } //User-defined - internal CommandBuilder(ModuleBuilder module, string primaryAlias, Func callback) + internal CommandBuilder(ModuleBuilder module, string primaryAlias, Func callback) : this(module) { Discord.Preconditions.NotNull(primaryAlias, nameof(primaryAlias)); Discord.Preconditions.NotNull(callback, nameof(callback)); Callback = callback; + PrimaryAlias = primaryAlias; _aliases.Add(primaryAlias); } @@ -74,12 +80,17 @@ namespace Discord.Commands.Builders { for (int i = 0; i < aliases.Length; i++) { - var alias = aliases[i] ?? ""; + string alias = aliases[i] ?? ""; if (!_aliases.Contains(alias)) _aliases.Add(alias); } return this; } + public CommandBuilder AddAttributes(params Attribute[] attributes) + { + _attributes.AddRange(attributes); + return this; + } public CommandBuilder AddPrecondition(PreconditionAttribute precondition) { _preconditions.Add(precondition); @@ -109,9 +120,9 @@ namespace Discord.Commands.Builders internal CommandInfo Build(ModuleInfo info, CommandService service) { - //Default name to first alias + //Default name to primary alias if (Name == null) - Name = _aliases[0]; + Name = PrimaryAlias; if (_parameters.Count > 0) { @@ -119,11 +130,11 @@ namespace Discord.Commands.Builders var firstMultipleParam = _parameters.FirstOrDefault(x => x.IsMultiple); if ((firstMultipleParam != null) && (firstMultipleParam != lastParam)) - throw new InvalidOperationException("Only the last parameter in a command may have the Multiple flag."); + throw new InvalidOperationException($"Only the last parameter in a command may have the Multiple flag. Parameter: {firstMultipleParam.Name} in {PrimaryAlias}"); var firstRemainderParam = _parameters.FirstOrDefault(x => x.IsRemainder); if ((firstRemainderParam != null) && (firstRemainderParam != lastParam)) - throw new InvalidOperationException("Only the last parameter in a command may have the Remainder flag."); + throw new InvalidOperationException($"Only the last parameter in a command may have the Remainder flag. Parameter: {firstRemainderParam.Name} in {PrimaryAlias}"); } return new CommandInfo(this, info, service); diff --git a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs index 45c0034f2..0a33c9e26 100644 --- a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands.Builders { @@ -9,6 +10,7 @@ namespace Discord.Commands.Builders private readonly List _commands; private readonly List _submodules; private readonly List _preconditions; + private readonly List _attributes; private readonly List _aliases; public CommandService Service { get; } @@ -20,6 +22,7 @@ namespace Discord.Commands.Builders public IReadOnlyList Commands => _commands; public IReadOnlyList Modules => _submodules; public IReadOnlyList Preconditions => _preconditions; + public IReadOnlyList Attributes => _attributes; public IReadOnlyList Aliases => _aliases; //Automatic @@ -31,6 +34,7 @@ namespace Discord.Commands.Builders _commands = new List(); _submodules = new List(); _preconditions = new List(); + _attributes = new List(); _aliases = new List(); } //User-defined @@ -62,18 +66,23 @@ namespace Discord.Commands.Builders { for (int i = 0; i < aliases.Length; i++) { - var alias = aliases[i] ?? ""; + string alias = aliases[i] ?? ""; if (!_aliases.Contains(alias)) _aliases.Add(alias); } return this; } + public ModuleBuilder AddAttributes(params Attribute[] attributes) + { + _attributes.AddRange(attributes); + return this; + } public ModuleBuilder AddPrecondition(PreconditionAttribute precondition) { _preconditions.Add(precondition); return this; } - public ModuleBuilder AddCommand(string primaryAlias, Func callback, Action createFunc) + public ModuleBuilder AddCommand(string primaryAlias, Func callback, Action createFunc) { var builder = new CommandBuilder(this, primaryAlias, callback); createFunc(builder); diff --git a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs index 82850b091..6fae719ee 100644 --- a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -12,25 +12,42 @@ namespace Discord.Commands { private static readonly TypeInfo _moduleTypeInfo = typeof(IModuleBase).GetTypeInfo(); - public static IEnumerable Search(Assembly assembly) + public static async Task> SearchAsync(Assembly assembly, CommandService service) { - foreach (var type in assembly.ExportedTypes) + bool IsLoadableModule(TypeInfo info) { - var typeInfo = type.GetTypeInfo(); - if (IsValidModuleDefinition(typeInfo) && - !typeInfo.IsDefined(typeof(DontAutoLoadAttribute))) + return info.DeclaredMethods.Any(x => x.GetCustomAttribute() != null) && + info.GetCustomAttribute() == null; + } + + var result = new List(); + + foreach (var typeInfo in assembly.DefinedTypes) + { + if (typeInfo.IsPublic || typeInfo.IsNestedPublic) { - yield return typeInfo; + if (IsValidModuleDefinition(typeInfo) && + !typeInfo.IsDefined(typeof(DontAutoLoadAttribute))) + { + result.Add(typeInfo); + } + } + else if (IsLoadableModule(typeInfo)) + { + await service._cmdLogger.WarningAsync($"Class {typeInfo.FullName} is not public and cannot be loaded. To suppress this message, mark the class with {nameof(DontAutoLoadAttribute)}."); } } + + return result; } - public static Dictionary Build(CommandService service, params TypeInfo[] validTypes) => Build(validTypes, service); - public static Dictionary Build(IEnumerable validTypes, CommandService service) + + public static Task> BuildAsync(CommandService service, params TypeInfo[] validTypes) => BuildAsync(validTypes, service); + public static async Task> BuildAsync(IEnumerable validTypes, CommandService service) { /*if (!validTypes.Any()) throw new InvalidOperationException("Could not find any valid modules from the given selection");*/ - + var topLevelGroups = validTypes.Where(x => x.DeclaringType == null); var subGroups = validTypes.Intersect(topLevelGroups); @@ -48,10 +65,13 @@ namespace Discord.Commands BuildModule(module, typeInfo, service); BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service); + builtTypes.Add(typeInfo); result[typeInfo.AsType()] = module.Build(service); } + await service._cmdLogger.DebugAsync($"Successfully built {builtTypes.Count} modules.").ConfigureAwait(false); + return result; } @@ -81,23 +101,31 @@ namespace Discord.Commands foreach (var attribute in attributes) { - // TODO: C#7 type switch - if (attribute is NameAttribute) - builder.Name = (attribute as NameAttribute).Text; - else if (attribute is SummaryAttribute) - builder.Summary = (attribute as SummaryAttribute).Text; - else if (attribute is RemarksAttribute) - builder.Remarks = (attribute as RemarksAttribute).Text; - else if (attribute is AliasAttribute) - builder.AddAliases((attribute as AliasAttribute).Aliases); - else if (attribute is GroupAttribute) + switch (attribute) { - var groupAttr = attribute as GroupAttribute; - builder.Name = builder.Name ?? groupAttr.Prefix; - builder.AddAliases(groupAttr.Prefix); + case NameAttribute name: + builder.Name = name.Text; + break; + case SummaryAttribute summary: + builder.Summary = summary.Text; + break; + case RemarksAttribute remarks: + builder.Remarks = remarks.Text; + break; + case AliasAttribute alias: + builder.AddAliases(alias.Aliases); + break; + case GroupAttribute group: + builder.Name = builder.Name ?? group.Prefix; + builder.AddAliases(group.Prefix); + break; + case PreconditionAttribute precondition: + builder.AddPrecondition(precondition); + break; + default: + builder.AddAttributes(attribute); + break; } - else if (attribute is PreconditionAttribute) - builder.AddPrecondition(attribute as PreconditionAttribute); } //Check for unspecified info @@ -123,26 +151,35 @@ namespace Discord.Commands foreach (var attribute in attributes) { - // TODO: C#7 type switch - if (attribute is CommandAttribute) + switch (attribute) { - var cmdAttr = attribute as CommandAttribute; - builder.AddAliases(cmdAttr.Text); - builder.RunMode = cmdAttr.RunMode; - builder.Name = builder.Name ?? cmdAttr.Text; + case CommandAttribute command: + builder.AddAliases(command.Text); + builder.RunMode = command.RunMode; + builder.Name = builder.Name ?? command.Text; + break; + case NameAttribute name: + builder.Name = name.Text; + break; + case PriorityAttribute priority: + builder.Priority = priority.Priority; + break; + case SummaryAttribute summary: + builder.Summary = summary.Text; + break; + case RemarksAttribute remarks: + builder.Remarks = remarks.Text; + break; + case AliasAttribute alias: + builder.AddAliases(alias.Aliases); + break; + case PreconditionAttribute precondition: + builder.AddPrecondition(precondition); + break; + default: + builder.AddAttributes(attribute); + break; } - else if (attribute is NameAttribute) - builder.Name = (attribute as NameAttribute).Text; - else if (attribute is PriorityAttribute) - builder.Priority = (attribute as PriorityAttribute).Priority; - else if (attribute is SummaryAttribute) - builder.Summary = (attribute as SummaryAttribute).Text; - else if (attribute is RemarksAttribute) - builder.Remarks = (attribute as RemarksAttribute).Text; - else if (attribute is AliasAttribute) - builder.AddAliases((attribute as AliasAttribute).Aliases); - else if (attribute is PreconditionAttribute) - builder.AddPrecondition(attribute as PreconditionAttribute); } if (builder.Name == null) @@ -160,21 +197,34 @@ namespace Discord.Commands var createInstance = ReflectionUtils.CreateBuilder(typeInfo, service); - builder.Callback = (ctx, args, map) => + async Task ExecuteCallback(ICommandContext context, object[] args, IServiceProvider services, CommandInfo cmd) { - var instance = createInstance(map); - instance.SetContext(ctx); + var instance = createInstance(services); + instance.SetContext(context); + try { - instance.BeforeExecute(); - return method.Invoke(instance, args) as Task ?? Task.Delay(0); + instance.BeforeExecute(cmd); + + var task = method.Invoke(instance, args) as Task ?? Task.Delay(0); + if (task is Task resultTask) + { + return await resultTask.ConfigureAwait(false); + } + else + { + await task.ConfigureAwait(false); + return ExecuteResult.FromSuccess(); + } } finally { - instance.AfterExecute(); + instance.AfterExecute(cmd); (instance as IDisposable)?.Dispose(); } - }; + } + + builder.Callback = ExecuteCallback; } private static void BuildParameter(ParameterBuilder builder, System.Reflection.ParameterInfo paramInfo, int position, int count, CommandService service) @@ -189,24 +239,30 @@ namespace Discord.Commands foreach (var attribute in attributes) { - // TODO: C#7 type switch - if (attribute is SummaryAttribute) - builder.Summary = (attribute as SummaryAttribute).Text; - else if (attribute is OverrideTypeReaderAttribute) - builder.TypeReader = GetTypeReader(service, paramType, (attribute as OverrideTypeReaderAttribute).TypeReader); - else if (attribute is ParameterPreconditionAttribute) - builder.AddPrecondition(attribute as ParameterPreconditionAttribute); - else if (attribute is ParamArrayAttribute) - { - builder.IsMultiple = true; - paramType = paramType.GetElementType(); - } - else if (attribute is RemainderAttribute) + switch (attribute) { - if (position != count-1) - throw new InvalidOperationException("Remainder parameters must be the last parameter in a command."); - - builder.IsRemainder = true; + case SummaryAttribute summary: + builder.Summary = summary.Text; + break; + case OverrideTypeReaderAttribute typeReader: + builder.TypeReader = GetTypeReader(service, paramType, typeReader.TypeReader); + break; + case ParamArrayAttribute _: + builder.IsMultiple = true; + paramType = paramType.GetElementType(); + break; + case ParameterPreconditionAttribute precon: + builder.AddPrecondition(precon); + break; + case RemainderAttribute _: + if (position != count - 1) + throw new InvalidOperationException($"Remainder parameters must be the last parameter in a command. Parameter: {paramInfo.Name} in {paramInfo.Member.DeclaringType.Name}.{paramInfo.Member.Name}"); + + builder.IsRemainder = true; + break; + default: + builder.AddAttributes(attribute); + break; } } @@ -237,7 +293,7 @@ namespace Discord.Commands } //We dont have a cached type reader, create one - reader = ReflectionUtils.CreateObject(typeReaderType.GetTypeInfo(), service, DependencyMap.Empty); + reader = ReflectionUtils.CreateObject(typeReaderType.GetTypeInfo(), service, EmptyServiceProvider.Instance); service.AddTypeReader(paramType, reader); return reader; @@ -252,9 +308,9 @@ namespace Discord.Commands private static bool IsValidCommandDefinition(MethodInfo methodInfo) { return methodInfo.IsDefined(typeof(CommandAttribute)) && - methodInfo.ReturnType == typeof(Task) && + (methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task)) && !methodInfo.IsStatic && !methodInfo.IsGenericMethod; } } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs index c9801f458..d1782d7ea 100644 --- a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs @@ -8,7 +8,8 @@ namespace Discord.Commands.Builders { public class ParameterBuilder { - private readonly List _preconditions; + private readonly List _preconditions; + private readonly List _attributes; public CommandBuilder Command { get; } public string Name { get; internal set; } @@ -22,11 +23,13 @@ namespace Discord.Commands.Builders public string Summary { get; set; } public IReadOnlyList Preconditions => _preconditions; + public IReadOnlyList Attributes => _attributes; //Automatic internal ParameterBuilder(CommandBuilder command) { _preconditions = new List(); + _attributes = new List(); Command = command; } @@ -49,7 +52,7 @@ namespace Discord.Commands.Builders TypeReader = Command.Module.Service.GetDefaultTypeReader(type); if (TypeReader == null) - throw new InvalidOperationException($"{type} does not have a TypeReader registered for it"); + throw new InvalidOperationException($"{type} does not have a TypeReader registered for it. Parameter: {Name} in {Command.PrimaryAlias}"); if (type.GetTypeInfo().IsValueType) DefaultValue = Activator.CreateInstance(type); @@ -84,6 +87,11 @@ namespace Discord.Commands.Builders return this; } + public ParameterBuilder AddAttributes(params Attribute[] attributes) + { + _attributes.AddRange(attributes); + return this; + } public ParameterBuilder AddPrecondition(ParameterPreconditionAttribute precondition) { _preconditions.Add(precondition); @@ -93,7 +101,7 @@ namespace Discord.Commands.Builders internal ParameterInfo Build(CommandInfo info) { if (TypeReader == null) - throw new InvalidOperationException($"No default TypeReader found, one must be specified"); + throw new InvalidOperationException($"No type reader found for type {ParameterType.Name}, one must be specified"); return new ParameterInfo(this, info, Command.Module.Service); } diff --git a/src/Discord.Net.Commands/CommandError.cs b/src/Discord.Net.Commands/CommandError.cs index 41b4822ad..abfc14e1d 100644 --- a/src/Discord.Net.Commands/CommandError.cs +++ b/src/Discord.Net.Commands/CommandError.cs @@ -18,6 +18,9 @@ UnmetPrecondition, //Execute - Exception + Exception, + + //Runtime + Unsuccessful } } diff --git a/src/Discord.Net.Commands/CommandException.cs b/src/Discord.Net.Commands/CommandException.cs new file mode 100644 index 000000000..d5300841a --- /dev/null +++ b/src/Discord.Net.Commands/CommandException.cs @@ -0,0 +1,17 @@ +using System; + +namespace Discord.Commands +{ + public class CommandException : Exception + { + public CommandInfo Command { get; } + public ICommandContext Context { get; } + + public CommandException(CommandInfo command, ICommandContext context, Exception ex) + : base($"Error occurred executing {command.GetLogText(context)}.", ex) + { + Command = command; + Context = context; + } + } +} diff --git a/src/Discord.Net.Commands/CommandMatch.cs b/src/Discord.Net.Commands/CommandMatch.cs index 6e78b8509..d922a2229 100644 --- a/src/Discord.Net.Commands/CommandMatch.cs +++ b/src/Discord.Net.Commands/CommandMatch.cs @@ -1,5 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { @@ -14,13 +16,13 @@ namespace Discord.Commands Alias = alias; } - public Task CheckPreconditionsAsync(ICommandContext context, IDependencyMap map = null) - => Command.CheckPreconditionsAsync(context, map); - public Task ParseAsync(ICommandContext context, SearchResult searchResult, PreconditionResult? preconditionResult = null) - => Command.ParseAsync(context, Alias.Length, searchResult, preconditionResult); - public Task ExecuteAsync(ICommandContext context, IEnumerable argList, IEnumerable paramList, IDependencyMap map) - => Command.ExecuteAsync(context, argList, paramList, map); - public Task ExecuteAsync(ICommandContext context, ParseResult parseResult, IDependencyMap map) - => Command.ExecuteAsync(context, parseResult, map); + public Task CheckPreconditionsAsync(ICommandContext context, IServiceProvider services = null) + => Command.CheckPreconditionsAsync(context, services); + public Task ParseAsync(ICommandContext context, SearchResult searchResult, PreconditionResult preconditionResult = null, IServiceProvider services = null) + => Command.ParseAsync(context, Alias.Length, searchResult, preconditionResult, services); + public Task ExecuteAsync(ICommandContext context, IEnumerable argList, IEnumerable paramList, IServiceProvider services) + => Command.ExecuteAsync(context, argList, paramList, services); + public Task ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) + => Command.ExecuteAsync(context, parseResult, services); } } diff --git a/src/Discord.Net.Commands/CommandParser.cs b/src/Discord.Net.Commands/CommandParser.cs index 5b4ba2480..394f8589d 100644 --- a/src/Discord.Net.Commands/CommandParser.cs +++ b/src/Discord.Net.Commands/CommandParser.cs @@ -1,4 +1,5 @@ -using System.Collections.Immutable; +using System; +using System.Collections.Immutable; using System.Text; using System.Threading.Tasks; @@ -13,7 +14,7 @@ namespace Discord.Commands QuotedParameter } - public static async Task ParseArgs(CommandInfo command, ICommandContext context, string input, int startPos) + public static async Task ParseArgs(CommandInfo command, ICommandContext context, IServiceProvider services, string input, int startPos) { ParameterInfo curParam = null; StringBuilder argBuilder = new StringBuilder(input.Length); @@ -110,7 +111,7 @@ namespace Discord.Commands if (curParam == null) return ParseResult.FromError(CommandError.BadArgCount, "The input text has too many parameters."); - var typeReaderResult = await curParam.Parse(context, argString).ConfigureAwait(false); + var typeReaderResult = await curParam.Parse(context, argString, services).ConfigureAwait(false); if (!typeReaderResult.IsSuccess && typeReaderResult.Error != CommandError.MultipleMatches) return ParseResult.FromError(typeReaderResult); @@ -133,7 +134,7 @@ namespace Discord.Commands if (curParam != null && curParam.IsRemainder) { - var typeReaderResult = await curParam.Parse(context, argBuilder.ToString()).ConfigureAwait(false); + var typeReaderResult = await curParam.Parse(context, argBuilder.ToString(), services).ConfigureAwait(false); if (!typeReaderResult.IsSuccess) return ParseResult.FromError(typeReaderResult); argList.Add(typeReaderResult); diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 2c7955028..6ea2abcf3 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -1,4 +1,6 @@ -using System; +using Discord.Commands.Builders; +using Discord.Logging; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; @@ -6,13 +8,15 @@ using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; - -using Discord.Commands.Builders; +using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { public class CommandService { + public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } + internal readonly AsyncEvent> _logEvent = new AsyncEvent>(); + private readonly SemaphoreSlim _moduleLock; private readonly ConcurrentDictionary _typedModuleDefs; private readonly ConcurrentDictionary> _typeReaders; @@ -21,22 +25,29 @@ namespace Discord.Commands private readonly HashSet _moduleDefs; private readonly CommandMap _map; - internal readonly bool _caseSensitive; + internal readonly bool _caseSensitive, _throwOnError; internal readonly char _separatorChar; internal readonly RunMode _defaultRunMode; + internal readonly Logger _cmdLogger; + internal readonly LogManager _logManager; public IEnumerable Modules => _moduleDefs.Select(x => x); public IEnumerable Commands => _moduleDefs.SelectMany(x => x.Commands); - public ILookup TypeReaders => _typeReaders.SelectMany(x => x.Value.Select(y => new {y.Key, y.Value})).ToLookup(x => x.Key, x => x.Value); + public ILookup TypeReaders => _typeReaders.SelectMany(x => x.Value.Select(y => new { y.Key, y.Value })).ToLookup(x => x.Key, x => x.Value); public CommandService() : this(new CommandServiceConfig()) { } public CommandService(CommandServiceConfig config) { _caseSensitive = config.CaseSensitiveCommands; + _throwOnError = config.ThrowOnError; _separatorChar = config.SeparatorChar; _defaultRunMode = config.DefaultRunMode; if (_defaultRunMode == RunMode.Default) - throw new InvalidOperationException("The default run mode cannot be set to Default, it must be one of Sync, Mixed, or Async"); + throw new InvalidOperationException("The default run mode cannot be set to Default."); + + _logManager = new LogManager(config.LogLevel); + _logManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); + _cmdLogger = _logManager.CreateLogger("Command"); _moduleLock = new SemaphoreSlim(1, 1); _typedModuleDefs = new ConcurrentDictionary(); @@ -48,6 +59,9 @@ namespace Discord.Commands foreach (var type in PrimitiveParsers.SupportedTypes) _defaultTypeReaders[type] = PrimitiveTypeReader.Create(type); + _defaultTypeReaders[typeof(string)] = + new PrimitiveTypeReader((string x, out string y) => { y = x; return true; }, 0); + var entityTypeReaders = ImmutableList.CreateBuilder>(); entityTypeReaders.Add(new Tuple(typeof(IMessage), typeof(MessageTypeReader<>))); entityTypeReaders.Add(new Tuple(typeof(IChannel), typeof(ChannelTypeReader<>))); @@ -73,20 +87,21 @@ namespace Discord.Commands _moduleLock.Release(); } } - public async Task AddModuleAsync() + public Task AddModuleAsync() => AddModuleAsync(typeof(T)); + public async Task AddModuleAsync(Type type) { await _moduleLock.WaitAsync().ConfigureAwait(false); try { - var typeInfo = typeof(T).GetTypeInfo(); + var typeInfo = type.GetTypeInfo(); - if (_typedModuleDefs.ContainsKey(typeof(T))) + if (_typedModuleDefs.ContainsKey(type)) throw new ArgumentException($"This module has already been added."); - var module = ModuleClassBuilder.Build(this, typeInfo).FirstOrDefault(); + var module = (await ModuleClassBuilder.BuildAsync(this, typeInfo).ConfigureAwait(false)).FirstOrDefault(); if (module.Value == default(ModuleInfo)) - throw new InvalidOperationException($"Could not build the module {typeof(T).FullName}, did you pass an invalid type?"); + throw new InvalidOperationException($"Could not build the module {type.FullName}, did you pass an invalid type?"); _typedModuleDefs[module.Key] = module.Value; @@ -102,8 +117,8 @@ namespace Discord.Commands await _moduleLock.WaitAsync().ConfigureAwait(false); try { - var types = ModuleClassBuilder.Search(assembly).ToArray(); - var moduleDefs = ModuleClassBuilder.Build(types, this); + var types = await ModuleClassBuilder.SearchAsync(assembly, this).ConfigureAwait(false); + var moduleDefs = await ModuleClassBuilder.BuildAsync(types, this).ConfigureAwait(false); foreach (var info in moduleDefs) { @@ -143,14 +158,13 @@ namespace Discord.Commands _moduleLock.Release(); } } - public async Task RemoveModuleAsync() + public Task RemoveModuleAsync() => RemoveModuleAsync(typeof(T)); + public async Task RemoveModuleAsync(Type type) { await _moduleLock.WaitAsync().ConfigureAwait(false); try { - ModuleInfo module; - _typedModuleDefs.TryGetValue(typeof(T), out module); - if (module == default(ModuleInfo)) + if (!_typedModuleDefs.TryRemove(type, out var module)) return false; return RemoveModuleInternal(module); @@ -184,20 +198,18 @@ namespace Discord.Commands } public void AddTypeReader(Type type, TypeReader reader) { - var readers = _typeReaders.GetOrAdd(type, x=> new ConcurrentDictionary()); + var readers = _typeReaders.GetOrAdd(type, x => new ConcurrentDictionary()); readers[reader.GetType()] = reader; } internal IDictionary GetTypeReaders(Type type) { - ConcurrentDictionary definedTypeReaders; - if (_typeReaders.TryGetValue(type, out definedTypeReaders)) + if (_typeReaders.TryGetValue(type, out var definedTypeReaders)) return definedTypeReaders; return null; } internal TypeReader GetDefaultTypeReader(Type type) { - TypeReader reader; - if (_defaultTypeReaders.TryGetValue(type, out reader)) + if (_defaultTypeReaders.TryGetValue(type, out var reader)) return reader; var typeInfo = type.GetTypeInfo(); @@ -223,70 +235,110 @@ namespace Discord.Commands } //Execution - public SearchResult Search(ICommandContext context, int argPos) + public SearchResult Search(ICommandContext context, int argPos) => Search(context, context.Message.Content.Substring(argPos)); public SearchResult Search(ICommandContext context, string input) { string searchInput = _caseSensitive ? input : input.ToLowerInvariant(); var matches = _map.GetCommands(searchInput).OrderByDescending(x => x.Command.Priority).ToImmutableArray(); - + if (matches.Length > 0) return SearchResult.FromSuccess(input, matches); else return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command."); } - public Task ExecuteAsync(ICommandContext context, int argPos, IDependencyMap dependencyMap = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) - => ExecuteAsync(context, context.Message.Content.Substring(argPos), dependencyMap, multiMatchHandling); - public async Task ExecuteAsync(ICommandContext context, string input, IDependencyMap dependencyMap = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) + public Task ExecuteAsync(ICommandContext context, int argPos, IServiceProvider services = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) + => ExecuteAsync(context, context.Message.Content.Substring(argPos), services, multiMatchHandling); + public async Task ExecuteAsync(ICommandContext context, string input, IServiceProvider services = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) { - dependencyMap = dependencyMap ?? DependencyMap.Empty; + services = services ?? EmptyServiceProvider.Instance; var searchResult = Search(context, input); if (!searchResult.IsSuccess) return searchResult; var commands = searchResult.Commands; - for (int i = commands.Count - 1; i >= 0; i--) + var preconditionResults = new Dictionary(); + + foreach (var match in commands) { - var preconditionResult = await commands[i].CheckPreconditionsAsync(context, dependencyMap).ConfigureAwait(false); - if (!preconditionResult.IsSuccess) - { - if (commands.Count == 1) - return preconditionResult; - else - continue; - } + preconditionResults[match] = await match.Command.CheckPreconditionsAsync(context, services).ConfigureAwait(false); + } + + var successfulPreconditions = preconditionResults + .Where(x => x.Value.IsSuccess) + .ToArray(); - var parseResult = await commands[i].ParseAsync(context, searchResult, preconditionResult).ConfigureAwait(false); - if (!parseResult.IsSuccess) + if (successfulPreconditions.Length == 0) + { + //All preconditions failed, return the one from the highest priority command + var bestCandidate = preconditionResults + .OrderByDescending(x => x.Key.Command.Priority) + .FirstOrDefault(x => !x.Value.IsSuccess); + return bestCandidate.Value; + } + + //If we get this far, at least one precondition was successful. + + var parseResultsDict = new Dictionary(); + foreach (var pair in successfulPreconditions) + { + var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, services).ConfigureAwait(false); + + if (parseResult.Error == CommandError.MultipleMatches) { - if (parseResult.Error == CommandError.MultipleMatches) + IReadOnlyList argList, paramList; + switch (multiMatchHandling) { - IReadOnlyList argList, paramList; - switch (multiMatchHandling) - { - case MultiMatchHandling.Best: - argList = parseResult.ArgValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); - paramList = parseResult.ParamValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); - parseResult = ParseResult.FromSuccess(argList, paramList); - break; - } + case MultiMatchHandling.Best: + argList = parseResult.ArgValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); + paramList = parseResult.ParamValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); + parseResult = ParseResult.FromSuccess(argList, paramList); + break; } + } - if (!parseResult.IsSuccess) - { - if (commands.Count == 1) - return parseResult; - else - continue; - } + parseResultsDict[pair.Key] = parseResult; + } + + // Calculates the 'score' of a command given a parse result + float CalculateScore(CommandMatch match, ParseResult parseResult) + { + float argValuesScore = 0, paramValuesScore = 0; + + if (match.Command.Parameters.Count > 0) + { + var argValuesSum = parseResult.ArgValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; + var paramValuesSum = parseResult.ParamValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; + + argValuesScore = argValuesSum / match.Command.Parameters.Count; + paramValuesScore = paramValuesSum / match.Command.Parameters.Count; } - return await commands[i].ExecuteAsync(context, parseResult, dependencyMap).ConfigureAwait(false); + var totalArgsScore = (argValuesScore + paramValuesScore) / 2; + return match.Command.Priority + totalArgsScore * 0.99f; + } + + //Order the parse results by their score so that we choose the most likely result to execute + var parseResults = parseResultsDict + .OrderByDescending(x => CalculateScore(x.Key, x.Value)); + + var successfulParses = parseResults + .Where(x => x.Value.IsSuccess) + .ToArray(); + + if (successfulParses.Length == 0) + { + //All parses failed, return the one from the highest priority command, using score as a tie breaker + var bestMatch = parseResults + .FirstOrDefault(x => !x.Value.IsSuccess); + return bestMatch.Value; } - return SearchResult.FromError(CommandError.UnknownCommand, "This input does not match any overload."); + //If we get this far, at least one parse was successful. Execute the most likely overload. + var chosenOverload = successfulParses[0]; + return await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, services).ConfigureAwait(false); } } } diff --git a/src/Discord.Net.Commands/CommandServiceConfig.cs b/src/Discord.Net.Commands/CommandServiceConfig.cs index 037e315c7..5dcd50cd8 100644 --- a/src/Discord.Net.Commands/CommandServiceConfig.cs +++ b/src/Discord.Net.Commands/CommandServiceConfig.cs @@ -8,5 +8,11 @@ public char SeparatorChar { get; set; } = ' '; /// Should commands be case-sensitive? public bool CaseSensitiveCommands { get; set; } = false; + + /// Gets or sets the minimum log level severity that will be sent to the Log event. + public LogSeverity LogLevel { get; set; } = LogSeverity.Info; + + /// Gets or sets whether RunMode.Sync commands should push exceptions up to the caller. + public bool ThrowOnError { get; set; } = true; } -} +} \ No newline at end of file diff --git a/src/Discord.Net.Commands/Dependencies/DependencyMap.cs b/src/Discord.Net.Commands/Dependencies/DependencyMap.cs deleted file mode 100644 index f5adf1a8c..000000000 --- a/src/Discord.Net.Commands/Dependencies/DependencyMap.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Discord.Commands -{ - public class DependencyMap : IDependencyMap - { - private Dictionary> map; - - public static DependencyMap Empty => new DependencyMap(); - - public DependencyMap() - { - map = new Dictionary>(); - } - - /// - public void Add(T obj) where T : class - => AddFactory(() => obj); - /// - public bool TryAdd(T obj) where T : class - => TryAddFactory(() => obj); - /// - public void AddTransient() where T : class, new() - => AddFactory(() => new T()); - /// - public bool TryAddTransient() where T : class, new() - => TryAddFactory(() => new T()); - /// - public void AddTransient() where TKey : class - where TImpl : class, TKey, new() - => AddFactory(() => new TImpl()); - public bool TryAddTransient() where TKey : class - where TImpl : class, TKey, new() - => TryAddFactory(() => new TImpl()); - - /// - public void AddFactory(Func factory) where T : class - { - var t = typeof(T); - if (map.ContainsKey(t)) - throw new InvalidOperationException($"The dependency map already contains \"{t.FullName}\""); - map.Add(t, factory); - } - /// - public bool TryAddFactory(Func factory) where T : class - { - var t = typeof(T); - if (map.ContainsKey(t)) - return false; - map.Add(t, factory); - return true; - } - - /// - public T Get() - { - return (T)Get(typeof(T)); - } - /// - public object Get(Type t) - { - object result; - if (!TryGet(t, out result)) - throw new KeyNotFoundException($"The dependency map does not contain \"{t.FullName}\""); - else - return result; - } - - /// - public bool TryGet(out T result) - { - object untypedResult; - if (TryGet(typeof(T), out untypedResult)) - { - result = (T)untypedResult; - return true; - } - else - { - result = default(T); - return false; - } - } - /// - public bool TryGet(Type t, out object result) - { - Func func; - if (map.TryGetValue(t, out func)) - { - result = func(); - return true; - } - result = null; - return false; - } - } -} diff --git a/src/Discord.Net.Commands/Dependencies/IDependencyMap.cs b/src/Discord.Net.Commands/Dependencies/IDependencyMap.cs deleted file mode 100644 index a55a9e4c5..000000000 --- a/src/Discord.Net.Commands/Dependencies/IDependencyMap.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; - -namespace Discord.Commands -{ - public interface IDependencyMap - { - /// - /// Add an instance of a service to be injected. - /// - /// The type of service. - /// The instance of a service. - void Add(T obj) where T : class; - /// - /// Tries to add an instance of a service to be injected. - /// - /// The type of service. - /// The instance of a service. - /// A bool, indicating if the service was successfully added to the DependencyMap. - bool TryAdd(T obj) where T : class; - /// - /// Add a service that will be injected by a new instance every time. - /// - /// The type of instance to inject. - void AddTransient() where T : class, new(); - /// - /// Tries to add a service that will be injected by a new instance every time. - /// - /// The type of instance to inject. - /// A bool, indicating if the service was successfully added to the DependencyMap. - bool TryAddTransient() where T : class, new(); - /// - /// Add a service that will be injected by a new instance every time. - /// - /// The type to look for when injecting. - /// The type to inject when injecting. - /// - /// map.AddTransient<IService, Service> - /// - void AddTransient() where TKey: class where TImpl : class, TKey, new(); - /// - /// Tries to add a service that will be injected by a new instance every time. - /// - /// The type to look for when injecting. - /// The type to inject when injecting. - /// A bool, indicating if the service was successfully added to the DependencyMap. - bool TryAddTransient() where TKey : class where TImpl : class, TKey, new(); - /// - /// Add a service that will be injected by a factory. - /// - /// The type to look for when injecting. - /// The factory that returns a type of this service. - void AddFactory(Func factory) where T : class; - /// - /// Tries to add a service that will be injected by a factory. - /// - /// The type to look for when injecting. - /// The factory that returns a type of this service. - /// A bool, indicating if the service was successfully added to the DependencyMap. - bool TryAddFactory(Func factory) where T : class; - - /// - /// Pull an object from the map. - /// - /// The type of service. - /// An instance of this service. - T Get(); - /// - /// Try to pull an object from the map. - /// - /// The type of service. - /// The instance of this service. - /// Whether or not this object could be found in the map. - bool TryGet(out T result); - - /// - /// Pull an object from the map. - /// - /// The type of service. - /// An instance of this service. - object Get(Type t); - /// - /// Try to pull an object from the map. - /// - /// The type of service. - /// An instance of this service. - /// Whether or not this object could be found in the map. - bool TryGet(Type t, out object result); - } -} diff --git a/src/Discord.Net.Commands/Discord.Net.Commands.csproj b/src/Discord.Net.Commands/Discord.Net.Commands.csproj index 452b52f21..eaac79a55 100644 --- a/src/Discord.Net.Commands/Discord.Net.Commands.csproj +++ b/src/Discord.Net.Commands/Discord.Net.Commands.csproj @@ -1,26 +1,15 @@ - + + - 1.0.0 - rc-dev - rc-$(BuildNumber) - netstandard1.1;netstandard1.3 Discord.Net.Commands - RogueException - A Discord.Net extension adding support for bot commands. - discord;discordapp - https://github.com/RogueException/Discord.Net - http://opensource.org/licenses/MIT - git - git://github.com/RogueException/Discord.Net Discord.Commands - true + A Discord.Net extension adding support for bot commands. + netstandard1.1 - - $(NoWarn);CS1573;CS1591 - true - true - + + + \ No newline at end of file diff --git a/src/Discord.Net.Commands/EmptyServiceProvider.cs b/src/Discord.Net.Commands/EmptyServiceProvider.cs new file mode 100644 index 000000000..0bef3760e --- /dev/null +++ b/src/Discord.Net.Commands/EmptyServiceProvider.cs @@ -0,0 +1,11 @@ +using System; + +namespace Discord.Commands +{ + internal class EmptyServiceProvider : IServiceProvider + { + public static readonly EmptyServiceProvider Instance = new EmptyServiceProvider(); + + public object GetService(Type serviceType) => null; + } +} diff --git a/src/Discord.Net.Commands/Extensions/MessageExtensions.cs b/src/Discord.Net.Commands/Extensions/MessageExtensions.cs index 4354cbb88..096b03f6b 100644 --- a/src/Discord.Net.Commands/Extensions/MessageExtensions.cs +++ b/src/Discord.Net.Commands/Extensions/MessageExtensions.cs @@ -1,4 +1,6 @@ -namespace Discord.Commands +using System; + +namespace Discord.Commands { public static class MessageExtensions { @@ -12,10 +14,10 @@ } return false; } - public static bool HasStringPrefix(this IUserMessage msg, string str, ref int argPos) + public static bool HasStringPrefix(this IUserMessage msg, string str, ref int argPos, StringComparison comparisonType = StringComparison.Ordinal) { var text = msg.Content; - if (text.StartsWith(str)) + if (text.StartsWith(str, comparisonType)) { argPos = str.Length; return true; diff --git a/src/Discord.Net.Commands/IModuleBase.cs b/src/Discord.Net.Commands/IModuleBase.cs index fda768b53..479724ae3 100644 --- a/src/Discord.Net.Commands/IModuleBase.cs +++ b/src/Discord.Net.Commands/IModuleBase.cs @@ -4,8 +4,8 @@ { void SetContext(ICommandContext context); - void BeforeExecute(); + void BeforeExecute(CommandInfo command); - void AfterExecute(); + void AfterExecute(CommandInfo command); } } diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index 031d37581..ebef80baf 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -1,13 +1,14 @@ +using Discord.Commands.Builders; using System; -using System.Linq; using System.Collections.Generic; using System.Collections.Immutable; using System.Collections.Concurrent; -using System.Threading.Tasks; -using System.Reflection; - -using Discord.Commands.Builders; using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { @@ -17,7 +18,7 @@ namespace Discord.Commands private static readonly System.Reflection.MethodInfo _convertParamsMethod = typeof(CommandInfo).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList)); private static readonly ConcurrentDictionary, object>> _arrayConverters = new ConcurrentDictionary, object>>(); - private readonly Func _action; + private readonly Func _action; public ModuleInfo Module { get; } public string Name { get; } @@ -30,18 +31,19 @@ namespace Discord.Commands public IReadOnlyList Aliases { get; } public IReadOnlyList Parameters { get; } public IReadOnlyList Preconditions { get; } + public IReadOnlyList Attributes { get; } internal CommandInfo(CommandBuilder builder, ModuleInfo module, CommandService service) { Module = module; - + Name = builder.Name; Summary = builder.Summary; Remarks = builder.Remarks; RunMode = (builder.RunMode == RunMode.Default ? service._defaultRunMode : builder.RunMode); Priority = builder.Priority; - + Aliases = module.Aliases .Permutate(builder.Aliases, (first, second) => { @@ -56,6 +58,7 @@ namespace Discord.Commands .ToImmutableArray(); Preconditions = builder.Preconditions.ToImmutableArray(); + Attributes = builder.Attributes.ToImmutableArray(); Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray(); HasVarArgs = builder.Parameters.Count > 0 ? builder.Parameters[builder.Parameters.Count - 1].IsMultiple : false; @@ -63,74 +66,96 @@ namespace Discord.Commands _action = builder.Callback; } - public async Task CheckPreconditionsAsync(ICommandContext context, IDependencyMap map = null) + public async Task CheckPreconditionsAsync(ICommandContext context, IServiceProvider services = null) { - if (map == null) - map = DependencyMap.Empty; + services = services ?? EmptyServiceProvider.Instance; - foreach (PreconditionAttribute precondition in Module.Preconditions) + async Task CheckGroups(IEnumerable preconditions, string type) { - var result = await precondition.CheckPermissions(context, this, map).ConfigureAwait(false); - if (!result.IsSuccess) - return result; - } + foreach (IGrouping preconditionGroup in preconditions.GroupBy(p => p.Group, StringComparer.Ordinal)) + { + if (preconditionGroup.Key == null) + { + foreach (PreconditionAttribute precondition in preconditionGroup) + { + var result = await precondition.CheckPermissions(context, this, services).ConfigureAwait(false); + if (!result.IsSuccess) + return result; + } + } + else + { + var results = new List(); + foreach (PreconditionAttribute precondition in preconditionGroup) + results.Add(await precondition.CheckPermissions(context, this, services).ConfigureAwait(false)); - foreach (PreconditionAttribute precondition in Preconditions) - { - var result = await precondition.CheckPermissions(context, this, map).ConfigureAwait(false); - if (!result.IsSuccess) - return result; + if (!results.Any(p => p.IsSuccess)) + return PreconditionGroupResult.FromError($"{type} precondition group {preconditionGroup.Key} failed.", results); + } + } + return PreconditionGroupResult.FromSuccess(); } + var moduleResult = await CheckGroups(Module.Preconditions, "Module"); + if (!moduleResult.IsSuccess) + return moduleResult; + + var commandResult = await CheckGroups(Preconditions, "Command"); + if (!commandResult.IsSuccess) + return commandResult; + return PreconditionResult.FromSuccess(); } - - public async Task ParseAsync(ICommandContext context, int startIndex, SearchResult searchResult, PreconditionResult? preconditionResult = null) + + public async Task ParseAsync(ICommandContext context, int startIndex, SearchResult searchResult, PreconditionResult preconditionResult = null, IServiceProvider services = null) { + services = services ?? EmptyServiceProvider.Instance; + if (!searchResult.IsSuccess) return ParseResult.FromError(searchResult); - if (preconditionResult != null && !preconditionResult.Value.IsSuccess) - return ParseResult.FromError(preconditionResult.Value); - + if (preconditionResult != null && !preconditionResult.IsSuccess) + return ParseResult.FromError(preconditionResult); + string input = searchResult.Text.Substring(startIndex); - return await CommandParser.ParseArgs(this, context, input, 0).ConfigureAwait(false); + return await CommandParser.ParseArgs(this, context, services, input, 0).ConfigureAwait(false); } - public Task ExecuteAsync(ICommandContext context, ParseResult parseResult, IDependencyMap map) + public Task ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) { if (!parseResult.IsSuccess) - return Task.FromResult(ExecuteResult.FromError(parseResult)); + return Task.FromResult((IResult)ExecuteResult.FromError(parseResult)); var argList = new object[parseResult.ArgValues.Count]; for (int i = 0; i < parseResult.ArgValues.Count; i++) { if (!parseResult.ArgValues[i].IsSuccess) - return Task.FromResult(ExecuteResult.FromError(parseResult.ArgValues[i])); + return Task.FromResult((IResult)ExecuteResult.FromError(parseResult.ArgValues[i])); argList[i] = parseResult.ArgValues[i].Values.First().Value; } - + var paramList = new object[parseResult.ParamValues.Count]; for (int i = 0; i < parseResult.ParamValues.Count; i++) { if (!parseResult.ParamValues[i].IsSuccess) - return Task.FromResult(ExecuteResult.FromError(parseResult.ParamValues[i])); + return Task.FromResult((IResult)ExecuteResult.FromError(parseResult.ParamValues[i])); paramList[i] = parseResult.ParamValues[i].Values.First().Value; } - return ExecuteAsync(context, argList, paramList, map); + return ExecuteAsync(context, argList, paramList, services); } - public async Task ExecuteAsync(ICommandContext context, IEnumerable argList, IEnumerable paramList, IDependencyMap map) + public async Task ExecuteAsync(ICommandContext context, IEnumerable argList, IEnumerable paramList, IServiceProvider services) { - if (map == null) - map = DependencyMap.Empty; + services = services ?? EmptyServiceProvider.Instance; try { object[] args = GenerateArgs(argList, paramList); - foreach (var parameter in Parameters) + for (int position = 0; position < Parameters.Count; position++) { - var result = await parameter.CheckPreconditionsAsync(context, args, map).ConfigureAwait(false); + var parameter = Parameters[position]; + object argument = args[position]; + var result = await parameter.CheckPreconditionsAsync(context, argument, services).ConfigureAwait(false); if (!result.IsSuccess) return ExecuteResult.FromError(result); } @@ -138,13 +163,12 @@ namespace Discord.Commands switch (RunMode) { case RunMode.Sync: //Always sync - await _action(context, args, map).ConfigureAwait(false); - break; - case RunMode.Mixed: //Sync until first await statement - var t1 = _action(context, args, map); - break; + return await ExecuteAsyncInternal(context, args, services).ConfigureAwait(false); case RunMode.Async: //Always async - var t2 = Task.Run(() => _action(context, args, map)); + var t2 = Task.Run(async () => + { + await ExecuteAsyncInternal(context, args, services).ConfigureAwait(false); + }); break; } return ExecuteResult.FromSuccess(); @@ -155,6 +179,51 @@ namespace Discord.Commands } } + private async Task ExecuteAsyncInternal(ICommandContext context, object[] args, IServiceProvider services) + { + await Module.Service._cmdLogger.DebugAsync($"Executing {GetLogText(context)}").ConfigureAwait(false); + try + { + var task = _action(context, args, services, this); + if (task is Task resultTask) + { + var result = await resultTask.ConfigureAwait(false); + if (result is RuntimeResult execResult) + return execResult; + } + else if (task is Task execTask) + { + return await execTask.ConfigureAwait(false); + } + else + await task.ConfigureAwait(false); + + return ExecuteResult.FromSuccess(); + } + catch (Exception ex) + { + var originalEx = ex; + while (ex is TargetInvocationException) //Happens with void-returning commands + ex = ex.InnerException; + + var wrappedEx = new CommandException(this, context, ex); + await Module.Service._cmdLogger.ErrorAsync(wrappedEx).ConfigureAwait(false); + if (Module.Service._throwOnError) + { + if (ex == originalEx) + throw; + else + ExceptionDispatchInfo.Capture(ex).Throw(); + } + + return ExecuteResult.FromError(CommandError.Exception, ex.Message); + } + finally + { + await Module.Service._cmdLogger.VerboseAsync($"Executed {GetLogText(context)}").ConfigureAwait(false); + } + } + private object[] GenerateArgs(IEnumerable argList, IEnumerable paramsList) { int argCount = Parameters.Count; @@ -163,7 +232,7 @@ namespace Discord.Commands argCount--; int i = 0; - foreach (var arg in argList) + foreach (object arg in argList) { if (i == argCount) throw new InvalidOperationException("Command was invoked with too many parameters"); @@ -187,5 +256,13 @@ namespace Discord.Commands private static T[] ConvertParamsList(IEnumerable paramsList) => paramsList.Cast().ToArray(); + + internal string GetLogText(ICommandContext context) + { + if (context.Guild != null) + return $"\"{Name}\" for {context.User} in {context.Guild}/{context.Channel}"; + else + return $"\"{Name}\" for {context.User} in {context.Channel}"; + } } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Commands/Info/ModuleInfo.cs b/src/Discord.Net.Commands/Info/ModuleInfo.cs index a2094df65..97b90bf4e 100644 --- a/src/Discord.Net.Commands/Info/ModuleInfo.cs +++ b/src/Discord.Net.Commands/Info/ModuleInfo.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using System.Collections.Generic; using System.Collections.Immutable; @@ -16,6 +17,7 @@ namespace Discord.Commands public IReadOnlyList Aliases { get; } public IReadOnlyList Commands { get; } public IReadOnlyList Preconditions { get; } + public IReadOnlyList Attributes { get; } public IReadOnlyList Submodules { get; } public ModuleInfo Parent { get; } public bool IsSubmodule => Parent != null; @@ -32,6 +34,7 @@ namespace Discord.Commands Aliases = BuildAliases(builder, service).ToImmutableArray(); Commands = builder.Commands.Select(x => x.Build(this, service)).ToImmutableArray(); Preconditions = BuildPreconditions(builder).ToImmutableArray(); + Attributes = BuildAttributes(builder).ToImmutableArray(); Submodules = BuildSubmodules(builder, service).ToImmutableArray(); } @@ -86,5 +89,19 @@ namespace Discord.Commands return result; } + + private static List BuildAttributes(ModuleBuilder builder) + { + var result = new List(); + + ModuleBuilder parent = builder; + while (parent != null) + { + result.AddRange(parent.Attributes); + parent = parent.Parent; + } + + return result; + } } } diff --git a/src/Discord.Net.Commands/Info/ParameterInfo.cs b/src/Discord.Net.Commands/Info/ParameterInfo.cs index a0cdf03d7..e417b1ab6 100644 --- a/src/Discord.Net.Commands/Info/ParameterInfo.cs +++ b/src/Discord.Net.Commands/Info/ParameterInfo.cs @@ -1,9 +1,9 @@ +using Discord.Commands.Builders; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Threading.Tasks; - -using Discord.Commands.Builders; +using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { @@ -21,6 +21,7 @@ namespace Discord.Commands public object DefaultValue { get; } public IReadOnlyList Preconditions { get; } + public IReadOnlyList Attributes { get; } internal ParameterInfo(ParameterBuilder builder, CommandInfo command, CommandService service) { @@ -36,23 +37,18 @@ namespace Discord.Commands DefaultValue = builder.DefaultValue; Preconditions = builder.Preconditions.ToImmutableArray(); + Attributes = builder.Attributes.ToImmutableArray(); _reader = builder.TypeReader; } - public async Task CheckPreconditionsAsync(ICommandContext context, object[] args, IDependencyMap map = null) + public async Task CheckPreconditionsAsync(ICommandContext context, object arg, IServiceProvider services = null) { - if (map == null) - map = DependencyMap.Empty; - - int position = 0; - for(position = 0; position < Command.Parameters.Count; position++) - if (Command.Parameters[position] == this) - break; + services = services ?? EmptyServiceProvider.Instance; foreach (var precondition in Preconditions) { - var result = await precondition.CheckPermissions(context, this, args[position], map).ConfigureAwait(false); + var result = await precondition.CheckPermissions(context, this, arg, services).ConfigureAwait(false); if (!result.IsSuccess) return result; } @@ -60,9 +56,10 @@ namespace Discord.Commands return PreconditionResult.FromSuccess(); } - public async Task Parse(ICommandContext context, string input) + public async Task Parse(ICommandContext context, string input, IServiceProvider services = null) { - return await _reader.Read(context, input).ConfigureAwait(false); + services = services ?? EmptyServiceProvider.Instance; + return await _reader.Read(context, input, services).ConfigureAwait(false); } public override string ToString() => Name; diff --git a/src/Discord.Net.Commands/ModuleBase.cs b/src/Discord.Net.Commands/ModuleBase.cs index a38ffce06..f51656e40 100644 --- a/src/Discord.Net.Commands/ModuleBase.cs +++ b/src/Discord.Net.Commands/ModuleBase.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; namespace Discord.Commands { - public abstract class ModuleBase : ModuleBase { } + public abstract class ModuleBase : ModuleBase { } public abstract class ModuleBase : IModuleBase where T : class, ICommandContext @@ -15,11 +15,11 @@ namespace Discord.Commands return await Context.Channel.SendMessageAsync(message, isTTS, embed, options).ConfigureAwait(false); } - protected virtual void BeforeExecute() + protected virtual void BeforeExecute(CommandInfo command) { } - protected virtual void AfterExecute() + protected virtual void AfterExecute(CommandInfo command) { } @@ -27,13 +27,11 @@ namespace Discord.Commands void IModuleBase.SetContext(ICommandContext context) { var newValue = context as T; - if (newValue == null) - throw new InvalidOperationException($"Invalid context type. Expected {typeof(T).Name}, got {context.GetType().Name}"); - Context = newValue; + Context = newValue ?? throw new InvalidOperationException($"Invalid context type. Expected {typeof(T).Name}, got {context.GetType().Name}"); } - void IModuleBase.BeforeExecute() => BeforeExecute(); + void IModuleBase.BeforeExecute(CommandInfo command) => BeforeExecute(command); - void IModuleBase.AfterExecute() => AfterExecute(); + void IModuleBase.AfterExecute(CommandInfo command) => AfterExecute(command); } } diff --git a/src/Discord.Net.Commands/PrimitiveParsers.cs b/src/Discord.Net.Commands/PrimitiveParsers.cs index 623ddafa7..6a54ba402 100644 --- a/src/Discord.Net.Commands/PrimitiveParsers.cs +++ b/src/Discord.Net.Commands/PrimitiveParsers.cs @@ -31,11 +31,6 @@ namespace Discord.Commands parserBuilder[typeof(DateTimeOffset)] = (TryParseDelegate)DateTimeOffset.TryParse; parserBuilder[typeof(TimeSpan)] = (TryParseDelegate)TimeSpan.TryParse; parserBuilder[typeof(char)] = (TryParseDelegate)char.TryParse; - parserBuilder[typeof(string)] = (TryParseDelegate)delegate (string str, out string value) - { - value = str; - return true; - }; return parserBuilder.ToImmutable(); } diff --git a/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs b/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs index 08821c62f..72c62282e 100644 --- a/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs @@ -9,7 +9,7 @@ namespace Discord.Commands internal class ChannelTypeReader : TypeReader where T : class, IChannel { - public override async Task Read(ICommandContext context, string input) + public override async Task Read(ICommandContext context, string input, IServiceProvider services) { if (context.Guild != null) { @@ -30,7 +30,7 @@ namespace Discord.Commands AddResult(results, channel as T, channel.Name == input ? 0.80f : 0.70f); if (results.Count > 0) - return TypeReaderResult.FromSuccess(results.Values); + return TypeReaderResult.FromSuccess(results.Values.ToReadOnlyCollection()); } return TypeReaderResult.FromError(CommandError.ObjectNotFound, "Channel not found."); diff --git a/src/Discord.Net.Commands/Readers/EnumTypeReader.cs b/src/Discord.Net.Commands/Readers/EnumTypeReader.cs index 7b2ff505a..383b8e63c 100644 --- a/src/Discord.Net.Commands/Readers/EnumTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/EnumTypeReader.cs @@ -44,12 +44,11 @@ namespace Discord.Commands _enumsByValue = byValueBuilder.ToImmutable(); } - public override Task Read(ICommandContext context, string input) + public override Task Read(ICommandContext context, string input, IServiceProvider services) { - T baseValue; object enumValue; - if (_tryParse(input, out baseValue)) + if (_tryParse(input, out T baseValue)) { if (_enumsByValue.TryGetValue(baseValue, out enumValue)) return Task.FromResult(TypeReaderResult.FromSuccess(enumValue)); diff --git a/src/Discord.Net.Commands/Readers/MessageTypeReader.cs b/src/Discord.Net.Commands/Readers/MessageTypeReader.cs index 9baa1901a..895713e4f 100644 --- a/src/Discord.Net.Commands/Readers/MessageTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/MessageTypeReader.cs @@ -1,4 +1,5 @@ -using System.Globalization; +using System; +using System.Globalization; using System.Threading.Tasks; namespace Discord.Commands @@ -6,15 +7,14 @@ namespace Discord.Commands internal class MessageTypeReader : TypeReader where T : class, IMessage { - public override async Task Read(ICommandContext context, string input) + public override async Task Read(ICommandContext context, string input, IServiceProvider services) { ulong id; //By Id (1.0) if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id)) { - var msg = await context.Channel.GetMessageAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T; - if (msg != null) + if (await context.Channel.GetMessageAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) is T msg) return TypeReaderResult.FromSuccess(msg); } diff --git a/src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs b/src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs index aa4c7c7a4..2656741f0 100644 --- a/src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs @@ -15,17 +15,25 @@ namespace Discord.Commands internal class PrimitiveTypeReader : TypeReader { private readonly TryParseDelegate _tryParse; + private readonly float _score; public PrimitiveTypeReader() + : this(PrimitiveParsers.Get(), 1) + { } + + public PrimitiveTypeReader(TryParseDelegate tryParse, float score) { - _tryParse = PrimitiveParsers.Get(); + if (score < 0 || score > 1) + throw new ArgumentOutOfRangeException(nameof(score), score, "Scores must be within the range [0, 1]"); + + _tryParse = tryParse; + _score = score; } - public override Task Read(ICommandContext context, string input) + public override Task Read(ICommandContext context, string input, IServiceProvider services) { - T value; - if (_tryParse(input, out value)) - return Task.FromResult(TypeReaderResult.FromSuccess(value)); + if (_tryParse(input, out T value)) + return Task.FromResult(TypeReaderResult.FromSuccess(new TypeReaderValue(value, _score))); return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, $"Failed to parse {typeof(T).Name}")); } } diff --git a/src/Discord.Net.Commands/Readers/RoleTypeReader.cs b/src/Discord.Net.Commands/Readers/RoleTypeReader.cs index 48544eeda..17786e6f0 100644 --- a/src/Discord.Net.Commands/Readers/RoleTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/RoleTypeReader.cs @@ -9,7 +9,7 @@ namespace Discord.Commands internal class RoleTypeReader : TypeReader where T : class, IRole { - public override Task Read(ICommandContext context, string input) + public override Task Read(ICommandContext context, string input, IServiceProvider services) { ulong id; @@ -31,7 +31,7 @@ namespace Discord.Commands AddResult(results, role as T, role.Name == input ? 0.80f : 0.70f); if (results.Count > 0) - return Task.FromResult(TypeReaderResult.FromSuccess(results.Values)); + return Task.FromResult(TypeReaderResult.FromSuccess(results.Values.ToReadOnlyCollection())); } return Task.FromResult(TypeReaderResult.FromError(CommandError.ObjectNotFound, "Role not found.")); } diff --git a/src/Discord.Net.Commands/Readers/TypeReader.cs b/src/Discord.Net.Commands/Readers/TypeReader.cs index d53491e92..2c4644376 100644 --- a/src/Discord.Net.Commands/Readers/TypeReader.cs +++ b/src/Discord.Net.Commands/Readers/TypeReader.cs @@ -1,9 +1,10 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; namespace Discord.Commands { public abstract class TypeReader { - public abstract Task Read(ICommandContext context, string input); + public abstract Task Read(ICommandContext context, string input, IServiceProvider services); } } diff --git a/src/Discord.Net.Commands/Readers/UserTypeReader.cs b/src/Discord.Net.Commands/Readers/UserTypeReader.cs index a5f92a277..c71dac2d2 100644 --- a/src/Discord.Net.Commands/Readers/UserTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/UserTypeReader.cs @@ -10,11 +10,11 @@ namespace Discord.Commands internal class UserTypeReader : TypeReader where T : class, IUser { - public override async Task Read(ICommandContext context, string input) + public override async Task Read(ICommandContext context, string input, IServiceProvider services) { var results = new Dictionary(); IReadOnlyCollection channelUsers = (await context.Channel.GetUsersAsync(CacheMode.CacheOnly).Flatten().ConfigureAwait(false)).ToArray(); //TODO: must be a better way? - IReadOnlyCollection guildUsers = null; + IReadOnlyCollection guildUsers = ImmutableArray.Create(); ulong id; if (context.Guild != null) @@ -43,14 +43,13 @@ namespace Discord.Commands if (index >= 0) { string username = input.Substring(0, index); - ushort discriminator; - if (ushort.TryParse(input.Substring(index + 1), out discriminator)) + if (ushort.TryParse(input.Substring(index + 1), out ushort discriminator)) { var channelUser = channelUsers.FirstOrDefault(x => x.DiscriminatorValue == discriminator && string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase)); AddResult(results, channelUser as T, channelUser?.Username == username ? 0.85f : 0.75f); - var guildUser = channelUsers.FirstOrDefault(x => x.DiscriminatorValue == discriminator && + var guildUser = guildUsers.FirstOrDefault(x => x.DiscriminatorValue == discriminator && string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase)); AddResult(results, guildUser as T, guildUser?.Username == username ? 0.80f : 0.70f); } @@ -60,14 +59,14 @@ namespace Discord.Commands { foreach (var channelUser in channelUsers.Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase))) AddResult(results, channelUser as T, channelUser.Username == input ? 0.65f : 0.55f); - + foreach (var guildUser in guildUsers.Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase))) AddResult(results, guildUser as T, guildUser.Username == input ? 0.60f : 0.50f); } //By Nickname (0.5-0.6) { - foreach (var channelUser in channelUsers.Where(x => string.Equals(input, (x as IGuildUser).Nickname, StringComparison.OrdinalIgnoreCase))) + foreach (var channelUser in channelUsers.Where(x => string.Equals(input, (x as IGuildUser)?.Nickname, StringComparison.OrdinalIgnoreCase))) AddResult(results, channelUser as T, (channelUser as IGuildUser).Nickname == input ? 0.65f : 0.55f); foreach (var guildUser in guildUsers.Where(x => string.Equals(input, (x as IGuildUser).Nickname, StringComparison.OrdinalIgnoreCase))) diff --git a/src/Discord.Net.Commands/Results/PreconditionGroupResult.cs b/src/Discord.Net.Commands/Results/PreconditionGroupResult.cs new file mode 100644 index 000000000..1d7f29122 --- /dev/null +++ b/src/Discord.Net.Commands/Results/PreconditionGroupResult.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Diagnostics; + +namespace Discord.Commands +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class PreconditionGroupResult : PreconditionResult + { + public IReadOnlyCollection PreconditionResults { get; } + + protected PreconditionGroupResult(CommandError? error, string errorReason, ICollection preconditions) + : base(error, errorReason) + { + PreconditionResults = (preconditions ?? new List(0)).ToReadOnlyCollection(); + } + + public static new PreconditionGroupResult FromSuccess() + => new PreconditionGroupResult(null, null, null); + public static PreconditionGroupResult FromError(string reason, ICollection preconditions) + => new PreconditionGroupResult(CommandError.UnmetPrecondition, reason, preconditions); + public static new PreconditionGroupResult FromError(IResult result) //needed? + => new PreconditionGroupResult(result.Error, result.ErrorReason, null); + + public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; + private string DebuggerDisplay => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; + } +} diff --git a/src/Discord.Net.Commands/Results/PreconditionResult.cs b/src/Discord.Net.Commands/Results/PreconditionResult.cs index 77ba1b5b9..ca65a373e 100644 --- a/src/Discord.Net.Commands/Results/PreconditionResult.cs +++ b/src/Discord.Net.Commands/Results/PreconditionResult.cs @@ -3,14 +3,14 @@ namespace Discord.Commands { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public struct PreconditionResult : IResult + public class PreconditionResult : IResult { public CommandError? Error { get; } public string ErrorReason { get; } public bool IsSuccess => !Error.HasValue; - private PreconditionResult(CommandError? error, string errorReason) + protected PreconditionResult(CommandError? error, string errorReason) { Error = error; ErrorReason = errorReason; diff --git a/src/Discord.Net.Commands/Results/RuntimeResult.cs b/src/Discord.Net.Commands/Results/RuntimeResult.cs new file mode 100644 index 000000000..2a326a7a3 --- /dev/null +++ b/src/Discord.Net.Commands/Results/RuntimeResult.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; + +namespace Discord.Commands +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public abstract class RuntimeResult : IResult + { + protected RuntimeResult(CommandError? error, string reason) + { + Error = error; + Reason = reason; + } + + public CommandError? Error { get; } + public string Reason { get; } + + public bool IsSuccess => !Error.HasValue; + + string IResult.ErrorReason => Reason; + + public override string ToString() => Reason ?? (IsSuccess ? "Successful" : "Unsuccessful"); + private string DebuggerDisplay => IsSuccess ? $"Success: {Reason ?? "No Reason"}" : $"{Error}: {Reason}"; + } +} diff --git a/src/Discord.Net.Commands/RunMode.cs b/src/Discord.Net.Commands/RunMode.cs index 2bb5dbbf6..ecb6a4b58 100644 --- a/src/Discord.Net.Commands/RunMode.cs +++ b/src/Discord.Net.Commands/RunMode.cs @@ -4,7 +4,6 @@ { Default, Sync, - Mixed, Async } } diff --git a/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs index 1333b9640..ab88f66ae 100644 --- a/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs +++ b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs @@ -1,69 +1,80 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Reflection; +using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { - internal class ReflectionUtils + internal static class ReflectionUtils { - internal static T CreateObject(TypeInfo typeInfo, CommandService service, IDependencyMap map = null) - => CreateBuilder(typeInfo, service)(map); + private static readonly TypeInfo _objectTypeInfo = typeof(object).GetTypeInfo(); - internal static Func CreateBuilder(TypeInfo typeInfo, CommandService service) + internal static T CreateObject(TypeInfo typeInfo, CommandService commands, IServiceProvider services = null) + => CreateBuilder(typeInfo, commands)(services); + internal static Func CreateBuilder(TypeInfo typeInfo, CommandService commands) { - var constructors = typeInfo.DeclaredConstructors.Where(x => !x.IsStatic).ToArray(); - if (constructors.Length == 0) - throw new InvalidOperationException($"No constructor found for \"{typeInfo.FullName}\""); - else if (constructors.Length > 1) - throw new InvalidOperationException($"Multiple constructors found for \"{typeInfo.FullName}\""); + var constructor = GetConstructor(typeInfo); + var parameters = constructor.GetParameters(); + var properties = GetProperties(typeInfo); - var constructor = constructors[0]; - System.Reflection.ParameterInfo[] parameters = constructor.GetParameters(); - System.Reflection.PropertyInfo[] properties = typeInfo.DeclaredProperties - .Where(p => p.SetMethod?.IsPublic == true && p.GetCustomAttribute() == null) - .ToArray(); - - return (map) => + return (services) => { - object[] args = new object[parameters.Length]; - + var args = new object[parameters.Length]; for (int i = 0; i < parameters.Length; i++) - { - var parameter = parameters[i]; - args[i] = GetMember(parameter.ParameterType, map, service, typeInfo); - } - - T obj; - try - { - obj = (T)constructor.Invoke(args); - } - catch (Exception ex) - { - throw new Exception($"Failed to create \"{typeInfo.FullName}\"", ex); - } + args[i] = GetMember(commands, services, parameters[i].ParameterType, typeInfo); + var obj = InvokeConstructor(constructor, args, typeInfo); foreach(var property in properties) - { - property.SetValue(obj, GetMember(property.PropertyType, map, service, typeInfo)); - } + property.SetValue(obj, GetMember(commands, services, property.PropertyType, typeInfo)); return obj; }; } + private static T InvokeConstructor(ConstructorInfo constructor, object[] args, TypeInfo ownerType) + { + try + { + return (T)constructor.Invoke(args); + } + catch (Exception ex) + { + throw new Exception($"Failed to create \"{ownerType.FullName}\"", ex); + } + } - internal static object GetMember(Type targetType, IDependencyMap map, CommandService service, TypeInfo baseType) + private static ConstructorInfo GetConstructor(TypeInfo ownerType) { - object arg; - if (map == null || !map.TryGet(targetType, out arg)) + var constructors = ownerType.DeclaredConstructors.Where(x => !x.IsStatic).ToArray(); + if (constructors.Length == 0) + throw new InvalidOperationException($"No constructor found for \"{ownerType.FullName}\""); + else if (constructors.Length > 1) + throw new InvalidOperationException($"Multiple constructors found for \"{ownerType.FullName}\""); + return constructors[0]; + } + private static System.Reflection.PropertyInfo[] GetProperties(TypeInfo ownerType) + { + var result = new List(); + while (ownerType != _objectTypeInfo) { - if (targetType == typeof(CommandService)) - arg = service; - else if (targetType == typeof(IDependencyMap)) - arg = map; - else - throw new InvalidOperationException($"Failed to create \"{baseType.FullName}\", dependency \"{targetType.Name}\" was not found."); + foreach (var prop in ownerType.DeclaredProperties) + { + if (prop.SetMethod?.IsStatic == false && prop.SetMethod?.IsPublic == true && prop.GetCustomAttribute() == null) + result.Add(prop); + } + ownerType = ownerType.BaseType.GetTypeInfo(); } - return arg; + return result.ToArray(); + } + private static object GetMember(CommandService commands, IServiceProvider services, Type memberType, TypeInfo ownerType) + { + if (memberType == typeof(CommandService)) + return commands; + if (memberType == typeof(IServiceProvider) || memberType == services.GetType()) + return services; + var service = services?.GetService(memberType); + if (service != null) + return service; + throw new InvalidOperationException($"Failed to create \"{ownerType.FullName}\", dependency \"{memberType.Name}\" was not found."); } } } diff --git a/src/Discord.Net.Core/AssemblyInfo.cs b/src/Discord.Net.Core/AssemblyInfo.cs index c75729acf..116bc3850 100644 --- a/src/Discord.Net.Core/AssemblyInfo.cs +++ b/src/Discord.Net.Core/AssemblyInfo.cs @@ -4,5 +4,6 @@ [assembly: InternalsVisibleTo("Discord.Net.Rest")] [assembly: InternalsVisibleTo("Discord.Net.Rpc")] [assembly: InternalsVisibleTo("Discord.Net.WebSocket")] +[assembly: InternalsVisibleTo("Discord.Net.Webhook")] [assembly: InternalsVisibleTo("Discord.Net.Commands")] [assembly: InternalsVisibleTo("Discord.Net.Tests")] \ No newline at end of file diff --git a/src/Discord.Net.Core/Audio/AudioInStream.cs b/src/Discord.Net.Core/Audio/AudioInStream.cs index a6b5c5e6b..656c0bc48 100644 --- a/src/Discord.Net.Core/Audio/AudioInStream.cs +++ b/src/Discord.Net.Core/Audio/AudioInStream.cs @@ -1,41 +1,19 @@ using System; -using System.IO; using System.Threading; using System.Threading.Tasks; namespace Discord.Audio { - public abstract class AudioInStream : Stream + public abstract class AudioInStream : AudioStream { - public override bool CanRead => true; - public override bool CanSeek => false; - public override bool CanWrite => true; - - public abstract Task ReadFrameAsync(CancellationToken cancelToken); + public abstract int AvailableFrames { get; } - public RTPFrame? ReadFrame() - { - return ReadFrameAsync(CancellationToken.None).GetAwaiter().GetResult(); - } - public override int Read(byte[] buffer, int offset, int count) - { - return ReadAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); - } - public override void Write(byte[] buffer, int offset, int count) - { - WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); - } - - public override void Flush() { throw new NotSupportedException(); } + public override bool CanRead => true; + public override bool CanWrite => true; - public override long Length { get { throw new NotSupportedException(); } } - public override long Position - { - get { throw new NotSupportedException(); } - set { throw new NotSupportedException(); } - } + public abstract Task ReadFrameAsync(CancellationToken cancelToken); + public abstract bool TryReadFrame(CancellationToken cancelToken, out RTPFrame frame); - public override void SetLength(long value) { throw new NotSupportedException(); } - public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); } + public override Task FlushAsync(CancellationToken cancelToken) { throw new NotSupportedException(); } } } diff --git a/src/Discord.Net.Core/Audio/AudioOutStream.cs b/src/Discord.Net.Core/Audio/AudioOutStream.cs index 2b4b012ee..7019ba8cd 100644 --- a/src/Discord.Net.Core/Audio/AudioOutStream.cs +++ b/src/Discord.Net.Core/Audio/AudioOutStream.cs @@ -1,39 +1,12 @@ using System; using System.IO; -using System.Threading; -using System.Threading.Tasks; namespace Discord.Audio { - public abstract class AudioOutStream : Stream + public abstract class AudioOutStream : AudioStream { - public override bool CanRead => false; - public override bool CanSeek => false; public override bool CanWrite => true; - public override void Write(byte[] buffer, int offset, int count) - { - WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); - } - public override void Flush() - { - FlushAsync(CancellationToken.None).GetAwaiter().GetResult(); - } - public void Clear() - { - ClearAsync(CancellationToken.None).GetAwaiter().GetResult(); - } - - public virtual Task ClearAsync(CancellationToken cancellationToken) { return Task.Delay(0); } - //public virtual Task WriteSilenceAsync(CancellationToken cancellationToken) { return Task.Delay(0); } - - public override long Length { get { throw new NotSupportedException(); } } - public override long Position - { - get { throw new NotSupportedException(); } - set { throw new NotSupportedException(); } - } - public override int Read(byte[] buffer, int offset, int count) { throw new NotSupportedException(); } public override void SetLength(long value) { throw new NotSupportedException(); } public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); } diff --git a/src/Discord.Net.Core/Audio/AudioStream.cs b/src/Discord.Net.Core/Audio/AudioStream.cs new file mode 100644 index 000000000..97820ea73 --- /dev/null +++ b/src/Discord.Net.Core/Audio/AudioStream.cs @@ -0,0 +1,44 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Audio +{ + public abstract class AudioStream : Stream + { + public override bool CanRead => false; + public override bool CanSeek => false; + public override bool CanWrite => false; + + public virtual void WriteHeader(ushort seq, uint timestamp, bool missed) + { + throw new InvalidOperationException("This stream does not accept headers"); + } + public override void Write(byte[] buffer, int offset, int count) + { + WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); + } + public override void Flush() + { + FlushAsync(CancellationToken.None).GetAwaiter().GetResult(); + } + public void Clear() + { + ClearAsync(CancellationToken.None).GetAwaiter().GetResult(); + } + + public virtual Task ClearAsync(CancellationToken cancellationToken) { return Task.Delay(0); } + + public override long Length { get { throw new NotSupportedException(); } } + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + public override int Read(byte[] buffer, int offset, int count) { throw new NotSupportedException(); } + public override void SetLength(long value) { throw new NotSupportedException(); } + public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); } + } +} diff --git a/src/Discord.Net.Core/Audio/IAudioClient.cs b/src/Discord.Net.Core/Audio/IAudioClient.cs index bea44fcf4..9be8ceef5 100644 --- a/src/Discord.Net.Core/Audio/IAudioClient.cs +++ b/src/Discord.Net.Core/Audio/IAudioClient.cs @@ -8,39 +8,28 @@ namespace Discord.Audio event Func Connected; event Func Disconnected; event Func LatencyUpdated; - + event Func UdpLatencyUpdated; + event Func StreamCreated; + event Func StreamDestroyed; + event Func SpeakingUpdated; + /// Gets the current connection state of this client. ConnectionState ConnectionState { get; } - /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. + /// Gets the estimated round-trip latency, in milliseconds, to the voice websocket server. int Latency { get; } + /// Gets the estimated round-trip latency, in milliseconds, to the voice UDP server. + int UdpLatency { get; } Task StopAsync(); + Task SetSpeakingAsync(bool value); - /// - /// Creates a new outgoing stream accepting Opus-encoded data. - /// - /// Samples per frame. Must be 120, 240, 480, 960, 1920 or 2880, representing 2.5, 5, 10, 20, 40 or 60 milliseconds respectively. - /// - AudioOutStream CreateOpusStream(int samplesPerFrame, int bufferMillis = 1000); - /// - /// Creates a new outgoing stream accepting Opus-encoded data. This is a direct stream with no internal timer. - /// - /// Samples per frame. Must be 120, 240, 480, 960, 1920 or 2880, representing 2.5, 5, 10, 20, 40 or 60 milliseconds respectively. - /// - AudioOutStream CreateDirectOpusStream(int samplesPerFrame); - /// - /// Creates a new outgoing stream accepting PCM (raw) data. - /// - /// Samples per frame. Must be 120, 240, 480, 960, 1920 or 2880, representing 2.5, 5, 10, 20, 40 or 60 milliseconds respectively. - /// - /// - AudioOutStream CreatePCMStream(AudioApplication application, int samplesPerFrame, int channels = 2, int? bitrate = null, int bufferMillis = 1000); - /// - /// Creates a new direct outgoing stream accepting PCM (raw) data. This is a direct stream with no internal timer. - /// - /// Samples per frame. Must be 120, 240, 480, 960, 1920 or 2880, representing 2.5, 5, 10, 20, 40 or 60 milliseconds respectively. - /// - /// - AudioOutStream CreateDirectPCMStream(AudioApplication application, int samplesPerFrame, int channels = 2, int? bitrate = null); + /// Creates a new outgoing stream accepting Opus-encoded data. + AudioOutStream CreateOpusStream(int bufferMillis = 1000); + /// Creates a new outgoing stream accepting Opus-encoded data. This is a direct stream with no internal timer. + AudioOutStream CreateDirectOpusStream(); + /// Creates a new outgoing stream accepting PCM (raw) data. + AudioOutStream CreatePCMStream(AudioApplication application, int? bitrate = null, int bufferMillis = 1000, int packetLoss = 30); + /// Creates a new direct outgoing stream accepting PCM (raw) data. This is a direct stream with no internal timer. + AudioOutStream CreateDirectPCMStream(AudioApplication application, int? bitrate = null, int packetLoss = 30); } } diff --git a/src/Discord.Net.Core/Audio/RTPFrame.cs b/src/Discord.Net.Core/Audio/RTPFrame.cs index 5005870f4..6254b7173 100644 --- a/src/Discord.Net.Core/Audio/RTPFrame.cs +++ b/src/Discord.Net.Core/Audio/RTPFrame.cs @@ -5,12 +5,14 @@ namespace Discord.Audio public readonly ushort Sequence; public readonly uint Timestamp; public readonly byte[] Payload; + public readonly bool Missed; - public RTPFrame(ushort sequence, uint timestamp, byte[] payload) + public RTPFrame(ushort sequence, uint timestamp, byte[] payload, bool missed) { Sequence = sequence; Timestamp = timestamp; Payload = payload; + Missed = missed; } } } \ No newline at end of file diff --git a/src/Discord.Net.Core/CDN.cs b/src/Discord.Net.Core/CDN.cs index b7a5346ea..d3ade3722 100644 --- a/src/Discord.Net.Core/CDN.cs +++ b/src/Discord.Net.Core/CDN.cs @@ -1,11 +1,18 @@ -namespace Discord +using System; + +namespace Discord { public static class CDN { public static string GetApplicationIconUrl(ulong appId, string iconId) => iconId != null ? $"{DiscordConfig.CDNUrl}app-icons/{appId}/{iconId}.jpg" : null; - public static string GetUserAvatarUrl(ulong userId, string avatarId, ushort size, AvatarFormat format) - => avatarId != null ? $"{DiscordConfig.CDNUrl}avatars/{userId}/{avatarId}.{format.ToString().ToLower()}?size={size}" : null; + public static string GetUserAvatarUrl(ulong userId, string avatarId, ushort size, ImageFormat format) + { + if (avatarId == null) + return null; + string extension = FormatToExtension(format, avatarId); + return $"{DiscordConfig.CDNUrl}avatars/{userId}/{avatarId}.{extension}?size={size}"; + } public static string GetGuildIconUrl(ulong guildId, string iconId) => iconId != null ? $"{DiscordConfig.CDNUrl}icons/{guildId}/{iconId}.jpg" : null; public static string GetGuildSplashUrl(ulong guildId, string splashId) @@ -14,5 +21,19 @@ => iconId != null ? $"{DiscordConfig.CDNUrl}channel-icons/{channelId}/{iconId}.jpg" : null; public static string GetEmojiUrl(ulong emojiId) => $"{DiscordConfig.CDNUrl}emojis/{emojiId}.png"; + + private static string FormatToExtension(ImageFormat format, string imageId) + { + if (format == ImageFormat.Auto) + format = imageId.StartsWith("a_") ? ImageFormat.Gif : ImageFormat.Png; + switch (format) + { + case ImageFormat.Gif: return "gif"; + case ImageFormat.Jpeg: return "jpeg"; + case ImageFormat.Png: return "png"; + case ImageFormat.WebP: return "webp"; + default: throw new ArgumentException(nameof(format)); + } + } } } diff --git a/src/Discord.Net.Core/Discord.Net.Core.csproj b/src/Discord.Net.Core/Discord.Net.Core.csproj index d78840281..bce577ddd 100644 --- a/src/Discord.Net.Core/Discord.Net.Core.csproj +++ b/src/Discord.Net.Core/Discord.Net.Core.csproj @@ -1,36 +1,14 @@ - + + - 1.0.0 - rc-dev - rc-$(BuildNumber) - netstandard1.1;netstandard1.3 Discord.Net.Core - RogueException - A .Net API wrapper and bot framework for Discord. - discord;discordapp - https://github.com/RogueException/Discord.Net - http://opensource.org/licenses/MIT - git - git://github.com/RogueException/Discord.Net Discord - true + The core components for the Discord.Net library. + net45;netstandard1.1;netstandard1.3 - - - - - - - - - - - + + + - - $(NoWarn);CS1573;CS1591 - true - true - \ No newline at end of file diff --git a/src/Discord.Net.Core/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs index 78a5b0e1e..fd2fe92e8 100644 --- a/src/Discord.Net.Core/DiscordConfig.cs +++ b/src/Discord.Net.Core/DiscordConfig.cs @@ -10,7 +10,8 @@ namespace Discord typeof(DiscordConfig).GetTypeInfo().Assembly.GetName().Version.ToString(3) ?? "Unknown"; - public static readonly string ClientAPIUrl = $"https://discordapp.com/api/v{APIVersion}/"; + public static string UserAgent { get; } = $"DiscordBot (https://github.com/RogueException/Discord.Net, v{Version})"; + public static readonly string APIUrl = $"https://discordapp.com/api/v{APIVersion}/"; public const string CDNUrl = "https://cdn.discordapp.com/"; public const string InviteUrl = "https://discord.gg/"; @@ -18,11 +19,12 @@ namespace Discord public const int MaxMessageSize = 2000; public const int MaxMessagesPerBatch = 100; public const int MaxUsersPerBatch = 1000; + public const int MaxGuildsPerBatch = 100; /// Gets or sets how a request should act in the case of an error, by default. public RetryMode DefaultRetryMode { get; set; } = RetryMode.AlwaysRetry; - /// Gets or sets the minimum log level severity that will be sent to the LogMessage event. + /// Gets or sets the minimum log level severity that will be sent to the Log event. public LogSeverity LogLevel { get; set; } = LogSeverity.Info; /// Gets or sets whether the initial log entry should be printed. diff --git a/src/Discord.Net.Core/Entities/Channels/BulkGuildChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/BulkGuildChannelProperties.cs deleted file mode 100644 index 2358b2e2e..000000000 --- a/src/Discord.Net.Core/Entities/Channels/BulkGuildChannelProperties.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Discord -{ - public class BulkGuildChannelProperties - { - /// - /// The id of the channel to apply this position to. - /// - public ulong Id { get; set; } - /// - /// The new zero-based position of this channel. - /// - public int Position { get; set; } - - public BulkGuildChannelProperties(ulong id, int position) - { - Id = id; - Position = position; - } - } -} diff --git a/src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs b/src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs index 6c9507299..afb81d92f 100644 --- a/src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs @@ -1,6 +1,12 @@ -namespace Discord +using Discord.Audio; +using System; +using System.Threading.Tasks; + +namespace Discord { public interface IAudioChannel : IChannel { + /// Connects to this audio channel. + Task ConnectAsync(Action configAction = null); } } diff --git a/src/Discord.Net.Core/Entities/Channels/IChannel.cs b/src/Discord.Net.Core/Entities/Channels/IChannel.cs index 72608ec6a..fbb979951 100644 --- a/src/Discord.Net.Core/Entities/Channels/IChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IChannel.cs @@ -8,6 +8,9 @@ namespace Discord /// Gets the name of this channel. string Name { get; } + /// Checks if the channel is NSFW. + bool IsNsfw { get; } + /// Gets a collection of all users in this channel. IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); diff --git a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs index 9c9c63929..a465b3ad8 100644 --- a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs @@ -9,7 +9,7 @@ namespace Discord { /// Sends a message to this message channel. Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null); -#if NETSTANDARD1_3 +#if FILESYSTEM /// Sends a file to this text channel, with an optional caption. Task SendFileAsync(string filePath, string text = null, bool isTTS = false, RequestOptions options = null); #endif @@ -30,7 +30,9 @@ namespace Discord /// Gets a collection of pinned messages in this channel. Task> GetPinnedMessagesAsync(RequestOptions options = null); /// Bulk deletes multiple messages. - Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null); + Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null); + /// Bulk deletes multiple messages. + Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null); /// Broadcasts the "user is typing" message to all users in this channel, lasting 10 seconds. Task TriggerTypingAsync(RequestOptions options = null); diff --git a/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs b/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs index 80c90e4bd..e2a2ad8eb 100644 --- a/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs @@ -1,5 +1,4 @@ -using Discord.Audio; -using System; +using System; using System.Threading.Tasks; namespace Discord @@ -13,7 +12,5 @@ namespace Discord /// Modifies this voice channel. Task ModifyAsync(Action func, RequestOptions options = null); - /// Connects to this voice channel. - Task ConnectAsync(); } } \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Channels/ReorderChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/ReorderChannelProperties.cs new file mode 100644 index 000000000..31f814334 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/ReorderChannelProperties.cs @@ -0,0 +1,16 @@ +namespace Discord +{ + public class ReorderChannelProperties + { + /// The id of the channel to apply this position to. + public ulong Id { get; } + /// The new zero-based position of this channel. + public int Position { get; } + + public ReorderChannelProperties(ulong id, int position) + { + Id = id; + Position = position; + } + } +} diff --git a/src/Discord.Net.Core/Entities/Emotes/Emoji.cs b/src/Discord.Net.Core/Entities/Emotes/Emoji.cs new file mode 100644 index 000000000..c2dfc31ad --- /dev/null +++ b/src/Discord.Net.Core/Entities/Emotes/Emoji.cs @@ -0,0 +1,39 @@ +namespace Discord +{ + /// + /// A unicode emoji + /// + public class Emoji : IEmote + { + // TODO: need to constrain this to unicode-only emojis somehow + + /// + /// The unicode representation of this emote. + /// + public string Name { get; } + + public override string ToString() => Name; + + /// + /// Creates a unicode emoji. + /// + /// The pure UTF-8 encoding of an emoji + public Emoji(string unicode) + { + Name = unicode; + } + + public override bool Equals(object other) + { + if (other == null) return false; + if (other == this) return true; + + var otherEmoji = other as Emoji; + if (otherEmoji == null) return false; + + return string.Equals(Name, otherEmoji.Name); + } + + public override int GetHashCode() => Name.GetHashCode(); + } +} diff --git a/src/Discord.Net.Core/Entities/Emotes/Emote.cs b/src/Discord.Net.Core/Entities/Emotes/Emote.cs new file mode 100644 index 000000000..f498c818e --- /dev/null +++ b/src/Discord.Net.Core/Entities/Emotes/Emote.cs @@ -0,0 +1,82 @@ +using System; +using System.Globalization; + +namespace Discord +{ + /// + /// A custom image-based emote + /// + public class Emote : IEmote, ISnowflakeEntity + { + /// + /// The display name (tooltip) of this emote + /// + public string Name { get; } + /// + /// The ID of this emote + /// + public ulong Id { get; } + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + public string Url => CDN.GetEmojiUrl(Id); + + internal Emote(ulong id, string name) + { + Id = id; + Name = name; + } + + public override bool Equals(object other) + { + if (other == null) return false; + if (other == this) return true; + + var otherEmote = other as Emote; + if (otherEmote == null) return false; + + return string.Equals(Name, otherEmote.Name) && Id == otherEmote.Id; + } + + public override int GetHashCode() + { + unchecked + { + return (Name.GetHashCode() * 397) ^ Id.GetHashCode(); + } + } + + /// + /// Parse an Emote from its raw format + /// + /// The raw encoding of an emote; for example, <:dab:277855270321782784> + /// An emote + public static Emote Parse(string text) + { + if (TryParse(text, out Emote result)) + return result; + throw new ArgumentException("Invalid emote format", nameof(text)); + } + + public static bool TryParse(string text, out Emote result) + { + result = null; + if (text.Length >= 4 && text[0] == '<' && text[1] == ':' && text[text.Length - 1] == '>') + { + int splitIndex = text.IndexOf(':', 2); + if (splitIndex == -1) + return false; + + if (!ulong.TryParse(text.Substring(splitIndex + 1, text.Length - splitIndex - 2), NumberStyles.None, CultureInfo.InvariantCulture, out ulong id)) + return false; + + string name = text.Substring(2, splitIndex - 2); + result = new Emote(id, name); + return true; + } + return false; + + } + + private string DebuggerDisplay => $"{Name} ({Id})"; + public override string ToString() => $"<:{Name}:{Id}>"; + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildEmoji.cs b/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs similarity index 58% rename from src/Discord.Net.Core/Entities/Guilds/GuildEmoji.cs rename to src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs index e925991eb..8d776a4cd 100644 --- a/src/Discord.Net.Core/Entities/Guilds/GuildEmoji.cs +++ b/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs @@ -3,25 +3,24 @@ using System.Diagnostics; namespace Discord { + /// + /// An image-based emote that is attached to a guild + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public struct GuildEmoji + public class GuildEmote : Emote { - public ulong Id { get; } - public string Name { get; } public bool IsManaged { get; } public bool RequireColons { get; } public IReadOnlyList RoleIds { get; } - internal GuildEmoji(ulong id, string name, bool isManaged, bool requireColons, IReadOnlyList roleIds) + internal GuildEmote(ulong id, string name, bool isManaged, bool requireColons, IReadOnlyList roleIds) : base(id, name) { - Id = id; - Name = name; IsManaged = isManaged; RequireColons = requireColons; RoleIds = roleIds; } - public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id})"; + public override string ToString() => $"<:{Name}:{Id}>"; } } diff --git a/src/Discord.Net.Core/Entities/Emotes/IEmote.cs b/src/Discord.Net.Core/Entities/Emotes/IEmote.cs new file mode 100644 index 000000000..fac61402a --- /dev/null +++ b/src/Discord.Net.Core/Entities/Emotes/IEmote.cs @@ -0,0 +1,13 @@ +namespace Discord +{ + /// + /// A general container for any type of emote in a message. + /// + public interface IEmote + { + /// + /// The display name or unicode representation of this emote + /// + string Name { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 2ce9b48d0..7874f5fd1 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -45,7 +45,7 @@ namespace Discord /// Gets the built-in role containing all users in this guild. IRole EveryoneRole { get; } /// Gets a collection of all custom emojis for this guild. - IReadOnlyCollection Emojis { get; } + IReadOnlyCollection Emotes { get; } /// Gets a collection of all extra features added to this guild. IReadOnlyCollection Features { get; } /// Gets a collection of all roles in this guild. @@ -56,18 +56,20 @@ namespace Discord /// Modifies this guild's embed. Task ModifyEmbedAsync(Action func, RequestOptions options = null); /// Bulk modifies the channels of this guild. - Task ModifyChannelsAsync(IEnumerable args, RequestOptions options = null); + Task ReorderChannelsAsync(IEnumerable args, RequestOptions options = null); /// Bulk modifies the roles of this guild. - Task ModifyRolesAsync(IEnumerable args, RequestOptions options = null); + Task ReorderRolesAsync(IEnumerable args, RequestOptions options = null); /// Leaves this guild. If you are the owner, use Delete instead. Task LeaveAsync(RequestOptions options = null); /// Gets a collection of all users banned on this guild. Task> GetBansAsync(RequestOptions options = null); /// Bans the provided user from this guild and optionally prunes their recent messages. - Task AddBanAsync(IUser user, int pruneDays = 0, RequestOptions options = null); + /// The number of days to remove messages from this user for - must be between [0, 7] + Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null); /// Bans the provided user id from this guild and optionally prunes their recent messages. - Task AddBanAsync(ulong userId, int pruneDays = 0, RequestOptions options = null); + /// The number of days to remove messages from this user for - must be between [0, 7] + Task AddBanAsync(ulong userId, int pruneDays = 0, string reason = null, RequestOptions options = null); /// Unbans the provided user if it is currently banned. Task RemoveBanAsync(IUser user, RequestOptions options = null); /// Unbans the provided user id if it is currently banned. @@ -83,7 +85,7 @@ namespace Discord Task GetVoiceChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); Task GetAFKChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); Task GetDefaultChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - Task GetEmbedChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task GetEmbedChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// Creates a new text channel. Task CreateTextChannelAsync(string name, RequestOptions options = null); /// Creates a new voice channel. diff --git a/src/Discord.Net.Core/Entities/Guilds/VerificationLevel.cs b/src/Discord.Net.Core/Entities/Guilds/VerificationLevel.cs index d6828b5c9..ac51fe927 100644 --- a/src/Discord.Net.Core/Entities/Guilds/VerificationLevel.cs +++ b/src/Discord.Net.Core/Entities/Guilds/VerificationLevel.cs @@ -9,6 +9,8 @@ /// Users must fulfill the requirements of Low, and be registered on Discord for at least 5 minutes. Medium = 2, /// Users must fulfill the requirements of Medium, and be a member of this guild for at least 10 minutes. - High = 3 + High = 3, + /// Users must fulfill the requirements of High, and must have a verified phone on their Discord account. + Extreme = 4 } } diff --git a/src/Discord.Net.Core/Entities/Image.cs b/src/Discord.Net.Core/Entities/Image.cs index 59fe8bbdb..c2c997365 100644 --- a/src/Discord.Net.Core/Entities/Image.cs +++ b/src/Discord.Net.Core/Entities/Image.cs @@ -15,7 +15,7 @@ namespace Discord { Stream = stream; } -#if NETSTANDARD1_3 +#if FILESYSTEM /// /// Create the image from a file path. /// diff --git a/src/Discord.Net.Core/Entities/Users/AvatarFormat.cs b/src/Discord.Net.Core/Entities/ImageFormat.cs similarity index 68% rename from src/Discord.Net.Core/Entities/Users/AvatarFormat.cs rename to src/Discord.Net.Core/Entities/ImageFormat.cs index 29c17cede..302da79c8 100644 --- a/src/Discord.Net.Core/Entities/Users/AvatarFormat.cs +++ b/src/Discord.Net.Core/Entities/ImageFormat.cs @@ -1,7 +1,8 @@ namespace Discord { - public enum AvatarFormat + public enum ImageFormat { + Auto, WebP, Png, Jpeg, diff --git a/src/Discord.Net.Core/Entities/Invites/IInvite.cs b/src/Discord.Net.Core/Entities/Invites/IInvite.cs index a023749c2..73555e453 100644 --- a/src/Discord.Net.Core/Entities/Invites/IInvite.cs +++ b/src/Discord.Net.Core/Entities/Invites/IInvite.cs @@ -13,10 +13,15 @@ namespace Discord IChannel Channel { get; } /// Gets the id of the channel this invite is linked to. ulong ChannelId { get; } + /// Gets the name of the channel this invite is linked to. + string ChannelName { get; } + /// Gets the guild this invite is linked to. IGuild Guild { get; } /// Gets the id of the guild this invite is linked to. ulong GuildId { get; } + /// Gets the name of the guild this invite is linked to. + string GuildName { get; } /// Accepts this invite and joins the target guild. This will fail on bot accounts. Task AcceptAsync(RequestOptions options = null); diff --git a/src/Discord.Net.Core/Entities/Messages/Embed.cs b/src/Discord.Net.Core/Entities/Messages/Embed.cs index ebde05d4c..5fae7acde 100644 --- a/src/Discord.Net.Core/Entities/Messages/Embed.cs +++ b/src/Discord.Net.Core/Entities/Messages/Embed.cs @@ -1,13 +1,14 @@ using System; using System.Collections.Immutable; using System.Diagnostics; +using System.Linq; namespace Discord { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class Embed : IEmbed { - public string Type { get; } + public EmbedType Type { get; } public string Description { get; internal set; } public string Url { get; internal set; } @@ -22,12 +23,12 @@ namespace Discord public EmbedThumbnail? Thumbnail { get; internal set; } public ImmutableArray Fields { get; internal set; } - internal Embed(string type) + internal Embed(EmbedType type) { Type = type; Fields = ImmutableArray.Create(); } - internal Embed(string type, + internal Embed(EmbedType type, string title, string description, string url, @@ -56,6 +57,8 @@ namespace Discord Fields = fields; } + public int Length => Title?.Length + Author?.Name?.Length + Description?.Length + Footer?.Text?.Length + Fields.Sum(f => f.Name.Length + f.Value.ToString().Length) ?? 0; + public override string ToString() => Title; private string DebuggerDisplay => $"{Title} ({Type})"; } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs b/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs index 142e36832..c59473704 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; namespace Discord { diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs b/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs index 33582070a..29d85cd90 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; namespace Discord { diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs b/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs index fa4847721..f21d42c0c 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; namespace Discord { @@ -19,6 +20,6 @@ namespace Discord } private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})"; - public override string ToString() => Url; + public override string ToString() => Url.ToString(); } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs b/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs index 943ac5b52..24722b158 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; namespace Discord { diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs b/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs index 4e125bf2a..209a93e37 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; namespace Discord { @@ -19,6 +20,6 @@ namespace Discord } private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})"; - public override string ToString() => Url; + public override string ToString() => Url.ToString(); } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedType.cs b/src/Discord.Net.Core/Entities/Messages/EmbedType.cs new file mode 100644 index 000000000..469e968a5 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/EmbedType.cs @@ -0,0 +1,13 @@ +namespace Discord +{ + public enum EmbedType + { + Rich, + Link, + Video, + Image, + Gifv, + Article, + Tweet + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs b/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs index eaf6f4a4c..f00681d89 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; namespace Discord { @@ -17,6 +18,6 @@ namespace Discord } private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})"; - public override string ToString() => Url; + public override string ToString() => Url.ToString(); } } diff --git a/src/Discord.Net.Core/Entities/Messages/Emoji.cs b/src/Discord.Net.Core/Entities/Messages/Emoji.cs deleted file mode 100644 index f0a0489e2..000000000 --- a/src/Discord.Net.Core/Entities/Messages/Emoji.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Diagnostics; -using System.Globalization; - -namespace Discord -{ - [DebuggerDisplay("{DebuggerDisplay,nq}")] - public struct Emoji - { - public ulong? Id { get; } - public string Name { get; } - - public string Url => Id != null ? CDN.GetEmojiUrl(Id.Value) : null; - - internal Emoji(ulong? id, string name) - { - Id = id; - Name = name; - } - - public static Emoji Parse(string text) - { - Emoji result; - if (TryParse(text, out result)) - return result; - throw new ArgumentException("Invalid emoji format", nameof(text)); - } - - public static bool TryParse(string text, out Emoji result) - { - result = default(Emoji); - if (text.Length >= 4 && text[0] == '<' && text[1] == ':' && text[text.Length - 1] == '>') - { - int splitIndex = text.IndexOf(':', 2); - if (splitIndex == -1) - return false; - - ulong id; - if (!ulong.TryParse(text.Substring(splitIndex + 1, text.Length - splitIndex - 2), NumberStyles.None, CultureInfo.InvariantCulture, out id)) - return false; - - string name = text.Substring(2, splitIndex - 2); - result = new Emoji(id, name); - return true; - } - return false; - - } - - private string DebuggerDisplay => $"{Name} ({Id})"; - public override string ToString() => Name; - } -} diff --git a/src/Discord.Net.Core/Entities/Messages/IEmbed.cs b/src/Discord.Net.Core/Entities/Messages/IEmbed.cs index 5eef5ec9b..f390c4c28 100644 --- a/src/Discord.Net.Core/Entities/Messages/IEmbed.cs +++ b/src/Discord.Net.Core/Entities/Messages/IEmbed.cs @@ -6,9 +6,9 @@ namespace Discord public interface IEmbed { string Url { get; } - string Type { get; } string Title { get; } string Description { get; } + EmbedType Type { get; } DateTimeOffset? Timestamp { get; } Color? Color { get; } EmbedImage? Image { get; } diff --git a/src/Discord.Net.Core/Entities/Messages/IMessage.cs b/src/Discord.Net.Core/Entities/Messages/IMessage.cs index 6bb44368b..4266f893a 100644 --- a/src/Discord.Net.Core/Entities/Messages/IMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IMessage.cs @@ -7,12 +7,12 @@ namespace Discord { /// Gets the type of this system message. MessageType Type { get; } + /// Gets the source of this message. + MessageSource Source { get; } /// Returns true if this message was sent as a text-to-speech message. bool IsTTS { get; } /// Returns true if this message was added to its channel's pinned messages. bool IsPinned { get; } - /// Returns true if this message was created using a webhook. - bool IsWebhook { get; } /// Returns the content for this message. string Content { get; } /// Gets the time this message was sent. @@ -24,8 +24,6 @@ namespace Discord IMessageChannel Channel { get; } /// Gets the author of this message. IUser Author { get; } - /// Gets the id of the webhook used to created this message, if any. - ulong? WebhookId { get; } /// Returns all attachments included in this message. IReadOnlyCollection Attachments { get; } diff --git a/src/Discord.Net.Core/Entities/Messages/IReaction.cs b/src/Discord.Net.Core/Entities/Messages/IReaction.cs index 7145fce7f..37ead42ae 100644 --- a/src/Discord.Net.Core/Entities/Messages/IReaction.cs +++ b/src/Discord.Net.Core/Entities/Messages/IReaction.cs @@ -2,6 +2,6 @@ { public interface IReaction { - Emoji Emoji { get; } + IEmote Emote { get; } } } diff --git a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs index 73d402041..61f908394 100644 --- a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs @@ -14,16 +14,12 @@ namespace Discord Task UnpinAsync(RequestOptions options = null); /// Returns all reactions included in this message. - IReadOnlyDictionary Reactions { get; } + IReadOnlyDictionary Reactions { get; } /// Adds a reaction to this message. - Task AddReactionAsync(Emoji emoji, RequestOptions options = null); - /// Adds a reaction to this message. - Task AddReactionAsync(string emoji, RequestOptions options = null); + Task AddReactionAsync(IEmote emote, RequestOptions options = null); /// Removes a reaction from message. - Task RemoveReactionAsync(Emoji emoji, IUser user, RequestOptions options = null); - /// Removes a reaction from this message. - Task RemoveReactionAsync(string emoji, IUser user, RequestOptions options = null); + Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions options = null); /// Removes all reactions from this message. Task RemoveAllReactionsAsync(RequestOptions options = null); Task> GetReactionUsersAsync(string emoji, int limit = 100, ulong? afterUserId = null, RequestOptions options = null); diff --git a/src/Discord.Net.Core/Entities/Messages/MessageSource.cs b/src/Discord.Net.Core/Entities/Messages/MessageSource.cs new file mode 100644 index 000000000..1cb2f8b94 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/MessageSource.cs @@ -0,0 +1,10 @@ +namespace Discord +{ + public enum MessageSource + { + System, + User, + Bot, + Webhook + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/ReactionMetadata.cs b/src/Discord.Net.Core/Entities/Messages/ReactionMetadata.cs new file mode 100644 index 000000000..005276202 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/ReactionMetadata.cs @@ -0,0 +1,11 @@ +namespace Discord +{ + public struct ReactionMetadata + { + /// Gets the number of reactions + public int ReactionCount { get; internal set; } + + /// Returns true if the current user has used this reaction + public bool IsMe { get; internal set; } + } +} diff --git a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs index 2824a1426..94596e0e6 100644 --- a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs @@ -7,24 +7,27 @@ namespace Discord [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct ChannelPermissions { - //TODO: C#7 Candidate for binary literals - private static ChannelPermissions _allDM { get; } = new ChannelPermissions(Convert.ToUInt64("00000000000001011100110000000000", 2)); - private static ChannelPermissions _allVoice { get; } = new ChannelPermissions(Convert.ToUInt64("00010011111100000000000000010001", 2)); - private static ChannelPermissions _allText { get; } = new ChannelPermissions(Convert.ToUInt64("00010000000001111111110001010001", 2)); - private static ChannelPermissions _allGroup { get; } = new ChannelPermissions(Convert.ToUInt64("00000000000001111110110000000000", 2)); - /// Gets a blank ChannelPermissions that grants no permissions. - public static ChannelPermissions None { get; } = new ChannelPermissions(); + public static readonly ChannelPermissions None = new ChannelPermissions(); + /// Gets a ChannelPermissions that grants all permissions for text channels. + public static readonly ChannelPermissions Text = new ChannelPermissions(0b00100_0000000_1111111110001_010001); + /// Gets a ChannelPermissions that grants all permissions for voice channels. + public static readonly ChannelPermissions Voice = new ChannelPermissions(0b00100_1111110_0000000000000_010001); + /// Gets a ChannelPermissions that grants all permissions for direct message channels. + public static readonly ChannelPermissions DM = new ChannelPermissions(0b00000_1000110_1011100110000_000000); + /// Gets a ChannelPermissions that grants all permissions for group channels. + public static readonly ChannelPermissions Group = new ChannelPermissions(0b00000_1000110_0001101100000_000000); /// Gets a ChannelPermissions that grants all permissions for a given channelType. public static ChannelPermissions All(IChannel channel) { - //TODO: C#7 Candidate for typeswitch - if (channel is ITextChannel) return _allText; - if (channel is IVoiceChannel) return _allVoice; - if (channel is IDMChannel) return _allDM; - if (channel is IGroupChannel) return _allGroup; - - throw new ArgumentException("Unknown channel type", nameof(channel)); + switch (channel) + { + case ITextChannel _: return Text; + case IVoiceChannel _: return Voice; + case IDMChannel _: return DM; + case IGroupChannel _: return Group; + default: throw new ArgumentException("Unknown channel type", nameof(channel)); + } } /// Gets a packed value representing all the permissions in this ChannelPermissions. @@ -77,7 +80,7 @@ namespace Discord /// Creates a new ChannelPermissions with the provided packed value. public ChannelPermissions(ulong rawValue) { RawValue = rawValue; } - private ChannelPermissions(ulong initialValue, bool? createInstantInvite = null, bool? manageChannel = null, + private ChannelPermissions(ulong initialValue, bool? createInstantInvite = null, bool? manageChannel = null, bool? addReactions = null, bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, @@ -111,25 +114,26 @@ namespace Discord } /// Creates a new ChannelPermissions with the provided permissions. - public ChannelPermissions(bool createInstantInvite = false, bool manageChannel = false, + public ChannelPermissions(bool createInstantInvite = false, bool manageChannel = false, bool addReactions = false, bool readMessages = false, bool sendMessages = false, bool sendTTSMessages = false, bool manageMessages = false, bool embedLinks = false, bool attachFiles = false, bool readMessageHistory = false, bool mentionEveryone = false, bool useExternalEmojis = false, bool connect = false, bool speak = false, bool muteMembers = false, bool deafenMembers = false, bool moveMembers = false, bool useVoiceActivation = false, bool managePermissions = false, bool manageWebhooks = false) - : this(0, createInstantInvite, manageChannel, addReactions, readMessages, sendMessages, sendTTSMessages, manageMessages, - embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, - speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, managePermissions, manageWebhooks) { } + : this(0, createInstantInvite, manageChannel, addReactions, readMessages, sendMessages, sendTTSMessages, manageMessages, + embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, + speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, managePermissions, manageWebhooks) + { } /// Creates a new ChannelPermissions from this one, changing the provided non-null permissions. - public ChannelPermissions Modify(bool? createInstantInvite = null, bool? manageChannel = null, + public ChannelPermissions Modify(bool? createInstantInvite = null, bool? manageChannel = null, bool? addReactions = null, bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, bool useExternalEmojis = false, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, bool? moveMembers = null, bool? useVoiceActivation = null, bool? managePermissions = null, bool? manageWebhooks = null) => new ChannelPermissions(RawValue, createInstantInvite, manageChannel, addReactions, readMessages, sendMessages, sendTTSMessages, manageMessages, - embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, + embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, managePermissions, manageWebhooks); public bool Has(ChannelPermission permission) => Permissions.GetValue(RawValue, permission); diff --git a/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs index e7461915c..c5f1efab0 100644 --- a/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; namespace Discord @@ -9,9 +8,10 @@ namespace Discord { /// Gets a blank GuildPermissions that grants no permissions. public static readonly GuildPermissions None = new GuildPermissions(); - /// Gets a GuildPermissions that grants all permissions. - //TODO: C#7 Candidate for binary literals - public static readonly GuildPermissions All = new GuildPermissions(Convert.ToUInt64("01111111111100111111110001111111", 2)); + /// Gets a GuildPermissions that grants all guild permissions for webhook users. + public static readonly GuildPermissions Webhook = new GuildPermissions(0b00000_0000000_0001101100000_000000); + /// Gets a GuildPermissions that grants all guild permissions. + public static readonly GuildPermissions All = new GuildPermissions(0b11111_1111110_0111111110001_111111); /// Gets a packed value representing all the permissions in this GuildPermissions. public ulong RawValue { get; } diff --git a/src/Discord.Net.Core/Entities/Roles/BulkRoleProperties.cs b/src/Discord.Net.Core/Entities/Roles/BulkRoleProperties.cs deleted file mode 100644 index eacb6689d..000000000 --- a/src/Discord.Net.Core/Entities/Roles/BulkRoleProperties.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Discord -{ - public class BulkRoleProperties : RoleProperties - { - /// - /// The id of the role to be edited - /// - public ulong Id { get; } - - public BulkRoleProperties(ulong id) - { - Id = id; - } - } -} diff --git a/src/Discord.Net.Core/Entities/Roles/Color.cs b/src/Discord.Net.Core/Entities/Roles/Color.cs index ead46fd8a..89e76df6d 100644 --- a/src/Discord.Net.Core/Entities/Roles/Color.cs +++ b/src/Discord.Net.Core/Entities/Roles/Color.cs @@ -8,6 +8,46 @@ namespace Discord { /// Gets the default user color value. public static readonly Color Default = new Color(0); + /// Gets the teal color value + public static readonly Color Teal = new Color(0x1ABC9C); + /// Gets the dark teal color value + public static readonly Color DarkTeal = new Color(0x11806A); + /// Gets the green color value + public static readonly Color Green = new Color(0x2ECC71); + /// Gets the dark green color value + public static readonly Color DarkGreen = new Color(0x1F8B4C); + /// Gets the blue color value + public static readonly Color Blue = new Color(0x3498DB); + /// Gets the dark blue color value + public static readonly Color DarkBlue = new Color(0x206694); + /// Gets the purple color value + public static readonly Color Purple = new Color(0x9B59B6); + /// Gets the dark purple color value + public static readonly Color DarkPurple = new Color(0x71368A); + /// Gets the magenta color value + public static readonly Color Magenta = new Color(0xE91E63); + /// Gets the dark magenta color value + public static readonly Color DarkMagenta = new Color(0xAD1457); + /// Gets the gold color value + public static readonly Color Gold = new Color(0xF1C40F); + /// Gets the light orange color value + public static readonly Color LightOrange = new Color(0xC27C0E); + /// Gets the orange color value + public static readonly Color Orange = new Color(0xE67E22); + /// Gets the dark orange color value + public static readonly Color DarkOrange = new Color(0xA84300); + /// Gets the red color value + public static readonly Color Red = new Color(0xE74C3C); + /// Gets the dark red color value + public static readonly Color DarkRed = new Color(0x992D22); + /// Gets the light grey color value + public static readonly Color LightGrey = new Color(0x979C9F); + /// Gets the lighter grey color value + public static readonly Color LighterGrey = new Color(0x95A5A6); + /// Gets the dark grey color value + public static readonly Color DarkGrey = new Color(0x607D8B); + /// Gets the darker grey color value + public static readonly Color DarkerGrey = new Color(0x546E7A); /// Gets the encoded value for this color. public uint RawValue { get; } @@ -28,16 +68,29 @@ namespace Discord RawValue = ((uint)r << 16) | ((uint)g << 8) | - b; + (uint)b; + } + public Color(int r, int g, int b) + { + if (r < 0 || r > 255) + throw new ArgumentOutOfRangeException(nameof(r), "Value must be within [0,255]"); + if (g < 0 || g > 255) + throw new ArgumentOutOfRangeException(nameof(g), "Value must be within [0,255]"); + if (b < 0 || b > 255) + throw new ArgumentOutOfRangeException(nameof(b), "Value must be within [0,255]"); + RawValue = + ((uint)r << 16) | + ((uint)g << 8) | + (uint)b; } public Color(float r, float g, float b) { if (r < 0.0f || r > 1.0f) - throw new ArgumentOutOfRangeException(nameof(r), "A float value must be within [0,1]"); + throw new ArgumentOutOfRangeException(nameof(r), "Value must be within [0,1]"); if (g < 0.0f || g > 1.0f) - throw new ArgumentOutOfRangeException(nameof(g), "A float value must be within [0,1]"); + throw new ArgumentOutOfRangeException(nameof(g), "Value must be within [0,1]"); if (b < 0.0f || b > 1.0f) - throw new ArgumentOutOfRangeException(nameof(b), "A float value must be within [0,1]"); + throw new ArgumentOutOfRangeException(nameof(b), "Value must be within [0,1]"); RawValue = ((uint)(r * 255.0f) << 16) | ((uint)(g * 255.0f) << 8) | diff --git a/src/Discord.Net.Core/Entities/Roles/ReorderRoleProperties.cs b/src/Discord.Net.Core/Entities/Roles/ReorderRoleProperties.cs new file mode 100644 index 000000000..0c8afa24c --- /dev/null +++ b/src/Discord.Net.Core/Entities/Roles/ReorderRoleProperties.cs @@ -0,0 +1,16 @@ +namespace Discord +{ + public class ReorderRoleProperties + { + /// The id of the role to be edited + public ulong Id { get; } + /// The new zero-based position of the role. + public int Position { get; } + + public ReorderRoleProperties(ulong id, int pos) + { + Id = id; + Position = pos; + } + } +} diff --git a/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs b/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs index 5ceffef0e..33b311604 100644 --- a/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs +++ b/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs @@ -41,16 +41,16 @@ namespace Discord /// What roles should the user have? /// /// - /// To add a role to a user: - /// To remove a role from a user: + /// To add a role to a user: + /// To remove a role from a user: /// public Optional> Roles { get; set; } /// /// What roles should the user have? /// /// - /// To add a role to a user: - /// To remove a role from a user: + /// To add a role to a user: + /// To remove a role from a user: /// public Optional> RoleIds { get; set; } /// diff --git a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs index 79e8f5dcc..57cad1333 100644 --- a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs @@ -25,8 +25,17 @@ namespace Discord ChannelPermissions GetPermissions(IGuildChannel channel); /// Kicks this user from this guild. - Task KickAsync(RequestOptions options = null); + Task KickAsync(string reason = null, RequestOptions options = null); /// Modifies this user's properties in this guild. Task ModifyAsync(Action func, RequestOptions options = null); + + /// Adds a role to this user in this guild. + Task AddRoleAsync(IRole role, RequestOptions options = null); + /// Adds roles to this user in this guild. + Task AddRolesAsync(IEnumerable roles, RequestOptions options = null); + /// Removes a role from this user in this guild. + Task RemoveRoleAsync(IRole role, RequestOptions options = null); + /// Removes roles from this user in this guild. + Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Users/IUser.cs b/src/Discord.Net.Core/Entities/Users/IUser.cs index aeb65aa9c..3f8f33012 100644 --- a/src/Discord.Net.Core/Entities/Users/IUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IUser.cs @@ -7,13 +7,15 @@ namespace Discord /// Gets the id of this user's avatar. string AvatarId { get; } /// Gets the url to this user's avatar. - string GetAvatarUrl(AvatarFormat format = AvatarFormat.Png, ushort size = 128); + string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128); /// Gets the per-username unique id for this user. string Discriminator { get; } /// Gets the per-username unique id for this user. ushort DiscriminatorValue { get; } - /// Returns true if this user is a bot account. + /// Returns true if this user is a bot user. bool IsBot { get; } + /// Returns true if this user is a webhook user. + bool IsWebhook { get; } /// Gets the username for this user. string Username { get; } @@ -28,5 +30,6 @@ namespace Discord Task BlockUserAsync(RequestOptions options = null); /// Removes the relationship of this user Task RemoveRelationshipAsync(RequestOptions options = null); + Task GetOrCreateDMChannelAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs b/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs new file mode 100644 index 000000000..8f4d42187 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs @@ -0,0 +1,8 @@ +namespace Discord +{ + //TODO: Add webhook endpoints + public interface IWebhookUser : IGuildUser + { + ulong WebhookId { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Users/UserStatus.cs b/src/Discord.Net.Core/Entities/Users/UserStatus.cs index d183c139d..74a52a0fa 100644 --- a/src/Discord.Net.Core/Entities/Users/UserStatus.cs +++ b/src/Discord.Net.Core/Entities/Users/UserStatus.cs @@ -2,12 +2,11 @@ { public enum UserStatus { - Unknown, + Offline, Online, Idle, AFK, DoNotDisturb, Invisible, - Offline } } diff --git a/src/Discord.Net.Core/Extensions/GuildUserExtensions.cs b/src/Discord.Net.Core/Extensions/GuildUserExtensions.cs deleted file mode 100644 index 9d152adf9..000000000 --- a/src/Discord.Net.Core/Extensions/GuildUserExtensions.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Discord -{ - public static class GuildUserExtensions - { - public static Task AddRolesAsync(this IGuildUser user, params IRole[] roles) - => ChangeRolesAsync(user, add: roles); - public static Task AddRolesAsync(this IGuildUser user, IEnumerable roles) - => ChangeRolesAsync(user, add: roles); - public static Task RemoveRolesAsync(this IGuildUser user, params IRole[] roles) - => ChangeRolesAsync(user, remove: roles); - public static Task RemoveRolesAsync(this IGuildUser user, IEnumerable roles) - => ChangeRolesAsync(user, remove: roles); - public static async Task ChangeRolesAsync(this IGuildUser user, IEnumerable add = null, IEnumerable remove = null) - { - IEnumerable roleIds = user.RoleIds; - if (remove != null) - roleIds = roleIds.Except(remove.Select(x => x.Id)); - if (add != null) - roleIds = roleIds.Concat(add.Select(x => x.Id)); - await user.ModifyAsync(x => x.RoleIds = roleIds.ToArray()).ConfigureAwait(false); - } - } -} diff --git a/src/Discord.Net.Core/Extensions/StringExtensions.cs b/src/Discord.Net.Core/Extensions/StringExtensions.cs new file mode 100644 index 000000000..c0ebb2626 --- /dev/null +++ b/src/Discord.Net.Core/Extensions/StringExtensions.cs @@ -0,0 +1,10 @@ +using System; + +namespace Discord +{ + internal static class StringExtensions + { + public static bool IsNullOrUri(this string url) => + string.IsNullOrEmpty(url) || Uri.IsWellFormedUriString(url, UriKind.Absolute); + } +} diff --git a/src/Discord.Net.Core/Extensions/UserExtensions.cs b/src/Discord.Net.Core/Extensions/UserExtensions.cs new file mode 100644 index 000000000..0861ed33e --- /dev/null +++ b/src/Discord.Net.Core/Extensions/UserExtensions.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; + +namespace Discord +{ + public static class UserExtensions + { + public static async Task SendMessageAsync(this IUser user, + string text, + bool isTTS = false, + Embed embed = null, + RequestOptions options = null) + { + return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + } + } +} diff --git a/src/Discord.Net.Core/IDiscordClient.cs b/src/Discord.Net.Core/IDiscordClient.cs index 1d3bf3df7..23e8e9c5b 100644 --- a/src/Discord.Net.Core/IDiscordClient.cs +++ b/src/Discord.Net.Core/IDiscordClient.cs @@ -9,31 +9,30 @@ namespace Discord { ConnectionState ConnectionState { get; } ISelfUser CurrentUser { get; } + TokenType TokenType { get; } Task StartAsync(); Task StopAsync(); - Task GetApplicationInfoAsync(); + Task GetApplicationInfoAsync(RequestOptions options = null); - Task GetChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload); - Task> GetPrivateChannelsAsync(CacheMode mode = CacheMode.AllowDownload); - Task> GetDMChannelsAsync(CacheMode mode = CacheMode.AllowDownload); - Task> GetGroupChannelsAsync(CacheMode mode = CacheMode.AllowDownload); + Task GetChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task> GetPrivateChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task> GetDMChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task> GetGroupChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - Task> GetConnectionsAsync(); + Task> GetConnectionsAsync(RequestOptions options = null); - Task GetGuildAsync(ulong id, CacheMode mode = CacheMode.AllowDownload); - Task> GetGuildsAsync(CacheMode mode = CacheMode.AllowDownload); - Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null); + Task GetGuildAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task> GetGuildsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null, RequestOptions options = null); + + Task GetInviteAsync(string inviteId, RequestOptions options = null); - Task GetInviteAsync(string inviteId); + Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task GetUserAsync(string username, string discriminator, RequestOptions options = null); - Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload); - Task GetUserAsync(string username, string discriminator); - - Task> GetRelationshipsAsync(); - - Task> GetVoiceRegionsAsync(); - Task GetVoiceRegionAsync(string id); + Task> GetVoiceRegionsAsync(RequestOptions options = null); + Task GetVoiceRegionAsync(string id, RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Logging/LogManager.cs b/src/Discord.Net.Core/Logging/LogManager.cs index 21f956b99..995a5d96a 100644 --- a/src/Discord.Net.Core/Logging/LogManager.cs +++ b/src/Discord.Net.Core/Logging/LogManager.cs @@ -19,19 +19,31 @@ namespace Discord.Logging public async Task LogAsync(LogSeverity severity, string source, Exception ex) { - if (severity <= Level) - await _messageEvent.InvokeAsync(new LogMessage(severity, source, null, ex)).ConfigureAwait(false); + try + { + if (severity <= Level) + await _messageEvent.InvokeAsync(new LogMessage(severity, source, null, ex)).ConfigureAwait(false); + } + catch { } } public async Task LogAsync(LogSeverity severity, string source, string message, Exception ex = null) { - if (severity <= Level) + try + { + if (severity <= Level) await _messageEvent.InvokeAsync(new LogMessage(severity, source, message, ex)).ConfigureAwait(false); + } + catch { } } -#if NETSTANDARD1_3 +#if FORMATSTR public async Task LogAsync(LogSeverity severity, string source, FormattableString message, Exception ex = null) { - if (severity <= Level) - await _messageEvent.InvokeAsync(new LogMessage(severity, source, message.ToString(), ex)).ConfigureAwait(false); + try + { + if (severity <= Level) + await _messageEvent.InvokeAsync(new LogMessage(severity, source, message.ToString(), ex)).ConfigureAwait(false); + } + catch { } } #endif @@ -39,7 +51,7 @@ namespace Discord.Logging => LogAsync(LogSeverity.Error, source, ex); public Task ErrorAsync(string source, string message, Exception ex = null) => LogAsync(LogSeverity.Error, source, message, ex); -#if NETSTANDARD1_3 +#if FORMATSTR public Task ErrorAsync(string source, FormattableString message, Exception ex = null) => LogAsync(LogSeverity.Error, source, message, ex); #endif @@ -48,7 +60,7 @@ namespace Discord.Logging => LogAsync(LogSeverity.Warning, source, ex); public Task WarningAsync(string source, string message, Exception ex = null) => LogAsync(LogSeverity.Warning, source, message, ex); -#if NETSTANDARD1_3 +#if FORMATSTR public Task WarningAsync(string source, FormattableString message, Exception ex = null) => LogAsync(LogSeverity.Warning, source, message, ex); #endif @@ -57,7 +69,7 @@ namespace Discord.Logging => LogAsync(LogSeverity.Info, source, ex); public Task InfoAsync(string source, string message, Exception ex = null) => LogAsync(LogSeverity.Info, source, message, ex); -#if NETSTANDARD1_3 +#if FORMATSTR public Task InfoAsync(string source, FormattableString message, Exception ex = null) => LogAsync(LogSeverity.Info, source, message, ex); #endif @@ -66,7 +78,7 @@ namespace Discord.Logging => LogAsync(LogSeverity.Verbose, source, ex); public Task VerboseAsync(string source, string message, Exception ex = null) => LogAsync(LogSeverity.Verbose, source, message, ex); -#if NETSTANDARD1_3 +#if FORMATSTR public Task VerboseAsync(string source, FormattableString message, Exception ex = null) => LogAsync(LogSeverity.Verbose, source, message, ex); #endif @@ -75,7 +87,7 @@ namespace Discord.Logging => LogAsync(LogSeverity.Debug, source, ex); public Task DebugAsync(string source, string message, Exception ex = null) => LogAsync(LogSeverity.Debug, source, message, ex); -#if NETSTANDARD1_3 +#if FORMATSTR public Task DebugAsync(string source, FormattableString message, Exception ex = null) => LogAsync(LogSeverity.Debug, source, message, ex); #endif diff --git a/src/Discord.Net.Core/Logging/Logger.cs b/src/Discord.Net.Core/Logging/Logger.cs index cff69a84c..a8d88b2b4 100644 --- a/src/Discord.Net.Core/Logging/Logger.cs +++ b/src/Discord.Net.Core/Logging/Logger.cs @@ -20,7 +20,7 @@ namespace Discord.Logging => _manager.LogAsync(severity, Name, exception); public Task LogAsync(LogSeverity severity, string message, Exception exception = null) => _manager.LogAsync(severity, Name, message, exception); -#if NETSTANDARD1_3 +#if FORMATSTR public Task LogAsync(LogSeverity severity, FormattableString message, Exception exception = null) => _manager.LogAsync(severity, Name, message, exception); #endif @@ -29,7 +29,7 @@ namespace Discord.Logging => _manager.ErrorAsync(Name, exception); public Task ErrorAsync(string message, Exception exception = null) => _manager.ErrorAsync(Name, message, exception); -#if NETSTANDARD1_3 +#if FORMATSTR public Task ErrorAsync(FormattableString message, Exception exception = null) => _manager.ErrorAsync(Name, message, exception); #endif @@ -38,7 +38,7 @@ namespace Discord.Logging => _manager.WarningAsync(Name, exception); public Task WarningAsync(string message, Exception exception = null) => _manager.WarningAsync(Name, message, exception); -#if NETSTANDARD1_3 +#if FORMATSTR public Task WarningAsync(FormattableString message, Exception exception = null) => _manager.WarningAsync(Name, message, exception); #endif @@ -47,7 +47,7 @@ namespace Discord.Logging => _manager.InfoAsync(Name, exception); public Task InfoAsync(string message, Exception exception = null) => _manager.InfoAsync(Name, message, exception); -#if NETSTANDARD1_3 +#if FORMATSTR public Task InfoAsync(FormattableString message, Exception exception = null) => _manager.InfoAsync(Name, message, exception); #endif @@ -56,7 +56,7 @@ namespace Discord.Logging => _manager.VerboseAsync(Name, exception); public Task VerboseAsync(string message, Exception exception = null) => _manager.VerboseAsync(Name, message, exception); -#if NETSTANDARD1_3 +#if FORMATSTR public Task VerboseAsync(FormattableString message, Exception exception = null) => _manager.VerboseAsync(Name, message, exception); #endif @@ -65,7 +65,7 @@ namespace Discord.Logging => _manager.DebugAsync(Name, exception); public Task DebugAsync(string message, Exception exception = null) => _manager.DebugAsync(Name, message, exception); -#if NETSTANDARD1_3 +#if FORMATSTR public Task DebugAsync(FormattableString message, Exception exception = null) => _manager.DebugAsync(Name, message, exception); #endif diff --git a/src/Discord.Net.Core/Net/HttpException.cs b/src/Discord.Net.Core/Net/HttpException.cs index 4141979a0..1c872245c 100644 --- a/src/Discord.Net.Core/Net/HttpException.cs +++ b/src/Discord.Net.Core/Net/HttpException.cs @@ -20,7 +20,7 @@ namespace Discord.Net private static string CreateMessage(HttpStatusCode httpCode, int? discordCode = null, string reason = null) { string msg; - if (discordCode != null) + if (discordCode != null && discordCode != 0) { if (reason != null) msg = $"The server responded with error {(int)discordCode}: {reason}"; diff --git a/src/Discord.Net.Core/Net/Rest/IRestClient.cs b/src/Discord.Net.Core/Net/Rest/IRestClient.cs index b5f136cb0..addfa9061 100644 --- a/src/Discord.Net.Core/Net/Rest/IRestClient.cs +++ b/src/Discord.Net.Core/Net/Rest/IRestClient.cs @@ -9,8 +9,8 @@ namespace Discord.Net.Rest void SetHeader(string key, string value); void SetCancelToken(CancellationToken cancelToken); - Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly = false); - Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly = false); - Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly = false); + Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly = false, string reason = null); + Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly = false, string reason = null); + Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly = false, string reason = null); } } diff --git a/src/Discord.Net.Core/Net/Udp/IUdpSocket.cs b/src/Discord.Net.Core/Net/Udp/IUdpSocket.cs index 8da948d1a..10ac652b3 100644 --- a/src/Discord.Net.Core/Net/Udp/IUdpSocket.cs +++ b/src/Discord.Net.Core/Net/Udp/IUdpSocket.cs @@ -8,8 +8,10 @@ namespace Discord.Net.Udp { event Func ReceivedDatagram; + ushort Port { get; } + void SetCancelToken(CancellationToken cancelToken); - void SetDestination(string host, int port); + void SetDestination(string ip, int port); Task StartAsync(); Task StopAsync(); diff --git a/src/Discord.Net.Core/RequestOptions.cs b/src/Discord.Net.Core/RequestOptions.cs index 4f5910c53..5f3a8814b 100644 --- a/src/Discord.Net.Core/RequestOptions.cs +++ b/src/Discord.Net.Core/RequestOptions.cs @@ -14,6 +14,10 @@ namespace Discord public CancellationToken CancelToken { get; set; } = CancellationToken.None; public RetryMode? RetryMode { get; set; } public bool HeaderOnly { get; internal set; } + /// + /// The reason for this action in the guild's audit log + /// + public string AuditLogReason { get; set; } internal bool IgnoreState { get; set; } internal string BucketId { get; set; } diff --git a/src/Discord.Net.Core/TokenType.cs b/src/Discord.Net.Core/TokenType.cs index 519f4bf0b..e19197cd6 100644 --- a/src/Discord.Net.Core/TokenType.cs +++ b/src/Discord.Net.Core/TokenType.cs @@ -5,5 +5,6 @@ User, Bearer, Bot, + Webhook } } diff --git a/src/Discord.Net.Core/Utils/AsyncEvent.cs b/src/Discord.Net.Core/Utils/AsyncEvent.cs index a7fdeddf2..731489dea 100644 --- a/src/Discord.Net.Core/Utils/AsyncEvent.cs +++ b/src/Discord.Net.Core/Utils/AsyncEvent.cs @@ -11,6 +11,7 @@ namespace Discord private readonly object _subLock = new object(); internal ImmutableArray _subscriptions; + public bool HasSubscribers => _subscriptions.Length != 0; public IReadOnlyList Subscriptions => _subscriptions; public AsyncEvent() @@ -37,11 +38,8 @@ namespace Discord public static async Task InvokeAsync(this AsyncEvent> eventHandler) { var subscribers = eventHandler.Subscriptions; - if (subscribers.Count > 0) - { - for (int i = 0; i < subscribers.Count; i++) - await subscribers[i].Invoke().ConfigureAwait(false); - } + for (int i = 0; i < subscribers.Count; i++) + await subscribers[i].Invoke().ConfigureAwait(false); } public static async Task InvokeAsync(this AsyncEvent> eventHandler, T arg) { diff --git a/src/Discord.Net.Core/Utils/Cacheable.cs b/src/Discord.Net.Core/Utils/Cacheable.cs index 10b61be90..f17aa8699 100644 --- a/src/Discord.Net.Core/Utils/Cacheable.cs +++ b/src/Discord.Net.Core/Utils/Cacheable.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Threading.Tasks; namespace Discord diff --git a/src/Discord.Net.Core/Utils/ConcurrentHashSet.cs b/src/Discord.Net.Core/Utils/ConcurrentHashSet.cs index 1ef105527..1fc11587e 100644 --- a/src/Discord.Net.Core/Utils/ConcurrentHashSet.cs +++ b/src/Discord.Net.Core/Utils/ConcurrentHashSet.cs @@ -239,10 +239,8 @@ namespace Discord { while (true) { - int bucketNo, lockNo; - Tables tables = _tables; - GetBucketAndLockNo(hashcode, out bucketNo, out lockNo, tables._buckets.Length, tables._locks.Length); + GetBucketAndLockNo(hashcode, out int bucketNo, out int lockNo, tables._buckets.Length, tables._locks.Length); bool resizeDesired = false; bool lockTaken = false; @@ -292,9 +290,7 @@ namespace Discord while (true) { Tables tables = _tables; - - int bucketNo, lockNo; - GetBucketAndLockNo(hashcode, out bucketNo, out lockNo, tables._buckets.Length, tables._locks.Length); + GetBucketAndLockNo(hashcode, out int bucketNo, out int lockNo, tables._buckets.Length, tables._locks.Length); lock (tables._locks[lockNo]) { @@ -426,8 +422,7 @@ namespace Discord while (current != null) { Node next = current._next; - int newBucketNo, newLockNo; - GetBucketAndLockNo(current._hashcode, out newBucketNo, out newLockNo, newBuckets.Length, newLocks.Length); + GetBucketAndLockNo(current._hashcode, out int newBucketNo, out int newLockNo, newBuckets.Length, newLocks.Length); newBuckets[newBucketNo] = new Node(current._value, current._hashcode, newBuckets[newBucketNo]); diff --git a/src/Discord.Net.Core/Utils/DateTimeUtils.cs b/src/Discord.Net.Core/Utils/DateTimeUtils.cs index fc9ef4b7b..af2126853 100644 --- a/src/Discord.Net.Core/Utils/DateTimeUtils.cs +++ b/src/Discord.Net.Core/Utils/DateTimeUtils.cs @@ -2,19 +2,15 @@ namespace Discord { + //Source: https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/DateTimeOffset.cs internal static class DateTimeUtils { -#if !NETSTANDARD1_3 - //https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/DateTimeOffset.cs - private const long UnixEpochTicks = 621355968000000000; - private const long UnixEpochSeconds = 62135596800; +#if !UNIXTIME + private const long UnixEpochTicks = 621_355_968_000_000_000; + private const long UnixEpochSeconds = 62_135_596_800; + private const long UnixEpochMilliseconds = 62_135_596_800_000; #endif - public static DateTimeOffset FromSnowflake(ulong value) - => FromUnixMilliseconds((long)((value >> 22) + 1420070400000UL)); - public static ulong ToSnowflake(DateTimeOffset value) - => (ulong)(ToUnixMilliseconds(value) - 1420070400000L) << 22; - public static DateTimeOffset FromTicks(long ticks) => new DateTimeOffset(ticks, TimeSpan.Zero); public static DateTimeOffset? FromTicks(long? ticks) @@ -22,26 +18,26 @@ namespace Discord public static DateTimeOffset FromUnixSeconds(long seconds) { -#if NETSTANDARD1_3 +#if UNIXTIME return DateTimeOffset.FromUnixTimeSeconds(seconds); #else long ticks = seconds * TimeSpan.TicksPerSecond + UnixEpochTicks; return new DateTimeOffset(ticks, TimeSpan.Zero); #endif } - public static DateTimeOffset FromUnixMilliseconds(long seconds) + public static DateTimeOffset FromUnixMilliseconds(long milliseconds) { -#if NETSTANDARD1_3 - return DateTimeOffset.FromUnixTimeMilliseconds(seconds); +#if UNIXTIME + return DateTimeOffset.FromUnixTimeMilliseconds(milliseconds); #else - long ticks = seconds * TimeSpan.TicksPerMillisecond + UnixEpochTicks; + long ticks = milliseconds * TimeSpan.TicksPerMillisecond + UnixEpochTicks; return new DateTimeOffset(ticks, TimeSpan.Zero); #endif } public static long ToUnixSeconds(DateTimeOffset dto) { -#if NETSTANDARD1_3 +#if UNIXTIME return dto.ToUnixTimeSeconds(); #else long seconds = dto.UtcDateTime.Ticks / TimeSpan.TicksPerSecond; @@ -50,11 +46,11 @@ namespace Discord } public static long ToUnixMilliseconds(DateTimeOffset dto) { -#if NETSTANDARD1_3 +#if UNIXTIME return dto.ToUnixTimeMilliseconds(); #else - long seconds = dto.UtcDateTime.Ticks / TimeSpan.TicksPerMillisecond; - return seconds - UnixEpochSeconds; + long milliseconds = dto.UtcDateTime.Ticks / TimeSpan.TicksPerMillisecond; + return milliseconds - UnixEpochMilliseconds; #endif } } diff --git a/src/Discord.Net.Core/Utils/MentionUtils.cs b/src/Discord.Net.Core/Utils/MentionUtils.cs index 4d9add8fd..6c69827b4 100644 --- a/src/Discord.Net.Core/Utils/MentionUtils.cs +++ b/src/Discord.Net.Core/Utils/MentionUtils.cs @@ -19,8 +19,7 @@ namespace Discord /// Parses a provided user mention string. public static ulong ParseUser(string text) { - ulong id; - if (TryParseUser(text, out id)) + if (TryParseUser(text, out ulong id)) return id; throw new ArgumentException("Invalid mention format", nameof(text)); } @@ -44,8 +43,7 @@ namespace Discord /// Parses a provided channel mention string. public static ulong ParseChannel(string text) { - ulong id; - if (TryParseChannel(text, out id)) + if (TryParseChannel(text, out ulong id)) return id; throw new ArgumentException("Invalid mention format", nameof(text)); } @@ -66,8 +64,7 @@ namespace Discord /// Parses a provided role mention string. public static ulong ParseRole(string text) { - ulong id; - if (TryParseRole(text, out id)) + if (TryParseRole(text, out ulong id)) return id; throw new ArgumentException("Invalid mention format", nameof(text)); } @@ -255,7 +252,16 @@ namespace Discord { if (mode != TagHandling.Remove) { - Emoji emoji = (Emoji)tag.Value; + Emote emoji = (Emote)tag.Value; + + //Remove if its name contains any bad chars (prevents a few tag exploits) + for (int i = 0; i < emoji.Name.Length; i++) + { + char c = emoji.Name[i]; + if (!char.IsLetterOrDigit(c) && c != '_' && c != '-') + return ""; + } + switch (mode) { case TagHandling.Name: diff --git a/src/Discord.Net.Core/Utils/Optional.cs b/src/Discord.Net.Core/Utils/Optional.cs index e2d55cf7f..df927b7ea 100644 --- a/src/Discord.Net.Core/Utils/Optional.cs +++ b/src/Discord.Net.Core/Utils/Optional.cs @@ -51,5 +51,9 @@ namespace Discord { public static Optional Create() => Optional.Unspecified; public static Optional Create(T value) => new Optional(value); + + public static T? ToNullable(this Optional val) + where T : struct + => val.IsSpecified ? val.Value : (T?)null; } } diff --git a/src/Discord.Net.Core/Utils/Permissions.cs b/src/Discord.Net.Core/Utils/Permissions.cs index a6c545da0..c2b7e83ea 100644 --- a/src/Discord.Net.Core/Utils/Permissions.cs +++ b/src/Discord.Net.Core/Utils/Permissions.cs @@ -86,12 +86,16 @@ namespace Discord [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void UnsetBit(ref ulong value, byte bit) => value &= ~(1U << bit); + public static ChannelPermissions ToChannelPerms(IGuildChannel channel, ulong guildPermissions) + => new ChannelPermissions(guildPermissions & ChannelPermissions.All(channel).RawValue); public static ulong ResolveGuild(IGuild guild, IGuildUser user) { ulong resolvedPermissions = 0; if (user.Id == guild.OwnerId) resolvedPermissions = GuildPermissions.All.RawValue; //Owners always have all permissions + else if (user.IsWebhook) + resolvedPermissions = GuildPermissions.Webhook.RawValue; else { foreach (var roleId in user.RoleIds) @@ -111,20 +115,25 @@ namespace Discord ulong resolvedPermissions = 0; ulong mask = ChannelPermissions.All(channel).RawValue; - if (/*user.Id == user.Guild.OwnerId || */GetValue(guildPermissions, GuildPermission.Administrator)) + if (GetValue(guildPermissions, GuildPermission.Administrator)) //Includes owner resolvedPermissions = mask; //Owners and administrators always have all permissions else { + OverwritePermissions? perms; + //Start with this user's guild permissions resolvedPermissions = guildPermissions; + //Give/Take Everyone permissions + perms = channel.GetPermissionOverwrite(guild.EveryoneRole); + if (perms != null) + resolvedPermissions = (resolvedPermissions & ~perms.Value.DenyValue) | perms.Value.AllowValue; + //Give/Take Role permissions - OverwritePermissions? perms; - var roleIds = user.RoleIds; - if (roleIds.Count > 0) + ulong deniedPermissions = 0UL, allowedPermissions = 0UL; + foreach (var roleId in user.RoleIds) { - ulong deniedPermissions = 0UL, allowedPermissions = 0UL; - foreach (var roleId in roleIds) + if (roleId != guild.EveryoneRole.Id) { perms = channel.GetPermissionOverwrite(guild.GetRole(roleId)); if (perms != null) @@ -133,21 +142,30 @@ namespace Discord deniedPermissions |= perms.Value.DenyValue; } } - resolvedPermissions = (resolvedPermissions | allowedPermissions) & ~deniedPermissions; } + resolvedPermissions = (resolvedPermissions & ~deniedPermissions) | allowedPermissions; //Give/Take User permissions perms = channel.GetPermissionOverwrite(user); if (perms != null) - resolvedPermissions = (resolvedPermissions | perms.Value.AllowValue) & ~perms.Value.DenyValue; - - //TODO: C#7 Typeswitch candidate - var textChannel = channel as ITextChannel; - var voiceChannel = channel as IVoiceChannel; - if (textChannel != null && !GetValue(resolvedPermissions, ChannelPermission.ReadMessages)) - resolvedPermissions = 0; //No read permission on a text channel removes all other permissions - else if (voiceChannel != null && !GetValue(resolvedPermissions, ChannelPermission.Connect)) - resolvedPermissions = 0; //No connect permission on a voice channel removes all other permissions + resolvedPermissions = (resolvedPermissions & ~perms.Value.DenyValue) | perms.Value.AllowValue; + + if (channel is ITextChannel textChannel) + { + if (!GetValue(resolvedPermissions, ChannelPermission.ReadMessages)) + { + //No read permission on a text channel removes all other permissions + resolvedPermissions = 0; + } + else if (!GetValue(resolvedPermissions, ChannelPermission.SendMessages)) + { + //No send permissions on a text channel removes all send-related permissions + resolvedPermissions &= ~(1UL << (int)ChannelPermission.SendTTSMessages); + resolvedPermissions &= ~(1UL << (int)ChannelPermission.MentionEveryone); + resolvedPermissions &= ~(1UL << (int)ChannelPermission.EmbedLinks); + resolvedPermissions &= ~(1UL << (int)ChannelPermission.AttachFiles); + } + } resolvedPermissions &= mask; //Ensure we didnt get any permissions this channel doesnt support (from guildPerms, for example) } diff --git a/src/Discord.Net.Core/Utils/Preconditions.cs b/src/Discord.Net.Core/Utils/Preconditions.cs index 65af6e49b..705a15249 100644 --- a/src/Discord.Net.Core/Utils/Preconditions.cs +++ b/src/Discord.Net.Core/Utils/Preconditions.cs @@ -46,7 +46,7 @@ namespace Discord private static ArgumentException CreateNotEmptyException(string name, string msg) { - if (msg == null) return new ArgumentException(name, "Argument cannot be blank."); + if (msg == null) return new ArgumentException("Argument cannot be blank", name); else return new ArgumentException(name, msg); } @@ -185,9 +185,12 @@ namespace Discord // Bulk Delete public static void YoungerThanTwoWeeks(ulong[] collection, string name) { - var minimum = DateTimeUtils.ToSnowflake(DateTimeOffset.Now.Subtract(TimeSpan.FromMilliseconds(1209540000))); + var minimum = SnowflakeUtils.ToSnowflake(DateTimeOffset.UtcNow.Subtract(TimeSpan.FromDays(14))); for (var i = 0; i < collection.Length; i++) - if (collection[i] <= minimum) throw new ArgumentOutOfRangeException(name, "Messages must be younger than two weeks to delete."); + { + if (collection[i] <= minimum) + throw new ArgumentOutOfRangeException(name, "Messages must be younger than two weeks old."); + } } } } diff --git a/src/Discord.Net.Core/Utils/SnowflakeUtils.cs b/src/Discord.Net.Core/Utils/SnowflakeUtils.cs new file mode 100644 index 000000000..c9d0d130b --- /dev/null +++ b/src/Discord.Net.Core/Utils/SnowflakeUtils.cs @@ -0,0 +1,12 @@ +using System; + +namespace Discord +{ + public static class SnowflakeUtils + { + public static DateTimeOffset FromSnowflake(ulong value) + => DateTimeUtils.FromUnixMilliseconds((long)((value >> 22) + 1420070400000UL)); + public static ulong ToSnowflake(DateTimeOffset value) + => ((ulong)DateTimeUtils.ToUnixMilliseconds(value) - 1420070400000UL) << 22; + } +} diff --git a/src/Discord.Net.DebugTools/Discord.Net.DebugTools.csproj b/src/Discord.Net.DebugTools/Discord.Net.DebugTools.csproj index 829951d19..43d627c99 100644 --- a/src/Discord.Net.DebugTools/Discord.Net.DebugTools.csproj +++ b/src/Discord.Net.DebugTools/Discord.Net.DebugTools.csproj @@ -1,31 +1,12 @@ - + + - 1.0.0 - rc-dev - rc-$(BuildNumber) - netstandard1.6 Discord.Net.DebugTools - RogueException - A Discord.Net extension adding some helper classes for diagnosing issues. - discord;discordapp - https://github.com/RogueException/Discord.Net - http://opensource.org/licenses/MIT - git - git://github.com/RogueException/Discord.Net Discord - true + A Discord.Net extension adding some helper classes for diagnosing issues. + net45;netstandard1.3 - - - - - - - $(NoWarn);CS1573;CS1591 - true - true - \ No newline at end of file diff --git a/src/Discord.Net.DebugTools/UnstableRestClient.cs b/src/Discord.Net.DebugTools/UnstableRestClient.cs new file mode 100644 index 000000000..847b8d64e --- /dev/null +++ b/src/Discord.Net.DebugTools/UnstableRestClient.cs @@ -0,0 +1,154 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.Rest +{ + internal sealed class UnstableRestClient : IRestClient, IDisposable + { + private const double FailureRate = 0.10; //10% + + private const int HR_SECURECHANNELFAILED = -2146233079; + + private readonly HttpClient _client; + private readonly string _baseUrl; + private readonly JsonSerializer _errorDeserializer; + private readonly Random _rand; + private CancellationToken _cancelToken; + private bool _isDisposed; + + public DefaultRestClient(string baseUrl) + { + _baseUrl = baseUrl; + + _client = new HttpClient(new HttpClientHandler + { + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, + UseCookies = false, + UseProxy = false + }); + SetHeader("accept-encoding", "gzip, deflate"); + + _cancelToken = CancellationToken.None; + _errorDeserializer = new JsonSerializer(); + _rand = new Random(); + } + private void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + _client.Dispose(); + _isDisposed = true; + } + } + public void Dispose() + { + Dispose(true); + } + + public void SetHeader(string key, string value) + { + _client.DefaultRequestHeaders.Remove(key); + if (value != null) + _client.DefaultRequestHeaders.Add(key, value); + } + public void SetCancelToken(CancellationToken cancelToken) + { + _cancelToken = cancelToken; + } + + public async Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly) + { + string uri = Path.Combine(_baseUrl, endpoint); + using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) + return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); + } + public async Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly) + { + string uri = Path.Combine(_baseUrl, endpoint); + using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) + { + restRequest.Content = new StringContent(json, Encoding.UTF8, "application/json"); + return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); + } + } + public async Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly) + { + string uri = Path.Combine(_baseUrl, endpoint); + using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) + { + var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture)); + if (multipartParams != null) + { + foreach (var p in multipartParams) + { + switch (p.Value) + { + case string stringValue: { content.Add(new StringContent(stringValue), p.Key); continue; } + case byte[] byteArrayValue: { content.Add(new ByteArrayContent(byteArrayValue), p.Key); continue; } + case Stream streamValue: { content.Add(new StreamContent(streamValue), p.Key); continue; } + case MultipartFile fileValue: + { + var stream = fileValue.Stream; + if (!stream.CanSeek) + { + var memoryStream = new MemoryStream(); + await stream.CopyToAsync(memoryStream).ConfigureAwait(false); + memoryStream.Position = 0; + stream = memoryStream; + } + content.Add(new StreamContent(stream), p.Key, fileValue.Filename); + continue; + } + default: throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\""); + } + } + } + restRequest.Content = content; + return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); + } + } + + private async Task SendInternalAsync(HttpRequestMessage request, CancellationToken cancelToken, bool headerOnly) + { + if (!UnstableCheck()) + throw new TimeoutException(); + + cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken, cancelToken).Token; + HttpResponseMessage response = await _client.SendAsync(request, cancelToken).ConfigureAwait(false); + + var headers = response.Headers.ToDictionary(x => x.Key, x => x.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase); + var stream = !headerOnly ? await response.Content.ReadAsStreamAsync().ConfigureAwait(false) : null; + + return new RestResponse(response.StatusCode, headers, stream); + } + + private static readonly HttpMethod _patch = new HttpMethod("PATCH"); + private HttpMethod GetMethod(string method) + { + switch (method) + { + case "DELETE": return HttpMethod.Delete; + case "GET": return HttpMethod.Get; + case "PATCH": return _patch; + case "POST": return HttpMethod.Post; + case "PUT": return HttpMethod.Put; + default: throw new ArgumentOutOfRangeException(nameof(method), $"Unknown HttpMethod: {method}"); + } + } + + private bool UnstableCheck() + { + return _rand.NextDouble() > FailureRate; + } + } +} diff --git a/src/Discord.Net.DebugTools/UnstableRestClientProvider.cs b/src/Discord.Net.DebugTools/UnstableRestClientProvider.cs new file mode 100644 index 000000000..80ed91c5b --- /dev/null +++ b/src/Discord.Net.DebugTools/UnstableRestClientProvider.cs @@ -0,0 +1,9 @@ +using Discord.Net.Rest; + +namespace Discord.Net.Providers.UnstableUdpSocket +{ + public static class UnstableRestClientProvider + { + public static readonly RestCientProvider Instance = () => new UnstableRestClientProvider(); + } +} diff --git a/src/Discord.Net.DebugTools/UnstableWebSocketClient.cs b/src/Discord.Net.DebugTools/UnstableWebSocketClient.cs index a0f28ba0a..4f45503c9 100644 --- a/src/Discord.Net.DebugTools/UnstableWebSocketClient.cs +++ b/src/Discord.Net.DebugTools/UnstableWebSocketClient.cs @@ -213,11 +213,14 @@ namespace Discord.Net.Providers.UnstableWebSocket //Use the internal buffer if we can get it resultCount = (int)stream.Length; - ArraySegment streamBuffer; - if (stream.TryGetBuffer(out streamBuffer)) +#if MSTRYBUFFER + if (stream.TryGetBuffer(out var streamBuffer)) result = streamBuffer.Array; else result = stream.ToArray(); +#else + result = stream.GetBuffer(); +#endif } } else diff --git a/src/Discord.Net.Providers.UdpClient/Discord.Net.Providers.UdpClient.csproj b/src/Discord.Net.Providers.UdpClient/Discord.Net.Providers.UdpClient.csproj deleted file mode 100644 index 984cd8f9c..000000000 --- a/src/Discord.Net.Providers.UdpClient/Discord.Net.Providers.UdpClient.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - 1.0.0 - rc-dev - rc-$(BuildNumber) - net45 - Discord.Net.Providers.UDPClient - RogueException - An optional UDP client provider for Discord.Net using System.Net.UdpClient - discord;discordapp - https://github.com/RogueException/Discord.Net - http://opensource.org/licenses/MIT - git - git://github.com/RogueException/Discord.Net - Discord.Providers.UDPClient - true - - - - - - $(NoWarn);CS1573;CS1591 - true - true - - \ No newline at end of file diff --git a/src/Discord.Net.Providers.UdpClient/UDPClient.cs b/src/Discord.Net.Providers.UdpClient/UDPClient.cs deleted file mode 100644 index 459feb335..000000000 --- a/src/Discord.Net.Providers.UdpClient/UDPClient.cs +++ /dev/null @@ -1,128 +0,0 @@ -using Discord.Net.Udp; -using System; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using UdpSocket = System.Net.Sockets.UdpClient; - -namespace Discord.Net.Providers.UDPClient -{ - internal class UDPClient : IUdpSocket, IDisposable - { - public event Func ReceivedDatagram; - - private readonly SemaphoreSlim _lock; - private UdpSocket _udp; - private IPEndPoint _destination; - private CancellationTokenSource _cancelTokenSource; - private CancellationToken _cancelToken, _parentToken; - private Task _task; - private bool _isDisposed; - - public UDPClient() - { - _lock = new SemaphoreSlim(1, 1); - } - private void Dispose(bool disposing) - { - if (!_isDisposed) - { - if (disposing) - StopInternalAsync(true).GetAwaiter().GetResult(); - _isDisposed = true; - } - } - public void Dispose() - { - Dispose(true); - } - - - public async Task StartAsync() - { - await _lock.WaitAsync().ConfigureAwait(false); - try - { - await StartInternalAsync(_cancelToken).ConfigureAwait(false); - } - finally - { - _lock.Release(); - } - } - public async Task StartInternalAsync(CancellationToken cancelToken) - { - await StopInternalAsync().ConfigureAwait(false); - - _cancelTokenSource = new CancellationTokenSource(); - _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; - - _udp = new UdpSocket(); - - _task = RunAsync(_cancelToken); - } - public async Task StopAsync() - { - await _lock.WaitAsync().ConfigureAwait(false); - try - { - await StopInternalAsync().ConfigureAwait(false); - } - finally - { - _lock.Release(); - } - } - public async Task StopInternalAsync(bool isDisposing = false) - { - try { _cancelTokenSource.Cancel(false); } catch { } - - if (!isDisposing) - await (_task ?? Task.Delay(0)).ConfigureAwait(false); - - if (_udp != null) - { - try { _udp.Close(); } - catch { } - _udp = null; - } - } - - public void SetDestination(string host, int port) - { - var entry = Dns.GetHostEntryAsync(host).GetAwaiter().GetResult(); - _destination = new IPEndPoint(entry.AddressList[0], port); - } - public void SetCancelToken(CancellationToken cancelToken) - { - _parentToken = cancelToken; - _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; - } - - public async Task SendAsync(byte[] data, int index, int count) - { - if (index != 0) //Should never happen? - { - var newData = new byte[count]; - Buffer.BlockCopy(data, index, newData, 0, count); - data = newData; - } - await _udp.SendAsync(data, count, _destination).ConfigureAwait(false); - } - - private async Task RunAsync(CancellationToken cancelToken) - { - var closeTask = Task.Delay(-1, cancelToken); - while (!cancelToken.IsCancellationRequested) - { - var receiveTask = _udp.ReceiveAsync(); - var task = await Task.WhenAny(closeTask, receiveTask).ConfigureAwait(false); - if (task == closeTask) - break; - - var result = receiveTask.Result; - await ReceivedDatagram(result.Buffer, 0, result.Buffer.Length).ConfigureAwait(false); - } - } - } -} \ No newline at end of file diff --git a/src/Discord.Net.Providers.UdpClient/UDPClientProvider.cs b/src/Discord.Net.Providers.UdpClient/UDPClientProvider.cs deleted file mode 100644 index 6bdf9eb63..000000000 --- a/src/Discord.Net.Providers.UdpClient/UDPClientProvider.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Discord.Net.Udp; - -namespace Discord.Net.Providers.UDPClient -{ - public static class UDPClientProvider - { - public static readonly UdpSocketProvider Instance = () => new UDPClient(); - } -} diff --git a/src/Discord.Net.Providers.WS4Net/Discord.Net.Providers.WS4Net.csproj b/src/Discord.Net.Providers.WS4Net/Discord.Net.Providers.WS4Net.csproj index 62adfc0b1..0115d91c0 100644 --- a/src/Discord.Net.Providers.WS4Net/Discord.Net.Providers.WS4Net.csproj +++ b/src/Discord.Net.Providers.WS4Net/Discord.Net.Providers.WS4Net.csproj @@ -1,20 +1,10 @@ - + + - 1.0.0 - rc-dev - rc-$(BuildNumber) - net45 - true Discord.Net.Providers.WS4Net - RogueException - An optional WebSocket client provider for Discord.Net using WebSocket4Net - discord;discordapp - https://github.com/RogueException/Discord.Net - http://opensource.org/licenses/MIT - git - git://github.com/RogueException/Discord.Net Discord.Providers.WS4Net - true + An optional WebSocket client provider for Discord.Net using WebSocket4Net + net45 @@ -22,9 +12,4 @@ - - $(NoWarn);CS1573;CS1591 - true - true - \ No newline at end of file diff --git a/src/Discord.Net.Relay/Discord.Net.Relay.csproj b/src/Discord.Net.Relay/Discord.Net.Relay.csproj deleted file mode 100644 index 8fee12d14..000000000 --- a/src/Discord.Net.Relay/Discord.Net.Relay.csproj +++ /dev/null @@ -1,32 +0,0 @@ - - - 1.0.0 - rc-dev - rc-$(BuildNumber) - netstandard1.3 - Discord.Net.Relay - RogueException - A core Discord.Net library containing the Relay server. - discord;discordapp - https://github.com/RogueException/Discord.Net - http://opensource.org/licenses/MIT - git - git://github.com/RogueException/Discord.Net - Discord.Relay - true - - - - - - - - - - - - $(NoWarn);CS1573;CS1591 - true - true - - \ No newline at end of file diff --git a/src/Discord.Net.Rest/API/Common/Embed.cs b/src/Discord.Net.Rest/API/Common/Embed.cs index f6325efbb..1c9fa34e2 100644 --- a/src/Discord.Net.Rest/API/Common/Embed.cs +++ b/src/Discord.Net.Rest/API/Common/Embed.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System; using Newtonsoft.Json; +using Newtonsoft.Json.Converters; namespace Discord.API { @@ -8,14 +9,14 @@ namespace Discord.API { [JsonProperty("title")] public string Title { get; set; } - [JsonProperty("type")] - public string Type { get; set; } [JsonProperty("description")] public string Description { get; set; } [JsonProperty("url")] public string Url { get; set; } [JsonProperty("color")] public uint? Color { get; set; } + [JsonProperty("type"), JsonConverter(typeof(StringEnumConverter))] + public EmbedType Type { get; set; } [JsonProperty("timestamp")] public DateTimeOffset? Timestamp { get; set; } [JsonProperty("author")] diff --git a/src/Discord.Net.Rest/API/Common/EmbedAuthor.cs b/src/Discord.Net.Rest/API/Common/EmbedAuthor.cs index e69fee6eb..4381a9da3 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedAuthor.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedAuthor.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +using System; +using Newtonsoft.Json; namespace Discord.API { diff --git a/src/Discord.Net.Rest/API/Common/EmbedFooter.cs b/src/Discord.Net.Rest/API/Common/EmbedFooter.cs index 27048972e..3dd7020d9 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedFooter.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedFooter.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +using System; +using Newtonsoft.Json; namespace Discord.API { diff --git a/src/Discord.Net.Rest/API/Common/EmbedImage.cs b/src/Discord.Net.Rest/API/Common/EmbedImage.cs index a5ef748f8..c6b3562a3 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedImage.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedImage.cs @@ -1,4 +1,5 @@ #pragma warning disable CS1591 +using System; using Newtonsoft.Json; namespace Discord.API diff --git a/src/Discord.Net.Rest/API/Common/EmbedProvider.cs b/src/Discord.Net.Rest/API/Common/EmbedProvider.cs index 8c46b10dc..1658eda1a 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedProvider.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedProvider.cs @@ -1,4 +1,5 @@ #pragma warning disable CS1591 +using System; using Newtonsoft.Json; namespace Discord.API diff --git a/src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs b/src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs index f22953a25..993beb72b 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs @@ -1,4 +1,5 @@ #pragma warning disable CS1591 +using System; using Newtonsoft.Json; namespace Discord.API diff --git a/src/Discord.Net.Rest/API/Common/EmbedVideo.cs b/src/Discord.Net.Rest/API/Common/EmbedVideo.cs index 09e933784..610cf58a8 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedVideo.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedVideo.cs @@ -1,4 +1,5 @@ #pragma warning disable CS1591 +using System; using Newtonsoft.Json; namespace Discord.API diff --git a/src/Discord.Net.Rest/API/Common/GuildMember.cs b/src/Discord.Net.Rest/API/Common/GuildMember.cs index daba36d23..24ad17c14 100644 --- a/src/Discord.Net.Rest/API/Common/GuildMember.cs +++ b/src/Discord.Net.Rest/API/Common/GuildMember.cs @@ -11,12 +11,12 @@ namespace Discord.API [JsonProperty("nick")] public Optional Nick { get; set; } [JsonProperty("roles")] - public ulong[] Roles { get; set; } + public Optional Roles { get; set; } [JsonProperty("joined_at")] public Optional JoinedAt { get; set; } [JsonProperty("deaf")] - public bool Deaf { get; set; } + public Optional Deaf { get; set; } [JsonProperty("mute")] - public bool Mute { get; set; } + public Optional Mute { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/CreateGuildBanParams.cs b/src/Discord.Net.Rest/API/Rest/CreateGuildBanParams.cs index 0c148fe70..f0432e517 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateGuildBanParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateGuildBanParams.cs @@ -4,5 +4,6 @@ namespace Discord.API.Rest internal class CreateGuildBanParams { public Optional DeleteMessageDays { get; set; } + public string Reason { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs b/src/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs new file mode 100644 index 000000000..970a30201 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs @@ -0,0 +1,28 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class CreateWebhookMessageParams + { + [JsonProperty("content")] + public string Content { get; } + + [JsonProperty("nonce")] + public Optional Nonce { get; set; } + [JsonProperty("tts")] + public Optional IsTTS { get; set; } + [JsonProperty("embeds")] + public Optional Embeds { get; set; } + [JsonProperty("username")] + public Optional Username { get; set; } + [JsonProperty("avatar_url")] + public Optional AvatarUrl { get; set; } + + public CreateWebhookMessageParams(string content) + { + Content = content; + } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/GetGuildSummariesParams.cs b/src/Discord.Net.Rest/API/Rest/GetGuildSummariesParams.cs new file mode 100644 index 000000000..f770ef398 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/GetGuildSummariesParams.cs @@ -0,0 +1,9 @@ +#pragma warning disable CS1591 +namespace Discord.API.Rest +{ + internal class GetGuildSummariesParams + { + public Optional Limit { get; set; } + public Optional AfterGuildId { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelsParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelsParams.cs index 2bbb58ea6..f97fbda0b 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelsParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelsParams.cs @@ -7,9 +7,9 @@ namespace Discord.API.Rest internal class ModifyGuildChannelsParams { [JsonProperty("id")] - public ulong Id { get; set; } + public ulong Id { get; } [JsonProperty("position")] - public int Position { get; set; } + public int Position { get; } public ModifyGuildChannelsParams(ulong id, int position) { diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildRoleParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildRoleParams.cs index c3c20706b..287e1cafe 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyGuildRoleParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildRoleParams.cs @@ -10,8 +10,6 @@ namespace Discord.API.Rest public Optional Name { get; set; } [JsonProperty("permissions")] public Optional Permissions { get; set; } - [JsonProperty("position")] - public Optional Position { get; set; } [JsonProperty("color")] public Optional Color { get; set; } [JsonProperty("hoist")] diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildRolesParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildRolesParams.cs index 38c3fb646..0e816a260 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyGuildRolesParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildRolesParams.cs @@ -8,10 +8,13 @@ namespace Discord.API.Rest { [JsonProperty("id")] public ulong Id { get; } + [JsonProperty("position")] + public int Position { get; } - public ModifyGuildRolesParams(ulong id) + public ModifyGuildRolesParams(ulong id, int position) { Id = id; + Position = position; } } } diff --git a/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs new file mode 100644 index 000000000..f2c34c015 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs @@ -0,0 +1,41 @@ +#pragma warning disable CS1591 +using Discord.Net.Rest; +using System.Collections.Generic; +using System.IO; + +namespace Discord.API.Rest +{ + internal class UploadWebhookFileParams + { + public Stream File { get; } + + public Optional Filename { get; set; } + public Optional Content { get; set; } + public Optional Nonce { get; set; } + public Optional IsTTS { get; set; } + public Optional Username { get; set; } + public Optional AvatarUrl { get; set; } + + public UploadWebhookFileParams(Stream file) + { + File = file; + } + + public IReadOnlyDictionary ToDictionary() + { + var d = new Dictionary(); + d["file"] = new MultipartFile(File, Filename.GetValueOrDefault("unknown.dat")); + if (Content.IsSpecified) + d["content"] = Content.Value; + if (IsTTS.IsSpecified) + d["tts"] = IsTTS.Value.ToString(); + if (Nonce.IsSpecified) + d["nonce"] = Nonce.Value; + if (Username.IsSpecified) + d["username"] = Username.Value; + if (AvatarUrl.IsSpecified) + d["avatar_url"] = AvatarUrl.Value; + return d; + } + } +} diff --git a/src/Discord.Net.Rest/AssemblyInfo.cs b/src/Discord.Net.Rest/AssemblyInfo.cs index aff0626bf..a4f045ab5 100644 --- a/src/Discord.Net.Rest/AssemblyInfo.cs +++ b/src/Discord.Net.Rest/AssemblyInfo.cs @@ -2,5 +2,6 @@ [assembly: InternalsVisibleTo("Discord.Net.Rpc")] [assembly: InternalsVisibleTo("Discord.Net.WebSocket")] +[assembly: InternalsVisibleTo("Discord.Net.Webhook")] [assembly: InternalsVisibleTo("Discord.Net.Commands")] [assembly: InternalsVisibleTo("Discord.Net.Tests")] \ No newline at end of file diff --git a/src/Discord.Net.Rest/BaseDiscordClient.cs b/src/Discord.Net.Rest/BaseDiscordClient.cs index f38b9506d..6239a2eb9 100644 --- a/src/Discord.Net.Rest/BaseDiscordClient.cs +++ b/src/Discord.Net.Rest/BaseDiscordClient.cs @@ -26,6 +26,7 @@ namespace Discord.Rest internal LogManager LogManager { get; } public LoginState LoginState { get; private set; } public ISelfUser CurrentUser { get; protected set; } + public TokenType TokenType => ApiClient.AuthTokenType; /// Creates a new REST-only discord client. internal BaseDiscordClient(DiscordRestConfig config, API.DiscordRestApiClient client) @@ -131,35 +132,35 @@ namespace Discord.Rest Task IDiscordClient.GetApplicationInfoAsync() { throw new NotSupportedException(); } - Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode) + Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); - Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode) + Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(ImmutableArray.Create()); - Task> IDiscordClient.GetDMChannelsAsync(CacheMode mode) + Task> IDiscordClient.GetDMChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(ImmutableArray.Create()); - Task> IDiscordClient.GetGroupChannelsAsync(CacheMode mode) + Task> IDiscordClient.GetGroupChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(ImmutableArray.Create()); - Task> IDiscordClient.GetConnectionsAsync() + Task> IDiscordClient.GetConnectionsAsync(RequestOptions options) => Task.FromResult>(ImmutableArray.Create()); - Task IDiscordClient.GetInviteAsync(string inviteId) + Task IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options) => Task.FromResult(null); - Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode) + Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); - Task> IDiscordClient.GetGuildsAsync(CacheMode mode) + Task> IDiscordClient.GetGuildsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(ImmutableArray.Create()); - Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon) { throw new NotSupportedException(); } + Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) { throw new NotSupportedException(); } - Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode) + Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); - Task IDiscordClient.GetUserAsync(string username, string discriminator) + Task IDiscordClient.GetUserAsync(string username, string discriminator, RequestOptions options) => Task.FromResult(null); - Task> IDiscordClient.GetVoiceRegionsAsync() + Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) => Task.FromResult>(ImmutableArray.Create()); - Task IDiscordClient.GetVoiceRegionAsync(string id) + Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) => Task.FromResult(null); Task IDiscordClient.StartAsync() diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs index 25a84802e..9a9afe877 100644 --- a/src/Discord.Net.Rest/ClientHelper.cs +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -10,80 +10,104 @@ namespace Discord.Rest internal static class ClientHelper { //Applications - public static async Task GetApplicationInfoAsync(BaseDiscordClient client) + public static async Task GetApplicationInfoAsync(BaseDiscordClient client, RequestOptions options) { - var model = await client.ApiClient.GetMyApplicationAsync().ConfigureAwait(false); + var model = await client.ApiClient.GetMyApplicationAsync(options).ConfigureAwait(false); return RestApplication.Create(client, model); } public static async Task GetChannelAsync(BaseDiscordClient client, - ulong id) + ulong id, RequestOptions options) { - var model = await client.ApiClient.GetChannelAsync(id).ConfigureAwait(false); + var model = await client.ApiClient.GetChannelAsync(id, options).ConfigureAwait(false); if (model != null) return RestChannel.Create(client, model); return null; } - public static async Task> GetPrivateChannelsAsync(BaseDiscordClient client) + public static async Task> GetPrivateChannelsAsync(BaseDiscordClient client, RequestOptions options) { - var models = await client.ApiClient.GetMyPrivateChannelsAsync().ConfigureAwait(false); + var models = await client.ApiClient.GetMyPrivateChannelsAsync(options).ConfigureAwait(false); return models.Select(x => RestChannel.CreatePrivate(client, x)).ToImmutableArray(); } - public static async Task> GetDMChannelsAsync(BaseDiscordClient client) + public static async Task> GetDMChannelsAsync(BaseDiscordClient client, RequestOptions options) { - var models = await client.ApiClient.GetMyPrivateChannelsAsync().ConfigureAwait(false); + var models = await client.ApiClient.GetMyPrivateChannelsAsync(options).ConfigureAwait(false); return models .Where(x => x.Type == ChannelType.DM) .Select(x => RestDMChannel.Create(client, x)).ToImmutableArray(); } - public static async Task> GetGroupChannelsAsync(BaseDiscordClient client) + public static async Task> GetGroupChannelsAsync(BaseDiscordClient client, RequestOptions options) { - var models = await client.ApiClient.GetMyPrivateChannelsAsync().ConfigureAwait(false); + var models = await client.ApiClient.GetMyPrivateChannelsAsync(options).ConfigureAwait(false); return models .Where(x => x.Type == ChannelType.Group) .Select(x => RestGroupChannel.Create(client, x)).ToImmutableArray(); } - public static async Task> GetConnectionsAsync(BaseDiscordClient client) + public static async Task> GetConnectionsAsync(BaseDiscordClient client, RequestOptions options) { - var models = await client.ApiClient.GetMyConnectionsAsync().ConfigureAwait(false); + var models = await client.ApiClient.GetMyConnectionsAsync(options).ConfigureAwait(false); return models.Select(x => RestConnection.Create(x)).ToImmutableArray(); } public static async Task GetInviteAsync(BaseDiscordClient client, - string inviteId) + string inviteId, RequestOptions options) { - var model = await client.ApiClient.GetInviteAsync(inviteId).ConfigureAwait(false); + var model = await client.ApiClient.GetInviteAsync(inviteId, options).ConfigureAwait(false); if (model != null) return RestInvite.Create(client, null, null, model); return null; } public static async Task GetGuildAsync(BaseDiscordClient client, - ulong id) + ulong id, RequestOptions options) { - var model = await client.ApiClient.GetGuildAsync(id).ConfigureAwait(false); + var model = await client.ApiClient.GetGuildAsync(id, options).ConfigureAwait(false); if (model != null) return RestGuild.Create(client, model); return null; } public static async Task GetGuildEmbedAsync(BaseDiscordClient client, - ulong id) + ulong id, RequestOptions options) { - var model = await client.ApiClient.GetGuildEmbedAsync(id).ConfigureAwait(false); + var model = await client.ApiClient.GetGuildEmbedAsync(id, options).ConfigureAwait(false); if (model != null) return RestGuildEmbed.Create(model); return null; } - public static async Task> GetGuildSummariesAsync(BaseDiscordClient client) - { - var models = await client.ApiClient.GetMyGuildsAsync().ConfigureAwait(false); - return models.Select(x => RestUserGuild.Create(client, x)).ToImmutableArray(); - } - public static async Task> GetGuildsAsync(BaseDiscordClient client) - { - var summaryModels = await client.ApiClient.GetMyGuildsAsync().ConfigureAwait(false); - var guilds = ImmutableArray.CreateBuilder(summaryModels.Count); + public static IAsyncEnumerable> GetGuildSummariesAsync(BaseDiscordClient client, + ulong? fromGuildId, int? limit, RequestOptions options) + { + return new PagedAsyncEnumerable( + DiscordConfig.MaxUsersPerBatch, + async (info, ct) => + { + var args = new GetGuildSummariesParams + { + Limit = info.PageSize + }; + if (info.Position != null) + args.AfterGuildId = info.Position.Value; + var models = await client.ApiClient.GetMyGuildsAsync(args, options).ConfigureAwait(false); + return models + .Select(x => RestUserGuild.Create(client, x)) + .ToImmutableArray(); + }, + nextPage: (info, lastPage) => + { + if (lastPage.Count != DiscordConfig.MaxMessagesPerBatch) + return false; + info.Position = lastPage.Max(x => x.Id); + return true; + }, + start: fromGuildId, + count: limit + ); + } + public static async Task> GetGuildsAsync(BaseDiscordClient client, RequestOptions options) + { + var summaryModels = await GetGuildSummariesAsync(client, null, null, options).Flatten(); + var guilds = ImmutableArray.CreateBuilder(); foreach (var summaryModel in summaryModels) { var guildModel = await client.ApiClient.GetGuildAsync(summaryModel.Id).ConfigureAwait(false); @@ -93,40 +117,40 @@ namespace Discord.Rest return guilds.ToImmutable(); } public static async Task CreateGuildAsync(BaseDiscordClient client, - string name, IVoiceRegion region, Stream jpegIcon = null) + string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) { var args = new CreateGuildParams(name, region.Id); - var model = await client.ApiClient.CreateGuildAsync(args).ConfigureAwait(false); + var model = await client.ApiClient.CreateGuildAsync(args, options).ConfigureAwait(false); return RestGuild.Create(client, model); } public static async Task GetUserAsync(BaseDiscordClient client, - ulong id) + ulong id, RequestOptions options) { - var model = await client.ApiClient.GetUserAsync(id).ConfigureAwait(false); + var model = await client.ApiClient.GetUserAsync(id, options).ConfigureAwait(false); if (model != null) return RestUser.Create(client, model); return null; } public static async Task GetGuildUserAsync(BaseDiscordClient client, - ulong guildId, ulong id) + ulong guildId, ulong id, RequestOptions options) { - var model = await client.ApiClient.GetGuildMemberAsync(guildId, id).ConfigureAwait(false); + var model = await client.ApiClient.GetGuildMemberAsync(guildId, id, options).ConfigureAwait(false); if (model != null) return RestGuildUser.Create(client, new RestGuild(client, guildId), model); return null; } - public static async Task> GetVoiceRegionsAsync(BaseDiscordClient client) + public static async Task> GetVoiceRegionsAsync(BaseDiscordClient client, RequestOptions options) { - var models = await client.ApiClient.GetVoiceRegionsAsync().ConfigureAwait(false); + var models = await client.ApiClient.GetVoiceRegionsAsync(options).ConfigureAwait(false); return models.Select(x => RestVoiceRegion.Create(client, x)).ToImmutableArray(); } public static async Task GetVoiceRegionAsync(BaseDiscordClient client, - string id) + string id, RequestOptions options) { - var models = await client.ApiClient.GetVoiceRegionsAsync().ConfigureAwait(false); - return models.Select(x => RestVoiceRegion.Create(client, x)).Where(x => x.Id == id).FirstOrDefault(); + var models = await client.ApiClient.GetVoiceRegionsAsync(options).ConfigureAwait(false); + return models.Select(x => RestVoiceRegion.Create(client, x)).FirstOrDefault(x => x.Id == id); } public static async Task> GetRelationshipsAsync(BaseDiscordClient client) diff --git a/src/Discord.Net.Rest/Discord.Net.Rest.csproj b/src/Discord.Net.Rest/Discord.Net.Rest.csproj index b7495f273..439b7bbb1 100644 --- a/src/Discord.Net.Rest/Discord.Net.Rest.csproj +++ b/src/Discord.Net.Rest/Discord.Net.Rest.csproj @@ -1,29 +1,18 @@ - + + - 1.0.0 - rc-dev - rc-$(BuildNumber) - netstandard1.1;netstandard1.3 Discord.Net.Rest - RogueException - A core Discord.Net library containing the REST client and models. - discord;discordapp - https://github.com/RogueException/Discord.Net - http://opensource.org/licenses/MIT - git - git://github.com/RogueException/Discord.Net Discord.Rest - true + A core Discord.Net library containing the REST client and models. + net45;netstandard1.1;netstandard1.3 - - + + + + + - - $(NoWarn);CS1573;CS1591 - true - true - \ No newline at end of file diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 3587d95f8..e690ac4f5 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -8,7 +8,6 @@ using Newtonsoft.Json; using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Collections.Immutable; using System.Diagnostics; using System.Globalization; using System.IO; @@ -31,7 +30,7 @@ namespace Discord.API protected readonly JsonSerializer _serializer; protected readonly SemaphoreSlim _stateLock; - private readonly RestClientProvider RestClientProvider; + private readonly RestClientProvider _restClientProvider; protected bool _isDisposed; private CancellationTokenSource _loginCancelToken; @@ -49,7 +48,7 @@ namespace Discord.API public DiscordRestApiClient(RestClientProvider restClientProvider, string userAgent, RetryMode defaultRetryMode = RetryMode.AlwaysRetry, JsonSerializer serializer = null) { - RestClientProvider = restClientProvider; + _restClientProvider = restClientProvider; UserAgent = userAgent; DefaultRetryMode = defaultRetryMode; _serializer = serializer ?? new JsonSerializer { DateFormatString = "yyyy-MM-ddTHH:mm:ssZ", ContractResolver = new DiscordContractResolver() }; @@ -57,11 +56,11 @@ namespace Discord.API RequestQueue = new RequestQueue(); _stateLock = new SemaphoreSlim(1, 1); - SetBaseUrl(DiscordConfig.ClientAPIUrl); + SetBaseUrl(DiscordConfig.APIUrl); } internal void SetBaseUrl(string baseUrl) { - RestClient = RestClientProvider(baseUrl); + RestClient = _restClientProvider(baseUrl); RestClient.SetHeader("accept", "*/*"); RestClient.SetHeader("user-agent", UserAgent); RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken)); @@ -120,7 +119,8 @@ namespace Discord.API AuthTokenType = tokenType; AuthToken = token; - RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken)); + if (tokenType != TokenType.Webhook) + RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken)); LoginState = LoginState.LoggedIn; } @@ -189,7 +189,7 @@ namespace Discord.API options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; options.IsClientBucket = AuthTokenType == TokenType.User; - var json = payload != null ? SerializeJson(payload) : null; + string json = payload != null ? SerializeJson(payload) : null; var request = new JsonRestRequest(RestClient, method, endpoint, json, options); await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); } @@ -233,7 +233,7 @@ namespace Discord.API options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; options.IsClientBucket = AuthTokenType == TokenType.User; - var json = payload != null ? SerializeJson(payload) : null; + string json = payload != null ? SerializeJson(payload) : null; var request = new JsonRestRequest(RestClient, method, endpoint, json, options); return DeserializeJson(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false)); } @@ -387,7 +387,27 @@ namespace Discord.API break; } } + public async Task AddRoleAsync(ulong guildId, ulong userId, ulong roleId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(userId, 0, nameof(userId)); + Preconditions.NotEqual(roleId, 0, nameof(roleId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + await SendAsync("PUT", () => $"guilds/{guildId}/members/{userId}/roles/{roleId}", ids, options: options); + } + public async Task RemoveRoleAsync(ulong guildId, ulong userId, ulong roleId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(userId, 0, nameof(userId)); + Preconditions.NotEqual(roleId, 0, nameof(roleId)); + options = RequestOptions.CreateOrClone(options); + var ids = new BucketIds(guildId: guildId); + await SendAsync("DELETE", () => $"guilds/{guildId}/members/{userId}/roles/{roleId}", ids, options: options); + } + //Channel Messages public async Task GetChannelMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) { @@ -438,8 +458,8 @@ namespace Discord.API } public async Task CreateMessageAsync(ulong channelId, CreateMessageParams args, RequestOptions options = null) { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotNull(args, nameof(args)); + Preconditions.NotEqual(channelId, 0, nameof(channelId)); if (!args.Embed.IsSpecified || args.Embed.Value == null) Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); @@ -450,12 +470,45 @@ namespace Discord.API var ids = new BucketIds(channelId: channelId); return await SendJsonAsync("POST", () => $"channels/{channelId}/messages", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } + public async Task CreateWebhookMessageAsync(ulong webhookId, CreateWebhookMessageParams args, RequestOptions options = null) + { + if (AuthTokenType != TokenType.Webhook) + throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); + + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotEqual(webhookId, 0, nameof(webhookId)); + if (!args.Embeds.IsSpecified || args.Embeds.Value == null || args.Embeds.Value.Length == 0) + Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); + + if (args.Content.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); + options = RequestOptions.CreateOrClone(options); + + await SendJsonAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}", args, new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + } public async Task UploadFileAsync(ulong channelId, UploadFileParams args, RequestOptions options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(channelId, 0, nameof(channelId)); options = RequestOptions.CreateOrClone(options); + if (args.Content.GetValueOrDefault(null) == null) + args.Content = ""; + else if (args.Content.IsSpecified && args.Content.Value?.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); + + var ids = new BucketIds(channelId: channelId); + return await SendMultipartAsync("POST", () => $"channels/{channelId}/messages", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + } + public async Task UploadWebhookFileAsync(ulong webhookId, UploadWebhookFileParams args, RequestOptions options = null) + { + if (AuthTokenType != TokenType.Webhook) + throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); + + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotEqual(webhookId, 0, nameof(webhookId)); + options = RequestOptions.CreateOrClone(options); + if (args.Content.GetValueOrDefault(null) == null) args.Content = ""; else if (args.Content.IsSpecified) @@ -466,8 +519,7 @@ namespace Discord.API throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); } - var ids = new BucketIds(channelId: channelId); - return await SendMultipartAsync("POST", () => $"channels/{channelId}/messages", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + await SendMultipartAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}", args.ToDictionary(), new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } public async Task DeleteMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) { @@ -746,11 +798,13 @@ namespace Discord.API Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(userId, 0, nameof(userId)); Preconditions.NotNull(args, nameof(args)); - Preconditions.AtLeast(args.DeleteMessageDays, 0, nameof(args.DeleteMessageDays)); + Preconditions.AtLeast(args.DeleteMessageDays, 0, nameof(args.DeleteMessageDays), "Prune length must be within [0, 7]"); + Preconditions.AtMost(args.DeleteMessageDays, 7, nameof(args.DeleteMessageDays), "Prune length must be within [0, 7]"); options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(guildId: guildId); - await SendAsync("PUT", () => $"guilds/{guildId}/bans/{userId}?delete-message-days={args.DeleteMessageDays}", ids, options: options).ConfigureAwait(false); + string reason = string.IsNullOrWhiteSpace(args.Reason) ? "" : $"&reason={args.Reason}"; + await SendAsync("PUT", () => $"guilds/{guildId}/bans/{userId}?delete-message-days={args.DeleteMessageDays}{reason}", ids, options: options).ConfigureAwait(false); } public async Task RemoveGuildBanAsync(ulong guildId, ulong userId, RequestOptions options = null) { @@ -927,14 +981,15 @@ namespace Discord.API Expression> endpoint = () => $"guilds/{guildId}/members?limit={limit}&after={afterUserId}"; return await SendAsync>("GET", endpoint, ids, options: options).ConfigureAwait(false); } - public async Task RemoveGuildMemberAsync(ulong guildId, ulong userId, RequestOptions options = null) + public async Task RemoveGuildMemberAsync(ulong guildId, ulong userId, string reason, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(userId, 0, nameof(userId)); options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(guildId: guildId); - await SendAsync("DELETE", () => $"guilds/{guildId}/members/{userId}", ids, options: options).ConfigureAwait(false); + reason = string.IsNullOrWhiteSpace(reason) ? "" : $"?reason={reason}"; + await SendAsync("DELETE", () => $"guilds/{guildId}/members/{userId}{reason}", ids, options: options).ConfigureAwait(false); } public async Task ModifyGuildMemberAsync(ulong guildId, ulong userId, Rest.ModifyGuildMemberParams args, RequestOptions options = null) { @@ -991,7 +1046,6 @@ namespace Discord.API Preconditions.NotNull(args, nameof(args)); Preconditions.AtLeast(args.Color, 0, nameof(args.Color)); Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name)); - Preconditions.AtLeast(args.Position, 0, nameof(args.Position)); options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(guildId: guildId); @@ -1003,17 +1057,8 @@ namespace Discord.API Preconditions.NotNull(args, nameof(args)); options = RequestOptions.CreateOrClone(options); - var roles = args.ToImmutableArray(); - switch (roles.Length) - { - case 0: - return ImmutableArray.Create(); - case 1: - return ImmutableArray.Create(await ModifyGuildRoleAsync(guildId, roles[0].Id, roles[0]).ConfigureAwait(false)); - default: - var ids = new BucketIds(guildId: guildId); - return await SendJsonAsync>("PATCH", () => $"guilds/{guildId}/roles", args, ids, options: options).ConfigureAwait(false); - } + var ids = new BucketIds(guildId: guildId); + return await SendJsonAsync>("PATCH", () => $"guilds/{guildId}/roles", args, ids, options: options).ConfigureAwait(false); } //Users @@ -1073,10 +1118,18 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); return await SendAsync>("GET", () => "users/@me/channels", new BucketIds(), options: options).ConfigureAwait(false); } - public async Task> GetMyGuildsAsync(RequestOptions options = null) + public async Task> GetMyGuildsAsync(GetGuildSummariesParams args, RequestOptions options = null) { + Preconditions.NotNull(args, nameof(args)); + Preconditions.GreaterThan(args.Limit, 0, nameof(args.Limit)); + Preconditions.AtMost(args.Limit, DiscordConfig.MaxGuildsPerBatch, nameof(args.Limit)); + Preconditions.GreaterThan(args.AfterGuildId, 0, nameof(args.AfterGuildId)); options = RequestOptions.CreateOrClone(options); - return await SendAsync>("GET", () => "users/@me/guilds", new BucketIds(), options: options).ConfigureAwait(false); + + int limit = args.Limit.GetValueOrDefault(int.MaxValue); + ulong afterGuildId = args.AfterGuildId.GetValueOrDefault(0); + + return await SendAsync>("GET", () => $"users/@me/guilds?limit={limit}&after={afterGuildId}", new BucketIds(), options: options).ConfigureAwait(false); } public async Task GetMyApplicationAsync(RequestOptions options = null) { diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index 5163e7613..9228c5c30 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -35,130 +35,133 @@ namespace Discord.Rest } /// - public async Task GetApplicationInfoAsync() + public async Task GetApplicationInfoAsync(RequestOptions options = null) { - return _applicationInfo ?? (_applicationInfo = await ClientHelper.GetApplicationInfoAsync(this)); + return _applicationInfo ?? (_applicationInfo = await ClientHelper.GetApplicationInfoAsync(this, options)); } /// - public Task GetChannelAsync(ulong id) - => ClientHelper.GetChannelAsync(this, id); + public Task GetChannelAsync(ulong id, RequestOptions options = null) + => ClientHelper.GetChannelAsync(this, id, options); /// - public Task> GetPrivateChannelsAsync() - => ClientHelper.GetPrivateChannelsAsync(this); - public Task> GetDMChannelsAsync() - => ClientHelper.GetDMChannelsAsync(this); - public Task> GetGroupChannelsAsync() - => ClientHelper.GetGroupChannelsAsync(this); + public Task> GetPrivateChannelsAsync(RequestOptions options = null) + => ClientHelper.GetPrivateChannelsAsync(this, options); + public Task> GetDMChannelsAsync(RequestOptions options = null) + => ClientHelper.GetDMChannelsAsync(this, options); + public Task> GetGroupChannelsAsync(RequestOptions options = null) + => ClientHelper.GetGroupChannelsAsync(this, options); /// - public Task> GetConnectionsAsync() - => ClientHelper.GetConnectionsAsync(this); + public Task> GetConnectionsAsync(RequestOptions options = null) + => ClientHelper.GetConnectionsAsync(this, options); /// - public Task GetInviteAsync(string inviteId) - => ClientHelper.GetInviteAsync(this, inviteId); + public Task GetInviteAsync(string inviteId, RequestOptions options = null) + => ClientHelper.GetInviteAsync(this, inviteId, options); /// - public Task GetGuildAsync(ulong id) - => ClientHelper.GetGuildAsync(this, id); + public Task GetGuildAsync(ulong id, RequestOptions options = null) + => ClientHelper.GetGuildAsync(this, id, options); /// - public Task GetGuildEmbedAsync(ulong id) - => ClientHelper.GetGuildEmbedAsync(this, id); + public Task GetGuildEmbedAsync(ulong id, RequestOptions options = null) + => ClientHelper.GetGuildEmbedAsync(this, id, options); /// - public Task> GetGuildSummariesAsync() - => ClientHelper.GetGuildSummariesAsync(this); + public IAsyncEnumerable> GetGuildSummariesAsync(RequestOptions options = null) + => ClientHelper.GetGuildSummariesAsync(this, null, null, options); /// - public Task> GetGuildsAsync() - => ClientHelper.GetGuildsAsync(this); + public IAsyncEnumerable> GetGuildSummariesAsync(ulong fromGuildId, int limit, RequestOptions options = null) + => ClientHelper.GetGuildSummariesAsync(this, fromGuildId, limit, options); /// - public Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null) - => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon); + public Task> GetGuildsAsync(RequestOptions options = null) + => ClientHelper.GetGuildsAsync(this, options); + /// + public Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null, RequestOptions options = null) + => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon, options); /// - public Task GetUserAsync(ulong id) - => ClientHelper.GetUserAsync(this, id); + public Task GetUserAsync(ulong id, RequestOptions options = null) + => ClientHelper.GetUserAsync(this, id, options); /// - public Task GetGuildUserAsync(ulong guildId, ulong id) - => ClientHelper.GetGuildUserAsync(this, guildId, id); + public Task GetGuildUserAsync(ulong guildId, ulong id, RequestOptions options = null) + => ClientHelper.GetGuildUserAsync(this, guildId, id, options); /// - public Task> GetVoiceRegionsAsync() - => ClientHelper.GetVoiceRegionsAsync(this); + public Task> GetVoiceRegionsAsync(RequestOptions options = null) + => ClientHelper.GetVoiceRegionsAsync(this, options); /// - public Task GetVoiceRegionAsync(string id) - => ClientHelper.GetVoiceRegionAsync(this, id); + public Task GetVoiceRegionAsync(string id, RequestOptions options = null) + => ClientHelper.GetVoiceRegionAsync(this, id, options); public Task> GetRelationshipsAsync() => ClientHelper.GetRelationshipsAsync(this); //IDiscordClient - async Task IDiscordClient.GetApplicationInfoAsync() - => await GetApplicationInfoAsync().ConfigureAwait(false); + async Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) + => await GetApplicationInfoAsync(options).ConfigureAwait(false); - async Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode) + async Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) - return await GetChannelAsync(id).ConfigureAwait(false); + return await GetChannelAsync(id, options).ConfigureAwait(false); else return null; } - async Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode) + async Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) - return await GetPrivateChannelsAsync().ConfigureAwait(false); + return await GetPrivateChannelsAsync(options).ConfigureAwait(false); else return ImmutableArray.Create(); } - async Task> IDiscordClient.GetDMChannelsAsync(CacheMode mode) + async Task> IDiscordClient.GetDMChannelsAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) - return await GetDMChannelsAsync().ConfigureAwait(false); + return await GetDMChannelsAsync(options).ConfigureAwait(false); else return ImmutableArray.Create(); } - async Task> IDiscordClient.GetGroupChannelsAsync(CacheMode mode) + async Task> IDiscordClient.GetGroupChannelsAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) - return await GetGroupChannelsAsync().ConfigureAwait(false); + return await GetGroupChannelsAsync(options).ConfigureAwait(false); else return ImmutableArray.Create(); } - async Task> IDiscordClient.GetConnectionsAsync() - => await GetConnectionsAsync().ConfigureAwait(false); + async Task> IDiscordClient.GetConnectionsAsync(RequestOptions options) + => await GetConnectionsAsync(options).ConfigureAwait(false); - async Task IDiscordClient.GetInviteAsync(string inviteId) - => await GetInviteAsync(inviteId).ConfigureAwait(false); + async Task IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options) + => await GetInviteAsync(inviteId, options).ConfigureAwait(false); - async Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode) + async Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) - return await GetGuildAsync(id).ConfigureAwait(false); + return await GetGuildAsync(id, options).ConfigureAwait(false); else return null; } - async Task> IDiscordClient.GetGuildsAsync(CacheMode mode) + async Task> IDiscordClient.GetGuildsAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) - return await GetGuildsAsync().ConfigureAwait(false); + return await GetGuildsAsync(options).ConfigureAwait(false); else return ImmutableArray.Create(); } - async Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon) - => await CreateGuildAsync(name, region, jpegIcon).ConfigureAwait(false); + async Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) + => await CreateGuildAsync(name, region, jpegIcon, options).ConfigureAwait(false); - async Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode) + async Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) - return await GetUserAsync(id).ConfigureAwait(false); + return await GetUserAsync(id, options).ConfigureAwait(false); else return null; } - async Task> IDiscordClient.GetVoiceRegionsAsync() - => await GetVoiceRegionsAsync().ConfigureAwait(false); - async Task IDiscordClient.GetVoiceRegionAsync(string id) - => await GetVoiceRegionAsync(id).ConfigureAwait(false); + async Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) + => await GetVoiceRegionsAsync(options).ConfigureAwait(false); + async Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) + => await GetVoiceRegionAsync(id, options).ConfigureAwait(false); } } diff --git a/src/Discord.Net.Rest/DiscordRestConfig.cs b/src/Discord.Net.Rest/DiscordRestConfig.cs index c3cd70683..4a7aae287 100644 --- a/src/Discord.Net.Rest/DiscordRestConfig.cs +++ b/src/Discord.Net.Rest/DiscordRestConfig.cs @@ -4,11 +4,6 @@ namespace Discord.Rest { public class DiscordRestConfig : DiscordConfig { - public static string UserAgent { get; } = $"DiscordBot (https://github.com/RogueException/Discord.Net, v{Version})"; - - internal const int MessageQueueInterval = 100; - internal const int WebSocketQueueInterval = 100; - /// Gets or sets the provider used to generate new REST connections. public RestClientProvider RestClientProvider { get; set; } = DefaultRestClientProvider.Instance; } diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index 07bdfe0eb..6b7dca3a9 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -91,7 +91,9 @@ namespace Discord.Rest var guildId = (channel as IGuildChannel)?.GuildId; var guild = guildId != null ? await (client as IDiscordClient).GetGuildAsync(guildId.Value, CacheMode.CacheOnly).ConfigureAwait(false) : null; var model = await client.ApiClient.GetChannelMessageAsync(channel.Id, id, options).ConfigureAwait(false); - var author = GetAuthor(client, guild, model.Author.Value); + if (model == null) + return null; + var author = GetAuthor(client, guild, model.Author.Value, model.WebhookId.ToNullable()); return RestMessage.Create(client, channel, author, model); } public static IAsyncEnumerable> GetMessagesAsync(IMessageChannel channel, BaseDiscordClient client, @@ -119,7 +121,7 @@ namespace Discord.Rest var builder = ImmutableArray.CreateBuilder(); foreach (var model in models) { - var author = GetAuthor(client, guild, model.Author.Value); + var author = GetAuthor(client, guild, model.Author.Value, model.WebhookId.ToNullable()); builder.Add(RestMessage.Create(client, channel, author, model)); } return builder.ToImmutable(); @@ -147,7 +149,7 @@ namespace Discord.Rest var builder = ImmutableArray.CreateBuilder(); foreach (var model in models) { - var author = GetAuthor(client, guild, model.Author.Value); + var author = GetAuthor(client, guild, model.Author.Value, model.WebhookId.ToNullable()); builder.Add(RestMessage.Create(client, channel, author, model)); } return builder.ToImmutable(); @@ -161,7 +163,7 @@ namespace Discord.Rest return RestUserMessage.Create(client, channel, client.CurrentUser, model); } -#if NETSTANDARD1_3 +#if FILESYSTEM public static async Task SendFileAsync(IMessageChannel channel, BaseDiscordClient client, string filePath, string text, bool isTTS, RequestOptions options) { @@ -176,13 +178,27 @@ namespace Discord.Rest var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS }; var model = await client.ApiClient.UploadFileAsync(channel.Id, args, options).ConfigureAwait(false); return RestUserMessage.Create(client, channel, client.CurrentUser, model); - } + } - public static async Task DeleteMessagesAsync(IMessageChannel channel, BaseDiscordClient client, - IEnumerable messages, RequestOptions options) + public static async Task DeleteMessagesAsync(IMessageChannel channel, BaseDiscordClient client, + IEnumerable messageIds, RequestOptions options) { - var args = new DeleteMessagesParams(messages.Select(x => x.Id).ToArray()); - await client.ApiClient.DeleteMessagesAsync(channel.Id, args, options).ConfigureAwait(false); + var msgs = messageIds.ToArray(); + if (msgs.Length < 100) + { + var args = new DeleteMessagesParams(msgs); + await client.ApiClient.DeleteMessagesAsync(channel.Id, args, options).ConfigureAwait(false); + } + else + { + var batch = new ulong[100]; + for (int i = 0; i < (msgs.Length + 99) / 100; i++) + { + Array.Copy(msgs, i * 100, batch, 0, Math.Min(msgs.Length - (100 * i), 100)); + var args = new DeleteMessagesParams(batch); + await client.ApiClient.DeleteMessagesAsync(channel.Id, args, options).ConfigureAwait(false); + } + } } //Permission Overwrites @@ -264,14 +280,19 @@ namespace Discord.Rest => new TypingNotifier(client, channel, options); //Helpers - private static IUser GetAuthor(BaseDiscordClient client, IGuild guild, UserModel model) + private static IUser GetAuthor(BaseDiscordClient client, IGuild guild, UserModel model, ulong? webhookId) { IUser author = null; if (guild != null) author = guild.GetUserAsync(model.Id, CacheMode.CacheOnly).Result; if (author == null) - author = RestUser.Create(client, model); + author = RestUser.Create(client, guild, model, webhookId); return author; } + + public static bool IsNsfw(IChannel channel) => + IsNsfw(channel.Name); + public static bool IsNsfw(string channelName) => + channelName == "nsfw" || channelName.StartsWith("nsfw-"); } } diff --git a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs index 2c006834c..3a104dd9c 100644 --- a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs @@ -8,7 +8,7 @@ namespace Discord.Rest { /// Sends a message to this message channel. new Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null); -#if NETSTANDARD1_3 +#if FILESYSTEM /// Sends a file to this text channel, with an optional caption. new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, RequestOptions options = null); #endif diff --git a/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs index 0481d37ed..7291b591e 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs @@ -8,7 +8,7 @@ namespace Discord.Rest { public abstract class RestChannel : RestEntity, IChannel, IUpdateable { - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); internal RestChannel(BaseDiscordClient discord, ulong id) : base(discord, id) @@ -46,6 +46,7 @@ namespace Discord.Rest //IChannel string IChannel.Name => null; + bool IChannel.IsNsfw => ChannelHelper.IsNsfw(this); Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); //Overriden diff --git a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs index 75b331499..8a31da3f1 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs @@ -65,7 +65,7 @@ namespace Discord.Rest public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); -#if NETSTANDARD1_3 +#if FILESYSTEM public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); #endif @@ -73,7 +73,9 @@ namespace Discord.Rest => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); @@ -124,7 +126,7 @@ namespace Discord.Rest async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); -#if NETSTANDARD1_3 +#if FILESYSTEM async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); #endif diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs index a4b49b118..44c118fee 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs @@ -1,4 +1,5 @@ -using System; +using Discord.Audio; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; @@ -59,8 +60,7 @@ namespace Discord.Rest public RestUser GetUser(ulong id) { - RestGroupUser user; - if (_users.TryGetValue(id, out user)) + if (_users.TryGetValue(id, out RestGroupUser user)) return user; return null; } @@ -78,7 +78,7 @@ namespace Discord.Rest public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); -#if NETSTANDARD1_3 +#if FILESYSTEM public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); #endif @@ -86,7 +86,9 @@ namespace Discord.Rest => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); @@ -134,7 +136,7 @@ namespace Discord.Rest async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); -#if NETSTANDARD1_3 +#if FILESYSTEM async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); #endif @@ -145,6 +147,9 @@ namespace Discord.Rest IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); + //IAudioChannel + Task IAudioChannel.ConnectAsync(Action configAction) { throw new NotSupportedException(); } + //IChannel Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index 2687312a7..d7405fb4a 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -56,7 +56,7 @@ namespace Discord.Rest public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); -#if NETSTANDARD1_3 +#if FILESYSTEM public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); #endif @@ -64,7 +64,9 @@ namespace Discord.Rest => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); @@ -105,7 +107,7 @@ namespace Discord.Rest async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); -#if NETSTANDARD1_3 +#if FILESYSTEM async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); #endif diff --git a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs index e5330f29e..300ebd08d 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs @@ -40,8 +40,8 @@ namespace Discord.Rest private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; - //IVoiceChannel - Task IVoiceChannel.ConnectAsync() { throw new NotSupportedException(); } + //IAudioChannel + Task IAudioChannel.ConnectAsync(Action configAction) { throw new NotSupportedException(); } //IGuildChannel Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) diff --git a/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs index 664e9c9fc..775f2ea82 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs @@ -10,7 +10,7 @@ namespace Discord.Rest [DebuggerDisplay(@"{DebuggerDisplay,nq}")] internal class RestVirtualMessageChannel : RestEntity, IMessageChannel { - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public string Mention => MentionUtils.MentionChannel(Id); internal RestVirtualMessageChannel(BaseDiscordClient discord, ulong id) @@ -35,7 +35,7 @@ namespace Discord.Rest public Task SendMessageAsync(string text, bool isTTS, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); -#if NETSTANDARD1_3 +#if FILESYSTEM public Task SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); #endif @@ -43,7 +43,9 @@ namespace Discord.Rest => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); @@ -84,7 +86,7 @@ namespace Discord.Rest async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options); -#if NETSTANDARD1_3 +#if FILESYSTEM async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options); #endif @@ -97,6 +99,7 @@ namespace Discord.Rest //IChannel string IChannel.Name { get { throw new NotSupportedException(); } } + bool IChannel.IsNsfw { get { throw new NotSupportedException(); } } IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) { throw new NotSupportedException(); diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 195ae27d0..5cfb1e566 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -75,23 +75,16 @@ namespace Discord.Rest return await client.ApiClient.ModifyGuildEmbedAsync(guild.Id, apiArgs, options).ConfigureAwait(false); } - public static async Task ModifyChannelsAsync(IGuild guild, BaseDiscordClient client, - IEnumerable args, RequestOptions options) + public static async Task ReorderChannelsAsync(IGuild guild, BaseDiscordClient client, + IEnumerable args, RequestOptions options) { var apiArgs = args.Select(x => new API.Rest.ModifyGuildChannelsParams(x.Id, x.Position)); await client.ApiClient.ModifyGuildChannelsAsync(guild.Id, apiArgs, options).ConfigureAwait(false); } - public static async Task> ModifyRolesAsync(IGuild guild, BaseDiscordClient client, - IEnumerable args, RequestOptions options) + public static async Task> ReorderRolesAsync(IGuild guild, BaseDiscordClient client, + IEnumerable args, RequestOptions options) { - var apiArgs = args.Select(x => new API.Rest.ModifyGuildRolesParams(x.Id) - { - Color = x.Color.IsSpecified ? x.Color.Value.RawValue : Optional.Create(), - Hoist = x.Hoist, - Name = x.Name, - Permissions = x.Permissions.IsSpecified ? x.Permissions.Value.RawValue : Optional.Create(), - Position = x.Position - }); + var apiArgs = args.Select(x => new API.Rest.ModifyGuildRolesParams(x.Id, x.Position)); return await client.ApiClient.ModifyGuildRolesAsync(guild.Id, apiArgs, options).ConfigureAwait(false); } public static async Task LeaveAsync(IGuild guild, BaseDiscordClient client, @@ -114,9 +107,9 @@ namespace Discord.Rest } public static async Task AddBanAsync(IGuild guild, BaseDiscordClient client, - ulong userId, int pruneDays, RequestOptions options) + ulong userId, int pruneDays, string reason, RequestOptions options) { - var args = new CreateGuildBanParams { DeleteMessageDays = pruneDays }; + var args = new CreateGuildBanParams { DeleteMessageDays = pruneDays, Reason = reason }; await client.ApiClient.CreateGuildBanAsync(guild.Id, userId, args, options).ConfigureAwait(false); } public static async Task RemoveBanAsync(IGuild guild, BaseDiscordClient client, diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 0622df6ce..11971a5c1 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -3,10 +3,10 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; +using System.Linq; using System.Threading.Tasks; -using Model = Discord.API.Guild; using EmbedModel = Discord.API.GuildEmbed; -using System.Linq; +using Model = Discord.API.Guild; namespace Discord.Rest { @@ -14,7 +14,7 @@ namespace Discord.Rest public class RestGuild : RestEntity, IGuild, IUpdateable { private ImmutableDictionary _roles; - private ImmutableArray _emojis; + private ImmutableArray _emotes; private ImmutableArray _features; public string Name { get; private set; } @@ -32,14 +32,14 @@ namespace Discord.Rest public string SplashId { get; private set; } internal bool Available { get; private set; } - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public ulong DefaultChannelId => Id; public string IconUrl => CDN.GetGuildIconUrl(Id, IconId); public string SplashUrl => CDN.GetGuildSplashUrl(Id, SplashId); public RestRole EveryoneRole => GetRole(Id); public IReadOnlyCollection Roles => _roles.ToReadOnlyCollection(); - public IReadOnlyCollection Emojis => _emojis; + public IReadOnlyCollection Emotes => _emotes; public IReadOnlyCollection Features => _features; internal RestGuild(BaseDiscordClient client, ulong id) @@ -69,13 +69,13 @@ namespace Discord.Rest if (model.Emojis != null) { - var emojis = ImmutableArray.CreateBuilder(model.Emojis.Length); + var emotes = ImmutableArray.CreateBuilder(model.Emojis.Length); for (int i = 0; i < model.Emojis.Length; i++) - emojis.Add(model.Emojis[i].ToEntity()); - _emojis = emojis.ToImmutableArray(); + emotes.Add(model.Emojis[i].ToEntity()); + _emotes = emotes.ToImmutableArray(); } else - _emojis = ImmutableArray.Create(); + _emotes = ImmutableArray.Create(); if (model.Features != null) _features = model.Features.ToImmutableArray(); @@ -114,14 +114,14 @@ namespace Discord.Rest var model = await GuildHelper.ModifyEmbedAsync(this, Discord, func, options).ConfigureAwait(false); Update(model); } - public async Task ModifyChannelsAsync(IEnumerable args, RequestOptions options = null) + public async Task ReorderChannelsAsync(IEnumerable args, RequestOptions options = null) { var arr = args.ToArray(); - await GuildHelper.ModifyChannelsAsync(this, Discord, arr, options); + await GuildHelper.ReorderChannelsAsync(this, Discord, arr, options); } - public async Task ModifyRolesAsync(IEnumerable args, RequestOptions options = null) + public async Task ReorderRolesAsync(IEnumerable args, RequestOptions options = null) { - var models = await GuildHelper.ModifyRolesAsync(this, Discord, args, options).ConfigureAwait(false); + var models = await GuildHelper.ReorderRolesAsync(this, Discord, args, options).ConfigureAwait(false); foreach (var model in models) { var role = GetRole(model.Id); @@ -137,10 +137,10 @@ namespace Discord.Rest public Task> GetBansAsync(RequestOptions options = null) => GuildHelper.GetBansAsync(this, Discord, options); - public Task AddBanAsync(IUser user, int pruneDays = 0, RequestOptions options = null) - => GuildHelper.AddBanAsync(this, Discord, user.Id, pruneDays, options); - public Task AddBanAsync(ulong userId, int pruneDays = 0, RequestOptions options = null) - => GuildHelper.AddBanAsync(this, Discord, userId, pruneDays, options); + public Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null) + => GuildHelper.AddBanAsync(this, Discord, user.Id, pruneDays, reason, options); + public Task AddBanAsync(ulong userId, int pruneDays = 0, string reason = null, RequestOptions options = null) + => GuildHelper.AddBanAsync(this, Discord, userId, pruneDays, reason, options); public Task RemoveBanAsync(IUser user, RequestOptions options = null) => GuildHelper.RemoveBanAsync(this, Discord, user.Id, options); @@ -188,14 +188,11 @@ namespace Discord.Rest var channel = await GuildHelper.GetChannelAsync(this, Discord, DefaultChannelId, options).ConfigureAwait(false); return channel as RestTextChannel; } - public async Task GetEmbedChannelAsync(RequestOptions options = null) + public async Task GetEmbedChannelAsync(RequestOptions options = null) { var embedId = EmbedChannelId; if (embedId.HasValue) - { - var channel = await GuildHelper.GetChannelAsync(this, Discord, embedId.Value, options).ConfigureAwait(false); - return channel as RestVoiceChannel; - } + return await GuildHelper.GetChannelAsync(this, Discord, embedId.Value, options).ConfigureAwait(false); return null; } public Task CreateTextChannelAsync(string name, RequestOptions options = null) @@ -216,8 +213,7 @@ namespace Discord.Rest //Roles public RestRole GetRole(ulong id) { - RestRole value; - if (_roles.TryGetValue(id, out value)) + if (_roles.TryGetValue(id, out RestRole value)) return value; return null; } @@ -311,7 +307,7 @@ namespace Discord.Rest else return null; } - async Task IGuild.GetEmbedChannelAsync(CacheMode mode, RequestOptions options) + async Task IGuild.GetEmbedChannelAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) return await GetEmbedChannelAsync(options).ConfigureAwait(false); diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs index 12601b72e..6bc9cea7a 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs @@ -14,7 +14,7 @@ namespace Discord.Rest public bool IsOwner { get; private set; } public GuildPermissions Permissions { get; private set; } - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public string IconUrl => CDN.GetGuildIconUrl(Id, _iconId); internal RestUserGuild(BaseDiscordClient discord, ulong id) diff --git a/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs b/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs index 9e2249bff..900d1f0ac 100644 --- a/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs +++ b/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs @@ -58,8 +58,7 @@ namespace Discord.Rest { if (Guild != null) return Guild; - var guildChannel = Channel as IGuildChannel; - if (guildChannel != null) + if (Channel is IGuildChannel guildChannel) return guildChannel.Guild; //If it fails, it'll still return this exception throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); } diff --git a/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs b/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs index 9298b42e9..42aeb40aa 100644 --- a/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs +++ b/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs @@ -29,7 +29,7 @@ namespace Discord.Rest internal void Update(Model model) { base.Update(model); - Inviter = RestUser.Create(Discord, model.Inviter); + Inviter = model.Inviter != null ? RestUser.Create(Discord, model.Inviter) : null; IsRevoked = model.Revoked; IsTemporary = model.Temporary; MaxAge = model.MaxAge != 0 ? model.MaxAge : (int?)null; diff --git a/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs index 8890df683..7b0285891 100644 --- a/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs +++ b/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs @@ -7,24 +7,82 @@ namespace Discord public class EmbedBuilder { private readonly Embed _embed; - private readonly List _fields; + + public const int MaxFieldCount = 25; + public const int MaxTitleLength = 256; + public const int MaxDescriptionLength = 2048; + public const int MaxEmbedLength = 6000; // user bot limit is 2000, but we don't validate that here. public EmbedBuilder() { - _embed = new Embed("rich"); - _fields = new List(); + _embed = new Embed(EmbedType.Rich); + Fields = new List(); + } + + public string Title + { + get => _embed.Title; + set + { + if (value?.Length > MaxTitleLength) throw new ArgumentException($"Title length must be less than or equal to {MaxTitleLength}.", nameof(Title)); + _embed.Title = value; + } + } + + public string Description + { + get => _embed.Description; + set + { + if (value?.Length > MaxDescriptionLength) throw new ArgumentException($"Description length must be less than or equal to {MaxDescriptionLength}.", nameof(Description)); + _embed.Description = value; + } } - public string Title { get { return _embed.Title; } set { _embed.Title = value; } } - public string Description { get { return _embed.Description; } set { _embed.Description = value; } } - public string Url { get { return _embed.Url; } set { _embed.Url = value; } } - public string ThumbnailUrl { get { return _embed.Thumbnail?.Url; } set { _embed.Thumbnail = new EmbedThumbnail(value, null, null, null); } } - public string ImageUrl { get { return _embed.Image?.Url; } set { _embed.Image = new EmbedImage(value, null, null, null); } } - public DateTimeOffset? Timestamp { get { return _embed.Timestamp; } set { _embed.Timestamp = value; } } - public Color? Color { get { return _embed.Color; } set { _embed.Color = value; } } + public string Url + { + get => _embed.Url; + set + { + if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(Url)); + _embed.Url = value; + } + } + public string ThumbnailUrl + { + get => _embed.Thumbnail?.Url; + set + { + if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(ThumbnailUrl)); + _embed.Thumbnail = new EmbedThumbnail(value, null, null, null); + } + } + public string ImageUrl + { + get => _embed.Image?.Url; + set + { + if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(ImageUrl)); + _embed.Image = new EmbedImage(value, null, null, null); + } + } + public DateTimeOffset? Timestamp { get => _embed.Timestamp; set { _embed.Timestamp = value; } } + public Color? Color { get => _embed.Color; set { _embed.Color = value; } } public EmbedAuthorBuilder Author { get; set; } public EmbedFooterBuilder Footer { get; set; } + private List _fields; + public List Fields + { + get => _fields; + set + { + + if (value == null) throw new ArgumentNullException("Cannot set an embed builder's fields collection to null", nameof(Fields)); + if (value.Count > MaxFieldCount) throw new ArgumentException($"Field count must be less than or equal to {MaxFieldCount}.", nameof(Fields)); + _fields = value; + } + } public EmbedBuilder WithTitle(string title) { @@ -79,6 +137,17 @@ namespace Discord Author = author; return this; } + public EmbedBuilder WithAuthor(string name, string iconUrl = null, string url = null) + { + var author = new EmbedAuthorBuilder + { + Name = name, + IconUrl = iconUrl, + Url = url + }; + Author = author; + return this; + } public EmbedBuilder WithFooter(EmbedFooterBuilder footer) { Footer = footer; @@ -91,16 +160,60 @@ namespace Discord Footer = footer; return this; } + public EmbedBuilder WithFooter(string text, string iconUrl = null) + { + var footer = new EmbedFooterBuilder + { + Text = text, + IconUrl = iconUrl + }; + Footer = footer; + return this; + } + public EmbedBuilder AddField(string name, object value) + { + var field = new EmbedFieldBuilder() + .WithIsInline(false) + .WithName(name) + .WithValue(value); + AddField(field); + return this; + } + public EmbedBuilder AddInlineField(string name, object value) + { + var field = new EmbedFieldBuilder() + .WithIsInline(true) + .WithName(name) + .WithValue(value); + AddField(field); + return this; + } public EmbedBuilder AddField(EmbedFieldBuilder field) { - _fields.Add(field); + if (Fields.Count >= MaxFieldCount) + { + throw new ArgumentException($"Field count must be less than or equal to {MaxFieldCount}.", nameof(field)); + } + + Fields.Add(field); return this; } public EmbedBuilder AddField(Action action) { var field = new EmbedFieldBuilder(); action(field); + this.AddField(field); + return this; + } + public EmbedBuilder AddField(string title, string text, bool inline = false) + { + var field = new EmbedFieldBuilder + { + Name = title, + Value = text, + IsInline = inline + }; _fields.Add(field); return this; } @@ -109,10 +222,16 @@ namespace Discord { _embed.Footer = Footer?.Build(); _embed.Author = Author?.Build(); - var fields = ImmutableArray.CreateBuilder(_fields.Count); - for (int i = 0; i < _fields.Count; i++) - fields.Add(_fields[i].Build()); + var fields = ImmutableArray.CreateBuilder(Fields.Count); + for (int i = 0; i < Fields.Count; i++) + fields.Add(Fields[i].Build()); _embed.Fields = fields.ToImmutable(); + + if (_embed.Length > MaxEmbedLength) + { + throw new InvalidOperationException($"Total embed length must be less than or equal to {MaxEmbedLength}"); + } + return _embed; } public static implicit operator Embed(EmbedBuilder builder) => builder?.Build(); @@ -122,9 +241,32 @@ namespace Discord { private EmbedField _field; - public string Name { get { return _field.Name; } set { _field.Name = value; } } - public object Value { get { return _field.Value; } set { _field.Value = value.ToString(); } } - public bool IsInline { get { return _field.Inline; } set { _field.Inline = value; } } + public const int MaxFieldNameLength = 256; + public const int MaxFieldValueLength = 1024; + + public string Name + { + get => _field.Name; + set + { + if (string.IsNullOrEmpty(value)) throw new ArgumentException($"Field name must not be null or empty.", nameof(Name)); + if (value.Length > MaxFieldNameLength) throw new ArgumentException($"Field name length must be less than or equal to {MaxFieldNameLength}.", nameof(Name)); + _field.Name = value; + } + } + + public object Value + { + get => _field.Value; + set + { + var stringValue = value?.ToString(); + if (string.IsNullOrEmpty(stringValue)) throw new ArgumentException($"Field value must not be null or empty.", nameof(Value)); + if (stringValue.Length > MaxFieldValueLength) throw new ArgumentException($"Field value length must be less than or equal to {MaxFieldValueLength}.", nameof(Value)); + _field.Value = stringValue; + } + } + public bool IsInline { get => _field.Inline; set { _field.Inline = value; } } public EmbedFieldBuilder() { @@ -155,9 +297,35 @@ namespace Discord { private EmbedAuthor _author; - public string Name { get { return _author.Name; } set { _author.Name = value; } } - public string Url { get { return _author.Url; } set { _author.Url = value; } } - public string IconUrl { get { return _author.IconUrl; } set { _author.IconUrl = value; } } + public const int MaxAuthorNameLength = 256; + + public string Name + { + get => _author.Name; + set + { + if (value?.Length > MaxAuthorNameLength) throw new ArgumentException($"Author name length must be less than or equal to {MaxAuthorNameLength}.", nameof(Name)); + _author.Name = value; + } + } + public string Url + { + get => _author.Url; + set + { + if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(Url)); + _author.Url = value; + } + } + public string IconUrl + { + get => _author.IconUrl; + set + { + if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(IconUrl)); + _author.IconUrl = value; + } + } public EmbedAuthorBuilder() { @@ -188,8 +356,26 @@ namespace Discord { private EmbedFooter _footer; - public string Text { get { return _footer.Text; } set { _footer.Text = value; } } - public string IconUrl { get { return _footer.IconUrl; } set { _footer.IconUrl = value; } } + public const int MaxFooterTextLength = 2048; + + public string Text + { + get => _footer.Text; + set + { + if (value?.Length > MaxFooterTextLength) throw new ArgumentException($"Footer text length must be less than or equal to {MaxFooterTextLength}.", nameof(Text)); + _footer.Text = value; + } + } + public string IconUrl + { + get => _footer.IconUrl; + set + { + if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(IconUrl)); + _footer.IconUrl = value; + } + } public EmbedFooterBuilder() { diff --git a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs index 4f8d52263..ccb683d1f 100644 --- a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -28,19 +28,14 @@ namespace Discord.Rest await client.ApiClient.DeleteMessageAsync(msg.Channel.Id, msg.Id, options).ConfigureAwait(false); } - public static async Task AddReactionAsync(IMessage msg, Emoji emoji, BaseDiscordClient client, RequestOptions options) - => await AddReactionAsync(msg, $"{emoji.Name}:{emoji.Id}", client, options).ConfigureAwait(false); - public static async Task AddReactionAsync(IMessage msg, string emoji, BaseDiscordClient client, RequestOptions options) + public static async Task AddReactionAsync(IMessage msg, IEmote emote, BaseDiscordClient client, RequestOptions options) { - await client.ApiClient.AddReactionAsync(msg.Channel.Id, msg.Id, emoji, options).ConfigureAwait(false); + await client.ApiClient.AddReactionAsync(msg.Channel.Id, msg.Id, emote is Emote e ? $"{e.Name}:{e.Id}" : emote.Name, options).ConfigureAwait(false); } - public static async Task RemoveReactionAsync(IMessage msg, IUser user, Emoji emoji, BaseDiscordClient client, RequestOptions options) - => await RemoveReactionAsync(msg, user, emoji.Id == 0 ? emoji.Name : $"{emoji.Name}:{emoji.Id}", client, options).ConfigureAwait(false); - public static async Task RemoveReactionAsync(IMessage msg, IUser user, string emoji, BaseDiscordClient client, - RequestOptions options) + public static async Task RemoveReactionAsync(IMessage msg, IUser user, IEmote emote, BaseDiscordClient client, RequestOptions options) { - await client.ApiClient.RemoveReactionAsync(msg.Channel.Id, msg.Id, user.Id, emoji, options).ConfigureAwait(false); + await client.ApiClient.RemoveReactionAsync(msg.Channel.Id, msg.Id, user.Id, emote is Emote e ? $"{e.Name}:{e.Id}" : emote.Name, options).ConfigureAwait(false); } public static async Task RemoveAllReactionsAsync(IMessage msg, BaseDiscordClient client, RequestOptions options) @@ -53,7 +48,7 @@ namespace Discord.Rest { var args = new GetReactionUsersParams(); func(args); - return (await client.ApiClient.GetReactionUsersAsync(msg.Channel.Id, msg.Id, emoji, args, options).ConfigureAwait(false)).Select(u => u as IUser).Where(u => u != null).ToImmutableArray(); + return (await client.ApiClient.GetReactionUsersAsync(msg.Channel.Id, msg.Id, emoji, args, options).ConfigureAwait(false)).Select(u => RestUser.Create(client, u)).ToImmutableArray(); } public static async Task PinAsync(IMessage msg, BaseDiscordClient client, @@ -67,7 +62,7 @@ namespace Discord.Rest await client.ApiClient.RemovePinAsync(msg.Channel.Id, msg.Id, options).ConfigureAwait(false); } - public static ImmutableArray ParseTags(string text, IMessageChannel channel, IGuild guild, ImmutableArray userMentions) + public static ImmutableArray ParseTags(string text, IMessageChannel channel, IGuild guild, IReadOnlyCollection userMentions) { var tags = ImmutableArray.CreateBuilder(); @@ -80,8 +75,7 @@ namespace Discord.Rest if (endIndex == -1) break; string content = text.Substring(index, endIndex - index + 1); - ulong id; - if (MentionUtils.TryParseUser(content, out id)) + if (MentionUtils.TryParseUser(content, out ulong id)) { IUser mentionedUser = null; foreach (var mention in userMentions) @@ -110,11 +104,12 @@ namespace Discord.Rest mentionedRole = guild.GetRole(id); tags.Add(new Tag(TagType.RoleMention, index, content.Length, id, mentionedRole)); } - else + else if (Emote.TryParse(content, out var emoji)) + tags.Add(new Tag(TagType.Emoji, index, content.Length, emoji.Id, emoji)); + else //Bad Tag { - Emoji emoji; - if (Emoji.TryParse(content, out emoji)) - tags.Add(new Tag(TagType.Emoji, index, content.Length, id, emoji)); + index = index + 1; + continue; } index = endIndex + 1; } @@ -125,7 +120,9 @@ namespace Discord.Rest index = text.IndexOf("@everyone", index); if (index == -1) break; - tags.Add(new Tag(TagType.EveryoneMention, index, "@everyone".Length, 0, null)); + var tagIndex = FindIndex(tags, index); + if (tagIndex.HasValue) + tags.Insert(tagIndex.Value, new Tag(TagType.EveryoneMention, index, "@everyone".Length, 0, null)); index++; } @@ -135,12 +132,27 @@ namespace Discord.Rest index = text.IndexOf("@here", index); if (index == -1) break; - tags.Add(new Tag(TagType.HereMention, index, "@here".Length, 0, null)); + var tagIndex = FindIndex(tags, index); + if (tagIndex.HasValue) + tags.Insert(tagIndex.Value, new Tag(TagType.HereMention, index, "@here".Length, 0, null)); index++; } return tags.ToImmutable(); } + private static int? FindIndex(IReadOnlyList tags, int index) + { + int i = 0; + for (; i < tags.Count; i++) + { + var tag = tags[i]; + if (index < tag.Index) + break; //Position before this tag + } + if (i > 0 && index < tags[i - 1].Index + tags[i - 1].Length) + return null; //Overlaps tag before this + return i; + } public static ImmutableArray FilterTagsByKey(TagType type, ImmutableArray tags) { return tags @@ -156,5 +168,16 @@ namespace Discord.Rest .Where(x => x != null) .ToImmutableArray(); } + + public static MessageSource GetSource(Model msg) + { + if (msg.Type != MessageType.Default) + return MessageSource.System; + else if (msg.WebhookId.IsSpecified) + return MessageSource.Webhook; + else if (msg.Author.GetValueOrDefault()?.Bot.GetValueOrDefault(false) == true) + return MessageSource.Bot; + return MessageSource.User; + } } } diff --git a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs index b50edf03b..590886886 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs @@ -13,10 +13,11 @@ namespace Discord.Rest public IMessageChannel Channel { get; } public IUser Author { get; } + public MessageSource Source { get; } public string Content { get; private set; } - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public virtual bool IsTTS => false; public virtual bool IsPinned => false; public virtual DateTimeOffset? EditedTimestamp => null; @@ -26,16 +27,15 @@ namespace Discord.Rest public virtual IReadOnlyCollection MentionedRoleIds => ImmutableArray.Create(); public virtual IReadOnlyCollection MentionedUsers => ImmutableArray.Create(); public virtual IReadOnlyCollection Tags => ImmutableArray.Create(); - public virtual ulong? WebhookId => null; - public bool IsWebhook => WebhookId != null; public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); - internal RestMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author) + internal RestMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author, MessageSource source) : base(discord, id) { Channel = channel; Author = author; + Source = source; } internal static RestMessage Create(BaseDiscordClient discord, IMessageChannel channel, IUser author, Model model) { diff --git a/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs b/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs index 933833d56..05c817935 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs @@ -4,19 +4,24 @@ namespace Discord.Rest { public class RestReaction : IReaction { - public Emoji Emoji { get; } + public IEmote Emote { get; } public int Count { get; } public bool Me { get; } - internal RestReaction(Emoji emoji, int count, bool me) + internal RestReaction(IEmote emote, int count, bool me) { - Emoji = emoji; + Emote = emote; Count = count; Me = me; } internal static RestReaction Create(Model model) { - return new RestReaction(new Emoji(model.Emoji.Id, model.Emoji.Name), model.Count, model.Me); + IEmote emote; + if (model.Emoji.Id.HasValue) + emote = new Emote(model.Emoji.Id.Value, model.Emoji.Name); + else + emote = new Emoji(model.Emoji.Name); + return new RestReaction(emote, model.Count, model.Me); } } } diff --git a/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs index a5ced8c8f..b9dda08ae 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs @@ -9,7 +9,7 @@ namespace Discord.Rest public MessageType Type { get; private set; } internal RestSystemMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author) - : base(discord, id, channel, author) + : base(discord, id, channel, author, MessageSource.System) { } internal new static RestSystemMessage Create(BaseDiscordClient discord, IMessageChannel channel, IUser author, Model model) diff --git a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs index ee806dbc1..c79c67b38 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs @@ -13,7 +13,6 @@ namespace Discord.Rest { private bool _isMentioningEveryone, _isTTS, _isPinned; private long? _editedTimestampTicks; - private ulong? _webhookId; private ImmutableArray _attachments; private ImmutableArray _embeds; private ImmutableArray _tags; @@ -21,7 +20,6 @@ namespace Discord.Rest public override bool IsTTS => _isTTS; public override bool IsPinned => _isPinned; - public override ulong? WebhookId => _webhookId; public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks); public override IReadOnlyCollection Attachments => _attachments; public override IReadOnlyCollection Embeds => _embeds; @@ -29,15 +27,15 @@ namespace Discord.Rest public override IReadOnlyCollection MentionedRoleIds => MessageHelper.FilterTagsByKey(TagType.RoleMention, _tags); public override IReadOnlyCollection MentionedUsers => MessageHelper.FilterTagsByValue(TagType.UserMention, _tags); public override IReadOnlyCollection Tags => _tags; - public IReadOnlyDictionary Reactions => _reactions.ToDictionary(x => x.Emoji, x => x.Count); + public IReadOnlyDictionary Reactions => _reactions.ToDictionary(x => x.Emote, x => new ReactionMetadata { ReactionCount = x.Count, IsMe = x.Me }); - internal RestUserMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author) - : base(discord, id, channel, author) + internal RestUserMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author, MessageSource source) + : base(discord, id, channel, author, source) { } - internal new static RestUserMessage Create(BaseDiscordClient discord, IMessageChannel channel, IUser author, Model model) + internal static new RestUserMessage Create(BaseDiscordClient discord, IMessageChannel channel, IUser author, Model model) { - var entity = new RestUserMessage(discord, model.Id, channel, author); + var entity = new RestUserMessage(discord, model.Id, channel, author, MessageHelper.GetSource(model)); entity.Update(model); return entity; } @@ -54,8 +52,6 @@ namespace Discord.Rest _editedTimestampTicks = model.EditedTimestamp.Value?.UtcTicks; if (model.MentionEveryone.IsSpecified) _isMentioningEveryone = model.MentionEveryone.Value; - if (model.WebhookId.IsSpecified) - _webhookId = model.WebhookId.Value; if (model.Attachments.IsSpecified) { @@ -134,21 +130,15 @@ namespace Discord.Rest Update(model); } - public Task AddReactionAsync(Emoji emoji, RequestOptions options = null) - => MessageHelper.AddReactionAsync(this, emoji, Discord, options); - public Task AddReactionAsync(string emoji, RequestOptions options = null) - => MessageHelper.AddReactionAsync(this, emoji, Discord, options); - - public Task RemoveReactionAsync(Emoji emoji, IUser user, RequestOptions options = null) - => MessageHelper.RemoveReactionAsync(this, user, emoji, Discord, options); - public Task RemoveReactionAsync(string emoji, IUser user, RequestOptions options = null) - => MessageHelper.RemoveReactionAsync(this, user, emoji, Discord, options); - + public Task AddReactionAsync(IEmote emote, RequestOptions options = null) + => MessageHelper.AddReactionAsync(this, emote, Discord, options); + public Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions options = null) + => MessageHelper.RemoveReactionAsync(this, user, emote, Discord, options); public Task RemoveAllReactionsAsync(RequestOptions options = null) => MessageHelper.RemoveAllReactionsAsync(this, Discord, options); public Task> GetReactionUsersAsync(string emoji, int limit = 100, ulong? afterUserId = null, RequestOptions options = null) - => MessageHelper.GetReactionUsersAsync(this, emoji, x => { x.Limit = limit; x.AfterUserId = afterUserId.HasValue ? afterUserId.Value : Optional.Create(); }, Discord, options); + => MessageHelper.GetReactionUsersAsync(this, emoji, x => { x.Limit = limit; x.AfterUserId = afterUserId ?? Optional.Create(); }, Discord, options); public Task PinAsync(RequestOptions options = null) diff --git a/src/Discord.Net.Rest/Entities/RestApplication.cs b/src/Discord.Net.Rest/Entities/RestApplication.cs index f81e4cd7b..827c33cf7 100644 --- a/src/Discord.Net.Rest/Entities/RestApplication.cs +++ b/src/Discord.Net.Rest/Entities/RestApplication.cs @@ -17,7 +17,7 @@ namespace Discord.Rest public IUser Owner { get; private set; } - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public string IconUrl => CDN.GetApplicationIconUrl(Id, _iconId); internal RestApplication(BaseDiscordClient discord, ulong id) diff --git a/src/Discord.Net.Rest/Entities/RestEntity.cs b/src/Discord.Net.Rest/Entities/RestEntity.cs index f893600ba..2b1bb888c 100644 --- a/src/Discord.Net.Rest/Entities/RestEntity.cs +++ b/src/Discord.Net.Rest/Entities/RestEntity.cs @@ -5,7 +5,7 @@ namespace Discord.Rest public abstract class RestEntity : IEntity where T : IEquatable { - public BaseDiscordClient Discord { get; } + internal BaseDiscordClient Discord { get; } public T Id { get; } internal RestEntity(BaseDiscordClient discord, T id) diff --git a/src/Discord.Net.Rest/Entities/Roles/RestRole.cs b/src/Discord.Net.Rest/Entities/Roles/RestRole.cs index dfdbb150d..9807b8357 100644 --- a/src/Discord.Net.Rest/Entities/Roles/RestRole.cs +++ b/src/Discord.Net.Rest/Entities/Roles/RestRole.cs @@ -17,7 +17,7 @@ namespace Discord.Rest public GuildPermissions Permissions { get; private set; } public int Position { get; private set; } - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public bool IsEveryone => Id == Guild.Id; public string Mention => MentionUtils.MentionRole(Id); diff --git a/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs b/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs index 0081351f0..d570f078b 100644 --- a/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs +++ b/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using Model = Discord.API.Role; +using BulkParams = Discord.API.Rest.ModifyGuildRolesParams; namespace Discord.Rest { @@ -23,10 +24,17 @@ namespace Discord.Rest Hoist = args.Hoist, Mentionable = args.Mentionable, Name = args.Name, - Permissions = args.Permissions.IsSpecified ? args.Permissions.Value.RawValue : Optional.Create(), - Position = args.Position + Permissions = args.Permissions.IsSpecified ? args.Permissions.Value.RawValue : Optional.Create() }; - return await client.ApiClient.ModifyGuildRoleAsync(role.Guild.Id, role.Id, apiArgs, options).ConfigureAwait(false); + var model = await client.ApiClient.ModifyGuildRoleAsync(role.Guild.Id, role.Id, apiArgs, options).ConfigureAwait(false); + + if (args.Position.IsSpecified) + { + var bulkArgs = new[] { new BulkParams(role.Id, args.Position.Value) }; + await client.ApiClient.ModifyGuildRolesAsync(role.Guild.Id, bulkArgs, options).ConfigureAwait(false); + model.Position = args.Position.Value; + } + return model; } } } diff --git a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs index 8de42608d..2fce5f619 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs @@ -50,9 +50,12 @@ namespace Discord.Rest _joinedAtTicks = model.JoinedAt.Value.UtcTicks; if (model.Nick.IsSpecified) Nickname = model.Nick.Value; - IsDeafened = model.Deaf; - IsMuted = model.Mute; - UpdateRoles(model.Roles); + if (model.Deaf.IsSpecified) + IsDeafened = model.Deaf.Value; + if (model.Mute.IsSpecified) + IsMuted = model.Mute.Value; + if (model.Roles.IsSpecified) + UpdateRoles(model.Roles.Value); } private void UpdateRoles(ulong[] roleIds) { @@ -82,8 +85,20 @@ namespace Discord.Rest else if (args.RoleIds.IsSpecified) UpdateRoles(args.RoleIds.Value.ToArray()); } - public Task KickAsync(RequestOptions options = null) - => UserHelper.KickAsync(this, Discord, options); + public Task KickAsync(string reason = null, RequestOptions options = null) + => UserHelper.KickAsync(this, Discord, reason, options); + /// + public Task AddRoleAsync(IRole role, RequestOptions options = null) + => AddRolesAsync(new[] { role }, options); + /// + public Task AddRolesAsync(IEnumerable roles, RequestOptions options = null) + => UserHelper.AddRolesAsync(this, Discord, roles, options); + /// + public Task RemoveRoleAsync(IRole role, RequestOptions options = null) + => RemoveRolesAsync(new[] { role }, options); + /// + public Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null) + => UserHelper.RemoveRolesAsync(this, Discord, roles, options); public ChannelPermissions GetPermissions(IGuildChannel channel) { diff --git a/src/Discord.Net.Rest/Entities/Users/RestUser.cs b/src/Discord.Net.Rest/Entities/Users/RestUser.cs index 7e3cdb4fd..df7922dd7 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -13,20 +13,26 @@ namespace Discord.Rest public ushort DiscriminatorValue { get; private set; } public string AvatarId { get; private set; } - public string GetAvatarUrl(AvatarFormat format = AvatarFormat.Png, ushort size = 128) => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); public virtual Game? Game => null; - public virtual UserStatus Status => UserStatus.Unknown; + public virtual UserStatus Status => UserStatus.Offline; + public virtual bool IsWebhook => false; internal RestUser(BaseDiscordClient discord, ulong id) : base(discord, id) { } internal static RestUser Create(BaseDiscordClient discord, Model model) + => Create(discord, null, model, null); + internal static RestUser Create(BaseDiscordClient discord, IGuild guild, Model model, ulong? webhookId) { - var entity = new RestUser(discord, model.Id); + RestUser entity; + if (webhookId.HasValue) + entity = new RestWebhookUser(discord, guild, model.Id, webhookId.Value); + else + entity = new RestUser(discord, model.Id); entity.Update(model); return entity; } @@ -41,16 +47,19 @@ namespace Discord.Rest if (model.Username.IsSpecified) Username = model.Username.Value; } - + public virtual async Task UpdateAsync(RequestOptions options = null) { var model = await Discord.ApiClient.GetUserAsync(Id, options).ConfigureAwait(false); Update(model); } - public Task CreateDMChannelAsync(RequestOptions options = null) + public Task GetOrCreateDMChannelAsync(RequestOptions options = null) => UserHelper.CreateDMChannelAsync(this, Discord, options); + public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + public override string ToString() => $"{Username}#{Discriminator}"; private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; @@ -62,9 +71,7 @@ namespace Discord.Rest => Discord.ApiClient.RemoveRelationshipAsync(Id, options); //IUser - Task IUser.GetDMChannelAsync(CacheMode mode, RequestOptions options) - => Task.FromResult(null); - async Task IUser.CreateDMChannelAsync(RequestOptions options) - => await CreateDMChannelAsync(options).ConfigureAwait(false); + async Task IUser.GetOrCreateDMChannelAsync(RequestOptions options) + => await GetOrCreateDMChannelAsync(options); } } diff --git a/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs b/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs new file mode 100644 index 000000000..bb44f2777 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.User; + +namespace Discord.Rest +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestWebhookUser : RestUser, IWebhookUser + { + public ulong WebhookId { get; } + internal IGuild Guild { get; } + + public override bool IsWebhook => true; + public ulong GuildId => Guild.Id; + + internal RestWebhookUser(BaseDiscordClient discord, IGuild guild, ulong id, ulong webhookId) + : base(discord, id) + { + Guild = guild; + WebhookId = webhookId; + } + internal static RestWebhookUser Create(BaseDiscordClient discord, IGuild guild, Model model, ulong webhookId) + { + var entity = new RestWebhookUser(discord, guild, model.Id, webhookId); + entity.Update(model); + return entity; + } + + //IGuildUser + IGuild IGuildUser.Guild + { + get + { + if (Guild != null) + return Guild; + throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); + } + } + IReadOnlyCollection IGuildUser.RoleIds => ImmutableArray.Create(); + DateTimeOffset? IGuildUser.JoinedAt => null; + string IGuildUser.Nickname => null; + GuildPermissions IGuildUser.GuildPermissions => GuildPermissions.Webhook; + + ChannelPermissions IGuildUser.GetPermissions(IGuildChannel channel) => Permissions.ToChannelPerms(channel, GuildPermissions.Webhook.RawValue); + Task IGuildUser.KickAsync(string reason, RequestOptions options) + { + throw new NotSupportedException("Webhook users cannot be kicked."); + } + Task IGuildUser.ModifyAsync(Action func, RequestOptions options) + { + throw new NotSupportedException("Webhook users cannot be modified."); + } + + Task IGuildUser.AddRoleAsync(IRole role, RequestOptions options) + { + throw new NotSupportedException("Roles are not supported on webhook users."); + } + Task IGuildUser.AddRolesAsync(IEnumerable roles, RequestOptions options) + { + throw new NotSupportedException("Roles are not supported on webhook users."); + } + Task IGuildUser.RemoveRoleAsync(IRole role, RequestOptions options) + { + throw new NotSupportedException("Roles are not supported on webhook users."); + } + Task IGuildUser.RemoveRolesAsync(IEnumerable roles, RequestOptions options) + { + throw new NotSupportedException("Roles are not supported on webhook users."); + } + + //IVoiceState + bool IVoiceState.IsDeafened => false; + bool IVoiceState.IsMuted => false; + bool IVoiceState.IsSelfDeafened => false; + bool IVoiceState.IsSelfMuted => false; + bool IVoiceState.IsSuppressed => false; + IVoiceChannel IVoiceState.VoiceChannel => null; + string IVoiceState.VoiceSessionId => null; + } +} diff --git a/src/Discord.Net.Rest/Entities/Users/UserHelper.cs b/src/Discord.Net.Rest/Entities/Users/UserHelper.cs index 5189851fd..562cfaae8 100644 --- a/src/Discord.Net.Rest/Entities/Users/UserHelper.cs +++ b/src/Discord.Net.Rest/Entities/Users/UserHelper.cs @@ -1,5 +1,6 @@ using Discord.API.Rest; using System; +using System.Collections.Generic; using System.Threading.Tasks; using Model = Discord.API.User; using ImageModel = Discord.API.Image; @@ -52,9 +53,9 @@ namespace Discord.Rest } public static async Task KickAsync(IGuildUser user, BaseDiscordClient client, - RequestOptions options) + string reason, RequestOptions options) { - await client.ApiClient.RemoveGuildMemberAsync(user.GuildId, user.Id, options).ConfigureAwait(false); + await client.ApiClient.RemoveGuildMemberAsync(user.GuildId, user.Id, reason, options).ConfigureAwait(false); } public static async Task CreateDMChannelAsync(IUser user, BaseDiscordClient client, @@ -63,5 +64,17 @@ namespace Discord.Rest var args = new CreateDMChannelParams(user.Id); return RestDMChannel.Create(client, await client.ApiClient.CreateDMChannelAsync(args, options).ConfigureAwait(false)); } + + public static async Task AddRolesAsync(IGuildUser user, BaseDiscordClient client, IEnumerable roles, RequestOptions options) + { + foreach (var role in roles) + await client.ApiClient.AddRoleAsync(user.Guild.Id, user.Id, role.Id, options); + } + + public static async Task RemoveRolesAsync(IGuildUser user, BaseDiscordClient client, IEnumerable roles, RequestOptions options) + { + foreach (var role in roles) + await client.ApiClient.RemoveRoleAsync(user.Guild.Id, user.Id, role.Id, options); + } } } diff --git a/src/Discord.Net.Rest/Extensions/EmbedBuilderExtensions.cs b/src/Discord.Net.Rest/Extensions/EmbedBuilderExtensions.cs new file mode 100644 index 000000000..cee9a136e --- /dev/null +++ b/src/Discord.Net.Rest/Extensions/EmbedBuilderExtensions.cs @@ -0,0 +1,23 @@ +namespace Discord +{ + public static class EmbedBuilderExtensions + { + public static EmbedBuilder WithColor(this EmbedBuilder builder, uint rawValue) => + builder.WithColor(new Color(rawValue)); + + public static EmbedBuilder WithColor(this EmbedBuilder builder, byte r, byte g, byte b) => + builder.WithColor(new Color(r, g, b)); + + public static EmbedBuilder WithColor(this EmbedBuilder builder, int r, int g, int b) => + builder.WithColor(new Color(r, g, b)); + + public static EmbedBuilder WithColor(this EmbedBuilder builder, float r, float g, float b) => + builder.WithColor(new Color(r, g, b)); + + public static EmbedBuilder WithAuthor(this EmbedBuilder builder, IUser user) => + builder.WithAuthor($"{user.Username}#{user.Discriminator}", user.GetAvatarUrl()); + + public static EmbedBuilder WithAuthor(this EmbedBuilder builder, IGuildUser user) => + builder.WithAuthor($"{user.Nickname ?? user.Username}#{user.Discriminator}", user.GetAvatarUrl()); + } +} diff --git a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs index 7a9643674..b88a5b515 100644 --- a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs @@ -5,9 +5,9 @@ namespace Discord.Rest { internal static class EntityExtensions { - public static GuildEmoji ToEntity(this API.Emoji model) + public static GuildEmote ToEntity(this API.Emoji model) { - return new GuildEmoji(model.Id.Value, model.Name, model.Managed, model.RequireColons, ImmutableArray.Create(model.Roles)); + return new GuildEmote(model.Id.Value, model.Name, model.Managed, model.RequireColons, ImmutableArray.Create(model.Roles)); } public static Embed ToEntity(this API.Embed model) @@ -24,6 +24,7 @@ namespace Discord.Rest } public static API.Embed ToModel(this Embed entity) { + if (entity == null) return null; var model = new API.Embed { Type = entity.Type, diff --git a/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs b/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs index 104b913da..b465fbed2 100644 --- a/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs +++ b/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs @@ -19,8 +19,7 @@ namespace Discord.Net.Converters if (property.Ignored) return property; - var propInfo = member as PropertyInfo; - if (propInfo != null) + if (member is PropertyInfo propInfo) { var converter = GetConverter(property, propInfo, propInfo.PropertyType, 0); if (converter != null) diff --git a/src/Discord.Net.Rest/Net/DefaultRestClient.cs b/src/Discord.Net.Rest/Net/DefaultRestClient.cs index 39b94294f..a54107829 100644 --- a/src/Discord.Net.Rest/Net/DefaultRestClient.cs +++ b/src/Discord.Net.Rest/Net/DefaultRestClient.cs @@ -19,8 +19,7 @@ namespace Discord.Net.Rest private readonly HttpClient _client; private readonly string _baseUrl; private readonly JsonSerializer _errorDeserializer; - private CancellationTokenSource _cancelTokenSource; - private CancellationToken _cancelToken, _parentToken; + private CancellationToken _cancelToken; private bool _isDisposed; public DefaultRestClient(string baseUrl) @@ -35,9 +34,7 @@ namespace Discord.Net.Rest }); SetHeader("accept-encoding", "gzip, deflate"); - _cancelTokenSource = new CancellationTokenSource(); _cancelToken = CancellationToken.None; - _parentToken = CancellationToken.None; _errorDeserializer = new JsonSerializer(); } private void Dispose(bool disposing) @@ -62,50 +59,59 @@ namespace Discord.Net.Rest } public void SetCancelToken(CancellationToken cancelToken) { - _parentToken = cancelToken; - _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; + _cancelToken = cancelToken; } - public async Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly) + public async Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly, string reason = null) { string uri = Path.Combine(_baseUrl, endpoint); using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) + { + if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); + } } - public async Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly) + public async Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly, string reason = null) { string uri = Path.Combine(_baseUrl, endpoint); using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) { + if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); restRequest.Content = new StringContent(json, Encoding.UTF8, "application/json"); return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); } } - public async Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly) + public async Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly, string reason = null) { string uri = Path.Combine(_baseUrl, endpoint); using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) { + if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture)); if (multipartParams != null) { foreach (var p in multipartParams) { - //TODO: C#7 Typeswitch candidate - var stringValue = p.Value as string; - if (stringValue != null) { content.Add(new StringContent(stringValue), p.Key); continue; } - var byteArrayValue = p.Value as byte[]; - if (byteArrayValue != null) { content.Add(new ByteArrayContent(byteArrayValue), p.Key); continue; } - var streamValue = p.Value as Stream; - if (streamValue != null) { content.Add(new StreamContent(streamValue), p.Key); continue; } - if (p.Value is MultipartFile) + switch (p.Value) { - var fileValue = (MultipartFile)p.Value; - content.Add(new StreamContent(fileValue.Stream), p.Key, fileValue.Filename); - continue; + case string stringValue: { content.Add(new StringContent(stringValue), p.Key); continue; } + case byte[] byteArrayValue: { content.Add(new ByteArrayContent(byteArrayValue), p.Key); continue; } + case Stream streamValue: { content.Add(new StreamContent(streamValue), p.Key); continue; } + case MultipartFile fileValue: + { + var stream = fileValue.Stream; + if (!stream.CanSeek) + { + var memoryStream = new MemoryStream(); + await stream.CopyToAsync(memoryStream).ConfigureAwait(false); + memoryStream.Position = 0; + stream = memoryStream; + } + content.Add(new StreamContent(stream), p.Key, fileValue.Filename); + continue; + } + default: throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\""); } - - throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\""); } } restRequest.Content = content; @@ -115,16 +121,13 @@ namespace Discord.Net.Rest private async Task SendInternalAsync(HttpRequestMessage request, CancellationToken cancelToken, bool headerOnly) { - while (true) - { - cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken, cancelToken).Token; - HttpResponseMessage response = await _client.SendAsync(request, cancelToken).ConfigureAwait(false); - - var headers = response.Headers.ToDictionary(x => x.Key, x => x.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase); - var stream = !headerOnly ? await response.Content.ReadAsStreamAsync().ConfigureAwait(false) : null; + cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken, cancelToken).Token; + HttpResponseMessage response = await _client.SendAsync(request, cancelToken).ConfigureAwait(false); + + var headers = response.Headers.ToDictionary(x => x.Key, x => x.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase); + var stream = !headerOnly ? await response.Content.ReadAsStreamAsync().ConfigureAwait(false) : null; - return new RestResponse(response.StatusCode, headers, stream); - } + return new RestResponse(response.StatusCode, headers, stream); } private static readonly HttpMethod _patch = new HttpMethod("PATCH"); diff --git a/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs b/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs index fce7e3e1b..943b76359 100644 --- a/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs +++ b/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs @@ -114,9 +114,8 @@ namespace Discord.Net.Queue var now = DateTimeOffset.UtcNow; foreach (var bucket in _buckets.Select(x => x.Value)) { - RequestBucket ignored; if ((now - bucket.LastAttemptAt).TotalMinutes > 1.0) - _buckets.TryRemove(bucket.Id, out ignored); + _buckets.TryRemove(bucket.Id, out RequestBucket ignored); } await Task.Delay(60000, _cancelToken.Token); //Runs each minute } diff --git a/src/Discord.Net.Rest/Net/Queue/Requests/JsonRestRequest.cs b/src/Discord.Net.Rest/Net/Queue/Requests/JsonRestRequest.cs index 83c5e0eb5..2949bab3c 100644 --- a/src/Discord.Net.Rest/Net/Queue/Requests/JsonRestRequest.cs +++ b/src/Discord.Net.Rest/Net/Queue/Requests/JsonRestRequest.cs @@ -15,7 +15,7 @@ namespace Discord.Net.Queue public override async Task SendAsync() { - return await Client.SendAsync(Method, Endpoint, Json, Options.CancelToken, Options.HeaderOnly).ConfigureAwait(false); + return await Client.SendAsync(Method, Endpoint, Json, Options.CancelToken, Options.HeaderOnly, Options.AuditLogReason).ConfigureAwait(false); } } } diff --git a/src/Discord.Net.Rest/Net/Queue/Requests/MultipartRestRequest.cs b/src/Discord.Net.Rest/Net/Queue/Requests/MultipartRestRequest.cs index 424a5325e..c8d97bbdf 100644 --- a/src/Discord.Net.Rest/Net/Queue/Requests/MultipartRestRequest.cs +++ b/src/Discord.Net.Rest/Net/Queue/Requests/MultipartRestRequest.cs @@ -16,7 +16,7 @@ namespace Discord.Net.Queue public override async Task SendAsync() { - return await Client.SendAsync(Method, Endpoint, MultipartParams, Options.CancelToken, Options.HeaderOnly).ConfigureAwait(false); + return await Client.SendAsync(Method, Endpoint, MultipartParams, Options.CancelToken, Options.HeaderOnly, Options.AuditLogReason).ConfigureAwait(false); } } } diff --git a/src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs b/src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs index 7f358e786..8f160273a 100644 --- a/src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs +++ b/src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs @@ -28,7 +28,7 @@ namespace Discord.Net.Queue public virtual async Task SendAsync() { - return await Client.SendAsync(Method, Endpoint, Options.CancelToken, Options.HeaderOnly).ConfigureAwait(false); + return await Client.SendAsync(Method, Endpoint, Options.CancelToken, Options.HeaderOnly, Options.AuditLogReason).ConfigureAwait(false); } } } diff --git a/src/Discord.Net.Rest/Net/RateLimitInfo.cs b/src/Discord.Net.Rest/Net/RateLimitInfo.cs index d8d168aec..9421221ed 100644 --- a/src/Discord.Net.Rest/Net/RateLimitInfo.cs +++ b/src/Discord.Net.Rest/Net/RateLimitInfo.cs @@ -14,15 +14,18 @@ namespace Discord.Net internal RateLimitInfo(Dictionary headers) { - string temp; - IsGlobal = headers.TryGetValue("X-RateLimit-Global", out temp) ? bool.Parse(temp) : false; - Limit = headers.TryGetValue("X-RateLimit-Limit", out temp) ? int.Parse(temp) : (int?)null; - Remaining = headers.TryGetValue("X-RateLimit-Remaining", out temp) ? int.Parse(temp) : (int?)null; - Reset = headers.TryGetValue("X-RateLimit-Reset", out temp) ? - DateTimeUtils.FromUnixSeconds(int.Parse(temp)) : (DateTimeOffset?)null; - RetryAfter = headers.TryGetValue("Retry-After", out temp) ? int.Parse(temp) : (int?)null; - Lag = headers.TryGetValue("Date", out temp) ? - DateTimeOffset.UtcNow - DateTimeOffset.Parse(temp) : (TimeSpan?)null; + IsGlobal = headers.TryGetValue("X-RateLimit-Global", out string temp) && + bool.TryParse(temp, out var isGlobal) ? isGlobal : false; + Limit = headers.TryGetValue("X-RateLimit-Limit", out temp) && + int.TryParse(temp, out var limit) ? limit : (int?)null; + Remaining = headers.TryGetValue("X-RateLimit-Remaining", out temp) && + int.TryParse(temp, out var remaining) ? remaining : (int?)null; + Reset = headers.TryGetValue("X-RateLimit-Reset", out temp) && + int.TryParse(temp, out var reset) ? DateTimeUtils.FromUnixSeconds(reset) : (DateTimeOffset?)null; + RetryAfter = headers.TryGetValue("Retry-After", out temp) && + int.TryParse(temp, out var retryAfter) ? retryAfter : (int?)null; + Lag = headers.TryGetValue("Date", out temp) && + DateTimeOffset.TryParse(temp, out var date) ? DateTimeOffset.UtcNow - date : (TimeSpan?)null; } } } diff --git a/src/Discord.Net.Rpc/Discord.Net.Rpc.csproj b/src/Discord.Net.Rpc/Discord.Net.Rpc.csproj index 85c2bf4e0..5572d69c5 100644 --- a/src/Discord.Net.Rpc/Discord.Net.Rpc.csproj +++ b/src/Discord.Net.Rpc/Discord.Net.Rpc.csproj @@ -1,19 +1,10 @@ - + + - 1.0.0 - rc-dev - rc-$(BuildNumber) - netstandard1.1;netstandard1.3 Discord.Net.Rpc - RogueException - A core Discord.Net library containing the RPC client and models. - discord;discordapp - https://github.com/RogueException/Discord.Net - http://opensource.org/licenses/MIT - git - git://github.com/RogueException/Discord.Net Discord.Rpc - true + A core Discord.Net library containing the RPC client and models. + net45;netstandard1.1;netstandard1.3 @@ -27,15 +18,7 @@ - - - - + - - $(NoWarn);CS1573;CS1591 - true - true - \ No newline at end of file diff --git a/src/Discord.Net.Rpc/DiscordRpcApiClient.cs b/src/Discord.Net.Rpc/DiscordRpcApiClient.cs index 8c83d24d6..50d467054 100644 --- a/src/Discord.Net.Rpc/DiscordRpcApiClient.cs +++ b/src/Discord.Net.Rpc/DiscordRpcApiClient.cs @@ -378,8 +378,7 @@ namespace Discord.API private bool ProcessMessage(API.Rpc.RpcFrame msg) { - RpcRequest requestTracker; - if (_requests.TryGetValue(msg.Nonce.Value.Value, out requestTracker)) + if (_requests.TryGetValue(msg.Nonce.Value.Value, out RpcRequest requestTracker)) { if (msg.Event.GetValueOrDefault("") == "ERROR") { diff --git a/src/Discord.Net.Rpc/DiscordRpcClient.cs b/src/Discord.Net.Rpc/DiscordRpcClient.cs index 01d641204..9c77fc919 100644 --- a/src/Discord.Net.Rpc/DiscordRpcClient.cs +++ b/src/Discord.Net.Rpc/DiscordRpcClient.cs @@ -468,7 +468,7 @@ namespace Discord.Rpc //IDiscordClient ConnectionState IDiscordClient.ConnectionState => _connection.State; - Task IDiscordClient.GetApplicationInfoAsync() => Task.FromResult(ApplicationInfo); + Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) => Task.FromResult(ApplicationInfo); async Task IDiscordClient.StartAsync() => await StartAsync().ConfigureAwait(false); diff --git a/src/Discord.Net.Rpc/DiscordRpcConfig.cs b/src/Discord.Net.Rpc/DiscordRpcConfig.cs index 1866e838b..fd6b74ff1 100644 --- a/src/Discord.Net.Rpc/DiscordRpcConfig.cs +++ b/src/Discord.Net.Rpc/DiscordRpcConfig.cs @@ -19,7 +19,7 @@ namespace Discord.Rpc public DiscordRpcConfig() { -#if NETSTANDARD1_3 +#if FILESYSTEM WebSocketProvider = () => new DefaultWebSocketClient(); #else WebSocketProvider = () => diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs index 934dae94b..d26c593ba 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs @@ -1,4 +1,5 @@ -using System; +using Discord.Rest; +using System; using Model = Discord.API.Rpc.Channel; @@ -7,8 +8,9 @@ namespace Discord.Rpc public class RpcChannel : RpcEntity { public string Name { get; private set; } + public bool IsNsfw => ChannelHelper.IsNsfw(Name); - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); internal RpcChannel(DiscordRpcClient discord, ulong id) : base(discord, id) diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcChannelSummary.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcChannelSummary.cs index 72679ac58..c35437e26 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcChannelSummary.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcChannelSummary.cs @@ -7,8 +7,8 @@ namespace Discord.Rpc public class RpcChannelSummary { public ulong Id { get; } - public string Name { get; set; } - public ChannelType Type { get; set; } + public string Name { get; private set; } + public ChannelType Type { get; private set; } internal RpcChannelSummary(ulong id) { diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs index 1fb6d5867..da9bce700 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs @@ -46,7 +46,7 @@ namespace Discord.Rpc public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); -#if NETSTANDARD1_3 +#if FILESYSTEM public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); #endif @@ -54,7 +54,9 @@ namespace Discord.Rpc => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); @@ -102,7 +104,7 @@ namespace Discord.Rpc async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); -#if NETSTANDARD1_3 +#if FILESYSTEM async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); #endif diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs index 504bf8670..d449688a4 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs @@ -1,4 +1,5 @@ -using Discord.Rest; +using Discord.Audio; +using Discord.Rest; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -48,7 +49,7 @@ namespace Discord.Rpc public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); -#if NETSTANDARD1_3 +#if FILESYSTEM public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); #endif @@ -56,7 +57,9 @@ namespace Discord.Rpc => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); @@ -101,7 +104,7 @@ namespace Discord.Rpc async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); -#if NETSTANDARD1_3 +#if FILESYSTEM async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); #endif @@ -112,6 +115,9 @@ namespace Discord.Rpc IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); + //IAudioChannel + Task IAudioChannel.ConnectAsync(Action configAction) { throw new NotSupportedException(); } + //IChannel string IChannel.Name { get { throw new NotSupportedException(); } } diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs index 9a88072b9..72b45e466 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs @@ -50,7 +50,7 @@ namespace Discord.Rpc public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); -#if NETSTANDARD1_3 +#if FILESYSTEM public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); #endif @@ -58,7 +58,9 @@ namespace Discord.Rpc => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); @@ -102,7 +104,7 @@ namespace Discord.Rpc async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); -#if NETSTANDARD1_3 +#if FILESYSTEM async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); #endif diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs index 3d5acfda9..067da6764 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs @@ -42,7 +42,7 @@ namespace Discord.Rpc private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; - //IVoiceChannel - Task IVoiceChannel.ConnectAsync() { throw new NotSupportedException(); } + //IAudioChannel + Task IAudioChannel.ConnectAsync(Action configAction) { throw new NotSupportedException(); } } } diff --git a/src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs b/src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs index b85071f2a..a2f7b4558 100644 --- a/src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs +++ b/src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs @@ -13,11 +13,12 @@ namespace Discord.Rpc public IMessageChannel Channel { get; } public RpcUser Author { get; } + public MessageSource Source { get; } public string Content { get; private set; } public Color AuthorColor { get; private set; } - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public virtual bool IsTTS => false; public virtual bool IsPinned => false; public virtual bool IsBlocked => false; @@ -33,11 +34,12 @@ namespace Discord.Rpc public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); - internal RpcMessage(DiscordRpcClient discord, ulong id, RestVirtualMessageChannel channel, RpcUser author) + internal RpcMessage(DiscordRpcClient discord, ulong id, RestVirtualMessageChannel channel, RpcUser author, MessageSource source) : base(discord, id) { Channel = channel; Author = author; + Source = source; } internal static RpcMessage Create(DiscordRpcClient discord, ulong channelId, Model model) { diff --git a/src/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs b/src/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs index e8c918bc6..39c6026a7 100644 --- a/src/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs +++ b/src/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs @@ -10,14 +10,14 @@ namespace Discord.Rpc public MessageType Type { get; private set; } internal RpcSystemMessage(DiscordRpcClient discord, ulong id, RestVirtualMessageChannel channel, RpcUser author) - : base(discord, id, channel, author) + : base(discord, id, channel, author, MessageSource.System) { } internal new static RpcSystemMessage Create(DiscordRpcClient discord, ulong channelId, Model model) { var entity = new RpcSystemMessage(discord, model.Id, RestVirtualMessageChannel.Create(discord, channelId), - RpcUser.Create(discord, model.Author.Value)); + RpcUser.Create(discord, model.Author.Value, model.WebhookId.ToNullable())); entity.Update(model); return entity; } diff --git a/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs b/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs index 240290fab..91a8d7b31 100644 --- a/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs +++ b/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs @@ -29,17 +29,18 @@ namespace Discord.Rpc public override IReadOnlyCollection MentionedRoleIds => MessageHelper.FilterTagsByKey(TagType.RoleMention, _tags); public override IReadOnlyCollection MentionedUserIds => MessageHelper.FilterTagsByKey(TagType.UserMention, _tags); public override IReadOnlyCollection Tags => _tags; - public IReadOnlyDictionary Reactions => ImmutableDictionary.Create(); + public IReadOnlyDictionary Reactions => ImmutableDictionary.Create(); - internal RpcUserMessage(DiscordRpcClient discord, ulong id, RestVirtualMessageChannel channel, RpcUser author) - : base(discord, id, channel, author) + internal RpcUserMessage(DiscordRpcClient discord, ulong id, RestVirtualMessageChannel channel, RpcUser author, MessageSource source) + : base(discord, id, channel, author, source) { } internal new static RpcUserMessage Create(DiscordRpcClient discord, ulong channelId, Model model) { var entity = new RpcUserMessage(discord, model.Id, RestVirtualMessageChannel.Create(discord, channelId), - RpcUser.Create(discord, model.Author.Value)); + RpcUser.Create(discord, model.Author.Value, model.WebhookId.ToNullable()), + MessageHelper.GetSource(model)); entity.Update(model); return entity; } @@ -101,16 +102,10 @@ namespace Discord.Rpc public Task ModifyAsync(Action func, RequestOptions options) => MessageHelper.ModifyAsync(this, Discord, func, options); - public Task AddReactionAsync(Emoji emoji, RequestOptions options = null) - => MessageHelper.AddReactionAsync(this, emoji, Discord, options); - public Task AddReactionAsync(string emoji, RequestOptions options = null) - => MessageHelper.AddReactionAsync(this, emoji, Discord, options); - - public Task RemoveReactionAsync(Emoji emoji, IUser user, RequestOptions options = null) - => MessageHelper.RemoveReactionAsync(this, user, emoji, Discord, options); - public Task RemoveReactionAsync(string emoji, IUser user, RequestOptions options = null) - => MessageHelper.RemoveReactionAsync(this, user, emoji, Discord, options); - + public Task AddReactionAsync(IEmote emote, RequestOptions options = null) + => MessageHelper.AddReactionAsync(this, emote, Discord, options); + public Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions options = null) + => MessageHelper.RemoveReactionAsync(this, user, emote, Discord, options); public Task RemoveAllReactionsAsync(RequestOptions options = null) => MessageHelper.RemoveAllReactionsAsync(this, Discord, options); diff --git a/src/Discord.Net.Rpc/Entities/RpcEntity.cs b/src/Discord.Net.Rpc/Entities/RpcEntity.cs index e5b26cbe7..3827175bb 100644 --- a/src/Discord.Net.Rpc/Entities/RpcEntity.cs +++ b/src/Discord.Net.Rpc/Entities/RpcEntity.cs @@ -5,7 +5,7 @@ namespace Discord.Rpc public abstract class RpcEntity : IEntity where T : IEquatable { - public DiscordRpcClient Discord { get; } + internal DiscordRpcClient Discord { get; } public T Id { get; } internal RpcEntity(DiscordRpcClient discord, T id) diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs b/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs index 86524b721..b30ba678f 100644 --- a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs +++ b/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs @@ -14,20 +14,26 @@ namespace Discord.Rpc public ushort DiscriminatorValue { get; private set; } public string AvatarId { get; private set; } - public string GetAvatarUrl(AvatarFormat format = AvatarFormat.Png, ushort size = 128) => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); + public virtual bool IsWebhook => false; public virtual Game? Game => null; - public virtual UserStatus Status => UserStatus.Unknown; + public virtual UserStatus Status => UserStatus.Offline; internal RpcUser(DiscordRpcClient discord, ulong id) : base(discord, id) { } internal static RpcUser Create(DiscordRpcClient discord, Model model) + => Create(discord, model, null); + internal static RpcUser Create(DiscordRpcClient discord, Model model, ulong? webhookId) { - var entity = new RpcUser(discord, model.Id); + RpcUser entity; + if (webhookId.HasValue) + entity = new RpcWebhookUser(discord, model.Id, webhookId.Value); + else + entity = new RpcUser(discord, model.Id); entity.Update(model); return entity; } @@ -43,9 +49,12 @@ namespace Discord.Rpc Username = model.Username.Value; } - public Task CreateDMChannelAsync(RequestOptions options = null) + public Task GetOrCreateDMChannelAsync(RequestOptions options = null) => UserHelper.CreateDMChannelAsync(this, Discord, options); + public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + public override string ToString() => $"{Username}#{Discriminator}"; private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcWebhookUser.cs b/src/Discord.Net.Rpc/Entities/Users/RpcWebhookUser.cs new file mode 100644 index 000000000..9ea4312c2 --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/Users/RpcWebhookUser.cs @@ -0,0 +1,25 @@ +using System.Diagnostics; +using Model = Discord.API.User; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RpcWebhookUser : RpcUser + { + public ulong WebhookId { get; } + + public override bool IsWebhook => true; + + internal RpcWebhookUser(DiscordRpcClient discord, ulong id, ulong webhookId) + : base(discord, id) + { + WebhookId = webhookId; + } + internal static RpcWebhookUser Create(DiscordRpcClient discord, Model model, ulong webhookId) + { + var entity = new RpcWebhookUser(discord, model.Id, webhookId); + entity.Update(model); + return entity; + } + } +} diff --git a/src/Discord.Net.WebSocket/API/Voice/ReadyEvent.cs b/src/Discord.Net.WebSocket/API/Voice/ReadyEvent.cs index e4446f814..2a134ced1 100644 --- a/src/Discord.Net.WebSocket/API/Voice/ReadyEvent.cs +++ b/src/Discord.Net.WebSocket/API/Voice/ReadyEvent.cs @@ -7,6 +7,8 @@ namespace Discord.API.Voice { [JsonProperty("ssrc")] public uint SSRC { get; set; } + [JsonProperty("ip")] + public string Ip { get; set; } [JsonProperty("port")] public ushort Port { get; set; } [JsonProperty("modes")] diff --git a/src/Discord.Net.WebSocket/API/Voice/SpeakingEvent.cs b/src/Discord.Net.WebSocket/API/Voice/SpeakingEvent.cs new file mode 100644 index 000000000..0272a8f53 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Voice/SpeakingEvent.cs @@ -0,0 +1,15 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Voice +{ + internal class SpeakingEvent + { + [JsonProperty("user_id")] + public ulong UserId { get; set; } + [JsonProperty("ssrc")] + public uint Ssrc { get; set; } + [JsonProperty("speaking")] + public bool Speaking { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.Events.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.Events.cs new file mode 100644 index 000000000..b3e438a01 --- /dev/null +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.Events.cs @@ -0,0 +1,51 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Audio +{ + internal partial class AudioClient + { + public event Func Connected + { + add { _connectedEvent.Add(value); } + remove { _connectedEvent.Remove(value); } + } + private readonly AsyncEvent> _connectedEvent = new AsyncEvent>(); + public event Func Disconnected + { + add { _disconnectedEvent.Add(value); } + remove { _disconnectedEvent.Remove(value); } + } + private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); + public event Func LatencyUpdated + { + add { _latencyUpdatedEvent.Add(value); } + remove { _latencyUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _latencyUpdatedEvent = new AsyncEvent>(); + public event Func UdpLatencyUpdated + { + add { _udpLatencyUpdatedEvent.Add(value); } + remove { _udpLatencyUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _udpLatencyUpdatedEvent = new AsyncEvent>(); + public event Func StreamCreated + { + add { _streamCreatedEvent.Add(value); } + remove { _streamCreatedEvent.Remove(value); } + } + private readonly AsyncEvent> _streamCreatedEvent = new AsyncEvent>(); + public event Func StreamDestroyed + { + add { _streamDestroyedEvent.Add(value); } + remove { _streamDestroyedEvent.Remove(value); } + } + private readonly AsyncEvent> _streamDestroyedEvent = new AsyncEvent>(); + public event Func SpeakingUpdated + { + add { _speakingUpdatedEvent.Add(value); } + remove { _speakingUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _speakingUpdatedEvent = new AsyncEvent>(); + } +} diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index e2586d0f3..1f33b3cc5 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -11,56 +11,57 @@ using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using System.Collections.Generic; namespace Discord.Audio { //TODO: Add audio reconnecting - internal class AudioClient : IAudioClient, IDisposable + internal partial class AudioClient : IAudioClient, IDisposable { - public event Func Connected + internal struct StreamPair { - add { _connectedEvent.Add(value); } - remove { _connectedEvent.Remove(value); } - } - private readonly AsyncEvent> _connectedEvent = new AsyncEvent>(); - public event Func Disconnected - { - add { _disconnectedEvent.Add(value); } - remove { _disconnectedEvent.Remove(value); } - } - private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); - public event Func LatencyUpdated - { - add { _latencyUpdatedEvent.Add(value); } - remove { _latencyUpdatedEvent.Remove(value); } + public AudioInStream Reader; + public AudioOutStream Writer; + + public StreamPair(AudioInStream reader, AudioOutStream writer) + { + Reader = reader; + Writer = writer; + } } - private readonly AsyncEvent> _latencyUpdatedEvent = new AsyncEvent>(); private readonly Logger _audioLogger; private readonly JsonSerializer _serializer; private readonly ConnectionManager _connection; private readonly SemaphoreSlim _stateLock; private readonly ConcurrentQueue _heartbeatTimes; + private readonly ConcurrentQueue> _keepaliveTimes; + private readonly ConcurrentDictionary _ssrcMap; + private readonly ConcurrentDictionary _streams; - private Task _heartbeatTask; + private Task _heartbeatTask, _keepaliveTask; private long _lastMessageTime; private string _url, _sessionId, _token; private ulong _userId; private uint _ssrc; - private byte[] _secretKey; + private bool _isSpeaking; public SocketGuild Guild { get; } public DiscordVoiceAPIClient ApiClient { get; private set; } public int Latency { get; private set; } + public int UdpLatency { get; private set; } + public ulong ChannelId { get; internal set; } + internal byte[] SecretKey { get; private set; } private DiscordSocketClient Discord => Guild.Discord; public ConnectionState ConnectionState => _connection.State; /// Creates a new REST/WebSocket discord client. - internal AudioClient(SocketGuild guild, int id) + internal AudioClient(SocketGuild guild, int clientId, ulong channelId) { Guild = guild; - _audioLogger = Discord.LogManager.CreateLogger($"Audio #{id}"); + ChannelId = channelId; + _audioLogger = Discord.LogManager.CreateLogger($"Audio #{clientId}"); ApiClient = new DiscordVoiceAPIClient(guild.Id, Discord.WebSocketProvider, Discord.UdpSocketProvider); ApiClient.SentGatewayMessage += async opCode => await _audioLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false); @@ -75,6 +76,9 @@ namespace Discord.Audio _connection.Connected += () => _connectedEvent.InvokeAsync(); _connection.Disconnected += (ex, recon) => _disconnectedEvent.InvokeAsync(ex); _heartbeatTimes = new ConcurrentQueue(); + _keepaliveTimes = new ConcurrentQueue>(); + _ssrcMap = new ConcurrentDictionary(); + _streams = new ConcurrentDictionary(); _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; _serializer.Error += (s, e) => @@ -83,7 +87,8 @@ namespace Discord.Audio e.ErrorContext.Handled = true; }; - LatencyUpdated += async (old, val) => await _audioLogger.VerboseAsync($"Latency = {val} ms").ConfigureAwait(false); + LatencyUpdated += async (old, val) => await _audioLogger.DebugAsync($"Latency = {val} ms").ConfigureAwait(false); + UdpLatencyUpdated += async (old, val) => await _audioLogger.DebugAsync($"UDP Latency = {val} ms").ConfigureAwait(false); } internal async Task StartAsync(string url, ulong userId, string sessionId, string token) @@ -94,13 +99,16 @@ namespace Discord.Audio _token = token; await _connection.StartAsync().ConfigureAwait(false); } - public async Task StopAsync() - => await _connection.StopAsync().ConfigureAwait(false); + public async Task StopAsync() + { + await _connection.StopAsync().ConfigureAwait(false); + } private async Task OnConnectingAsync() { await _audioLogger.DebugAsync("Connecting ApiClient").ConfigureAwait(false); await ApiClient.ConnectAsync("wss://" + _url).ConfigureAwait(false); + await _audioLogger.DebugAsync("Listening on port " + ApiClient.UdpPort).ConfigureAwait(false); await _audioLogger.DebugAsync("Sending Identity").ConfigureAwait(false); await ApiClient.SendIdentityAsync(_userId, _sessionId, _token).ConfigureAwait(false); @@ -118,52 +126,86 @@ namespace Discord.Audio if (heartbeatTask != null) await heartbeatTask.ConfigureAwait(false); _heartbeatTask = null; + var keepaliveTask = _keepaliveTask; + if (keepaliveTask != null) + await keepaliveTask.ConfigureAwait(false); + _keepaliveTask = null; - long time; - while (_heartbeatTimes.TryDequeue(out time)) { } + while (_heartbeatTimes.TryDequeue(out long time)) { } _lastMessageTime = 0; + await ClearInputStreamsAsync().ConfigureAwait(false); + await _audioLogger.DebugAsync("Sending Voice State").ConfigureAwait(false); await Discord.ApiClient.SendVoiceStateUpdateAsync(Guild.Id, null, false, false).ConfigureAwait(false); } - public AudioOutStream CreateOpusStream(int samplesPerFrame, int bufferMillis) + public AudioOutStream CreateOpusStream(int bufferMillis) { - CheckSamplesPerFrame(samplesPerFrame); - var outputStream = new OutputStream(ApiClient); - var sodiumEncrypter = new SodiumEncryptStream(outputStream, _secretKey); - var rtpWriter = new RTPWriteStream(sodiumEncrypter, samplesPerFrame, _ssrc); - return new BufferedWriteStream(rtpWriter, samplesPerFrame, bufferMillis, _connection.CancelToken, _audioLogger); + var outputStream = new OutputStream(ApiClient); //Ignores header + var sodiumEncrypter = new SodiumEncryptStream( outputStream, this); //Passes header + var rtpWriter = new RTPWriteStream(sodiumEncrypter, _ssrc); //Consumes header, passes + return new BufferedWriteStream(rtpWriter, this, bufferMillis, _connection.CancelToken, _audioLogger); //Generates header } - public AudioOutStream CreateDirectOpusStream(int samplesPerFrame) + public AudioOutStream CreateDirectOpusStream() { - CheckSamplesPerFrame(samplesPerFrame); - var outputStream = new OutputStream(ApiClient); - var sodiumEncrypter = new SodiumEncryptStream(outputStream, _secretKey); - return new RTPWriteStream(sodiumEncrypter, samplesPerFrame, _ssrc); + var outputStream = new OutputStream(ApiClient); //Ignores header + var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); //Passes header + return new RTPWriteStream(sodiumEncrypter, _ssrc); //Consumes header (external input), passes } - public AudioOutStream CreatePCMStream(AudioApplication application, int samplesPerFrame, int channels, int? bitrate, int bufferMillis) + public AudioOutStream CreatePCMStream(AudioApplication application, int? bitrate, int bufferMillis, int packetLoss) { - CheckSamplesPerFrame(samplesPerFrame); - var outputStream = new OutputStream(ApiClient); - var sodiumEncrypter = new SodiumEncryptStream(outputStream, _secretKey); - var rtpWriter = new RTPWriteStream(sodiumEncrypter, samplesPerFrame, _ssrc); - var bufferedStream = new BufferedWriteStream(rtpWriter, samplesPerFrame, bufferMillis, _connection.CancelToken, _audioLogger); - return new OpusEncodeStream(bufferedStream, channels, samplesPerFrame, bitrate ?? (96 * 1024), application); + var outputStream = new OutputStream(ApiClient); //Ignores header + var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); //Passes header + var rtpWriter = new RTPWriteStream(sodiumEncrypter, _ssrc); //Consumes header, passes + var bufferedStream = new BufferedWriteStream(rtpWriter, this, bufferMillis, _connection.CancelToken, _audioLogger); //Ignores header, generates header + return new OpusEncodeStream(bufferedStream, bitrate ?? (96 * 1024), application, packetLoss); //Generates header } - public AudioOutStream CreateDirectPCMStream(AudioApplication application, int samplesPerFrame, int channels, int? bitrate) + public AudioOutStream CreateDirectPCMStream(AudioApplication application, int? bitrate, int packetLoss) { - CheckSamplesPerFrame(samplesPerFrame); - var outputStream = new OutputStream(ApiClient); - var sodiumEncrypter = new SodiumEncryptStream(outputStream, _secretKey); - var rtpWriter = new RTPWriteStream(sodiumEncrypter, samplesPerFrame, _ssrc); - return new OpusEncodeStream(rtpWriter, channels, samplesPerFrame, bitrate ?? (96 * 1024), application); + var outputStream = new OutputStream(ApiClient); //Ignores header + var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); //Passes header + var rtpWriter = new RTPWriteStream(sodiumEncrypter, _ssrc); //Consumes header, passes + return new OpusEncodeStream(rtpWriter, bitrate ?? (96 * 1024), application, packetLoss); //Generates header } - private void CheckSamplesPerFrame(int samplesPerFrame) + + internal async Task CreateInputStreamAsync(ulong userId) { - if (samplesPerFrame != 120 && samplesPerFrame != 240 && samplesPerFrame != 480 && - samplesPerFrame != 960 && samplesPerFrame != 1920 && samplesPerFrame != 2880) - throw new ArgumentException("Value must be 120, 240, 480, 960, 1920 or 2880", nameof(samplesPerFrame)); + //Assume Thread-safe + if (!_streams.ContainsKey(userId)) + { + var readerStream = new InputStream(); //Consumes header + var opusDecoder = new OpusDecodeStream(readerStream); //Passes header + //var jitterBuffer = new JitterBuffer(opusDecoder, _audioLogger); + var rtpReader = new RTPReadStream(opusDecoder); //Generates header + var decryptStream = new SodiumDecryptStream(rtpReader, this); //No header + _streams.TryAdd(userId, new StreamPair(readerStream, decryptStream)); + await _streamCreatedEvent.InvokeAsync(userId, readerStream); + } + } + internal AudioInStream GetInputStream(ulong id) + { + if (_streams.TryGetValue(id, out StreamPair streamPair)) + return streamPair.Reader; + return null; + } + internal async Task RemoveInputStreamAsync(ulong userId) + { + if (_streams.TryRemove(userId, out var pair)) + { + await _streamDestroyedEvent.InvokeAsync(userId).ConfigureAwait(false); + pair.Reader.Dispose(); + } + } + internal async Task ClearInputStreamsAsync() + { + foreach (var pair in _streams) + { + await _streamDestroyedEvent.InvokeAsync(pair.Key).ConfigureAwait(false); + pair.Value.Reader.Dispose(); + } + _ssrcMap.Clear(); + _streams.Clear(); } private async Task ProcessMessageAsync(VoiceOpCode opCode, object payload) @@ -186,7 +228,7 @@ namespace Discord.Audio _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _connection.CancelToken); - ApiClient.SetUdpEndpoint(_url, data.Port); + ApiClient.SetUdpEndpoint(data.Ip, data.Port); await ApiClient.SendDiscoveryAsync(_ssrc).ConfigureAwait(false); } break; @@ -198,8 +240,10 @@ namespace Discord.Audio if (data.Mode != DiscordVoiceAPIClient.Mode) throw new InvalidOperationException($"Discord selected an unexpected mode: {data.Mode}"); - _secretKey = data.SecretKey; - await ApiClient.SendSetSpeaking(true).ConfigureAwait(false); + SecretKey = data.SecretKey; + _isSpeaking = false; + await ApiClient.SendSetSpeaking(false).ConfigureAwait(false); + _keepaliveTask = RunKeepaliveAsync(5000, _connection.CancelToken); var _ = _connection.CompleteAsync(); } @@ -208,8 +252,7 @@ namespace Discord.Audio { await _audioLogger.DebugAsync("Received HeartbeatAck").ConfigureAwait(false); - long time; - if (_heartbeatTimes.TryDequeue(out time)) + if (_heartbeatTimes.TryDequeue(out long time)) { int latency = (int)(Environment.TickCount - time); int before = Latency; @@ -219,6 +262,16 @@ namespace Discord.Audio } } break; + case VoiceOpCode.Speaking: + { + await _audioLogger.DebugAsync("Received Speaking").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + _ssrcMap[data.Ssrc] = data.UserId; //TODO: Memory Leak: SSRCs are never cleaned up + + await _speakingUpdatedEvent.InvokeAsync(data.UserId, data.Speaking); + } + break; default: await _audioLogger.WarningAsync($"Unknown OpCode ({opCode})").ConfigureAwait(false); return; @@ -232,22 +285,94 @@ namespace Discord.Audio } private async Task ProcessPacketAsync(byte[] packet) { - if (!_connection.IsCompleted) + try { - if (packet.Length == 70) + if (_connection.State == ConnectionState.Connecting) { + if (packet.Length != 70) + { + await _audioLogger.DebugAsync($"Malformed Packet").ConfigureAwait(false); + return; + } string ip; int port; try { ip = Encoding.UTF8.GetString(packet, 4, 70 - 6).TrimEnd('\0'); - port = packet[69] | (packet[68] << 8); + port = (packet[69] << 8) | packet[68]; + } + catch (Exception ex) + { + await _audioLogger.DebugAsync($"Malformed Packet", ex).ConfigureAwait(false); + return; } - catch { return; } await _audioLogger.DebugAsync("Received Discovery").ConfigureAwait(false); await ApiClient.SendSelectProtocol(ip, port).ConfigureAwait(false); } + else if (_connection.State == ConnectionState.Connected) + { + if (packet.Length == 8) + { + await _audioLogger.DebugAsync("Received Keepalive").ConfigureAwait(false); + + ulong value = + ((ulong)packet[0] >> 0) | + ((ulong)packet[1] >> 8) | + ((ulong)packet[2] >> 16) | + ((ulong)packet[3] >> 24) | + ((ulong)packet[4] >> 32) | + ((ulong)packet[5] >> 40) | + ((ulong)packet[6] >> 48) | + ((ulong)packet[7] >> 56); + + while (_keepaliveTimes.TryDequeue(out var pair)) + { + if (pair.Key == value) + { + int latency = (int)(Environment.TickCount - pair.Value); + int before = UdpLatency; + UdpLatency = latency; + + await _udpLatencyUpdatedEvent.InvokeAsync(before, latency).ConfigureAwait(false); + break; + } + } + } + else + { + if (!RTPReadStream.TryReadSsrc(packet, 0, out var ssrc)) + { + await _audioLogger.DebugAsync($"Malformed Frame").ConfigureAwait(false); + return; + } + if (!_ssrcMap.TryGetValue(ssrc, out var userId)) + { + await _audioLogger.DebugAsync($"Unknown SSRC {ssrc}").ConfigureAwait(false); + return; + } + if (!_streams.TryGetValue(userId, out var pair)) + { + await _audioLogger.DebugAsync($"Unknown User {userId}").ConfigureAwait(false); + return; + } + try + { + await pair.Writer.WriteAsync(packet, 0, packet.Length).ConfigureAwait(false); + } + catch (Exception ex) + { + await _audioLogger.DebugAsync($"Malformed Frame", ex).ConfigureAwait(false); + return; + } + //await _audioLogger.DebugAsync($"Received {packet.Length} bytes from user {userId}").ConfigureAwait(false); + } + } + } + catch (Exception ex) + { + await _audioLogger.WarningAsync($"Failed to process UDP packet", ex).ConfigureAwait(false); + return; } } @@ -276,7 +401,7 @@ namespace Discord.Audio } catch (Exception ex) { - await _audioLogger.WarningAsync("Heartbeat Errored", ex).ConfigureAwait(false); + await _audioLogger.WarningAsync("Failed to send heartbeat", ex).ConfigureAwait(false); } await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false); @@ -292,6 +417,49 @@ namespace Discord.Audio await _audioLogger.ErrorAsync("Heartbeat Errored", ex).ConfigureAwait(false); } } + private async Task RunKeepaliveAsync(int intervalMillis, CancellationToken cancelToken) + { + var packet = new byte[8]; + try + { + await _audioLogger.DebugAsync("Keepalive Started").ConfigureAwait(false); + while (!cancelToken.IsCancellationRequested) + { + var now = Environment.TickCount; + + try + { + ulong value = await ApiClient.SendKeepaliveAsync().ConfigureAwait(false); + if (_keepaliveTimes.Count < 12) //No reply for 60 Seconds + _keepaliveTimes.Enqueue(new KeyValuePair(value, now)); + } + catch (Exception ex) + { + await _audioLogger.WarningAsync("Failed to send keepalive", ex).ConfigureAwait(false); + } + + await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false); + } + await _audioLogger.DebugAsync("Keepalive Stopped").ConfigureAwait(false); + } + catch (OperationCanceledException) + { + await _audioLogger.DebugAsync("Keepalive Stopped").ConfigureAwait(false); + } + catch (Exception ex) + { + await _audioLogger.ErrorAsync("Keepalive Errored", ex).ConfigureAwait(false); + } + } + + public async Task SetSpeakingAsync(bool value) + { + if (_isSpeaking != value) + { + _isSpeaking = value; + await ApiClient.SendSetSpeaking(value).ConfigureAwait(false); + } + } internal void Dispose(bool disposing) { diff --git a/src/Discord.Net.WebSocket/Audio/AudioMode.cs b/src/Discord.Net.WebSocket/Audio/AudioMode.cs deleted file mode 100644 index 7cc5a08c1..000000000 --- a/src/Discord.Net.WebSocket/Audio/AudioMode.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace Discord.Audio -{ - [Flags] - public enum AudioMode : byte - { - Disabled = 0, - Outgoing = 1, - Incoming = 2, - Both = Outgoing | Incoming - } -} diff --git a/src/Discord.Net.WebSocket/Audio/Opus/OpusConverter.cs b/src/Discord.Net.WebSocket/Audio/Opus/OpusConverter.cs index 732006990..4179ce9c9 100644 --- a/src/Discord.Net.WebSocket/Audio/Opus/OpusConverter.cs +++ b/src/Discord.Net.WebSocket/Audio/Opus/OpusConverter.cs @@ -6,37 +6,22 @@ namespace Discord.Audio { protected IntPtr _ptr; - /// Gets the bit rate of this converter. - public const int BitsPerSample = 16; - /// Gets the bytes per sample. - public const int SampleSize = (BitsPerSample / 8) * MaxChannels; - /// Gets the maximum amount of channels this encoder supports. - public const int MaxChannels = 2; + public const int SamplingRate = 48000; + public const int Channels = 2; + public const int FrameMillis = 20; - /// Gets the input sampling rate of this converter. - public int SamplingRate { get; } - /// Gets the number of samples per second for this stream. - public int Channels { get; } + public const int SampleBytes = sizeof(short) * Channels; - protected OpusConverter(int samplingRate, int channels) - { - if (samplingRate != 8000 && samplingRate != 12000 && - samplingRate != 16000 && samplingRate != 24000 && - samplingRate != 48000) - throw new ArgumentOutOfRangeException(nameof(samplingRate)); - if (channels != 1 && channels != 2) - throw new ArgumentOutOfRangeException(nameof(channels)); - - SamplingRate = samplingRate; - Channels = channels; - } + public const int FrameSamplesPerChannel = SamplingRate / 1000 * FrameMillis; + public const int FrameSamples = FrameSamplesPerChannel * Channels; + public const int FrameBytes = FrameSamplesPerChannel * SampleBytes; - private bool disposedValue = false; // To detect redundant calls + protected bool _isDisposed = false; protected virtual void Dispose(bool disposing) { - if (!disposedValue) - disposedValue = true; + if (!_isDisposed) + _isDisposed = true; } ~OpusConverter() { @@ -47,5 +32,16 @@ namespace Discord.Audio Dispose(true); GC.SuppressFinalize(this); } + + protected static void CheckError(int result) + { + if (result < 0) + throw new Exception($"Opus Error: {(OpusError)result}"); + } + protected static void CheckError(OpusError error) + { + if ((int)error < 0) + throw new Exception($"Opus Error: {error}"); + } } } diff --git a/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs b/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs index b2ecf5987..41c48e1ac 100644 --- a/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs +++ b/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs @@ -14,37 +14,29 @@ namespace Discord.Audio [DllImport("opus", EntryPoint = "opus_decoder_ctl", CallingConvention = CallingConvention.Cdecl)] private static extern int DecoderCtl(IntPtr st, OpusCtl request, int value); - public OpusDecoder(int samplingRate, int channels) - : base(samplingRate, channels) + public OpusDecoder() { - OpusError error; - _ptr = CreateDecoder(samplingRate, channels, out error); - if (error != OpusError.OK) - throw new Exception($"Opus Error: {error}"); + _ptr = CreateDecoder(SamplingRate, Channels, out var error); + CheckError(error); } - /// Produces PCM samples from Opus-encoded audio. - /// PCM samples to decode. - /// Offset of the frame in input. - /// Buffer to store the decoded frame. - public unsafe int DecodeFrame(byte[] input, int inputOffset, int inputCount, byte[] output, int outputOffset) + public unsafe int DecodeFrame(byte[] input, int inputOffset, int inputCount, byte[] output, int outputOffset, bool decodeFEC) { int result = 0; fixed (byte* inPtr = input) fixed (byte* outPtr = output) - result = Decode(_ptr, inPtr + inputOffset, inputCount, outPtr + outputOffset, (output.Length - outputOffset) / SampleSize / MaxChannels, 0); - - if (result < 0) - throw new Exception($"Opus Error: {(OpusError)result}"); - return result; + result = Decode(_ptr, inPtr + inputOffset, inputCount, outPtr + outputOffset, FrameSamplesPerChannel, decodeFEC ? 1 : 0); + CheckError(result); + return result * SampleBytes; } protected override void Dispose(bool disposing) { - if (_ptr != IntPtr.Zero) + if (!_isDisposed) { - DestroyDecoder(_ptr); - _ptr = IntPtr.Zero; + if (_ptr != IntPtr.Zero) + DestroyDecoder(_ptr); + base.Dispose(disposing); } } } diff --git a/src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs b/src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs index 1f0b35d77..1ff5a5d9a 100644 --- a/src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs +++ b/src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs @@ -12,14 +12,12 @@ namespace Discord.Audio [DllImport("opus", EntryPoint = "opus_encode", CallingConvention = CallingConvention.Cdecl)] private static extern int Encode(IntPtr st, byte* pcm, int frame_size, byte* data, int max_data_bytes); [DllImport("opus", EntryPoint = "opus_encoder_ctl", CallingConvention = CallingConvention.Cdecl)] - private static extern int EncoderCtl(IntPtr st, OpusCtl request, int value); + private static extern OpusError EncoderCtl(IntPtr st, OpusCtl request, int value); - /// Gets the coding mode of the encoder. public AudioApplication Application { get; } public int BitRate { get;} - public OpusEncoder(int samplingRate, int channels, int bitrate, AudioApplication application) - : base(samplingRate, channels) + public OpusEncoder(int bitrate, AudioApplication application, int packetLoss) { if (bitrate < 1 || bitrate > DiscordVoiceAPIClient.MaxBitrate) throw new ArgumentOutOfRangeException(nameof(bitrate)); @@ -47,57 +45,31 @@ namespace Discord.Audio throw new ArgumentOutOfRangeException(nameof(application)); } - OpusError error; - _ptr = CreateEncoder(samplingRate, channels, (int)opusApplication, out error); - if (error != OpusError.OK) - throw new Exception($"Opus Error: {error}"); - - var result = EncoderCtl(_ptr, OpusCtl.SetSignal, (int)opusSignal); - if (result < 0) - throw new Exception($"Opus Error: {(OpusError)result}"); - - result = EncoderCtl(_ptr, OpusCtl.SetPacketLossPercent, 5); //%% - if (result < 0) - throw new Exception($"Opus Error: {(OpusError)result}"); - - result = EncoderCtl(_ptr, OpusCtl.SetInbandFEC, 1); //True - if (result < 0) - throw new Exception($"Opus Error: {(OpusError)result}"); - - result = EncoderCtl(_ptr, OpusCtl.SetBitrate, bitrate); - if (result < 0) - throw new Exception($"Opus Error: {(OpusError)result}"); - - /*if (application == AudioApplication.Music) - { - result = EncoderCtl(_ptr, OpusCtl.SetBandwidth, 1105); - if (result < 0) - throw new Exception($"Opus Error: {(OpusError)result}"); - }*/ + _ptr = CreateEncoder(SamplingRate, Channels, (int)opusApplication, out var error); + CheckError(error); + CheckError(EncoderCtl(_ptr, OpusCtl.SetSignal, (int)opusSignal)); + CheckError(EncoderCtl(_ptr, OpusCtl.SetPacketLossPercent, packetLoss)); //% + CheckError(EncoderCtl(_ptr, OpusCtl.SetInbandFEC, 1)); //True + CheckError(EncoderCtl(_ptr, OpusCtl.SetBitrate, bitrate)); } - /// Produces Opus encoded audio from PCM samples. - /// PCM samples to encode. - /// Buffer to store the encoded frame. - /// Length of the frame contained in outputBuffer. - public unsafe int EncodeFrame(byte[] input, int inputOffset, int inputCount, byte[] output, int outputOffset) + public unsafe int EncodeFrame(byte[] input, int inputOffset, byte[] output, int outputOffset) { int result = 0; fixed (byte* inPtr = input) fixed (byte* outPtr = output) - result = Encode(_ptr, inPtr + inputOffset, inputCount / SampleSize, outPtr + outputOffset, output.Length - outputOffset); - - if (result < 0) - throw new Exception($"Opus Error: {(OpusError)result}"); + result = Encode(_ptr, inPtr + inputOffset, FrameSamplesPerChannel, outPtr + outputOffset, output.Length - outputOffset); + CheckError(result); return result; } protected override void Dispose(bool disposing) { - if (_ptr != IntPtr.Zero) + if (!_isDisposed) { - DestroyEncoder(_ptr); - _ptr = IntPtr.Zero; + if (_ptr != IntPtr.Zero) + DestroyEncoder(_ptr); + base.Dispose(disposing); } } } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs index dcd053cc1..fb302f132 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs @@ -9,6 +9,8 @@ namespace Discord.Audio.Streams /// Wraps another stream with a timed buffer. public class BufferedWriteStream : AudioOutStream { + private const int MaxSilenceFrames = 10; + private struct Frame { public Frame(byte[] buffer, int bytes) @@ -23,7 +25,8 @@ namespace Discord.Audio.Streams private static readonly byte[] _silenceFrame = new byte[0]; - private readonly AudioOutStream _next; + private readonly AudioClient _client; + private readonly AudioStream _next; private readonly CancellationTokenSource _cancelTokenSource; private readonly CancellationToken _cancelToken; private readonly Task _task; @@ -33,14 +36,16 @@ namespace Discord.Audio.Streams private readonly Logger _logger; private readonly int _ticksPerFrame, _queueLength; private bool _isPreloaded; + private int _silenceFrames; - public BufferedWriteStream(AudioOutStream next, int samplesPerFrame, int bufferMillis, CancellationToken cancelToken, int maxFrameSize = 1500) - : this(next, samplesPerFrame, bufferMillis, cancelToken, null, maxFrameSize) { } - internal BufferedWriteStream(AudioOutStream next, int samplesPerFrame, int bufferMillis, CancellationToken cancelToken, Logger logger, int maxFrameSize = 1500) + public BufferedWriteStream(AudioStream next, IAudioClient client, int bufferMillis, CancellationToken cancelToken, int maxFrameSize = 1500) + : this(next, client as AudioClient, bufferMillis, cancelToken, null, maxFrameSize) { } + internal BufferedWriteStream(AudioStream next, AudioClient client, int bufferMillis, CancellationToken cancelToken, Logger logger, int maxFrameSize = 1500) { //maxFrameSize = 1275 was too limiting at 128kbps,2ch,60ms _next = next; - _ticksPerFrame = samplesPerFrame / 48; + _client = client; + _ticksPerFrame = OpusEncoder.FrameMillis; _logger = logger; _queueLength = (bufferMillis + (_ticksPerFrame - 1)) / _ticksPerFrame; //Round up @@ -51,49 +56,67 @@ namespace Discord.Audio.Streams for (int i = 0; i < _queueLength; i++) _bufferPool.Enqueue(new byte[maxFrameSize]); _queueLock = new SemaphoreSlim(_queueLength, _queueLength); + _silenceFrames = MaxSilenceFrames; _task = Run(); } + protected override void Dispose(bool disposing) + { + if (disposing) + _cancelTokenSource.Cancel(); + base.Dispose(disposing); + } private Task Run() { return Task.Run(async () => { -#if DEBUG - uint num = 0; -#endif try { while (!_isPreloaded && !_cancelToken.IsCancellationRequested) await Task.Delay(1).ConfigureAwait(false); long nextTick = Environment.TickCount; + ushort seq = 0; + uint timestamp = 0; while (!_cancelToken.IsCancellationRequested) { long tick = Environment.TickCount; long dist = nextTick - tick; if (dist <= 0) { - Frame frame; - if (_queuedFrames.TryDequeue(out frame)) + if (_queuedFrames.TryDequeue(out Frame frame)) { + await _client.SetSpeakingAsync(true).ConfigureAwait(false); + _next.WriteHeader(seq, timestamp, false); await _next.WriteAsync(frame.Buffer, 0, frame.Bytes).ConfigureAwait(false); _bufferPool.Enqueue(frame.Buffer); _queueLock.Release(); nextTick += _ticksPerFrame; + seq++; + timestamp += OpusEncoder.FrameSamplesPerChannel; + _silenceFrames = 0; #if DEBUG - var _ = _logger.DebugAsync($"{num++}: Sent {frame.Bytes} bytes ({_queuedFrames.Count} frames buffered)"); + var _ = _logger?.DebugAsync($"Sent {frame.Bytes} bytes ({_queuedFrames.Count} frames buffered)"); #endif } - else + else { while ((nextTick - tick) <= 0) { - await _next.WriteAsync(_silenceFrame, 0, _silenceFrame.Length).ConfigureAwait(false); + if (_silenceFrames++ < MaxSilenceFrames) + { + _next.WriteHeader(seq, timestamp, false); + await _next.WriteAsync(_silenceFrame, 0, _silenceFrame.Length).ConfigureAwait(false); + } + else + await _client.SetSpeakingAsync(false).ConfigureAwait(false); nextTick += _ticksPerFrame; + seq++; + timestamp += OpusEncoder.FrameSamplesPerChannel; } #if DEBUG - var _ = _logger.DebugAsync($"{num++}: Buffer underrun"); + var _ = _logger?.DebugAsync($"Buffer underrun"); #endif } } @@ -105,6 +128,7 @@ namespace Discord.Audio.Streams }); } + public override void WriteHeader(ushort seq, uint timestamp, bool missed) { } //Ignore, we use our own timing public override async Task WriteAsync(byte[] data, int offset, int count, CancellationToken cancelToken) { if (cancelToken.CanBeCanceled) @@ -113,23 +137,19 @@ namespace Discord.Audio.Streams cancelToken = _cancelToken; await _queueLock.WaitAsync(-1, cancelToken).ConfigureAwait(false); - byte[] buffer; - if (!_bufferPool.TryDequeue(out buffer)) + if (!_bufferPool.TryDequeue(out byte[] buffer)) { #if DEBUG - var _ = _logger.DebugAsync($"Buffer overflow"); //Should never happen because of the queueLock + var _ = _logger?.DebugAsync($"Buffer overflow"); //Should never happen because of the queueLock #endif return; } Buffer.BlockCopy(data, offset, buffer, 0, count); _queuedFrames.Enqueue(new Frame(buffer, count)); -#if DEBUG - //var _ await _logger.DebugAsync($"Queued {count} bytes ({_queuedFrames.Count} frames buffered)"); -#endif if (!_isPreloaded && _queuedFrames.Count == _queueLength) { #if DEBUG - var _ = _logger.DebugAsync($"Preloaded"); + var _ = _logger?.DebugAsync($"Preloaded"); #endif _isPreloaded = true; } @@ -147,16 +167,10 @@ namespace Discord.Audio.Streams } public override Task ClearAsync(CancellationToken cancelToken) { - Frame ignored; do cancelToken.ThrowIfCancellationRequested(); - while (_queuedFrames.TryDequeue(out ignored)); + while (_queuedFrames.TryDequeue(out Frame ignored)); return Task.Delay(0); } - protected override void Dispose(bool disposing) - { - if (disposing) - _cancelTokenSource.Cancel(); - } } } \ No newline at end of file diff --git a/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs index d46db128b..b9d6157ea 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs @@ -8,66 +8,95 @@ namespace Discord.Audio.Streams /// Reads the payload from an RTP frame public class InputStream : AudioInStream { + private const int MaxFrames = 100; //1-2 Seconds + private ConcurrentQueue _frames; + private SemaphoreSlim _signal; private ushort _nextSeq; private uint _nextTimestamp; + private bool _nextMissed; private bool _hasHeader; + private bool _isDisposed; - public override bool CanRead => true; + public override bool CanRead => !_isDisposed; public override bool CanSeek => false; - public override bool CanWrite => true; + public override bool CanWrite => false; + public override int AvailableFrames => _signal.CurrentCount; - public InputStream(byte[] secretKey) + public InputStream() { _frames = new ConcurrentQueue(); + _signal = new SemaphoreSlim(0, MaxFrames); } - public override Task ReadFrameAsync(CancellationToken cancelToken) - { - if (_frames.TryDequeue(out var frame)) - return Task.FromResult(frame); - return Task.FromResult(null); - } - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) + public override bool TryReadFrame(CancellationToken cancelToken, out RTPFrame frame) { cancelToken.ThrowIfCancellationRequested(); - - if (_frames.TryDequeue(out var frame)) + + if (_signal.Wait(0)) { - if (count < frame.Payload.Length) - throw new InvalidOperationException("Buffer is too small."); - Buffer.BlockCopy(frame.Payload, 0, buffer, offset, frame.Payload.Length); - return Task.FromResult(frame.Payload.Length); + _frames.TryDequeue(out frame); + return true; } - return Task.FromResult(0); + frame = default(RTPFrame); + return false; + } + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) + { + cancelToken.ThrowIfCancellationRequested(); + + var frame = await ReadFrameAsync(cancelToken).ConfigureAwait(false); + if (count < frame.Payload.Length) + throw new InvalidOperationException("Buffer is too small."); + Buffer.BlockCopy(frame.Payload, 0, buffer, offset, frame.Payload.Length); + return frame.Payload.Length; + } + public override async Task ReadFrameAsync(CancellationToken cancelToken) + { + cancelToken.ThrowIfCancellationRequested(); + + await _signal.WaitAsync(cancelToken).ConfigureAwait(false); + _frames.TryDequeue(out RTPFrame frame); + return frame; } - public void WriteHeader(ushort seq, uint timestamp) + public override void WriteHeader(ushort seq, uint timestamp, bool missed) { if (_hasHeader) throw new InvalidOperationException("Header received with no payload"); _hasHeader = true; _nextSeq = seq; _nextTimestamp = timestamp; + _nextMissed = missed; } public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) { cancelToken.ThrowIfCancellationRequested(); - if (_frames.Count > 1000) + if (_signal.CurrentCount >= MaxFrames) //1-2 seconds + { + _hasHeader = false; return Task.Delay(0); //Buffer overloaded - if (_hasHeader) - throw new InvalidOperationException("Received payload with an RTP header"); + } + if (!_hasHeader) + throw new InvalidOperationException("Received payload without an RTP header"); + _hasHeader = false; byte[] payload = new byte[count]; Buffer.BlockCopy(buffer, offset, payload, 0, count); _frames.Enqueue(new RTPFrame( sequence: _nextSeq, timestamp: _nextTimestamp, + missed: _nextMissed, payload: payload )); - _hasHeader = false; + _signal.Release(); return Task.Delay(0); } + + protected override void Dispose(bool isDisposing) + { + _isDisposed = true; + } } } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/JitterBuffer.cs b/src/Discord.Net.WebSocket/Audio/Streams/JitterBuffer.cs new file mode 100644 index 000000000..10f842a9d --- /dev/null +++ b/src/Discord.Net.WebSocket/Audio/Streams/JitterBuffer.cs @@ -0,0 +1,246 @@ +/*using Discord.Logging; +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Audio.Streams +{ + /// Wraps another stream with a timed buffer and packet loss detection. + public class JitterBuffer : AudioOutStream + { + private struct Frame + { + public Frame(byte[] buffer, int bytes, ushort sequence, uint timestamp) + { + Buffer = buffer; + Bytes = bytes; + Sequence = sequence; + Timestamp = timestamp; + } + + public readonly byte[] Buffer; + public readonly int Bytes; + public readonly ushort Sequence; + public readonly uint Timestamp; + } + + private static readonly byte[] _silenceFrame = new byte[0]; + + private readonly AudioStream _next; + private readonly CancellationTokenSource _cancelTokenSource; + private readonly CancellationToken _cancelToken; + private readonly Task _task; + private readonly ConcurrentQueue _queuedFrames; + private readonly ConcurrentQueue _bufferPool; + private readonly SemaphoreSlim _queueLock; + private readonly Logger _logger; + private readonly int _ticksPerFrame, _queueLength; + private bool _isPreloaded, _hasHeader; + + private ushort _seq, _nextSeq; + private uint _timestamp, _nextTimestamp; + private bool _isFirst; + + public JitterBuffer(AudioStream next, int bufferMillis = 60, int maxFrameSize = 1500) + : this(next, null, bufferMillis, maxFrameSize) { } + internal JitterBuffer(AudioStream next, Logger logger, int bufferMillis = 60, int maxFrameSize = 1500) + { + //maxFrameSize = 1275 was too limiting at 128kbps,2ch,60ms + _next = next; + _ticksPerFrame = OpusEncoder.FrameMillis; + _logger = logger; + _queueLength = (bufferMillis + (_ticksPerFrame - 1)) / _ticksPerFrame; //Round up + + _cancelTokenSource = new CancellationTokenSource(); + _cancelToken = _cancelTokenSource.Token; + _queuedFrames = new ConcurrentQueue(); + _bufferPool = new ConcurrentQueue(); + for (int i = 0; i < _queueLength; i++) + _bufferPool.Enqueue(new byte[maxFrameSize]); + _queueLock = new SemaphoreSlim(_queueLength, _queueLength); + + _isFirst = true; + _task = Run(); + } + protected override void Dispose(bool disposing) + { + if (disposing) + _cancelTokenSource.Cancel(); + base.Dispose(disposing); + } + + private Task Run() + { + return Task.Run(async () => + { + try + { + long nextTick = Environment.TickCount; + int silenceFrames = 0; + while (!_cancelToken.IsCancellationRequested) + { + long tick = Environment.TickCount; + long dist = nextTick - tick; + if (dist > 0) + { + await Task.Delay((int)dist).ConfigureAwait(false); + continue; + } + nextTick += _ticksPerFrame; + if (!_isPreloaded) + { + await Task.Delay(_ticksPerFrame).ConfigureAwait(false); + continue; + } + + if (_queuedFrames.TryPeek(out Frame frame)) + { + silenceFrames = 0; + uint distance = (uint)(frame.Timestamp - _timestamp); + bool restartSeq = _isFirst; + if (!_isFirst) + { + if (distance > uint.MaxValue - (OpusEncoder.FrameSamplesPerChannel * 50 * 5)) //Negative distances wraps + { + _queuedFrames.TryDequeue(out frame); + _bufferPool.Enqueue(frame.Buffer); + _queueLock.Release(); +#if DEBUG + var _ = _logger?.DebugAsync($"Dropped frame {_timestamp} ({_queuedFrames.Count} frames buffered)"); +#endif + continue; //This is a missed packet less than five seconds old, ignore it + } + } + + if (distance == 0 || restartSeq) + { + //This is the frame we expected + _seq = frame.Sequence; + _timestamp = frame.Timestamp; + _isFirst = false; + silenceFrames = 0; + + _next.WriteHeader(_seq++, _timestamp, false); + await _next.WriteAsync(frame.Buffer, 0, frame.Bytes).ConfigureAwait(false); + _queuedFrames.TryDequeue(out frame); + _bufferPool.Enqueue(frame.Buffer); + _queueLock.Release(); +#if DEBUG + var _ = _logger?.DebugAsync($"Read frame {_timestamp} ({_queuedFrames.Count} frames buffered)"); +#endif + } + else if (distance == OpusEncoder.FrameSamplesPerChannel) + { + //Missed this frame, but the next queued one might have FEC info + _next.WriteHeader(_seq++, _timestamp, true); + await _next.WriteAsync(frame.Buffer, 0, frame.Bytes).ConfigureAwait(false); +#if DEBUG + var _ = _logger?.DebugAsync($"Recreated Frame {_timestamp} (Next is {frame.Timestamp}) ({_queuedFrames.Count} frames buffered)"); +#endif + } + else + { + //Missed this frame and we have no FEC data to work with + _next.WriteHeader(_seq++, _timestamp, true); + await _next.WriteAsync(null, 0, 0).ConfigureAwait(false); +#if DEBUG + var _ = _logger?.DebugAsync($"Missed Frame {_timestamp} (Next is {frame.Timestamp}) ({_queuedFrames.Count} frames buffered)"); +#endif + } + } + else if (!_isFirst) + { + //Missed this frame and we have no FEC data to work with + _next.WriteHeader(_seq++, _timestamp, true); + await _next.WriteAsync(null, 0, 0).ConfigureAwait(false); + if (silenceFrames < 5) + silenceFrames++; + else + { + _isFirst = true; + _isPreloaded = false; + } +#if DEBUG + var _ = _logger?.DebugAsync($"Missed Frame {_timestamp} ({_queuedFrames.Count} frames buffered)"); +#endif + } + _timestamp += OpusEncoder.FrameSamplesPerChannel; + } + } + catch (OperationCanceledException) { } + }); + } + + public override void WriteHeader(ushort seq, uint timestamp, bool missed) + { + if (_hasHeader) + throw new InvalidOperationException("Header received with no payload"); + _nextSeq = seq; + _nextTimestamp = timestamp; + _hasHeader = true; + } + public override async Task WriteAsync(byte[] data, int offset, int count, CancellationToken cancelToken) + { + if (cancelToken.CanBeCanceled) + cancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _cancelToken).Token; + else + cancelToken = _cancelToken; + + if (!_hasHeader) + throw new InvalidOperationException("Received payload without an RTP header"); + _hasHeader = false; + + uint distance = (uint)(_nextTimestamp - _timestamp); + if (!_isFirst && (distance == 0 || distance > OpusEncoder.FrameSamplesPerChannel * 50 * 5)) //Negative distances wraps + { +#if DEBUG + var _ = _logger?.DebugAsync($"Frame {_nextTimestamp} was {distance} samples off. Ignoring."); +#endif + return; //This is an old frame, ignore + } + + if (!await _queueLock.WaitAsync(0).ConfigureAwait(false)) + { +#if DEBUG + var _ = _logger?.DebugAsync($"Buffer overflow"); +#endif + return; + } + _bufferPool.TryDequeue(out byte[] buffer); + + Buffer.BlockCopy(data, offset, buffer, 0, count); +#if DEBUG + { + var _ = _logger?.DebugAsync($"Queued Frame {_nextTimestamp}."); + } +#endif + _queuedFrames.Enqueue(new Frame(buffer, count, _nextSeq, _nextTimestamp)); + if (!_isPreloaded && _queuedFrames.Count >= _queueLength) + { +#if DEBUG + var _ = _logger?.DebugAsync($"Preloaded"); +#endif + _isPreloaded = true; + } + } + + public override async Task FlushAsync(CancellationToken cancelToken) + { + while (true) + { + cancelToken.ThrowIfCancellationRequested(); + if (_queuedFrames.Count == 0) + return; + await Task.Delay(250, cancelToken).ConfigureAwait(false); + } + } + public override Task ClearAsync(CancellationToken cancelToken) + { + do + cancelToken.ThrowIfCancellationRequested(); + while (_queuedFrames.TryDequeue(out Frame ignored)); + return Task.Delay(0); + } + } +}*/ \ No newline at end of file diff --git a/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs index 9df553bfe..58c4f4c70 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System; +using System.Threading; using System.Threading.Tasks; namespace Discord.Audio.Streams @@ -6,21 +7,51 @@ namespace Discord.Audio.Streams /// Converts Opus to PCM public class OpusDecodeStream : AudioOutStream { - private readonly AudioOutStream _next; - private readonly byte[] _buffer; + public const int SampleRate = OpusEncodeStream.SampleRate; + + private readonly AudioStream _next; private readonly OpusDecoder _decoder; + private readonly byte[] _buffer; + private bool _nextMissed; + private bool _hasHeader; - public OpusDecodeStream(AudioOutStream next, int samplingRate, int channels = OpusConverter.MaxChannels, int bufferSize = 4000) + public OpusDecodeStream(AudioStream next) { _next = next; - _buffer = new byte[bufferSize]; - _decoder = new OpusDecoder(samplingRate, channels); + _buffer = new byte[OpusConverter.FrameBytes]; + _decoder = new OpusDecoder(); } - public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + public override void WriteHeader(ushort seq, uint timestamp, bool missed) { - count = _decoder.DecodeFrame(buffer, offset, count, _buffer, 0); - await _next.WriteAsync(_buffer, 0, count, cancellationToken).ConfigureAwait(false); + if (_hasHeader) + throw new InvalidOperationException("Header received with no payload"); + _hasHeader = true; + + _nextMissed = missed; + _next.WriteHeader(seq, timestamp, missed); + } + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) + { + if (!_hasHeader) + throw new InvalidOperationException("Received payload without an RTP header"); + _hasHeader = false; + + if (!_nextMissed) + { + count = _decoder.DecodeFrame(buffer, offset, count, _buffer, 0, false); + await _next.WriteAsync(_buffer, 0, count, cancelToken).ConfigureAwait(false); + } + else if (count > 0) + { + count = _decoder.DecodeFrame(buffer, offset, count, _buffer, 0, true); + await _next.WriteAsync(_buffer, 0, count, cancelToken).ConfigureAwait(false); + } + else + { + count = _decoder.DecodeFrame(null, 0, 0, _buffer, 0, true); + await _next.WriteAsync(_buffer, 0, count, cancelToken).ConfigureAwait(false); + } } public override async Task FlushAsync(CancellationToken cancelToken) diff --git a/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs index ada8311fe..f5883ad4b 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs @@ -8,51 +8,65 @@ namespace Discord.Audio.Streams public class OpusEncodeStream : AudioOutStream { public const int SampleRate = 48000; - - private readonly AudioOutStream _next; + + private readonly AudioStream _next; private readonly OpusEncoder _encoder; private readonly byte[] _buffer; - - private int _frameSize; - private byte[] _partialFrameBuffer; private int _partialFramePos; - - public OpusEncodeStream(AudioOutStream next, int channels, int samplesPerFrame, int bitrate, AudioApplication application, int bufferSize = 4000) + private ushort _seq; + private uint _timestamp; + + public OpusEncodeStream(AudioStream next, int bitrate, AudioApplication application, int packetLoss) { _next = next; - _encoder = new OpusEncoder(SampleRate, channels, bitrate, application); - _frameSize = samplesPerFrame * channels * 2; - _buffer = new byte[bufferSize]; - _partialFrameBuffer = new byte[_frameSize]; + _encoder = new OpusEncoder(bitrate, application, packetLoss); + _buffer = new byte[OpusConverter.FrameBytes]; } - public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) { //Assume threadsafe while (count > 0) { - if (_partialFramePos + count >= _frameSize) + if (_partialFramePos == 0 && count >= OpusConverter.FrameBytes) { - int partialSize = _frameSize - _partialFramePos; - Buffer.BlockCopy(buffer, offset, _partialFrameBuffer, _partialFramePos, partialSize); + //We have enough data and no partial frames. Pass the buffer directly to the encoder + int encFrameSize = _encoder.EncodeFrame(buffer, offset, _buffer, 0); + _next.WriteHeader(_seq, _timestamp, false); + await _next.WriteAsync(_buffer, 0, encFrameSize, cancelToken).ConfigureAwait(false); + + offset += OpusConverter.FrameBytes; + count -= OpusConverter.FrameBytes; + _seq++; + _timestamp += OpusConverter.FrameSamplesPerChannel; + } + else if (_partialFramePos + count >= OpusConverter.FrameBytes) + { + //We have enough data to complete a previous partial frame. + int partialSize = OpusConverter.FrameBytes - _partialFramePos; + Buffer.BlockCopy(buffer, offset, _buffer, _partialFramePos, partialSize); + int encFrameSize = _encoder.EncodeFrame(_buffer, 0, _buffer, 0); + _next.WriteHeader(_seq, _timestamp, false); + await _next.WriteAsync(_buffer, 0, encFrameSize, cancelToken).ConfigureAwait(false); + offset += partialSize; count -= partialSize; _partialFramePos = 0; - - int encFrameSize = _encoder.EncodeFrame(_partialFrameBuffer, 0, _frameSize, _buffer, 0); - await _next.WriteAsync(_buffer, 0, encFrameSize, cancellationToken).ConfigureAwait(false); + _seq++; + _timestamp += OpusConverter.FrameSamplesPerChannel; } else { - Buffer.BlockCopy(buffer, offset, _partialFrameBuffer, _partialFramePos, count); + //Not enough data to build a complete frame, store this part for later + Buffer.BlockCopy(buffer, offset, _buffer, _partialFramePos, count); _partialFramePos += count; break; } } } - /* - public override async Task FlushAsync(CancellationToken cancellationToken) + /* //Opus throws memory errors on bad frames + public override async Task FlushAsync(CancellationToken cancelToken) { try { @@ -61,7 +75,7 @@ namespace Discord.Audio.Streams } catch (Exception) { } //Incomplete frame _partialFramePos = 0; - await base.FlushAsync(cancellationToken).ConfigureAwait(false); + await base.FlushAsync(cancelToken).ConfigureAwait(false); }*/ public override async Task FlushAsync(CancellationToken cancelToken) diff --git a/src/Discord.Net.WebSocket/Audio/Streams/OutputStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/OutputStream.cs index 6238e93b4..cba4e3cb6 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/OutputStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/OutputStream.cs @@ -13,7 +13,8 @@ namespace Discord.Audio.Streams { _client = client; } - + + public override void WriteHeader(ushort seq, uint timestamp, bool missed) { } //Ignore public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) { cancelToken.ThrowIfCancellationRequested(); diff --git a/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs index 9a57612bf..2cedea114 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs @@ -1,6 +1,4 @@ -using System; -using System.IO; -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace Discord.Audio.Streams @@ -8,21 +6,16 @@ namespace Discord.Audio.Streams /// Reads the payload from an RTP frame public class RTPReadStream : AudioOutStream { - private readonly InputStream _queue; - private readonly AudioOutStream _next; - private readonly byte[] _buffer, _nonce, _secretKey; + private readonly AudioStream _next; + private readonly byte[] _buffer, _nonce; public override bool CanRead => true; public override bool CanSeek => false; public override bool CanWrite => true; - public RTPReadStream(InputStream queue, byte[] secretKey, int bufferSize = 4000) - : this(queue, null, secretKey, bufferSize) { } - public RTPReadStream(InputStream queue, AudioOutStream next, byte[] secretKey, int bufferSize = 4000) + public RTPReadStream(AudioStream next, int bufferSize = 4000) { - _queue = queue; _next = next; - _secretKey = secretKey; _buffer = new byte[bufferSize]; _nonce = new byte[24]; } @@ -31,19 +24,54 @@ namespace Discord.Audio.Streams { cancelToken.ThrowIfCancellationRequested(); - var payload = new byte[count - 12]; - Buffer.BlockCopy(buffer, offset + 12, payload, 0, count - 12); + int headerSize = GetHeaderSize(buffer, offset); - ushort seq = (ushort)((buffer[offset + 3] << 8) | - (buffer[offset + 2] << 0)); + ushort seq = (ushort)((buffer[offset + 2] << 8) | + (buffer[offset + 3] << 0)); uint timestamp = (uint)((buffer[offset + 4] << 24) | (buffer[offset + 5] << 16) | - (buffer[offset + 6] << 16) | + (buffer[offset + 6] << 8) | (buffer[offset + 7] << 0)); - _queue.WriteHeader(seq, timestamp); - await (_next ?? _queue as Stream).WriteAsync(buffer, offset, count, cancelToken).ConfigureAwait(false); + _next.WriteHeader(seq, timestamp, false); + await _next.WriteAsync(buffer, offset + headerSize, count - headerSize, cancelToken).ConfigureAwait(false); + } + + public static bool TryReadSsrc(byte[] buffer, int offset, out uint ssrc) + { + ssrc = 0; + if (buffer.Length - offset < 12) + return false; + + int version = (buffer[offset + 0] & 0b1100_0000) >> 6; + if (version != 2) + return false; + int type = (buffer[offset + 1] & 0b01111_1111); + if (type != 120) //Dynamic Discord type + return false; + + ssrc = (uint)((buffer[offset + 8] << 24) | + (buffer[offset + 9] << 16) | + (buffer[offset + 10] << 8) | + (buffer[offset + 11] << 0)); + return true; + } + + public static int GetHeaderSize(byte[] buffer, int offset) + { + byte headerByte = buffer[offset]; + bool extension = (headerByte & 0b0001_0000) != 0; + int csics = (headerByte & 0b0000_1111) >> 4; + + if (!extension) + return 12 + csics * 4; + + int extensionOffset = offset + 12 + (csics * 4); + int extensionLength = + (buffer[extensionOffset + 2] << 8) | + (buffer[extensionOffset + 3]); + return extensionOffset + 4 + (extensionLength * 4); } } } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs index 836cb4852..ce407eada 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs @@ -7,17 +7,17 @@ namespace Discord.Audio.Streams /// Wraps data in an RTP frame public class RTPWriteStream : AudioOutStream { - private readonly AudioOutStream _next; + private readonly AudioStream _next; private readonly byte[] _header; - private int _samplesPerFrame; - private uint _ssrc, _timestamp = 0; - protected readonly byte[] _buffer; + private uint _ssrc; + private ushort _nextSeq; + private uint _nextTimestamp; + private bool _hasHeader; - public RTPWriteStream(AudioOutStream next, int samplesPerFrame, uint ssrc, int bufferSize = 4000) + public RTPWriteStream(AudioStream next, uint ssrc, int bufferSize = 4000) { _next = next; - _samplesPerFrame = samplesPerFrame; _ssrc = ssrc; _buffer = new byte[bufferSize]; _header = new byte[24]; @@ -29,24 +29,35 @@ namespace Discord.Audio.Streams _header[11] = (byte)(_ssrc >> 0); } - public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + public override void WriteHeader(ushort seq, uint timestamp, bool missed) + { + if (_hasHeader) + throw new InvalidOperationException("Header received with no payload"); + + _hasHeader = true; + _nextSeq = seq; + _nextTimestamp = timestamp; + } + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) { - cancellationToken.ThrowIfCancellationRequested(); + cancelToken.ThrowIfCancellationRequested(); + if (!_hasHeader) + throw new InvalidOperationException("Received payload without an RTP header"); + _hasHeader = false; unchecked { - if (_header[3]++ == byte.MaxValue) - _header[2]++; - - _timestamp += (uint)_samplesPerFrame; - _header[4] = (byte)(_timestamp >> 24); - _header[5] = (byte)(_timestamp >> 16); - _header[6] = (byte)(_timestamp >> 8); - _header[7] = (byte)(_timestamp >> 0); + _header[2] = (byte)(_nextSeq >> 8); + _header[3] = (byte)(_nextSeq >> 0); + _header[4] = (byte)(_nextTimestamp >> 24); + _header[5] = (byte)(_nextTimestamp >> 16); + _header[6] = (byte)(_nextTimestamp >> 8); + _header[7] = (byte)(_nextTimestamp >> 0); } Buffer.BlockCopy(_header, 0, _buffer, 0, 12); //Copy RTP header from to the buffer Buffer.BlockCopy(buffer, offset, _buffer, 12, count); + _next.WriteHeader(_nextSeq, _nextTimestamp, false); await _next.WriteAsync(_buffer, 0, count + 12).ConfigureAwait(false); } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/SodiumDecryptStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/SodiumDecryptStream.cs index f1421d28b..9ed849a5e 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/SodiumDecryptStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/SodiumDecryptStream.cs @@ -7,18 +7,18 @@ namespace Discord.Audio.Streams /// Decrypts an RTP frame using libsodium public class SodiumDecryptStream : AudioOutStream { - private readonly AudioOutStream _next; - private readonly byte[] _buffer, _nonce, _secretKey; + private readonly AudioClient _client; + private readonly AudioStream _next; + private readonly byte[] _nonce; public override bool CanRead => true; public override bool CanSeek => false; public override bool CanWrite => true; - public SodiumDecryptStream(AudioOutStream next, byte[] secretKey, int bufferSize = 4000) + public SodiumDecryptStream(AudioStream next, IAudioClient client) { _next = next; - _secretKey = secretKey; - _buffer = new byte[bufferSize]; + _client = (AudioClient)client; _nonce = new byte[24]; } @@ -26,11 +26,11 @@ namespace Discord.Audio.Streams { cancelToken.ThrowIfCancellationRequested(); - Buffer.BlockCopy(buffer, 0, _nonce, 0, 12); //Copy RTP header to nonce - count = SecretBox.Decrypt(buffer, offset, count, _buffer, 0, _nonce, _secretKey); + if (_client.SecretKey == null) + return; - var newBuffer = new byte[count]; - Buffer.BlockCopy(_buffer, 0, newBuffer, 0, count); + Buffer.BlockCopy(buffer, 0, _nonce, 0, 12); //Copy RTP header to nonce + count = SecretBox.Decrypt(buffer, offset + 12, count - 12, buffer, offset + 12, _nonce, _client.SecretKey); await _next.WriteAsync(buffer, 0, count + 12, cancelToken).ConfigureAwait(false); } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs index 90bc35e9d..bacc9be47 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs @@ -7,25 +7,42 @@ namespace Discord.Audio.Streams /// Encrypts an RTP frame using libsodium public class SodiumEncryptStream : AudioOutStream { - private readonly AudioOutStream _next; - private readonly byte[] _nonce, _secretKey; + private readonly AudioClient _client; + private readonly AudioStream _next; + private readonly byte[] _nonce; + private bool _hasHeader; + private ushort _nextSeq; + private uint _nextTimestamp; - //protected readonly byte[] _buffer; - - public SodiumEncryptStream(AudioOutStream next, byte[] secretKey/*, int bufferSize = 4000*/) + public SodiumEncryptStream(AudioStream next, IAudioClient client) { _next = next; - _secretKey = secretKey; - //_buffer = new byte[bufferSize]; //TODO: Can Sodium do an in-place encrypt? + _client = (AudioClient)client; _nonce = new byte[24]; } + + public override void WriteHeader(ushort seq, uint timestamp, bool missed) + { + if (_hasHeader) + throw new InvalidOperationException("Header received with no payload"); + _nextSeq = seq; + _nextTimestamp = timestamp; + _hasHeader = true; + } public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) { cancelToken.ThrowIfCancellationRequested(); + if (!_hasHeader) + throw new InvalidOperationException("Received payload without an RTP header"); + _hasHeader = false; + if (_client.SecretKey == null) + return; + Buffer.BlockCopy(buffer, offset, _nonce, 0, 12); //Copy nonce from RTP header - count = SecretBox.Encrypt(buffer, offset + 12, count - 12, buffer, 12, _nonce, _secretKey); + count = SecretBox.Encrypt(buffer, offset + 12, count - 12, buffer, 12, _nonce, _client.SecretKey); + _next.WriteHeader(_nextSeq, _nextTimestamp, false); await _next.WriteAsync(buffer, 0, count + 12, cancelToken).ConfigureAwait(false); } diff --git a/src/Discord.Net.WebSocket/ClientState.cs b/src/Discord.Net.WebSocket/ClientState.cs index ce61a0ba5..659b30240 100644 --- a/src/Discord.Net.WebSocket/ClientState.cs +++ b/src/Discord.Net.WebSocket/ClientState.cs @@ -44,15 +44,13 @@ namespace Discord.WebSocket internal SocketChannel GetChannel(ulong id) { - SocketChannel channel; - if (_channels.TryGetValue(id, out channel)) + if (_channels.TryGetValue(id, out SocketChannel channel)) return channel; return null; } internal SocketDMChannel GetDMChannel(ulong userId) { - SocketDMChannel channel; - if (_dmChannels.TryGetValue(userId, out channel)) + if (_dmChannels.TryGetValue(userId, out SocketDMChannel channel)) return channel; return null; } @@ -60,32 +58,28 @@ namespace Discord.WebSocket { _channels[channel.Id] = channel; - var dmChannel = channel as SocketDMChannel; - if (dmChannel != null) - _dmChannels[dmChannel.Recipient.Id] = dmChannel; - else + switch (channel) { - var groupChannel = channel as SocketGroupChannel; - if (groupChannel != null) + case SocketDMChannel dmChannel: + _dmChannels[dmChannel.Recipient.Id] = dmChannel; + break; + case SocketGroupChannel groupChannel: _groupChannels.TryAdd(groupChannel.Id); + break; } } internal SocketChannel RemoveChannel(ulong id) { - SocketChannel channel; - if (_channels.TryRemove(id, out channel)) + if (_channels.TryRemove(id, out SocketChannel channel)) { - var dmChannel = channel as SocketDMChannel; - if (dmChannel != null) + switch (channel) { - SocketDMChannel ignored; - _dmChannels.TryRemove(dmChannel.Recipient.Id, out ignored); - } - else - { - var groupChannel = channel as SocketGroupChannel; - if (groupChannel != null) + case SocketDMChannel dmChannel: + _dmChannels.TryRemove(dmChannel.Recipient.Id, out var ignored); + break; + case SocketGroupChannel groupChannel: _groupChannels.TryRemove(id); + break; } return channel; } @@ -94,8 +88,7 @@ namespace Discord.WebSocket internal SocketGuild GetGuild(ulong id) { - SocketGuild guild; - if (_guilds.TryGetValue(id, out guild)) + if (_guilds.TryGetValue(id, out SocketGuild guild)) return guild; return null; } @@ -105,16 +98,14 @@ namespace Discord.WebSocket } internal SocketGuild RemoveGuild(ulong id) { - SocketGuild guild; - if (_guilds.TryRemove(id, out guild)) + if (_guilds.TryRemove(id, out SocketGuild guild)) return guild; return null; } internal SocketGlobalUser GetUser(ulong id) { - SocketGlobalUser user; - if (_users.TryGetValue(id, out user)) + if (_users.TryGetValue(id, out SocketGlobalUser user)) return user; return null; } @@ -124,8 +115,7 @@ namespace Discord.WebSocket } internal SocketGlobalUser RemoveUser(ulong id) { - SocketGlobalUser user; - if (_users.TryRemove(id, out user)) + if (_users.TryRemove(id, out SocketGlobalUser user)) return user; return null; } diff --git a/src/Discord.Net.WebSocket/Commands/ShardedCommandContext.cs b/src/Discord.Net.WebSocket/Commands/ShardedCommandContext.cs index 627b9b390..a29c9bb70 100644 --- a/src/Discord.Net.WebSocket/Commands/ShardedCommandContext.cs +++ b/src/Discord.Net.WebSocket/Commands/ShardedCommandContext.cs @@ -2,30 +2,20 @@ namespace Discord.Commands { - public class ShardedCommandContext : ICommandContext + public class ShardedCommandContext : SocketCommandContext, ICommandContext { - public DiscordShardedClient Client { get; } - public SocketGuild Guild { get; } - public ISocketMessageChannel Channel { get; } - public SocketUser User { get; } - public SocketUserMessage Message { get; } - - public bool IsPrivate => Channel is IPrivateChannel; + public new DiscordShardedClient Client { get; } public ShardedCommandContext(DiscordShardedClient client, SocketUserMessage msg) + : base(client.GetShard(GetShardId(client, (msg.Channel as SocketGuildChannel)?.Guild)), msg) { Client = client; - Guild = (msg.Channel as SocketGuildChannel)?.Guild; - Channel = msg.Channel; - User = msg.Author; - Message = msg; } + private static int GetShardId(DiscordShardedClient client, IGuild guild) + => guild == null ? 0 : client.GetShardIdFor(guild); + //ICommandContext IDiscordClient ICommandContext.Client => Client; - IGuild ICommandContext.Guild => Guild; - IMessageChannel ICommandContext.Channel => Channel; - IUser ICommandContext.User => User; - IUserMessage ICommandContext.Message => Message; } } diff --git a/src/Discord.Net.WebSocket/ConnectionManager.cs b/src/Discord.Net.WebSocket/ConnectionManager.cs index 72926e2e3..decae4163 100644 --- a/src/Discord.Net.WebSocket/ConnectionManager.cs +++ b/src/Discord.Net.WebSocket/ConnectionManager.cs @@ -26,8 +26,6 @@ namespace Discord public ConnectionState State { get; private set; } public CancellationToken CancelToken { get; private set; } - public bool IsCompleted => _readyPromise.Task.IsCompleted; - internal ConnectionManager(SemaphoreSlim stateLock, Logger logger, int connectionTimeout, Func onConnecting, Func onDisconnecting, Action> clientDisconnectHandler) { @@ -193,6 +191,12 @@ namespace Discord _reconnectCancelToken?.Cancel(); Error(ex); } + public void Reconnect() + { + _readyPromise.TrySetCanceled(); + _connectionPromise.TrySetCanceled(); + _connectionCancelToken?.Cancel(); + } private async Task AcquireConnectionLock() { while (true) diff --git a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj index b5dab98e5..e4de43e51 100644 --- a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj +++ b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj @@ -1,37 +1,17 @@ - + + - 1.0.0 - rc-dev - rc-$(BuildNumber) - netstandard1.1;netstandard1.3 Discord.Net.WebSocket - RogueException - A core Discord.Net library containing the WebSocket client and models. - discord;discordapp - https://github.com/RogueException/Discord.Net - http://opensource.org/licenses/MIT - git - git://github.com/RogueException/Discord.Net Discord.WebSocket + A core Discord.Net library containing the WebSocket client and models. + net45;netstandard1.1;netstandard1.3 true - true - - - - - - - + - - $(NoWarn);CS1573;CS1591 - true - true - \ No newline at end of file diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.Events.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.Events.cs index 874062c56..c52675e70 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.Events.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.Events.cs @@ -1,6 +1,5 @@ using System; using System.Threading.Tasks; -using Discord.Net; namespace Discord.WebSocket { diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index 06f83c8dc..ab2cb9266 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -65,7 +65,7 @@ namespace Discord.WebSocket var newConfig = config.Clone(); newConfig.ShardId = _shardIds[i]; _shards[i] = new DiscordSocketClient(newConfig, _connectionGroupLock, i != 0 ? _shards[0] : null); - RegisterEvents(_shards[i]); + RegisterEvents(_shards[i], i == 0); } } } @@ -87,7 +87,7 @@ namespace Discord.WebSocket newConfig.ShardId = _shardIds[i]; newConfig.TotalShards = _totalShards; _shards[i] = new DiscordSocketClient(newConfig, _connectionGroupLock, i != 0 ? _shards[0] : null); - RegisterEvents(_shards[i]); + RegisterEvents(_shards[i], i == 0); } } @@ -98,8 +98,11 @@ namespace Discord.WebSocket internal override async Task OnLogoutAsync() { //Assume threadsafe: already in a connection lock - for (int i = 0; i < _shards.Length; i++) - await _shards[i].LogoutAsync(); + if (_shards != null) + { + for (int i = 0; i < _shards.Length; i++) + await _shards[i].LogoutAsync(); + } CurrentUser = null; if (_automaticShards) @@ -145,7 +148,7 @@ namespace Discord.WebSocket public SocketGuild GetGuild(ulong id) => GetShardFor(id).GetGuild(id); /// public Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null) - => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon); + => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon, new RequestOptions()); /// public SocketChannel GetChannel(ulong id) @@ -176,7 +179,7 @@ namespace Discord.WebSocket /// public Task> GetConnectionsAsync() - => ClientHelper.GetConnectionsAsync(this); + => ClientHelper.GetConnectionsAsync(this, new RequestOptions()); private IEnumerable GetGuilds() { @@ -196,7 +199,7 @@ namespace Discord.WebSocket /// public Task GetInviteAsync(string inviteId) - => ClientHelper.GetInviteAsync(this, inviteId); + => ClientHelper.GetInviteAsync(this, inviteId, new RequestOptions()); /// public SocketUser GetUser(ulong id) @@ -233,7 +236,7 @@ namespace Discord.WebSocket int id = _shardIds[i]; var arr = guilds.Where(x => GetShardIdFor(x) == id).ToArray(); if (arr.Length > 0) - await _shards[i].DownloadUsersAsync(arr); + await _shards[i].DownloadUsersAsync(arr).ConfigureAwait(false); } } @@ -256,7 +259,7 @@ namespace Discord.WebSocket await _shards[i].SetGameAsync(name, streamUrl, streamType).ConfigureAwait(false); } - private void RegisterEvents(DiscordSocketClient client) + private void RegisterEvents(DiscordSocketClient client, bool isPrimary) { client.Log += (msg) => _logEvent.InvokeAsync(msg); client.LoggedOut += () => @@ -269,6 +272,14 @@ namespace Discord.WebSocket } return Task.Delay(0); }; + if (isPrimary) + { + client.Ready += () => + { + CurrentUser = client.CurrentUser; + return Task.Delay(0); + }; + } client.ChannelCreated += (channel) => _channelCreatedEvent.InvokeAsync(channel); client.ChannelDestroyed += (channel) => _channelDestroyedEvent.InvokeAsync(channel); @@ -297,44 +308,44 @@ namespace Discord.WebSocket client.UserBanned += (user, guild) => _userBannedEvent.InvokeAsync(user, guild); client.UserUnbanned += (user, guild) => _userUnbannedEvent.InvokeAsync(user, guild); client.UserUpdated += (oldUser, newUser) => _userUpdatedEvent.InvokeAsync(oldUser, newUser); - client.UserPresenceUpdated += (guild, user, oldPresence, newPresence) => _userPresenceUpdatedEvent.InvokeAsync(guild, user, oldPresence, newPresence); + client.GuildMemberUpdated += (oldUser, newUser) => _guildMemberUpdatedEvent.InvokeAsync(oldUser, newUser); client.UserVoiceStateUpdated += (user, oldVoiceState, newVoiceState) => _userVoiceStateUpdatedEvent.InvokeAsync(user, oldVoiceState, newVoiceState); client.CurrentUserUpdated += (oldUser, newUser) => _selfUpdatedEvent.InvokeAsync(oldUser, newUser); client.UserIsTyping += (oldUser, newUser) => _userIsTypingEvent.InvokeAsync(oldUser, newUser); client.RecipientAdded += (user) => _recipientAddedEvent.InvokeAsync(user); - client.RecipientAdded += (user) => _recipientRemovedEvent.InvokeAsync(user); + client.RecipientRemoved += (user) => _recipientRemovedEvent.InvokeAsync(user); } //IDiscordClient - async Task IDiscordClient.GetApplicationInfoAsync() + async Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) => await GetApplicationInfoAsync().ConfigureAwait(false); - Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode) + Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetChannel(id)); - Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode) + Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(PrivateChannels); - async Task> IDiscordClient.GetConnectionsAsync() + async Task> IDiscordClient.GetConnectionsAsync(RequestOptions options) => await GetConnectionsAsync().ConfigureAwait(false); - async Task IDiscordClient.GetInviteAsync(string inviteId) + async Task IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options) => await GetInviteAsync(inviteId).ConfigureAwait(false); - Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode) + Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetGuild(id)); - Task> IDiscordClient.GetGuildsAsync(CacheMode mode) + Task> IDiscordClient.GetGuildsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(Guilds); - async Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon) + async Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) => await CreateGuildAsync(name, region, jpegIcon).ConfigureAwait(false); - Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode) + Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); - Task IDiscordClient.GetUserAsync(string username, string discriminator) + Task IDiscordClient.GetUserAsync(string username, string discriminator, RequestOptions options) => Task.FromResult(GetUser(username, discriminator)); - Task> IDiscordClient.GetVoiceRegionsAsync() + Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) => Task.FromResult>(VoiceRegions); - Task IDiscordClient.GetVoiceRegionAsync(string id) + Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) => Task.FromResult(GetVoiceRegion(id)); } } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs index d731e390a..7d7a18c39 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs @@ -185,12 +185,6 @@ namespace Discord.WebSocket remove { _guildMemberUpdatedEvent.Remove(value); } } private readonly AsyncEvent> _guildMemberUpdatedEvent = new AsyncEvent>(); - public event Func, SocketUser, SocketPresence, SocketPresence, Task> UserPresenceUpdated - { - add { _userPresenceUpdatedEvent.Add(value); } - remove { _userPresenceUpdatedEvent.Remove(value); } - } - private readonly AsyncEvent, SocketUser, SocketPresence, SocketPresence, Task>> _userPresenceUpdatedEvent = new AsyncEvent, SocketUser, SocketPresence, SocketPresence, Task>>(); public event Func UserVoiceStateUpdated { add { _userVoiceStateUpdatedEvent.Add(value); } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 393e29a20..9b5da78e5 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1,6 +1,5 @@ using Discord.API; using Discord.API.Gateway; -using Discord.Audio; using Discord.Logging; using Discord.Net.Converters; using Discord.Net.Udp; @@ -35,17 +34,16 @@ namespace Discord.WebSocket private int _lastSeq; private ImmutableDictionary _voiceRegions; private Task _heartbeatTask, _guildDownloadTask; - private int _unavailableGuilds; + private int _unavailableGuildCount; private long _lastGuildAvailableTime, _lastMessageTime; private int _nextAudioId; private DateTimeOffset? _statusSince; private RestApplication _applicationInfo; - private ConcurrentHashSet _downloadUsersFor; /// Gets the shard of of this client. public int ShardId { get; } /// Gets the current connection state of this client. - public ConnectionState ConnectionState { get; private set; } + public ConnectionState ConnectionState => _connection.State; /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. public int Latency { get; private set; } internal UserStatus Status { get; private set; } = UserStatus.Online; @@ -55,14 +53,14 @@ namespace Discord.WebSocket internal int TotalShards { get; private set; } internal int MessageCacheSize { get; private set; } internal int LargeThreshold { get; private set; } - internal AudioMode AudioMode { get; private set; } internal ClientState State { get; private set; } internal UdpSocketProvider UdpSocketProvider { get; private set; } internal WebSocketProvider WebSocketProvider { get; private set; } internal bool AlwaysDownloadUsers { get; private set; } + internal int? HandlerTimeout { get; private set; } internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; - public new SocketSelfUser CurrentUser { get { return base.CurrentUser as SocketSelfUser; } private set { base.CurrentUser = value; } } + public new SocketSelfUser CurrentUser { get => base.CurrentUser as SocketSelfUser; private set => base.CurrentUser = value; } public IReadOnlyCollection Guilds => State.Guilds; public IReadOnlyCollection PrivateChannels => State.PrivateChannels; public IReadOnlyCollection DMChannels @@ -84,20 +82,19 @@ namespace Discord.WebSocket TotalShards = config.TotalShards ?? 1; MessageCacheSize = config.MessageCacheSize; LargeThreshold = config.LargeThreshold; - AudioMode = config.AudioMode; UdpSocketProvider = config.UdpSocketProvider; WebSocketProvider = config.WebSocketProvider; AlwaysDownloadUsers = config.AlwaysDownloadUsers; + HandlerTimeout = config.HandlerTimeout; State = new ClientState(0, 0); - _downloadUsersFor = new ConcurrentHashSet(); _heartbeatTimes = new ConcurrentQueue(); _stateLock = new SemaphoreSlim(1, 1); _gatewayLogger = LogManager.CreateLogger(ShardId == 0 && TotalShards == 1 ? "Gateway" : $"Shard #{ShardId}"); _connection = new ConnectionManager(_stateLock, _gatewayLogger, config.ConnectionTimeout, OnConnectingAsync, OnDisconnectingAsync, x => ApiClient.Disconnected += x); - _connection.Connected += () => _connectedEvent.InvokeAsync(); - _connection.Disconnected += (ex, recon) => _disconnectedEvent.InvokeAsync(ex); + _connection.Connected += () => TimedInvokeAsync(_connectedEvent, nameof(Connected)); + _connection.Disconnected += (ex, recon) => TimedInvokeAsync(_disconnectedEvent, nameof(Disconnected), ex); _nextAudioId = 1; _connectionGroupLock = groupLock; @@ -117,16 +114,13 @@ namespace Discord.WebSocket JoinedGuild += async g => await _gatewayLogger.InfoAsync($"Joined {g.Name}").ConfigureAwait(false); GuildAvailable += async g => await _gatewayLogger.VerboseAsync($"Connected to {g.Name}").ConfigureAwait(false); GuildUnavailable += async g => await _gatewayLogger.VerboseAsync($"Disconnected from {g.Name}").ConfigureAwait(false); - LatencyUpdated += async (old, val) => await _gatewayLogger.VerboseAsync($"Latency = {val} ms").ConfigureAwait(false); + LatencyUpdated += async (old, val) => await _gatewayLogger.DebugAsync($"Latency = {val} ms").ConfigureAwait(false); GuildAvailable += g => { - if (ConnectionState == ConnectionState.Connected && (AlwaysDownloadUsers || _downloadUsersFor.ContainsKey(g.Id))) + if (ConnectionState == ConnectionState.Connected && AlwaysDownloadUsers && !g.HasAllMembers) { - if (!g.HasAllMembers) - { - var _ = g.DownloadUsersAsync(); - } + var _ = g.DownloadUsersAsync(); } return Task.Delay(0); }; @@ -160,7 +154,6 @@ namespace Discord.WebSocket await StopAsync().ConfigureAwait(false); _applicationInfo = null; _voiceRegions = ImmutableDictionary.Create(); - _downloadUsersFor.Clear(); } public async Task StartAsync() @@ -193,9 +186,6 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Sending Status").ConfigureAwait(false); await SendStatusAsync().ConfigureAwait(false); - - await ProcessUserDownloadsAsync(_downloadUsersFor.Select(x => GetGuild(x)) - .Where(x => x != null).ToImmutableArray()).ConfigureAwait(false); } finally { @@ -208,7 +198,6 @@ namespace Discord.WebSocket } private async Task OnDisconnectingAsync(Exception ex) { - ulong guildId; await _gatewayLogger.DebugAsync("Disconnecting ApiClient").ConfigureAwait(false); await ApiClient.DisconnectAsync().ConfigureAwait(false); @@ -220,8 +209,7 @@ namespace Discord.WebSocket await heartbeatTask.ConfigureAwait(false); _heartbeatTask = null; - long time; - while (_heartbeatTimes.TryDequeue(out time)) { } + while (_heartbeatTimes.TryDequeue(out long time)) { } _lastMessageTime = 0; await _gatewayLogger.DebugAsync("Waiting for guild downloader").ConfigureAwait(false); @@ -232,21 +220,21 @@ namespace Discord.WebSocket //Clear large guild queue await _gatewayLogger.DebugAsync("Clearing large guild queue").ConfigureAwait(false); - while (_largeGuilds.TryDequeue(out guildId)) { } + while (_largeGuilds.TryDequeue(out ulong guildId)) { } //Raise virtual GUILD_UNAVAILABLEs await _gatewayLogger.DebugAsync("Raising virtual GuildUnavailables").ConfigureAwait(false); foreach (var guild in State.Guilds) { - if (guild._available) - await _guildUnavailableEvent.InvokeAsync(guild).ConfigureAwait(false); + if (guild.IsAvailable) + await GuildUnavailableAsync(guild).ConfigureAwait(false); } } /// public async Task GetApplicationInfoAsync() { - return _applicationInfo ?? (_applicationInfo = await ClientHelper.GetApplicationInfoAsync(this)); + return _applicationInfo ?? (_applicationInfo = await ClientHelper.GetApplicationInfoAsync(this, new RequestOptions())); } /// @@ -256,7 +244,7 @@ namespace Discord.WebSocket } /// public Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null) - => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon); + => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon, new RequestOptions()); /// public SocketChannel GetChannel(ulong id) @@ -266,7 +254,7 @@ namespace Discord.WebSocket /// public Task> GetConnectionsAsync() - => ClientHelper.GetConnectionsAsync(this); + => ClientHelper.GetConnectionsAsync(this, new RequestOptions()); /// public Task GetInviteAsync(string inviteId) @@ -283,7 +271,7 @@ namespace Discord.WebSocket /// public SocketUser GetUser(string username, string discriminator) { - return State.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault(); + return State.Users.FirstOrDefault(x => x.Discriminator == discriminator && x.Username == username); } internal SocketGlobalUser GetOrCreateUser(ClientState state, Discord.API.User model) { @@ -312,8 +300,7 @@ namespace Discord.WebSocket /// public RestVoiceRegion GetVoiceRegion(string id) { - RestVoiceRegion region; - if (_voiceRegions.TryGetValue(id, out region)) + if (_voiceRegions.TryGetValue(id, out RestVoiceRegion region)) return region; return null; } @@ -321,9 +308,6 @@ namespace Discord.WebSocket /// Downloads the users list for the provided guilds, if they don't have a complete list. public async Task DownloadUsersAsync(IEnumerable guilds) { - foreach (var guild in guilds) - _downloadUsersFor.TryAdd(guild.Id); - if (ConnectionState == ConnectionState.Connected) { //Race condition leads to guilds being requested twice, probably okay @@ -375,11 +359,12 @@ namespace Discord.WebSocket Game = new Game(name, streamUrl, streamType); else Game = null; - CurrentUser.Presence = new SocketPresence(Status, Game); await SendStatusAsync().ConfigureAwait(false); } private async Task SendStatusAsync() { + if (CurrentUser == null) + return; var game = Game; var status = Status; var statusSince = _statusSince; @@ -434,14 +419,13 @@ namespace Discord.WebSocket { await _gatewayLogger.DebugAsync("Received HeartbeatAck").ConfigureAwait(false); - long time; - if (_heartbeatTimes.TryDequeue(out time)) + if (_heartbeatTimes.TryDequeue(out long time)) { int latency = (int)(Environment.TickCount - time); int before = Latency; Latency = latency; - await _latencyUpdatedEvent.InvokeAsync(before, latency).ConfigureAwait(false); + await TimedInvokeAsync(_latencyUpdatedEvent, nameof(LatencyUpdated), before, latency).ConfigureAwait(false); } } break; @@ -452,7 +436,11 @@ namespace Discord.WebSocket _sessionId = null; _lastSeq = 0; - await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards).ConfigureAwait(false); + bool retry = (bool)payload; + if (retry) + _connection.Reconnect(); //TODO: Untested + else + await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards).ConfigureAwait(false); } break; case GatewayOpCode.Reconnect: @@ -481,10 +469,10 @@ namespace Discord.WebSocket { var model = data.Guilds[i]; var guild = AddGuild(model, state); - if (!guild._available || ApiClient.AuthTokenType == TokenType.User) + if (!guild.IsAvailable || ApiClient.AuthTokenType == TokenType.User) unavailableGuilds++; else - await _guildAvailableEvent.InvokeAsync(guild).ConfigureAwait(false); + await GuildAvailableAsync(guild).ConfigureAwait(false); } for (int i = 0; i < data.PrivateChannels.Length; i++) AddPrivateChannel(data.PrivateChannels[i], state); @@ -492,7 +480,7 @@ namespace Discord.WebSocket AddRelationship(data.Relationships[i], state); _sessionId = data.SessionId; - _unavailableGuilds = unavailableGuilds; + _unavailableGuildCount = unavailableGuilds; CurrentUser = currentUser; State = state; } @@ -506,12 +494,21 @@ namespace Discord.WebSocket await SyncGuildsAsync().ConfigureAwait(false); _lastGuildAvailableTime = Environment.TickCount; - _guildDownloadTask = WaitForGuildsAsync(_connection.CancelToken, _gatewayLogger); - - await _readyEvent.InvokeAsync().ConfigureAwait(false); - + _guildDownloadTask = WaitForGuildsAsync(_connection.CancelToken, _gatewayLogger) + .ContinueWith(async x => + { + if (x.IsFaulted) + { + _connection.Error(x.Exception); + return; + } + else if (_connection.CancelToken.IsCancellationRequested) + return; + + await TimedInvokeAsync(_readyEvent, nameof(Ready)).ConfigureAwait(false); + await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false); + }); var _ = _connection.CompleteAsync(); - await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false); } break; case "RESUMED": @@ -523,13 +520,13 @@ namespace Discord.WebSocket //Notify the client that these guilds are available again foreach (var guild in State.Guilds) { - if (guild._available) - await _guildAvailableEvent.InvokeAsync(guild).ConfigureAwait(false); + if (guild.IsAvailable) + await GuildAvailableAsync(guild).ConfigureAwait(false); } await _gatewayLogger.InfoAsync("Resumed previous session").ConfigureAwait(false); } - return; + break; //Guilds case "GUILD_CREATE": @@ -546,15 +543,20 @@ namespace Discord.WebSocket if (guild != null) { guild.Update(State, data); + + if (_unavailableGuildCount != 0) + _unavailableGuildCount--; + await GuildAvailableAsync(guild).ConfigureAwait(false); - var unavailableGuilds = _unavailableGuilds; - if (unavailableGuilds != 0) - _unavailableGuilds = unavailableGuilds - 1; - await _guildAvailableEvent.InvokeAsync(guild).ConfigureAwait(false); + if (guild.DownloadedMemberCount >= guild.MemberCount && !guild.DownloaderPromise.IsCompleted) + { + guild.CompleteDownloadUsers(); + await TimedInvokeAsync(_guildMembersDownloadedEvent, nameof(GuildMembersDownloaded), guild).ConfigureAwait(false); + } } else { - await _gatewayLogger.WarningAsync($"GUILD_AVAILABLE referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); return; } } @@ -567,11 +569,11 @@ namespace Discord.WebSocket { if (ApiClient.AuthTokenType == TokenType.User) await SyncGuildsAsync().ConfigureAwait(false); - await _joinedGuildEvent.InvokeAsync(guild).ConfigureAwait(false); + await TimedInvokeAsync(_joinedGuildEvent, nameof(JoinedGuild), guild).ConfigureAwait(false); } else { - await _gatewayLogger.WarningAsync($"GUILD_CREATE referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); return; } } @@ -587,11 +589,11 @@ namespace Discord.WebSocket { var before = guild.Clone(); guild.Update(State, data); - await _guildUpdatedEvent.InvokeAsync(before, guild).ConfigureAwait(false); + await TimedInvokeAsync(_guildUpdatedEvent, nameof(GuildUpdated), before, guild).ConfigureAwait(false); } else { - await _gatewayLogger.WarningAsync("GUILD_UPDATE referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); return; } } @@ -606,15 +608,15 @@ namespace Discord.WebSocket { var before = guild.Clone(); guild.Update(State, data); - await _guildUpdatedEvent.InvokeAsync(before, guild).ConfigureAwait(false); + await TimedInvokeAsync(_guildUpdatedEvent, nameof(GuildUpdated), before, guild).ConfigureAwait(false); } else { - await _gatewayLogger.WarningAsync("GUILD_EMOJIS_UPDATE referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); return; } } - return; + break; case "GUILD_SYNC": { await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_SYNC)").ConfigureAwait(false); @@ -625,18 +627,18 @@ namespace Discord.WebSocket var before = guild.Clone(); guild.Update(State, data); //This is treated as an extension of GUILD_AVAILABLE - _unavailableGuilds--; + _unavailableGuildCount--; _lastGuildAvailableTime = Environment.TickCount; - await _guildAvailableEvent.InvokeAsync(guild).ConfigureAwait(false); - await _guildUpdatedEvent.InvokeAsync(before, guild).ConfigureAwait(false); + await GuildAvailableAsync(guild).ConfigureAwait(false); + await TimedInvokeAsync(_guildUpdatedEvent, nameof(GuildUpdated), before, guild).ConfigureAwait(false); } else { - await _gatewayLogger.WarningAsync("GUILD_SYNC referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); return; } } - return; + break; case "GUILD_DELETE": { var data = (payload as JToken).ToObject(_serializer); @@ -648,12 +650,12 @@ namespace Discord.WebSocket var guild = State.GetGuild(data.Id); if (guild != null) { - await _guildUnavailableEvent.InvokeAsync(guild).ConfigureAwait(false); - _unavailableGuilds++; + await GuildUnavailableAsync(guild).ConfigureAwait(false); + _unavailableGuildCount++; } else { - await _gatewayLogger.WarningAsync($"GUILD_UNAVAILABLE referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); return; } } @@ -661,16 +663,15 @@ namespace Discord.WebSocket { await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_DELETE)").ConfigureAwait(false); - _downloadUsersFor.TryRemove(data.Id); var guild = RemoveGuild(data.Id); if (guild != null) { - await _guildUnavailableEvent.InvokeAsync(guild).ConfigureAwait(false); - await _leftGuildEvent.InvokeAsync(guild).ConfigureAwait(false); + await GuildUnavailableAsync(guild).ConfigureAwait(false); + await TimedInvokeAsync(_leftGuildEvent, nameof(LeftGuild), guild).ConfigureAwait(false); } else { - await _gatewayLogger.WarningAsync($"GUILD_DELETE referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); return; } } @@ -693,13 +694,13 @@ namespace Discord.WebSocket if (!guild.IsSynced) { - await _gatewayLogger.DebugAsync("Ignored CHANNEL_CREATE, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } } else { - await _gatewayLogger.WarningAsync("CHANNEL_CREATE referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); return; } } @@ -707,7 +708,7 @@ namespace Discord.WebSocket channel = AddPrivateChannel(data, State) as SocketChannel; if (channel != null) - await _channelCreatedEvent.InvokeAsync(channel).ConfigureAwait(false); + await TimedInvokeAsync(_channelCreatedEvent, nameof(ChannelCreated), channel).ConfigureAwait(false); } break; case "CHANNEL_UPDATE": @@ -721,17 +722,18 @@ namespace Discord.WebSocket var before = channel.Clone(); channel.Update(State, data); - if (!((channel as SocketGuildChannel)?.Guild.IsSynced ?? true)) + var guild = (channel as SocketGuildChannel)?.Guild; + if (!(guild?.IsSynced ?? true)) { - await _gatewayLogger.DebugAsync("Ignored CHANNEL_UPDATE, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } - await _channelUpdatedEvent.InvokeAsync(before, channel).ConfigureAwait(false); + await TimedInvokeAsync(_channelUpdatedEvent, nameof(ChannelUpdated), before, channel).ConfigureAwait(false); } else { - await _gatewayLogger.WarningAsync("CHANNEL_UPDATE referenced an unknown channel.").ConfigureAwait(false); + await UnknownChannelAsync(type, data.Id).ConfigureAwait(false); return; } } @@ -751,13 +753,13 @@ namespace Discord.WebSocket if (!guild.IsSynced) { - await _gatewayLogger.DebugAsync("Ignored CHANNEL_DELETE, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } } else { - await _gatewayLogger.WarningAsync("CHANNEL_DELETE referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); return; } } @@ -765,10 +767,10 @@ namespace Discord.WebSocket channel = RemovePrivateChannel(data.Id) as SocketChannel; if (channel != null) - await _channelDestroyedEvent.InvokeAsync(channel).ConfigureAwait(false); + await TimedInvokeAsync(_channelDestroyedEvent, nameof(ChannelDestroyed), channel).ConfigureAwait(false); else { - await _gatewayLogger.WarningAsync("CHANNEL_DELETE referenced an unknown channel.").ConfigureAwait(false); + await UnknownChannelAsync(type, data.Id, data.GuildId.GetValueOrDefault(0)).ConfigureAwait(false); return; } } @@ -788,15 +790,15 @@ namespace Discord.WebSocket if (!guild.IsSynced) { - await _gatewayLogger.DebugAsync("Ignored GUILD_MEMBER_ADD, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } - await _userJoinedEvent.InvokeAsync(user).ConfigureAwait(false); + await TimedInvokeAsync(_userJoinedEvent, nameof(UserJoined), user).ConfigureAwait(false); } else { - await _gatewayLogger.WarningAsync("GUILD_MEMBER_ADD referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); return; } } @@ -813,7 +815,7 @@ namespace Discord.WebSocket if (!guild.IsSynced) { - await _gatewayLogger.DebugAsync("Ignored GUILD_MEMBER_UPDATE, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } @@ -821,23 +823,20 @@ namespace Discord.WebSocket { var before = user.Clone(); user.Update(State, data); - await _guildMemberUpdatedEvent.InvokeAsync(before, user).ConfigureAwait(false); + await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), before, user).ConfigureAwait(false); } else { if (!guild.HasAllMembers) - { - await _gatewayLogger.DebugAsync("Ignored GUILD_MEMBER_UPDATE, this user has not been downloaded yet.").ConfigureAwait(false); - return; - } - - await _gatewayLogger.WarningAsync("GUILD_MEMBER_UPDATE referenced an unknown user.").ConfigureAwait(false); + await IncompleteGuildUserAsync(type, data.User.Id, data.GuildId).ConfigureAwait(false); + else + await UnknownGuildUserAsync(type, data.User.Id, data.GuildId).ConfigureAwait(false); return; } } else { - await _gatewayLogger.WarningAsync("GUILD_MEMBER_UPDATE referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); return; } } @@ -855,27 +854,24 @@ namespace Discord.WebSocket if (!guild.IsSynced) { - await _gatewayLogger.DebugAsync("Ignored GUILD_MEMBER_REMOVE, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } if (user != null) - await _userLeftEvent.InvokeAsync(user).ConfigureAwait(false); + await TimedInvokeAsync(_userLeftEvent, nameof(UserLeft), user).ConfigureAwait(false); else { if (!guild.HasAllMembers) - { - await _gatewayLogger.DebugAsync("Ignored GUILD_MEMBER_REMOVE, this user has not been downloaded yet.").ConfigureAwait(false); - return; - } - - await _gatewayLogger.WarningAsync("GUILD_MEMBER_REMOVE referenced an unknown user.").ConfigureAwait(false); + await IncompleteGuildUserAsync(type, data.User.Id, data.GuildId).ConfigureAwait(false); + else + await UnknownGuildUserAsync(type, data.User.Id, data.GuildId).ConfigureAwait(false); return; } } else { - await _gatewayLogger.WarningAsync("GUILD_MEMBER_REMOVE referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); return; } } @@ -891,15 +887,15 @@ namespace Discord.WebSocket foreach (var memberModel in data.Members) guild.AddOrUpdateUser(memberModel); - if (guild.DownloadedMemberCount >= guild.MemberCount) //Finished downloading for there + if (guild.DownloadedMemberCount >= guild.MemberCount && !guild.DownloaderPromise.IsCompleted) { guild.CompleteDownloadUsers(); - await _guildMembersDownloadedEvent.InvokeAsync(guild).ConfigureAwait(false); + await TimedInvokeAsync(_guildMembersDownloadedEvent, nameof(GuildMembersDownloaded), guild).ConfigureAwait(false); } } else { - await _gatewayLogger.WarningAsync("GUILD_MEMBERS_CHUNK referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); return; } } @@ -909,15 +905,14 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_RECIPIENT_ADD)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = State.GetChannel(data.ChannelId) as SocketGroupChannel; - if (channel != null) + if (State.GetChannel(data.ChannelId) is SocketGroupChannel channel) { - var user = channel.AddUser(data.User); - await _recipientAddedEvent.InvokeAsync(user).ConfigureAwait(false); + var user = channel.GetOrAddUser(data.User); + await TimedInvokeAsync(_recipientAddedEvent, nameof(RecipientAdded), user).ConfigureAwait(false); } else { - await _gatewayLogger.WarningAsync("CHANNEL_RECIPIENT_ADD referenced an unknown channel.").ConfigureAwait(false); + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); return; } } @@ -927,21 +922,20 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_RECIPIENT_REMOVE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = State.GetChannel(data.ChannelId) as SocketGroupChannel; - if (channel != null) + if (State.GetChannel(data.ChannelId) is SocketGroupChannel channel) { var user = channel.RemoveUser(data.User.Id); if (user != null) - await _recipientRemovedEvent.InvokeAsync(user).ConfigureAwait(false); + await TimedInvokeAsync(_recipientRemovedEvent, nameof(RecipientRemoved), user).ConfigureAwait(false); else { - await _gatewayLogger.WarningAsync("CHANNEL_RECIPIENT_REMOVE referenced an unknown user.").ConfigureAwait(false); + await UnknownChannelUserAsync(type, data.User.Id, data.ChannelId).ConfigureAwait(false); return; } } else { - await _gatewayLogger.WarningAsync("CHANNEL_RECIPIENT_ADD referenced an unknown channel.").ConfigureAwait(false); + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); return; } } @@ -960,14 +954,14 @@ namespace Discord.WebSocket if (!guild.IsSynced) { - await _gatewayLogger.DebugAsync("Ignored GUILD_ROLE_CREATE, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } - await _roleCreatedEvent.InvokeAsync(role).ConfigureAwait(false); + await TimedInvokeAsync(_roleCreatedEvent, nameof(RoleCreated), role).ConfigureAwait(false); } else { - await _gatewayLogger.WarningAsync("GUILD_ROLE_CREATE referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); return; } } @@ -988,21 +982,21 @@ namespace Discord.WebSocket if (!guild.IsSynced) { - await _gatewayLogger.DebugAsync("Ignored GUILD_ROLE_UPDATE, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } - await _roleUpdatedEvent.InvokeAsync(before, role).ConfigureAwait(false); + await TimedInvokeAsync(_roleUpdatedEvent, nameof(RoleUpdated), before, role).ConfigureAwait(false); } else { - await _gatewayLogger.WarningAsync("GUILD_ROLE_UPDATE referenced an unknown role.").ConfigureAwait(false); + await UnknownRoleAsync(type, data.Role.Id, guild.Id).ConfigureAwait(false); return; } } else { - await _gatewayLogger.WarningAsync("GUILD_ROLE_UPDATE referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); return; } } @@ -1020,21 +1014,21 @@ namespace Discord.WebSocket { if (!guild.IsSynced) { - await _gatewayLogger.DebugAsync("Ignored GUILD_ROLE_DELETE, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } - await _roleDeletedEvent.InvokeAsync(role).ConfigureAwait(false); + await TimedInvokeAsync(_roleDeletedEvent, nameof(RoleDeleted), role).ConfigureAwait(false); } else { - await _gatewayLogger.WarningAsync("GUILD_ROLE_DELETE referenced an unknown role.").ConfigureAwait(false); + await UnknownRoleAsync(type, data.RoleId, guild.Id).ConfigureAwait(false); return; } } else { - await _gatewayLogger.WarningAsync("GUILD_ROLE_DELETE referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); return; } } @@ -1051,15 +1045,18 @@ namespace Discord.WebSocket { if (!guild.IsSynced) { - await _gatewayLogger.DebugAsync("Ignored GUILD_BAN_ADD, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } - - await _userBannedEvent.InvokeAsync(SocketSimpleUser.Create(this, State, data.User), guild).ConfigureAwait(false); + + SocketUser user = guild.GetUser(data.User.Id); + if (user == null) + user = SocketUnknownUser.Create(this, State, data.User); + await TimedInvokeAsync(_userBannedEvent, nameof(UserBanned), user, guild).ConfigureAwait(false); } else { - await _gatewayLogger.WarningAsync("GUILD_BAN_ADD referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); return; } } @@ -1074,18 +1071,18 @@ namespace Discord.WebSocket { if (!guild.IsSynced) { - await _gatewayLogger.DebugAsync("Ignored GUILD_BAN_REMOVE, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } SocketUser user = State.GetUser(data.User.Id); if (user == null) - user = SocketSimpleUser.Create(this, State, data.User); - await _userUnbannedEvent.InvokeAsync(user, guild).ConfigureAwait(false); + user = SocketUnknownUser.Create(this, State, data.User); + await TimedInvokeAsync(_userUnbannedEvent, nameof(UserUnbanned), user, guild).ConfigureAwait(false); } else { - await _gatewayLogger.WarningAsync("GUILD_BAN_REMOVE referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); return; } } @@ -1097,34 +1094,44 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_CREATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; - if (channel != null) + if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) { var guild = (channel as SocketGuildChannel)?.Guild; if (guild != null && !guild.IsSynced) - { - await _gatewayLogger.DebugAsync("Ignored MESSAGE_CREATE, guild is not synced yet.").ConfigureAwait(false); + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } - var author = (guild != null ? guild.GetUser(data.Author.Value.Id) : (channel as SocketChannel).GetUser(data.Author.Value.Id)) ?? - SocketSimpleUser.Create(this, State, data.Author.Value); - - if (author != null) + SocketUser author; + if (guild != null) { - var msg = SocketMessage.Create(this, State, author, channel, data); - SocketChannelHelper.AddMessage(channel, this, msg); - await _messageReceivedEvent.InvokeAsync(msg).ConfigureAwait(false); + if (data.WebhookId.IsSpecified) + author = SocketWebhookUser.Create(guild, State, data.Author.Value, data.WebhookId.Value); + else + author = guild.GetUser(data.Author.Value.Id); } else + author = (channel as SocketChannel).GetUser(data.Author.Value.Id); + + if (author == null) { - await _gatewayLogger.WarningAsync("MESSAGE_CREATE referenced an unknown user.").ConfigureAwait(false); + if (guild != null) + author = guild.AddOrUpdateUser(data.Author.Value); //User has no guild-specific data + else if (channel is SocketGroupChannel) + author = (channel as SocketGroupChannel).GetOrAddUser(data.Author.Value); + else + await UnknownChannelUserAsync(type, data.Author.Value.Id, channel.Id).ConfigureAwait(false); return; } + + var msg = SocketMessage.Create(this, State, author, channel, data); + SocketChannelHelper.AddMessage(channel, this, msg); + await TimedInvokeAsync(_messageReceivedEvent, nameof(MessageReceived), msg).ConfigureAwait(false); } else { - await _gatewayLogger.WarningAsync("MESSAGE_CREATE referenced an unknown channel.").ConfigureAwait(false); + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); return; } } @@ -1134,13 +1141,12 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; - if (channel != null) + if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) { var guild = (channel as SocketGuildChannel)?.Guild; if (guild != null && !guild.IsSynced) { - await _gatewayLogger.DebugAsync("Ignored MESSAGE_UPDATE, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } @@ -1162,17 +1168,17 @@ namespace Discord.WebSocket else author = (channel as SocketChannel).GetUser(data.Author.Value.Id); if (author == null) - author = SocketSimpleUser.Create(this, State, data.Author.Value); + author = SocketUnknownUser.Create(this, State, data.Author.Value); after = SocketMessage.Create(this, State, author, channel, data); } - var cacheableBefore = new Cacheable(before, data.Id, isCached , async () => await channel.GetMessageAsync(data.Id)); + var cacheableBefore = new Cacheable(before, data.Id, isCached, async () => await channel.GetMessageAsync(data.Id)); - await _messageUpdatedEvent.InvokeAsync(cacheableBefore, after, channel).ConfigureAwait(false); + await TimedInvokeAsync(_messageUpdatedEvent, nameof(MessageUpdated), cacheableBefore, after, channel).ConfigureAwait(false); } else { - await _gatewayLogger.WarningAsync("MESSAGE_UPDATE referenced an unknown channel.").ConfigureAwait(false); + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); return; } } @@ -1182,12 +1188,12 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; - if (channel != null) + if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) { - if (!((channel as SocketGuildChannel)?.Guild.IsSynced ?? true)) + var guild = (channel as SocketGuildChannel)?.Guild; + if (!(guild?.IsSynced ?? true)) { - await _gatewayLogger.DebugAsync("Ignored MESSAGE_DELETE, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } @@ -1195,11 +1201,11 @@ namespace Discord.WebSocket bool isCached = msg != null; var cacheable = new Cacheable(msg, data.Id, isCached, async () => await channel.GetMessageAsync(data.Id)); - await _messageDeletedEvent.InvokeAsync(cacheable, channel).ConfigureAwait(false); + await TimedInvokeAsync(_messageDeletedEvent, nameof(MessageDeleted), cacheable, channel).ConfigureAwait(false); } else { - await _gatewayLogger.WarningAsync("MESSAGE_DELETE referenced an unknown channel.").ConfigureAwait(false); + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); return; } } @@ -1209,99 +1215,96 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_ADD)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; - if (channel != null) + if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) { - SocketUserMessage cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; + var cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; bool isCached = cachedMsg != null; var user = await channel.GetUserAsync(data.UserId, CacheMode.CacheOnly); - SocketReaction reaction = SocketReaction.Create(data, channel, cachedMsg, Optional.Create(user)); + var reaction = SocketReaction.Create(data, channel, cachedMsg, Optional.Create(user)); var cacheable = new Cacheable(cachedMsg, data.MessageId, isCached, async () => await channel.GetMessageAsync(data.MessageId) as IUserMessage); cachedMsg?.AddReaction(reaction); - await _reactionAddedEvent.InvokeAsync(cacheable, channel, reaction).ConfigureAwait(false); + await TimedInvokeAsync(_reactionAddedEvent, nameof(ReactionAdded), cacheable, channel, reaction).ConfigureAwait(false); } else { - await _gatewayLogger.WarningAsync("MESSAGE_REACTION_ADD referenced an unknown channel.").ConfigureAwait(false); + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); return; } - break; } + break; case "MESSAGE_REACTION_REMOVE": { await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; - if (channel != null) + if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) { - SocketUserMessage cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; + var cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; bool isCached = cachedMsg != null; var user = await channel.GetUserAsync(data.UserId, CacheMode.CacheOnly); - SocketReaction reaction = SocketReaction.Create(data, channel, cachedMsg, Optional.Create(user)); + var reaction = SocketReaction.Create(data, channel, cachedMsg, Optional.Create(user)); var cacheable = new Cacheable(cachedMsg, data.MessageId, isCached, async () => await channel.GetMessageAsync(data.MessageId) as IUserMessage); cachedMsg?.RemoveReaction(reaction); - await _reactionRemovedEvent.InvokeAsync(cacheable, channel, reaction).ConfigureAwait(false); + await TimedInvokeAsync(_reactionRemovedEvent, nameof(ReactionRemoved), cacheable, channel, reaction).ConfigureAwait(false); } else { - await _gatewayLogger.WarningAsync("MESSAGE_REACTION_REMOVE referenced an unknown channel.").ConfigureAwait(false); + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); return; } - break; } + break; case "MESSAGE_REACTION_REMOVE_ALL": { await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE_ALL)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; - if (channel != null) + if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) { - SocketUserMessage cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; + var cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; bool isCached = cachedMsg != null; var cacheable = new Cacheable(cachedMsg, data.MessageId, isCached, async () => await channel.GetMessageAsync(data.MessageId) as IUserMessage); cachedMsg?.ClearReactions(); - await _reactionsClearedEvent.InvokeAsync(cacheable, channel).ConfigureAwait(false); + await TimedInvokeAsync(_reactionsClearedEvent, nameof(ReactionsCleared), cacheable, channel).ConfigureAwait(false); } else { - await _gatewayLogger.WarningAsync("MESSAGE_REACTION_REMOVE_ALL referenced an unknown channel.").ConfigureAwait(false); + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); return; } - break; } + break; case "MESSAGE_DELETE_BULK": { await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE_BULK)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; - if (channel != null) + if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) { - if (!((channel as SocketGuildChannel)?.Guild.IsSynced ?? true)) + var guild = (channel as SocketGuildChannel)?.Guild; + if (!(guild?.IsSynced ?? true)) { - await _gatewayLogger.DebugAsync("Ignored MESSAGE_DELETE_BULK, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } - foreach (var id in data.Ids) + foreach (ulong id in data.Ids) { var msg = SocketChannelHelper.RemoveMessage(channel, this, id); bool isCached = msg != null; var cacheable = new Cacheable(msg, id, isCached, async () => await channel.GetMessageAsync(id)); - await _messageDeletedEvent.InvokeAsync(cacheable, channel).ConfigureAwait(false); + await TimedInvokeAsync(_messageDeletedEvent, nameof(MessageDeleted), cacheable, channel).ConfigureAwait(false); } } else { - await _gatewayLogger.WarningAsync("MESSAGE_DELETE_BULK referenced an unknown channel.").ConfigureAwait(false); + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); return; } } @@ -1319,54 +1322,51 @@ namespace Discord.WebSocket var guild = State.GetGuild(data.GuildId.Value); if (guild == null) { - await _gatewayLogger.WarningAsync("PRESENCE_UPDATE referenced an unknown guild.").ConfigureAwait(false); - break; + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; } if (!guild.IsSynced) { - await _gatewayLogger.DebugAsync("Ignored PRESENCE_UPDATE, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } - SocketPresence beforePresence; - SocketGlobalUser beforeGlobal; var user = guild.GetUser(data.User.Id); - if (user != null) - { - beforePresence = user.Presence.Clone(); - beforeGlobal = user.GlobalUser.Clone(); - user.Update(State, data); - } - else + if (user == null) { - beforePresence = new SocketPresence(UserStatus.Offline, null); + if (data.Status == UserStatus.Offline) + { + return; + } user = guild.AddOrUpdateUser(data); - beforeGlobal = user.GlobalUser.Clone(); } - - if (data.User.Username.IsSpecified || data.User.Avatar.IsSpecified) + else { - await _userUpdatedEvent.InvokeAsync(beforeGlobal, user).ConfigureAwait(false); - return; + var globalBefore = user.GlobalUser.Clone(); + if (user.GlobalUser.Update(State, data.User)) + { + //Global data was updated, trigger UserUpdated + await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false); + } } - await _userPresenceUpdatedEvent.InvokeAsync(guild, user, beforePresence, user.Presence).ConfigureAwait(false); + + var before = user.Clone(); + user.Update(State, data, true); + await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), before, user).ConfigureAwait(false); } else { - var channel = State.GetChannel(data.User.Id); - if (channel != null) + var globalUser = State.GetUser(data.User.Id); + if (globalUser == null) { - var user = channel.GetUser(data.User.Id); - var beforePresence = user.Presence.Clone(); - var before = user.GlobalUser.Clone(); - user.Update(State, data); - - await _userPresenceUpdatedEvent.InvokeAsync(Optional.Create(), user, beforePresence, user.Presence).ConfigureAwait(false); - if (data.User.Username.IsSpecified || data.User.Avatar.IsSpecified) - { - await _userUpdatedEvent.InvokeAsync(before, user).ConfigureAwait(false); - } + await UnknownGlobalUserAsync(type, data.User.Id).ConfigureAwait(false); + return; } + + var before = globalUser.Clone(); + globalUser.Update(State, data.User); + globalUser.Update(State, data); + await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), before, globalUser).ConfigureAwait(false); } } break; @@ -1375,18 +1375,18 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (TYPING_START)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; - if (channel != null) + if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) { - if (!((channel as SocketGuildChannel)?.Guild.IsSynced ?? true)) + var guild = (channel as SocketGuildChannel)?.Guild; + if (!(guild?.IsSynced ?? true)) { - await _gatewayLogger.DebugAsync("Ignored TYPING_START, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } var user = (channel as SocketChannel).GetUser(data.UserId); if (user != null) - await _userIsTypingEvent.InvokeAsync(user, channel).ConfigureAwait(false); + await TimedInvokeAsync(_userIsTypingEvent, nameof(UserIsTyping), user, channel).ConfigureAwait(false); } } break; @@ -1401,7 +1401,7 @@ namespace Discord.WebSocket { var before = CurrentUser.Clone(); CurrentUser.Update(State, data); - await _selfUpdatedEvent.InvokeAsync(before, CurrentUser).ConfigureAwait(false); + await TimedInvokeAsync(_selfUpdatedEvent, nameof(CurrentUserUpdated), before, CurrentUser).ConfigureAwait(false); } else { @@ -1417,90 +1417,87 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (VOICE_STATE_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - if (data.GuildId.HasValue) + SocketUser user; + SocketVoiceState before, after; + if (data.GuildId != null) { - SocketUser user; - SocketVoiceState before, after; - if (data.GuildId != null) + var guild = State.GetGuild(data.GuildId.Value); + if (guild == null) { - var guild = State.GetGuild(data.GuildId.Value); - if (guild == null) - { - await _gatewayLogger.WarningAsync("VOICE_STATE_UPDATE referenced an unknown guild.").ConfigureAwait(false); - return; - } - else if (!guild.IsSynced) - { - await _gatewayLogger.DebugAsync("Ignored VOICE_STATE_UPDATE, guild is not synced yet.").ConfigureAwait(false); - return; - } + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } + else if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } - if (data.ChannelId != null) - { - before = guild.GetVoiceState(data.UserId)?.Clone() ?? SocketVoiceState.Default; - after = guild.AddOrUpdateVoiceState(State, data); - /*if (data.UserId == CurrentUser.Id) - { - var _ = guild.FinishJoinAudioChannel().ConfigureAwait(false); - }*/ - } - else + if (data.ChannelId != null) + { + before = guild.GetVoiceState(data.UserId)?.Clone() ?? SocketVoiceState.Default; + after = await guild.AddOrUpdateVoiceStateAsync(State, data).ConfigureAwait(false); + /*if (data.UserId == CurrentUser.Id) { - before = guild.RemoveVoiceState(data.UserId) ?? SocketVoiceState.Default; - after = SocketVoiceState.Create(null, data); - } - - user = guild.GetUser(data.UserId); + var _ = guild.FinishJoinAudioChannel().ConfigureAwait(false); + }*/ } else { - var groupChannel = State.GetChannel(data.ChannelId.Value) as SocketGroupChannel; - if (groupChannel != null) - { - if (data.ChannelId != null) - { - before = groupChannel.GetVoiceState(data.UserId)?.Clone() ?? SocketVoiceState.Default; - after = groupChannel.AddOrUpdateVoiceState(State, data); - } - else - { - before = groupChannel.RemoveVoiceState(data.UserId) ?? SocketVoiceState.Default; - after = SocketVoiceState.Create(null, data); - } - user = groupChannel.GetUser(data.UserId); - } - else - { - await _gatewayLogger.WarningAsync("VOICE_STATE_UPDATE referenced an unknown channel.").ConfigureAwait(false); - return; - } + before = await guild.RemoveVoiceStateAsync(data.UserId).ConfigureAwait(false) ?? SocketVoiceState.Default; + after = SocketVoiceState.Create(null, data); } - if (user != null) - await _userVoiceStateUpdatedEvent.InvokeAsync(user, before, after).ConfigureAwait(false); + user = guild.GetUser(data.UserId); + if (user == null) + { + await UnknownGuildUserAsync(type, data.UserId, guild.Id).ConfigureAwait(false); + return; + } + } + else + { + var groupChannel = State.GetChannel(data.ChannelId.Value) as SocketGroupChannel; + if (groupChannel == null) + { + await UnknownChannelAsync(type, data.ChannelId.Value).ConfigureAwait(false); + return; + } + if (data.ChannelId != null) + { + before = groupChannel.GetVoiceState(data.UserId)?.Clone() ?? SocketVoiceState.Default; + after = groupChannel.AddOrUpdateVoiceState(State, data); + } else { - await _gatewayLogger.WarningAsync("VOICE_STATE_UPDATE referenced an unknown user.").ConfigureAwait(false); + before = groupChannel.RemoveVoiceState(data.UserId) ?? SocketVoiceState.Default; + after = SocketVoiceState.Create(null, data); + } + user = groupChannel.GetUser(data.UserId); + if (user == null) + { + await UnknownChannelUserAsync(type, data.UserId, groupChannel.Id).ConfigureAwait(false); return; } } + + await TimedInvokeAsync(_userVoiceStateUpdatedEvent, nameof(UserVoiceStateUpdated), user, before, after).ConfigureAwait(false); } break; case "VOICE_SERVER_UPDATE": - await _gatewayLogger.DebugAsync("Received Dispatch (VOICE_SERVER_UPDATE)").ConfigureAwait(false); - - if (AudioMode != AudioMode.Disabled) { + await _gatewayLogger.DebugAsync("Received Dispatch (VOICE_SERVER_UPDATE)").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); var guild = State.GetGuild(data.GuildId); if (guild != null) { string endpoint = data.Endpoint.Substring(0, data.Endpoint.LastIndexOf(':')); - var _ = guild.FinishConnectAudio(_nextAudioId++, endpoint, data.Token).ConfigureAwait(false); + var _ = guild.FinishConnectAudio(endpoint, data.Token).ConfigureAwait(false); } else { - await _gatewayLogger.WarningAsync("VOICE_SERVER_UPDATE referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); return; } } @@ -1538,32 +1535,31 @@ namespace Discord.WebSocket break; case "GUILD_INTEGRATIONS_UPDATE": await _gatewayLogger.DebugAsync("Ignored Dispatch (GUILD_INTEGRATIONS_UPDATE)").ConfigureAwait(false); - return; + break; case "MESSAGE_ACK": await _gatewayLogger.DebugAsync("Ignored Dispatch (MESSAGE_ACK)").ConfigureAwait(false); - return; + break; case "USER_SETTINGS_UPDATE": await _gatewayLogger.DebugAsync("Ignored Dispatch (USER_SETTINGS_UPDATE)").ConfigureAwait(false); - return; + break; case "WEBHOOKS_UPDATE": await _gatewayLogger.DebugAsync("Ignored Dispatch (WEBHOOKS_UPDATE)").ConfigureAwait(false); - return; + break; //Others default: await _gatewayLogger.WarningAsync($"Unknown Dispatch ({type})").ConfigureAwait(false); - return; + break; } break; default: await _gatewayLogger.WarningAsync($"Unknown OpCode ({opCode})").ConfigureAwait(false); - return; + break; } } catch (Exception ex) { await _gatewayLogger.ErrorAsync($"Error handling {opCode}{(type != null ? $" ({type})" : "")}", ex).ConfigureAwait(false); - return; } } @@ -1574,7 +1570,7 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Heartbeat Started").ConfigureAwait(false); while (!cancelToken.IsCancellationRequested) { - var now = Environment.TickCount; + int now = Environment.TickCount; //Did server respond to our last heartbeat, or are we still receiving messages (long load?) if (_heartbeatTimes.Count != 0 && (now - _lastMessageTime) > intervalMillis) @@ -1609,19 +1605,19 @@ namespace Discord.WebSocket await _gatewayLogger.ErrorAsync("Heartbeat Errored", ex).ConfigureAwait(false); } } - public async Task WaitForGuildsAsync() + /*public async Task WaitForGuildsAsync() { var downloadTask = _guildDownloadTask; if (downloadTask != null) await _guildDownloadTask.ConfigureAwait(false); - } + }*/ private async Task WaitForGuildsAsync(CancellationToken cancelToken, Logger logger) { //Wait for GUILD_AVAILABLEs try { await logger.DebugAsync("GuildDownloader Started").ConfigureAwait(false); - while ((_unavailableGuilds != 0) && (Environment.TickCount - _lastGuildAvailableTime < 2000)) + while ((_unavailableGuildCount != 0) && (Environment.TickCount - _lastGuildAvailableTime < 2000)) await Task.Delay(500, cancelToken).ConfigureAwait(false); await logger.DebugAsync("GuildDownloader Stopped").ConfigureAwait(false); } @@ -1666,6 +1662,9 @@ namespace Discord.WebSocket { var channel = SocketChannel.CreatePrivate(this, state, model); state.AddChannel(channel as SocketChannel); + if (channel is SocketDMChannel dm) + dm.Recipient.GlobalUser.DMChannel = dm; + return channel; } internal ISocketPrivateChannel RemovePrivateChannel(ulong id) @@ -1673,12 +1672,161 @@ namespace Discord.WebSocket var channel = State.RemoveChannel(id) as ISocketPrivateChannel; if (channel != null) { + if (channel is SocketDMChannel dmChannel) + dmChannel.Recipient.GlobalUser.DMChannel = null; + foreach (var recipient in channel.Recipients) recipient.GlobalUser.RemoveRef(this); } return channel; } + private async Task GuildAvailableAsync(SocketGuild guild) + { + if (!guild.IsConnected) + { + guild.IsConnected = true; + await TimedInvokeAsync(_guildAvailableEvent, nameof(GuildAvailable), guild).ConfigureAwait(false); + } + } + private async Task GuildUnavailableAsync(SocketGuild guild) + { + if (guild.IsConnected) + { + guild.IsConnected = false; + await TimedInvokeAsync(_guildUnavailableEvent, nameof(GuildUnavailable), guild).ConfigureAwait(false); + } + } + + private async Task TimedInvokeAsync(AsyncEvent> eventHandler, string name) + { + if (eventHandler.HasSubscribers) + { + if (HandlerTimeout.HasValue) + await TimeoutWrap(name, () => eventHandler.InvokeAsync()).ConfigureAwait(false); + else + await eventHandler.InvokeAsync().ConfigureAwait(false); + } + } + private async Task TimedInvokeAsync(AsyncEvent> eventHandler, string name, T arg) + { + if (eventHandler.HasSubscribers) + { + if (HandlerTimeout.HasValue) + await TimeoutWrap(name, () => eventHandler.InvokeAsync(arg)).ConfigureAwait(false); + else + await eventHandler.InvokeAsync(arg).ConfigureAwait(false); + } + } + private async Task TimedInvokeAsync(AsyncEvent> eventHandler, string name, T1 arg1, T2 arg2) + { + if (eventHandler.HasSubscribers) + { + if (HandlerTimeout.HasValue) + await TimeoutWrap(name, () => eventHandler.InvokeAsync(arg1, arg2)).ConfigureAwait(false); + else + await eventHandler.InvokeAsync(arg1, arg2).ConfigureAwait(false); + } + } + private async Task TimedInvokeAsync(AsyncEvent> eventHandler, string name, T1 arg1, T2 arg2, T3 arg3) + { + if (eventHandler.HasSubscribers) + { + if (HandlerTimeout.HasValue) + await TimeoutWrap(name, () => eventHandler.InvokeAsync(arg1, arg2, arg3)).ConfigureAwait(false); + else + await eventHandler.InvokeAsync(arg1, arg2, arg3).ConfigureAwait(false); + } + } + private async Task TimedInvokeAsync(AsyncEvent> eventHandler, string name, T1 arg1, T2 arg2, T3 arg3, T4 arg4) + { + if (eventHandler.HasSubscribers) + { + if (HandlerTimeout.HasValue) + await TimeoutWrap(name, () => eventHandler.InvokeAsync(arg1, arg2, arg3, arg4)).ConfigureAwait(false); + else + await eventHandler.InvokeAsync(arg1, arg2, arg3, arg4).ConfigureAwait(false); + } + } + private async Task TimedInvokeAsync(AsyncEvent> eventHandler, string name, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) + { + if (eventHandler.HasSubscribers) + { + if (HandlerTimeout.HasValue) + await TimeoutWrap(name, () => eventHandler.InvokeAsync(arg1, arg2, arg3, arg4, arg5)).ConfigureAwait(false); + else + await eventHandler.InvokeAsync(arg1, arg2, arg3, arg4, arg5).ConfigureAwait(false); + } + } + private async Task TimeoutWrap(string name, Func action) + { + try + { + var timeoutTask = Task.Delay(HandlerTimeout.Value); + var handlersTask = action(); + if (await Task.WhenAny(timeoutTask, handlersTask).ConfigureAwait(false) == timeoutTask) + { + await _gatewayLogger.WarningAsync($"A {name} handler is blocking the gateway task.").ConfigureAwait(false); + await handlersTask.ConfigureAwait(false); //Ensure the handler completes + } + } + catch (Exception ex) + { + await _gatewayLogger.WarningAsync($"A {name} handler has thrown an unhandled exception.", ex).ConfigureAwait(false); + } + } + + private async Task UnknownGlobalUserAsync(string evnt, ulong userId) + { + string details = $"{evnt} User={userId}"; + await _gatewayLogger.WarningAsync($"Unknown User ({details}).").ConfigureAwait(false); + } + private async Task UnknownChannelUserAsync(string evnt, ulong userId, ulong channelId) + { + string details = $"{evnt} User={userId} Channel={channelId}"; + await _gatewayLogger.WarningAsync($"Unknown User ({details}).").ConfigureAwait(false); + } + private async Task UnknownGuildUserAsync(string evnt, ulong userId, ulong guildId) + { + string details = $"{evnt} User={userId} Guild={guildId}"; + await _gatewayLogger.WarningAsync($"Unknown User ({details}).").ConfigureAwait(false); + } + private async Task IncompleteGuildUserAsync(string evnt, ulong userId, ulong guildId) + { + string details = $"{evnt} User={userId} Guild={guildId}"; + await _gatewayLogger.DebugAsync($"User has not been downloaded ({details}).").ConfigureAwait(false); + } + private async Task UnknownChannelAsync(string evnt, ulong channelId) + { + string details = $"{evnt} Channel={channelId}"; + await _gatewayLogger.WarningAsync($"Unknown Channel ({details}).").ConfigureAwait(false); + } + private async Task UnknownChannelAsync(string evnt, ulong channelId, ulong guildId) + { + if (guildId == 0) + { + await UnknownChannelAsync(evnt, channelId).ConfigureAwait(false); + return; + } + string details = $"{evnt} Channel={channelId} Guild={guildId}"; + await _gatewayLogger.WarningAsync($"Unknown Channel ({details}).").ConfigureAwait(false); + } + private async Task UnknownRoleAsync(string evnt, ulong roleId, ulong guildId) + { + string details = $"{evnt} Role={roleId} Guild={guildId}"; + await _gatewayLogger.WarningAsync($"Unknown Role ({details}).").ConfigureAwait(false); + } + private async Task UnknownGuildAsync(string evnt, ulong guildId) + { + string details = $"{evnt} Guild={guildId}"; + await _gatewayLogger.WarningAsync($"Unknown Guild ({details}).").ConfigureAwait(false); + } + private async Task UnsyncedGuildAsync(string evnt, ulong guildId) + { + string details = $"{evnt} Guild={guildId}"; + await _gatewayLogger.DebugAsync($"Unsynced Guild ({details}).").ConfigureAwait(false); + } + internal SocketRelationship GetRelationship(ulong id) { return State.GetRelationship(id); @@ -1695,41 +1843,39 @@ namespace Discord.WebSocket } //IDiscordClient - ConnectionState IDiscordClient.ConnectionState => _connection.State; - - async Task IDiscordClient.GetApplicationInfoAsync() + async Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) => await GetApplicationInfoAsync().ConfigureAwait(false); - Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode) + Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetChannel(id)); - Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode) + Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(PrivateChannels); - Task> IDiscordClient.GetDMChannelsAsync(CacheMode mode) + Task> IDiscordClient.GetDMChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(DMChannels); - Task> IDiscordClient.GetGroupChannelsAsync(CacheMode mode) + Task> IDiscordClient.GetGroupChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(GroupChannels); - async Task> IDiscordClient.GetConnectionsAsync() + async Task> IDiscordClient.GetConnectionsAsync(RequestOptions options) => await GetConnectionsAsync().ConfigureAwait(false); - async Task IDiscordClient.GetInviteAsync(string inviteId) + async Task IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options) => await GetInviteAsync(inviteId).ConfigureAwait(false); - Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode) + Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetGuild(id)); - Task> IDiscordClient.GetGuildsAsync(CacheMode mode) + Task> IDiscordClient.GetGuildsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(Guilds); - async Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon) + async Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) => await CreateGuildAsync(name, region, jpegIcon).ConfigureAwait(false); - Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode) + Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); - Task IDiscordClient.GetUserAsync(string username, string discriminator) + Task IDiscordClient.GetUserAsync(string username, string discriminator, RequestOptions options) => Task.FromResult(GetUser(username, discriminator)); - Task> IDiscordClient.GetVoiceRegionsAsync() + Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) => Task.FromResult>(VoiceRegions); - Task IDiscordClient.GetVoiceRegionAsync(string id) + Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) => Task.FromResult(GetVoiceRegion(id)); async Task IDiscordClient.StartAsync() diff --git a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs index f42744c79..3f9c18863 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs @@ -1,5 +1,4 @@ -using Discord.Audio; -using Discord.Net.Udp; +using Discord.Net.Udp; using Discord.Net.WebSockets; using Discord.Rest; @@ -27,9 +26,6 @@ namespace Discord.WebSocket /// public int LargeThreshold { get; set; } = 250; - /// Gets or sets the type of audio this DiscordClient supports. - public AudioMode AudioMode { get; set; } = AudioMode.Disabled; - /// Gets or sets the provider used to generate new websocket connections. public WebSocketProvider WebSocketProvider { get; set; } /// Gets or sets the provider used to generate new udp sockets. @@ -37,6 +33,8 @@ namespace Discord.WebSocket /// Gets or sets whether or not all users should be downloaded as guilds come available. public bool AlwaysDownloadUsers { get; set; } = false; + /// Gets or sets the timeout for event handlers, in milliseconds, after which a warning will be logged. Null disables this check. + public int? HandlerTimeout { get; set; } = 3000; public DiscordSocketConfig() { diff --git a/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs b/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs index fa619b58c..25dc2cf7b 100644 --- a/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs +++ b/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs @@ -42,11 +42,14 @@ namespace Discord.Audio private CancellationTokenSource _connectCancelToken; private IUdpSocket _udp; private bool _isDisposed; + private ulong _nextKeepalive; public ulong GuildId { get; } internal IWebSocketClient WebSocketClient { get; } public ConnectionState ConnectionState { get; private set; } + public ushort UdpPort => _udp.Port; + internal DiscordVoiceAPIClient(ulong guildId, WebSocketProvider webSocketProvider, UdpSocketProvider udpSocketProvider, JsonSerializer serializer = null) { GuildId = guildId; @@ -54,7 +57,7 @@ namespace Discord.Audio _udp = udpSocketProvider(); _udp.ReceivedDatagram += async (data, index, count) => { - if (index != 0) + if (index != 0 || count != data.Length) { var newData = new byte[count]; Buffer.BlockCopy(data, index, newData, 0, count); @@ -227,10 +230,25 @@ namespace Discord.Audio await SendAsync(packet, 0, 70).ConfigureAwait(false); await _sentDiscoveryEvent.InvokeAsync().ConfigureAwait(false); } + public async Task SendKeepaliveAsync() + { + var value = _nextKeepalive++; + var packet = new byte[8]; + packet[0] = (byte)(value >> 0); + packet[1] = (byte)(value >> 8); + packet[2] = (byte)(value >> 16); + packet[3] = (byte)(value >> 24); + packet[4] = (byte)(value >> 32); + packet[5] = (byte)(value >> 40); + packet[6] = (byte)(value >> 48); + packet[7] = (byte)(value >> 56); + await SendAsync(packet, 0, 8).ConfigureAwait(false); + return value; + } - public void SetUdpEndpoint(string host, int port) + public void SetUdpEndpoint(string ip, int port) { - _udp.SetDestination(host, port); + _udp.SetDestination(ip, port); } //Helpers diff --git a/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs index 7b9bf07f0..7056a4df5 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs @@ -1,10 +1,6 @@ -using Discord.Audio; -using System.Threading.Tasks; - -namespace Discord.WebSocket +namespace Discord.WebSocket { public interface ISocketAudioChannel : IAudioChannel { - Task ConnectAsync(); } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs index 43246f5ca..e2119e7a2 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs @@ -12,7 +12,7 @@ namespace Discord.WebSocket /// Sends a message to this message channel. new Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null); -#if NETSTANDARD1_3 +#if FILESYSTEM /// Sends a file to this text channel, with an optional caption. new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, RequestOptions options = null); #endif diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs index f982e66b5..42c4156f3 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs @@ -1,4 +1,5 @@ -using System; +using Discord.Rest; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -10,7 +11,7 @@ namespace Discord.WebSocket [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public abstract class SocketChannel : SocketEntity, IChannel { - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public IReadOnlyCollection Users => GetUsersInternal(); internal SocketChannel(DiscordSocketClient discord, ulong id) @@ -40,6 +41,7 @@ namespace Discord.WebSocket //IChannel string IChannel.Name => null; + bool IChannel.IsNsfw => ChannelHelper.IsNsfw(this); Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); //Overridden diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs index 1bc0fc9b5..ca53315aa 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs @@ -61,28 +61,24 @@ namespace Discord.WebSocket public static void AddMessage(ISocketMessageChannel channel, DiscordSocketClient discord, SocketMessage msg) { - //TODO: C#7 Candidate for pattern matching - if (channel is SocketDMChannel) - (channel as SocketDMChannel).AddMessage(msg); - else if (channel is SocketGroupChannel) - (channel as SocketGroupChannel).AddMessage(msg); - else if (channel is SocketTextChannel) - (channel as SocketTextChannel).AddMessage(msg); - else - throw new NotSupportedException("Unexpected ISocketMessageChannel type"); + switch (channel) + { + case SocketDMChannel dmChannel: dmChannel.AddMessage(msg); break; + case SocketGroupChannel groupChannel: groupChannel.AddMessage(msg); break; + case SocketTextChannel textChannel: textChannel.AddMessage(msg); break; + default: throw new NotSupportedException("Unexpected ISocketMessageChannel type"); + } } public static SocketMessage RemoveMessage(ISocketMessageChannel channel, DiscordSocketClient discord, ulong id) { - //TODO: C#7 Candidate for pattern matching - if (channel is SocketDMChannel) - return (channel as SocketDMChannel).RemoveMessage(id); - else if (channel is SocketGroupChannel) - return (channel as SocketGroupChannel).RemoveMessage(id); - else if (channel is SocketTextChannel) - return (channel as SocketTextChannel).RemoveMessage(id); - else - throw new NotSupportedException("Unexpected ISocketMessageChannel type"); + switch (channel) + { + case SocketDMChannel dmChannel: return dmChannel.RemoveMessage(id); + case SocketGroupChannel groupChannel: return groupChannel.RemoveMessage(id); + case SocketTextChannel textChannel: return textChannel.RemoveMessage(id); + default: throw new NotSupportedException("Unexpected ISocketMessageChannel type"); + } } } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs index c976b64f8..322a99496 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs @@ -24,6 +24,7 @@ namespace Discord.WebSocket : base(discord, id) { Recipient = recipient; + recipient.GlobalUser.AddRef(); if (Discord.MessageCacheSize > 0) _messages = new MessageCache(Discord, this); } @@ -68,7 +69,7 @@ namespace Discord.WebSocket public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); -#if NETSTANDARD1_3 +#if FILESYSTEM public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); #endif @@ -76,7 +77,9 @@ namespace Discord.WebSocket => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); @@ -132,7 +135,7 @@ namespace Discord.WebSocket => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, mode, options); async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); -#if NETSTANDARD1_3 +#if FILESYSTEM async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); #endif diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index ceba50a6e..dc1853e73 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -97,7 +97,7 @@ namespace Discord.WebSocket public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); -#if NETSTANDARD1_3 +#if FILESYSTEM public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); #endif @@ -105,7 +105,9 @@ namespace Discord.WebSocket => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); @@ -120,27 +122,25 @@ namespace Discord.WebSocket //Users public new SocketGroupUser GetUser(ulong id) { - SocketGroupUser user; - if (_users.TryGetValue(id, out user)) + if (_users.TryGetValue(id, out SocketGroupUser user)) return user; return null; } - internal SocketGroupUser AddUser(UserModel model) + internal SocketGroupUser GetOrAddUser(UserModel model) { - SocketGroupUser user; - if (_users.TryGetValue(model.Id, out user)) + if (_users.TryGetValue(model.Id, out SocketGroupUser user)) return user as SocketGroupUser; else { var privateUser = SocketGroupUser.Create(this, Discord.State, model); + privateUser.GlobalUser.AddRef(); _users[privateUser.Id] = privateUser; return privateUser; } } internal SocketGroupUser RemoveUser(ulong id) { - SocketGroupUser user; - if (_users.TryRemove(id, out user)) + if (_users.TryRemove(id, out SocketGroupUser user)) { user.GlobalUser.RemoveRef(Discord); return user as SocketGroupUser; @@ -158,15 +158,13 @@ namespace Discord.WebSocket } internal SocketVoiceState? GetVoiceState(ulong id) { - SocketVoiceState voiceState; - if (_voiceStates.TryGetValue(id, out voiceState)) + if (_voiceStates.TryGetValue(id, out SocketVoiceState voiceState)) return voiceState; return null; } internal SocketVoiceState? RemoveVoiceState(ulong id) { - SocketVoiceState voiceState; - if (_voiceStates.TryRemove(id, out voiceState)) + if (_voiceStates.TryRemove(id, out SocketVoiceState voiceState)) return voiceState; return null; } @@ -201,7 +199,7 @@ namespace Discord.WebSocket => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, mode, options); async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); -#if NETSTANDARD1_3 +#if FILESYSTEM async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); #endif @@ -212,6 +210,9 @@ namespace Discord.WebSocket IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); + //IAudioChannel + Task IAudioChannel.ConnectAsync(Action configAction) { throw new NotSupportedException(); } + //IChannel Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index 98aefcf9b..c22523e00 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -73,7 +73,7 @@ namespace Discord.WebSocket public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); -#if NETSTANDARD1_3 +#if FILESYSTEM public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); #endif @@ -81,7 +81,9 @@ namespace Discord.WebSocket => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); @@ -132,7 +134,7 @@ namespace Discord.WebSocket => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, mode, options); async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); -#if NETSTANDARD1_3 +#if FILESYSTEM async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); #endif diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs index 71017a7c8..e8a669845 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs @@ -40,15 +40,9 @@ namespace Discord.WebSocket public Task ModifyAsync(Action func, RequestOptions options = null) => ChannelHelper.ModifyAsync(this, Discord, func, options); - public async Task ConnectAsync() + public async Task ConnectAsync(Action configAction = null) { - var audioMode = Discord.AudioMode; - if (audioMode == AudioMode.Disabled) - throw new InvalidOperationException($"Audio is not enabled on this client, {nameof(DiscordSocketConfig.AudioMode)} in {nameof(DiscordSocketConfig)} must be set."); - - return await Guild.ConnectAudioAsync(Id, - (audioMode & AudioMode.Incoming) == 0, - (audioMode & AudioMode.Outgoing) == 0).ConfigureAwait(false); + return await Guild.ConnectAudioAsync(Id, false, false, configAction).ConfigureAwait(false); } public override SocketGuildUser GetUser(ulong id) diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 007f52124..aae18be36 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -16,6 +16,7 @@ using MemberModel = Discord.API.GuildMember; using Model = Discord.API.Guild; using PresenceModel = Discord.API.Presence; using RoleModel = Discord.API.Role; +using UserModel = Discord.API.User; using VoiceStateModel = Discord.API.VoiceState; namespace Discord.WebSocket @@ -29,10 +30,9 @@ namespace Discord.WebSocket private ConcurrentDictionary _members; private ConcurrentDictionary _roles; private ConcurrentDictionary _voiceStates; - private ImmutableArray _emojis; + private ImmutableArray _emotes; private ImmutableArray _features; private AudioClient _audioClient; - internal bool _available; public string Name { get; private set; } public int AFKTimeout { get; private set; } @@ -40,8 +40,10 @@ namespace Discord.WebSocket public VerificationLevel VerificationLevel { get; private set; } public MfaLevel MfaLevel { get; private set; } public DefaultMessageNotifications DefaultMessageNotifications { get; private set; } - public int MemberCount { get; set; } + public int MemberCount { get; internal set; } public int DownloadedMemberCount { get; private set; } + internal bool IsAvailable { get; private set; } + public bool IsConnected { get; internal set; } internal ulong? AFKChannelId { get; private set; } internal ulong? EmbedChannelId { get; private set; } @@ -51,7 +53,7 @@ namespace Discord.WebSocket public string IconId { get; private set; } public string SplashId { get; private set; } - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public SocketTextChannel DefaultChannel => GetTextChannel(Id); public string IconUrl => CDN.GetGuildIconUrl(Id, IconId); public string SplashUrl => CDN.GetGuildSplashUrl(Id, SplashId); @@ -68,28 +70,19 @@ namespace Discord.WebSocket return id.HasValue ? GetVoiceChannel(id.Value) : null; } } - public SocketVoiceChannel EmbedChannel + public SocketGuildChannel EmbedChannel { get { var id = EmbedChannelId; - return id.HasValue ? GetVoiceChannel(id.Value) : null; + return id.HasValue ? GetChannel(id.Value) : null; } } public IReadOnlyCollection TextChannels => Channels.Select(x => x as SocketTextChannel).Where(x => x != null).ToImmutableArray(); public IReadOnlyCollection VoiceChannels => Channels.Select(x => x as SocketVoiceChannel).Where(x => x != null).ToImmutableArray(); - public SocketGuildUser CurrentUser - { - get - { - SocketGuildUser member; - if (_members.TryGetValue(Discord.CurrentUser.Id, out member)) - return member; - return null; - } - } + public SocketGuildUser CurrentUser => _members.TryGetValue(Discord.CurrentUser.Id, out SocketGuildUser member) ? member : null; public SocketRole EveryoneRole => GetRole(Id); public IReadOnlyCollection Channels { @@ -100,7 +93,7 @@ namespace Discord.WebSocket return channels.Select(x => state.GetChannel(x) as SocketGuildChannel).Where(x => x != null).ToReadOnlyCollection(channels); } } - public IReadOnlyCollection Emojis => _emojis; + public IReadOnlyCollection Emotes => _emotes; public IReadOnlyCollection Features => _features; public IReadOnlyCollection Users => _members.ToReadOnlyCollection(); public IReadOnlyCollection Roles => _roles.ToReadOnlyCollection(); @@ -109,7 +102,7 @@ namespace Discord.WebSocket : base(client, id) { _audioLock = new SemaphoreSlim(1, 1); - _emojis = ImmutableArray.Create(); + _emotes = ImmutableArray.Create(); _features = ImmutableArray.Create(); } internal static SocketGuild Create(DiscordSocketClient discord, ClientState state, ExtendedModel model) @@ -120,8 +113,8 @@ namespace Discord.WebSocket } internal void Update(ClientState state, ExtendedModel model) { - _available = !(model.Unavailable ?? false); - if (!_available) + IsAvailable = !(model.Unavailable ?? false); + if (!IsAvailable) { if (_channels == null) _channels = new ConcurrentHashSet(); @@ -133,6 +126,8 @@ namespace Discord.WebSocket _emojis = ImmutableArray.Create(); if (Features == null) _features = ImmutableArray.Create();*/ + _syncPromise = new TaskCompletionSource(); + _downloaderPromise = new TaskCompletionSource(); return; } @@ -160,9 +155,8 @@ namespace Discord.WebSocket for (int i = 0; i < model.Presences.Length; i++) { - SocketGuildUser member; - if (members.TryGetValue(model.Presences[i].User.Id, out member)) - member.Update(state, model.Presences[i]); + if (members.TryGetValue(model.Presences[i].User.Id, out SocketGuildUser member)) + member.Update(state, model.Presences[i], true); else Debug.Assert(false); } @@ -188,8 +182,8 @@ namespace Discord.WebSocket if (Discord.ApiClient.AuthTokenType != TokenType.User) { var _ = _syncPromise.TrySetResultAsync(true); - if (!model.Large) - _ = _downloaderPromise.TrySetResultAsync(true); + /*if (!model.Large) + _ = _downloaderPromise.TrySetResultAsync(true);*/ } } internal void Update(ClientState state, Model model) @@ -209,13 +203,13 @@ namespace Discord.WebSocket if (model.Emojis != null) { - var emojis = ImmutableArray.CreateBuilder(model.Emojis.Length); + var emojis = ImmutableArray.CreateBuilder(model.Emojis.Length); for (int i = 0; i < model.Emojis.Length; i++) emojis.Add(model.Emojis[i].ToEntity()); - _emojis = emojis.ToImmutable(); + _emotes = emojis.ToImmutable(); } else - _emojis = ImmutableArray.Create(); + _emotes = ImmutableArray.Create(); if (model.Features != null) _features = model.Features.ToImmutableArray(); @@ -246,9 +240,8 @@ namespace Discord.WebSocket for (int i = 0; i < model.Presences.Length; i++) { - SocketGuildUser member; - if (members.TryGetValue(model.Presences[i].User.Id, out member)) - member.Update(state, model.Presences[i]); + if (members.TryGetValue(model.Presences[i].User.Id, out SocketGuildUser member)) + member.Update(state, model.Presences[i], true); else Debug.Assert(false); } @@ -256,16 +249,16 @@ namespace Discord.WebSocket _members = members; var _ = _syncPromise.TrySetResultAsync(true); - if (!model.Large) - _ = _downloaderPromise.TrySetResultAsync(true); + /*if (!model.Large) + _ = _downloaderPromise.TrySetResultAsync(true);*/ } internal void Update(ClientState state, EmojiUpdateModel model) { - var emojis = ImmutableArray.CreateBuilder(model.Emojis.Length); + var emotes = ImmutableArray.CreateBuilder(model.Emojis.Length); for (int i = 0; i < model.Emojis.Length; i++) - emojis.Add(model.Emojis[i].ToEntity()); - _emojis = emojis.ToImmutable(); + emotes.Add(model.Emojis[i].ToEntity()); + _emotes = emotes.ToImmutable(); } //General @@ -276,10 +269,10 @@ namespace Discord.WebSocket => GuildHelper.ModifyAsync(this, Discord, func, options); public Task ModifyEmbedAsync(Action func, RequestOptions options = null) => GuildHelper.ModifyEmbedAsync(this, Discord, func, options); - public Task ModifyChannelsAsync(IEnumerable args, RequestOptions options = null) - => GuildHelper.ModifyChannelsAsync(this, Discord, args, options); - public Task ModifyRolesAsync(IEnumerable args, RequestOptions options = null) - => GuildHelper.ModifyRolesAsync(this, Discord, args, options); + public Task ReorderChannelsAsync(IEnumerable args, RequestOptions options = null) + => GuildHelper.ReorderChannelsAsync(this, Discord, args, options); + public Task ReorderRolesAsync(IEnumerable args, RequestOptions options = null) + => GuildHelper.ReorderRolesAsync(this, Discord, args, options); public Task LeaveAsync(RequestOptions options = null) => GuildHelper.LeaveAsync(this, Discord, options); @@ -288,10 +281,10 @@ namespace Discord.WebSocket public Task> GetBansAsync(RequestOptions options = null) => GuildHelper.GetBansAsync(this, Discord, options); - public Task AddBanAsync(IUser user, int pruneDays = 0, RequestOptions options = null) - => GuildHelper.AddBanAsync(this, Discord, user.Id, pruneDays, options); - public Task AddBanAsync(ulong userId, int pruneDays = 0, RequestOptions options = null) - => GuildHelper.AddBanAsync(this, Discord, userId, pruneDays, options); + public Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null) + => GuildHelper.AddBanAsync(this, Discord, user.Id, pruneDays, reason, options); + public Task AddBanAsync(ulong userId, int pruneDays = 0, string reason = null, RequestOptions options = null) + => GuildHelper.AddBanAsync(this, Discord, userId, pruneDays, reason, options); public Task RemoveBanAsync(IUser user, RequestOptions options = null) => GuildHelper.RemoveBanAsync(this, Discord, user.Id, options); @@ -341,8 +334,7 @@ namespace Discord.WebSocket //Roles public SocketRole GetRole(ulong id) { - SocketRole value; - if (_roles.TryGetValue(id, out value)) + if (_roles.TryGetValue(id, out SocketRole value)) return value; return null; } @@ -357,8 +349,7 @@ namespace Discord.WebSocket } internal SocketRole RemoveRole(ulong id) { - SocketRole role; - if (_roles.TryRemove(id, out role)) + if (_roles.TryRemove(id, out SocketRole role)) return role; return null; } @@ -366,22 +357,34 @@ namespace Discord.WebSocket //Users public SocketGuildUser GetUser(ulong id) { - SocketGuildUser member; - if (_members.TryGetValue(id, out member)) + if (_members.TryGetValue(id, out SocketGuildUser member)) return member; return null; } public Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null) => GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options); + internal SocketGuildUser AddOrUpdateUser(UserModel model) + { + if (_members.TryGetValue(model.Id, out SocketGuildUser member)) + member.GlobalUser?.Update(Discord.State, model); + else + { + member = SocketGuildUser.Create(this, Discord.State, model); + member.GlobalUser.AddRef(); + _members[member.Id] = member; + DownloadedMemberCount++; + } + return member; + } internal SocketGuildUser AddOrUpdateUser(MemberModel model) { - SocketGuildUser member; - if (_members.TryGetValue(model.User.Id, out member)) + if (_members.TryGetValue(model.User.Id, out SocketGuildUser member)) member.Update(Discord.State, model); else { member = SocketGuildUser.Create(this, Discord.State, model); + member.GlobalUser.AddRef(); _members[member.Id] = member; DownloadedMemberCount++; } @@ -389,12 +392,12 @@ namespace Discord.WebSocket } internal SocketGuildUser AddOrUpdateUser(PresenceModel model) { - SocketGuildUser member; - if (_members.TryGetValue(model.User.Id, out member)) - member.Update(Discord.State, model); + if (_members.TryGetValue(model.User.Id, out SocketGuildUser member)) + member.Update(Discord.State, model, false); else { member = SocketGuildUser.Create(this, Discord.State, model); + member.GlobalUser.AddRef(); _members[member.Id] = member; DownloadedMemberCount++; } @@ -402,8 +405,7 @@ namespace Discord.WebSocket } internal SocketGuildUser RemoveUser(ulong id) { - SocketGuildUser member; - if (_members.TryRemove(id, out member)) + if (_members.TryRemove(id, out SocketGuildUser member)) { DownloadedMemberCount--; member.GlobalUser.RemoveRef(Discord); @@ -422,30 +424,56 @@ namespace Discord.WebSocket } //Voice States - internal SocketVoiceState AddOrUpdateVoiceState(ClientState state, VoiceStateModel model) + internal async Task AddOrUpdateVoiceStateAsync(ClientState state, VoiceStateModel model) { var voiceChannel = state.GetChannel(model.ChannelId.Value) as SocketVoiceChannel; - var voiceState = SocketVoiceState.Create(voiceChannel, model); - _voiceStates[model.UserId] = voiceState; - return voiceState; + var before = GetVoiceState(model.UserId) ?? SocketVoiceState.Default; + var after = SocketVoiceState.Create(voiceChannel, model); + _voiceStates[model.UserId] = after; + + if (_audioClient != null && before.VoiceChannel?.Id != after.VoiceChannel?.Id) + { + if (model.UserId == CurrentUser.Id) + { + if (after.VoiceChannel != null && _audioClient.ChannelId != after.VoiceChannel?.Id) + { + _audioClient.ChannelId = after.VoiceChannel.Id; + await RepopulateAudioStreamsAsync().ConfigureAwait(false); + } + } + else + { + await _audioClient.RemoveInputStreamAsync(model.UserId).ConfigureAwait(false); //User changed channels, end their stream + if (CurrentUser.VoiceChannel != null && after.VoiceChannel?.Id == CurrentUser.VoiceChannel?.Id) + await _audioClient.CreateInputStreamAsync(model.UserId).ConfigureAwait(false); + } + } + + return after; } internal SocketVoiceState? GetVoiceState(ulong id) { - SocketVoiceState voiceState; - if (_voiceStates.TryGetValue(id, out voiceState)) + if (_voiceStates.TryGetValue(id, out SocketVoiceState voiceState)) return voiceState; return null; } - internal SocketVoiceState? RemoveVoiceState(ulong id) + internal async Task RemoveVoiceStateAsync(ulong id) { - SocketVoiceState voiceState; - if (_voiceStates.TryRemove(id, out voiceState)) + if (_voiceStates.TryRemove(id, out SocketVoiceState voiceState)) + { + if (_audioClient != null) + await _audioClient.RemoveInputStreamAsync(id).ConfigureAwait(false); //User changed channels, end their stream return voiceState; + } return null; } //Audio - internal async Task ConnectAudioAsync(ulong channelId, bool selfDeaf, bool selfMute) + internal AudioInStream GetAudioStream(ulong userId) + { + return _audioClient?.GetInputStream(userId); + } + internal async Task ConnectAudioAsync(ulong channelId, bool selfDeaf, bool selfMute, Action configAction) { selfDeaf = false; selfMute = false; @@ -458,6 +486,32 @@ namespace Discord.WebSocket await DisconnectAudioInternalAsync().ConfigureAwait(false); promise = new TaskCompletionSource(); _audioConnectPromise = promise; + + if (_audioClient == null) + { + var audioClient = new AudioClient(this, Discord.GetAudioId(), channelId); + audioClient.Disconnected += async ex => + { + if (!promise.Task.IsCompleted) + { + try { audioClient.Dispose(); } catch { } + _audioClient = null; + if (ex != null) + await promise.TrySetExceptionAsync(ex); + else + await promise.TrySetCanceledAsync(); + return; + } + }; + audioClient.Connected += () => + { + var _ = promise.TrySetResultAsync(_audioClient); + return Task.Delay(0); + }; + configAction?.Invoke(audioClient); + _audioClient = audioClient; + } + await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, channelId, selfDeaf, selfMute).ConfigureAwait(false); } catch (Exception) @@ -502,9 +556,10 @@ namespace Discord.WebSocket _audioConnectPromise = null; if (_audioClient != null) await _audioClient.StopAsync().ConfigureAwait(false); + await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, null, false, false).ConfigureAwait(false); _audioClient = null; } - internal async Task FinishConnectAudio(int id, string url, string token) + internal async Task FinishConnectAudio(string url, string token) { //TODO: Mem Leak: Disconnected/Connected handlers arent cleaned up var voiceState = GetVoiceState(Discord.CurrentUser.Id).Value; @@ -512,30 +567,7 @@ namespace Discord.WebSocket await _audioLock.WaitAsync().ConfigureAwait(false); try { - var promise = _audioConnectPromise; - if (_audioClient == null) - { - var audioClient = new AudioClient(this, id); - audioClient.Disconnected += async ex => - { - if (!promise.Task.IsCompleted) - { - try { audioClient.Dispose(); } catch { } - _audioClient = null; - if (ex != null) - await promise.TrySetExceptionAsync(ex); - else - await promise.TrySetCanceledAsync(); - return; - } - }; - _audioClient = audioClient; - } - _audioClient.Connected += () => - { - var _ = promise.TrySetResultAsync(_audioClient); - return Task.Delay(0); - }; + await RepopulateAudioStreamsAsync().ConfigureAwait(false); await _audioClient.StartAsync(url, Discord.CurrentUser.Id, voiceState.VoiceSessionId, token).ConfigureAwait(false); } catch (OperationCanceledException) @@ -553,6 +585,19 @@ namespace Discord.WebSocket } } + internal async Task RepopulateAudioStreamsAsync() + { + await _audioClient.ClearInputStreamsAsync().ConfigureAwait(false); //We changed channels, end all current streams + if (CurrentUser.VoiceChannel != null) + { + foreach (var pair in _voiceStates) + { + if (pair.Value.VoiceChannel?.Id == CurrentUser.VoiceChannel?.Id && pair.Key != CurrentUser.Id) + await _audioClient.CreateInputStreamAsync(pair.Key).ConfigureAwait(false); + } + } + } + public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id})"; internal SocketGuild Clone() => MemberwiseClone() as SocketGuild; @@ -585,8 +630,8 @@ namespace Discord.WebSocket => Task.FromResult(AFKChannel); Task IGuild.GetDefaultChannelAsync(CacheMode mode, RequestOptions options) => Task.FromResult(DefaultChannel); - Task IGuild.GetEmbedChannelAsync(CacheMode mode, RequestOptions options) - => Task.FromResult(EmbedChannel); + Task IGuild.GetEmbedChannelAsync(CacheMode mode, RequestOptions options) + => Task.FromResult(EmbedChannel); async Task IGuild.CreateTextChannelAsync(string name, RequestOptions options) => await CreateTextChannelAsync(name, options).ConfigureAwait(false); async Task IGuild.CreateVoiceChannelAsync(string name, RequestOptions options) diff --git a/src/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs b/src/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs index 7b8d9c2cd..c2cad4d86 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs @@ -27,24 +27,20 @@ namespace Discord.WebSocket { _orderedMessages.Enqueue(message.Id); - ulong msgId; - SocketMessage msg; - while (_orderedMessages.Count > _size && _orderedMessages.TryDequeue(out msgId)) - _messages.TryRemove(msgId, out msg); + while (_orderedMessages.Count > _size && _orderedMessages.TryDequeue(out ulong msgId)) + _messages.TryRemove(msgId, out SocketMessage msg); } } public SocketMessage Remove(ulong id) { - SocketMessage msg; - _messages.TryRemove(id, out msg); + _messages.TryRemove(id, out SocketMessage msg); return msg; } public SocketMessage Get(ulong id) { - SocketMessage result; - if (_messages.TryGetValue(id, out result)) + if (_messages.TryGetValue(id, out SocketMessage result)) return result; return null; } @@ -67,8 +63,7 @@ namespace Discord.WebSocket return cachedMessageIds .Select(x => { - SocketMessage msg; - if (_messages.TryGetValue(x, out msg)) + if (_messages.TryGetValue(x, out SocketMessage msg)) return msg; return null; }) diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs index 0b09d2d22..5442c888a 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs @@ -14,10 +14,11 @@ namespace Discord.WebSocket public SocketUser Author { get; } public ISocketMessageChannel Channel { get; } + public MessageSource Source { get; } public string Content { get; private set; } - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public virtual bool IsTTS => false; public virtual bool IsPinned => false; public virtual DateTimeOffset? EditedTimestamp => null; @@ -27,16 +28,15 @@ namespace Discord.WebSocket public virtual IReadOnlyCollection MentionedRoles => ImmutableArray.Create(); public virtual IReadOnlyCollection MentionedUsers => ImmutableArray.Create(); public virtual IReadOnlyCollection Tags => ImmutableArray.Create(); - public virtual ulong? WebhookId => null; - public bool IsWebhook => WebhookId != null; public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); - internal SocketMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author) + internal SocketMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author, MessageSource source) : base(discord, id) { Channel = channel; Author = author; + Source = source; } internal static SocketMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) { diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs index c12d0fdea..35bee9e68 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs @@ -9,20 +9,47 @@ namespace Discord.WebSocket public ulong MessageId { get; } public Optional Message { get; } public ISocketMessageChannel Channel { get; } - public Emoji Emoji { get; } + public IEmote Emote { get; } - internal SocketReaction(ISocketMessageChannel channel, ulong messageId, Optional message, ulong userId, Optional user, Emoji emoji) + internal SocketReaction(ISocketMessageChannel channel, ulong messageId, Optional message, ulong userId, Optional user, IEmote emoji) { Channel = channel; MessageId = messageId; Message = message; UserId = userId; User = user; - Emoji = emoji; + Emote = emoji; } internal static SocketReaction Create(Model model, ISocketMessageChannel channel, Optional message, Optional user) { - return new SocketReaction(channel, model.MessageId, message, model.UserId, user, new Emoji(model.Emoji.Id, model.Emoji.Name)); + IEmote emote; + if (model.Emoji.Id.HasValue) + emote = new Emote(model.Emoji.Id.Value, model.Emoji.Name); + else + emote = new Emoji(model.Emoji.Name); + return new SocketReaction(channel, model.MessageId, message, model.UserId, user, emote); + } + + public override bool Equals(object other) + { + if (other == null) return false; + if (other == this) return true; + + var otherReaction = other as SocketReaction; + if (otherReaction == null) return false; + + return UserId == otherReaction.UserId && MessageId == otherReaction.MessageId && Emote.Equals(otherReaction.Emote); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = UserId.GetHashCode(); + hashCode = (hashCode * 397) ^ MessageId.GetHashCode(); + hashCode = (hashCode * 397) ^ Emote.GetHashCode(); + return hashCode; + } } } } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs index 50cdb964b..e6c67159f 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs @@ -9,7 +9,7 @@ namespace Discord.WebSocket public MessageType Type { get; private set; } internal SocketSystemMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author) - : base(discord, id, channel, author) + : base(discord, id, channel, author, MessageSource.System) { } internal new static SocketSystemMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index e1a6853e2..40588e55a 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -14,7 +14,6 @@ namespace Discord.WebSocket { private bool _isMentioningEveryone, _isTTS, _isPinned; private long? _editedTimestampTicks; - private ulong? _webhookId; private ImmutableArray _attachments; private ImmutableArray _embeds; private ImmutableArray _tags; @@ -22,7 +21,6 @@ namespace Discord.WebSocket public override bool IsTTS => _isTTS; public override bool IsPinned => _isPinned; - public override ulong? WebhookId => _webhookId; public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks); public override IReadOnlyCollection Attachments => _attachments; public override IReadOnlyCollection Embeds => _embeds; @@ -30,15 +28,15 @@ namespace Discord.WebSocket public override IReadOnlyCollection MentionedChannels => MessageHelper.FilterTagsByValue(TagType.ChannelMention, _tags); public override IReadOnlyCollection MentionedRoles => MessageHelper.FilterTagsByValue(TagType.RoleMention, _tags); public override IReadOnlyCollection MentionedUsers => MessageHelper.FilterTagsByValue(TagType.UserMention, _tags); - public IReadOnlyDictionary Reactions => _reactions.GroupBy(r => r.Emoji).ToDictionary(x => x.Key, x => x.Count()); + public IReadOnlyDictionary Reactions => _reactions.GroupBy(r => r.Emote).ToDictionary(x => x.Key, x => new ReactionMetadata { ReactionCount = x.Count(), IsMe = x.Any(y => y.UserId == Discord.CurrentUser.Id) }); - internal SocketUserMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author) - : base(discord, id, channel, author) + internal SocketUserMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author, MessageSource source) + : base(discord, id, channel, author, source) { } - internal new static SocketUserMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) + internal static new SocketUserMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) { - var entity = new SocketUserMessage(discord, model.Id, channel, author); + var entity = new SocketUserMessage(discord, model.Id, channel, author, MessageHelper.GetSource(model)); entity.Update(state, model); return entity; } @@ -55,8 +53,6 @@ namespace Discord.WebSocket _editedTimestampTicks = model.EditedTimestamp.Value?.UtcTicks; if (model.MentionEveryone.IsSpecified) _isMentioningEveryone = model.MentionEveryone.Value; - if (model.WebhookId.IsSpecified) - _webhookId = model.WebhookId.Value; if (model.Attachments.IsSpecified) { @@ -86,18 +82,18 @@ namespace Discord.WebSocket _embeds = ImmutableArray.Create(); } - ImmutableArray mentions = ImmutableArray.Create(); + IReadOnlyCollection mentions = ImmutableArray.Create(); //Is passed to ParseTags to get real mention collection if (model.UserMentions.IsSpecified) { var value = model.UserMentions.Value; if (value.Length > 0) { - var newMentions = ImmutableArray.CreateBuilder(value.Length); + var newMentions = ImmutableArray.CreateBuilder(value.Length); for (int i = 0; i < value.Length; i++) { var val = value[i]; if (val.Object != null) - newMentions.Add(SocketSimpleUser.Create(Discord, Discord.State, val.Object)); + newMentions.Add(SocketUnknownUser.Create(Discord, state, val.Object)); } mentions = newMentions.ToImmutable(); } @@ -128,16 +124,10 @@ namespace Discord.WebSocket public Task ModifyAsync(Action func, RequestOptions options = null) => MessageHelper.ModifyAsync(this, Discord, func, options); - public Task AddReactionAsync(Emoji emoji, RequestOptions options = null) - => MessageHelper.AddReactionAsync(this, emoji, Discord, options); - public Task AddReactionAsync(string emoji, RequestOptions options = null) - => MessageHelper.AddReactionAsync(this, emoji, Discord, options); - - public Task RemoveReactionAsync(Emoji emoji, IUser user, RequestOptions options = null) - => MessageHelper.RemoveReactionAsync(this, user, emoji, Discord, options); - public Task RemoveReactionAsync(string emoji, IUser user, RequestOptions options = null) - => MessageHelper.RemoveReactionAsync(this, user, emoji, Discord, options); - + public Task AddReactionAsync(IEmote emote, RequestOptions options = null) + => MessageHelper.AddReactionAsync(this, emote, Discord, options); + public Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions options = null) + => MessageHelper.RemoveReactionAsync(this, user, emote, Discord, options); public Task RemoveAllReactionsAsync(RequestOptions options = null) => MessageHelper.RemoveAllReactionsAsync(this, Discord, options); diff --git a/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs b/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs index 61fd4310f..7d24d8e1c 100644 --- a/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs +++ b/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs @@ -1,6 +1,8 @@ using Discord.Rest; using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Threading.Tasks; using Model = Discord.API.Role; @@ -19,9 +21,11 @@ namespace Discord.WebSocket public GuildPermissions Permissions { get; private set; } public int Position { get; private set; } - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public bool IsEveryone => Id == Guild.Id; public string Mention => MentionUtils.MentionRole(Id); + public IEnumerable Members + => Guild.Users.Where(x => x.Roles.Any(r => r.Id == Id)); internal SocketRole(SocketGuild guild, ulong id) : base(guild.Discord, id) diff --git a/src/Discord.Net.WebSocket/Entities/SocketEntity.cs b/src/Discord.Net.WebSocket/Entities/SocketEntity.cs index db1b7dc4f..c8e14fb6c 100644 --- a/src/Discord.Net.WebSocket/Entities/SocketEntity.cs +++ b/src/Discord.Net.WebSocket/Entities/SocketEntity.cs @@ -5,7 +5,7 @@ namespace Discord.WebSocket public abstract class SocketEntity : IEntity where T : IEquatable { - public DiscordSocketClient Discord { get; } + internal DiscordSocketClient Discord { get; } public T Id { get; } internal SocketEntity(DiscordSocketClient discord, T id) diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs index f0b23543e..3117eb14c 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs @@ -1,5 +1,7 @@ using System.Diagnostics; +using System.Linq; using Model = Discord.API.User; +using PresenceModel = Discord.API.Presence; namespace Discord.WebSocket { @@ -11,9 +13,10 @@ namespace Discord.WebSocket public override ushort DiscriminatorValue { get; internal set; } public override string AvatarId { get; internal set; } public SocketDMChannel DMChannel { get; internal set; } + internal override SocketPresence Presence { get; set; } + public override bool IsWebhook => false; internal override SocketGlobalUser GlobalUser => this; - internal override SocketPresence Presence { get; set; } private readonly object _lockObj = new object(); private ushort _references; @@ -46,12 +49,12 @@ namespace Discord.WebSocket } } - internal new SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser; - - //Updates are only ever called from the gateway thread, thus threadsafe - internal override void Update(ClientState state, Model model) + internal void Update(ClientState state, PresenceModel model) { - base.Update(state, model); + Presence = SocketPresence.Create(model); + DMChannel = state.DMChannels.FirstOrDefault(x => x.Recipient.Id == Id); } + + internal new SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser; } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs index 694d0ccb9..8d1b360e3 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs @@ -15,6 +15,8 @@ namespace Discord.WebSocket public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } internal override SocketPresence Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } + public override bool IsWebhook => false; + internal SocketGroupUser(SocketGroupChannel channel, SocketGlobalUser globalUser) : base(channel.Discord, globalUser.Id) { diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index 5162839d7..844b0c7f4 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -1,11 +1,13 @@ -using Discord.Rest; +using Discord.Audio; +using Discord.Rest; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; -using Model = Discord.API.GuildMember; +using UserModel = Discord.API.User; +using MemberModel = Discord.API.GuildMember; using PresenceModel = Discord.API.Presence; namespace Discord.WebSocket @@ -25,18 +27,21 @@ namespace Discord.WebSocket public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } } public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } public GuildPermissions GuildPermissions => new GuildPermissions(Permissions.ResolveGuild(Guild, this)); - internal override SocketPresence Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } + internal override SocketPresence Presence { get; set; } + public override bool IsWebhook => false; public bool IsSelfDeafened => VoiceState?.IsSelfDeafened ?? false; public bool IsSelfMuted => VoiceState?.IsSelfMuted ?? false; public bool IsSuppressed => VoiceState?.IsSuppressed ?? false; public bool IsDeafened => VoiceState?.IsDeafened ?? false; public bool IsMuted => VoiceState?.IsMuted ?? false; public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); - public IEnumerable Roles => _roleIds.Select(id => Guild.GetRole(id)).ToReadOnlyCollection(() => _roleIds.Count()); + public IReadOnlyCollection Roles + => _roleIds.Select(id => Guild.GetRole(id)).Where(x => x != null).ToReadOnlyCollection(() => _roleIds.Length); public SocketVoiceChannel VoiceChannel => VoiceState?.VoiceChannel; public string VoiceSessionId => VoiceState?.VoiceSessionId ?? ""; public SocketVoiceState? VoiceState => Guild.GetVoiceState(Id); + public AudioInStream AudioStream => Guild.GetAudioStream(Id); /// The position of the user within the role hirearchy. /// The returned value equal to the position of the highest role the user has, @@ -65,7 +70,14 @@ namespace Discord.WebSocket Guild = guild; GlobalUser = globalUser; } - internal static SocketGuildUser Create(SocketGuild guild, ClientState state, Model model) + internal static SocketGuildUser Create(SocketGuild guild, ClientState state, UserModel model) + { + var entity = new SocketGuildUser(guild, guild.Discord.GetOrCreateUser(state, model)); + entity.Update(state, model); + entity.UpdateRoles(new ulong[0]); + return entity; + } + internal static SocketGuildUser Create(SocketGuild guild, ClientState state, MemberModel model) { var entity = new SocketGuildUser(guild, guild.Discord.GetOrCreateUser(state, model.User)); entity.Update(state, model); @@ -74,25 +86,30 @@ namespace Discord.WebSocket internal static SocketGuildUser Create(SocketGuild guild, ClientState state, PresenceModel model) { var entity = new SocketGuildUser(guild, guild.Discord.GetOrCreateUser(state, model.User)); - entity.Update(state, model); + entity.Update(state, model, false); return entity; } - internal void Update(ClientState state, Model model) + internal void Update(ClientState state, MemberModel model) { base.Update(state, model.User); if (model.JoinedAt.IsSpecified) _joinedAtTicks = model.JoinedAt.Value.UtcTicks; if (model.Nick.IsSpecified) Nickname = model.Nick.Value; - UpdateRoles(model.Roles); - } - internal override void Update(ClientState state, PresenceModel model) - { - base.Update(state, model); if (model.Roles.IsSpecified) UpdateRoles(model.Roles.Value); + } + internal void Update(ClientState state, PresenceModel model, bool updatePresence) + { + if (updatePresence) + { + Presence = SocketPresence.Create(model); + GlobalUser.Update(state, model); + } if (model.Nick.IsSpecified) Nickname = model.Nick.Value; + if (model.Roles.IsSpecified) + UpdateRoles(model.Roles.Value); } private void UpdateRoles(ulong[] roleIds) { @@ -105,8 +122,20 @@ namespace Discord.WebSocket public Task ModifyAsync(Action func, RequestOptions options = null) => UserHelper.ModifyAsync(this, Discord, func, options); - public Task KickAsync(RequestOptions options = null) - => UserHelper.KickAsync(this, Discord, options); + public Task KickAsync(string reason = null, RequestOptions options = null) + => UserHelper.KickAsync(this, Discord, reason, options); + /// + public Task AddRoleAsync(IRole role, RequestOptions options = null) + => AddRolesAsync(new[] { role }, options); + /// + public Task AddRolesAsync(IEnumerable roles, RequestOptions options = null) + => UserHelper.AddRolesAsync(this, Discord, roles, options); + /// + public Task RemoveRoleAsync(IRole role, RequestOptions options = null) + => RemoveRolesAsync(new[] { role }, options); + /// + public Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null) + => UserHelper.RemoveRolesAsync(this, Discord, roles, options); public ChannelPermissions GetPermissions(IGuildChannel channel) => new ChannelPermissions(Permissions.ResolveChannel(Guild, this, channel, GuildPermissions.RawValue)); @@ -117,11 +146,7 @@ namespace Discord.WebSocket IGuild IGuildUser.Guild => Guild; ulong IGuildUser.GuildId => Guild.Id; IReadOnlyCollection IGuildUser.RoleIds => _roleIds; - - //IUser - Task IUser.GetDMChannelAsync(CacheMode mode, RequestOptions options) - => Task.FromResult(GlobalUser.DMChannel); - + //IVoiceState IVoiceChannel IVoiceState.VoiceChannel => VoiceChannel; } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs index b7531ffc6..b4365baab 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs @@ -20,6 +20,8 @@ namespace Discord.WebSocket public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } internal override SocketPresence Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } + public override bool IsWebhook => false; + internal SocketSelfUser(DiscordSocketClient discord, SocketGlobalUser globalUser) : base(discord, globalUser.Id) { @@ -31,16 +33,25 @@ namespace Discord.WebSocket entity.Update(state, model); return entity; } - internal override void Update(ClientState state, Model model) + internal override bool Update(ClientState state, Model model) { - base.Update(state, model); - + bool hasGlobalChanges = base.Update(state, model); if (model.Email.IsSpecified) + { Email = model.Email.Value; + hasGlobalChanges = true; + } if (model.Verified.IsSpecified) + { IsVerified = model.Verified.Value; + hasGlobalChanges = true; + } if (model.MfaEnabled.IsSpecified) + { IsMfaEnabled = model.MfaEnabled.Value; + hasGlobalChanges = true; + } + return hasGlobalChanges; } public Task ModifyAsync(Action func, RequestOptions options = null) diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketSimpleUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs similarity index 52% rename from src/Discord.Net.WebSocket/Entities/Users/SocketSimpleUser.cs rename to src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs index 1ecb5e578..c7f6cb846 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketSimpleUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs @@ -1,36 +1,33 @@ using System; using System.Diagnostics; using Model = Discord.API.User; -using PresenceModel = Discord.API.Presence; namespace Discord.WebSocket { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class SocketSimpleUser : SocketUser + public class SocketUnknownUser : SocketUser { - public override bool IsBot { get; internal set; } public override string Username { get; internal set; } public override ushort DiscriminatorValue { get; internal set; } public override string AvatarId { get; internal set; } - internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null); } set { } } + public override bool IsBot { get; internal set; } + + public override bool IsWebhook => false; - internal override SocketGlobalUser GlobalUser { get { throw new NotSupportedException(); } } + internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null); } set { } } + internal override SocketGlobalUser GlobalUser { get { throw new NotSupportedException(); } } - internal SocketSimpleUser(DiscordSocketClient discord, ulong id) + internal SocketUnknownUser(DiscordSocketClient discord, ulong id) : base(discord, id) { } - internal static SocketSimpleUser Create(DiscordSocketClient discord, ClientState state, Model model) + internal static SocketUnknownUser Create(DiscordSocketClient discord, ClientState state, Model model) { - var entity = new SocketSimpleUser(discord, model.Id); + var entity = new SocketUnknownUser(discord, model.Id); entity.Update(state, model); return entity; } - internal override void Update(ClientState state, PresenceModel model) - { - } - - internal new SocketSimpleUser Clone() => MemberwiseClone() as SocketSimpleUser; + internal new SocketUnknownUser Clone() => MemberwiseClone() as SocketUnknownUser; } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index 2b29311ff..a31e2a90b 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -2,7 +2,6 @@ using System; using System.Threading.Tasks; using Model = Discord.API.User; -using PresenceModel = Discord.API.Presence; namespace Discord.WebSocket { @@ -12,11 +11,11 @@ namespace Discord.WebSocket public abstract string Username { get; internal set; } public abstract ushort DiscriminatorValue { get; internal set; } public abstract string AvatarId { get; internal set; } + public abstract bool IsWebhook { get; } internal abstract SocketGlobalUser GlobalUser { get; } internal abstract SocketPresence Presence { get; set; } - public string GetAvatarUrl(AvatarFormat format = AvatarFormat.Png, ushort size = 128) => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); public Game? Game => Presence.Game; @@ -27,25 +26,41 @@ namespace Discord.WebSocket : base(discord, id) { } - internal virtual void Update(ClientState state, Model model) + internal virtual bool Update(ClientState state, Model model) { - if (model.Avatar.IsSpecified) + bool hasChanges = false; + if (model.Avatar.IsSpecified && model.Avatar.Value != AvatarId) + { AvatarId = model.Avatar.Value; + hasChanges = true; + } if (model.Discriminator.IsSpecified) - DiscriminatorValue = ushort.Parse(model.Discriminator.Value); - if (model.Bot.IsSpecified) + { + var newVal = ushort.Parse(model.Discriminator.Value); + if (newVal != DiscriminatorValue) + { + DiscriminatorValue = ushort.Parse(model.Discriminator.Value); + hasChanges = true; + } + } + if (model.Bot.IsSpecified && model.Bot.Value != IsBot) + { IsBot = model.Bot.Value; - if (model.Username.IsSpecified) + hasChanges = true; + } + if (model.Username.IsSpecified && model.Username.Value != Username) + { Username = model.Username.Value; - } - internal virtual void Update(ClientState state, PresenceModel model) - { - Presence = SocketPresence.Create(model); - Update(state, model.User); - } + hasChanges = true; + } + return hasChanges; + } + + public async Task GetOrCreateDMChannelAsync(RequestOptions options = null) + => GlobalUser.DMChannel ?? await UserHelper.CreateDMChannelAsync(this, Discord, options) as IDMChannel; - public Task CreateDMChannelAsync(RequestOptions options = null) - => UserHelper.CreateDMChannelAsync(this, Discord, options); + public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); public override string ToString() => $"{Username}#{Discriminator}"; private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs new file mode 100644 index 000000000..78a29639b --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.User; + +namespace Discord.WebSocket +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketWebhookUser : SocketUser, IWebhookUser + { + public SocketGuild Guild { get; } + public ulong WebhookId { get; } + + public override string Username { get; internal set; } + public override ushort DiscriminatorValue { get; internal set; } + public override string AvatarId { get; internal set; } + public override bool IsBot { get; internal set; } + + public override bool IsWebhook => true; + + internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null); } set { } } + internal override SocketGlobalUser GlobalUser { get { throw new NotSupportedException(); } } + + internal SocketWebhookUser(SocketGuild guild, ulong id, ulong webhookId) + : base(guild.Discord, id) + { + WebhookId = webhookId; + } + internal static SocketWebhookUser Create(SocketGuild guild, ClientState state, Model model, ulong webhookId) + { + var entity = new SocketWebhookUser(guild, model.Id, webhookId); + entity.Update(state, model); + return entity; + } + + internal new SocketWebhookUser Clone() => MemberwiseClone() as SocketWebhookUser; + + + //IGuildUser + IGuild IGuildUser.Guild => Guild; + ulong IGuildUser.GuildId => Guild.Id; + IReadOnlyCollection IGuildUser.RoleIds => ImmutableArray.Create(); + DateTimeOffset? IGuildUser.JoinedAt => null; + string IGuildUser.Nickname => null; + GuildPermissions IGuildUser.GuildPermissions => GuildPermissions.Webhook; + + ChannelPermissions IGuildUser.GetPermissions(IGuildChannel channel) => Permissions.ToChannelPerms(channel, GuildPermissions.Webhook.RawValue); + Task IGuildUser.KickAsync(string reason, RequestOptions options) + { + throw new NotSupportedException("Webhook users cannot be kicked."); + } + Task IGuildUser.ModifyAsync(Action func, RequestOptions options) + { + throw new NotSupportedException("Webhook users cannot be modified."); + } + + Task IGuildUser.AddRoleAsync(IRole role, RequestOptions options) + { + throw new NotSupportedException("Roles are not supported on webhook users."); + } + Task IGuildUser.AddRolesAsync(IEnumerable roles, RequestOptions options) + { + throw new NotSupportedException("Roles are not supported on webhook users."); + } + Task IGuildUser.RemoveRoleAsync(IRole role, RequestOptions options) + { + throw new NotSupportedException("Roles are not supported on webhook users."); + } + Task IGuildUser.RemoveRolesAsync(IEnumerable roles, RequestOptions options) + { + throw new NotSupportedException("Roles are not supported on webhook users."); + } + + //IVoiceState + bool IVoiceState.IsDeafened => false; + bool IVoiceState.IsMuted => false; + bool IVoiceState.IsSelfDeafened => false; + bool IVoiceState.IsSelfMuted => false; + bool IVoiceState.IsSuppressed => false; + IVoiceChannel IVoiceState.VoiceChannel => null; + string IVoiceState.VoiceSessionId => null; + } +} diff --git a/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs b/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs index eb184e345..6a6194397 100644 --- a/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs +++ b/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs @@ -1,4 +1,4 @@ -#if NETSTANDARD1_3 +#if DEFAULTUDPCLIENT using System; using System.Net; using System.Net.Sockets; @@ -18,6 +18,8 @@ namespace Discord.Net.Udp private CancellationToken _cancelToken, _parentToken; private Task _task; private bool _isDisposed; + + public ushort Port => (ushort)((_udp?.Client.LocalEndPoint as IPEndPoint)?.Port ?? 0); public DefaultUdpSocket() { @@ -83,16 +85,19 @@ namespace Discord.Net.Udp if (_udp != null) { +#if UDPDISPOSE try { _udp.Dispose(); } +#else + try { _udp.Close(); } +#endif catch { } _udp = null; } } - public void SetDestination(string host, int port) + public void SetDestination(string ip, int port) { - var entry = Dns.GetHostEntryAsync(host).GetAwaiter().GetResult(); - _destination = new IPEndPoint(entry.AddressList[0], port); + _destination = new IPEndPoint(IPAddress.Parse(ip), port); } public void SetCancelToken(CancellationToken cancelToken) { diff --git a/src/Discord.Net.WebSocket/Net/DefaultUdpSocketProvider.cs b/src/Discord.Net.WebSocket/Net/DefaultUdpSocketProvider.cs index cba4fecb0..82b6ec4c0 100644 --- a/src/Discord.Net.WebSocket/Net/DefaultUdpSocketProvider.cs +++ b/src/Discord.Net.WebSocket/Net/DefaultUdpSocketProvider.cs @@ -4,7 +4,7 @@ namespace Discord.Net.Udp { public static class DefaultUdpSocketProvider { -#if NETSTANDARD1_3 +#if DEFAULTUDPCLIENT public static readonly UdpSocketProvider Instance = () => { try diff --git a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs index 6f667ae41..282ae210a 100644 --- a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs +++ b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs @@ -1,4 +1,4 @@ -#if NETSTANDARD1_3 +#if DEFAULTWEBSOCKET using System; using System.Collections.Generic; using System.ComponentModel; @@ -206,11 +206,14 @@ namespace Discord.Net.WebSockets //Use the internal buffer if we can get it resultCount = (int)stream.Length; - ArraySegment streamBuffer; - if (stream.TryGetBuffer(out streamBuffer)) +#if MSTRYBUFFER + if (stream.TryGetBuffer(out var streamBuffer)) result = streamBuffer.Array; else result = stream.ToArray(); +#else + result = stream.GetBuffer(); +#endif } } else diff --git a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs index d93ded57d..04b3f8388 100644 --- a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs +++ b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs @@ -4,7 +4,7 @@ namespace Discord.Net.WebSockets { public static class DefaultWebSocketProvider { -#if NETSTANDARD1_3 +#if DEFAULTWEBSOCKET public static readonly WebSocketProvider Instance = () => { try diff --git a/src/Discord.Net.Webhook/AssemblyInfo.cs b/src/Discord.Net.Webhook/AssemblyInfo.cs new file mode 100644 index 000000000..c6b5997b4 --- /dev/null +++ b/src/Discord.Net.Webhook/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Discord.Net.Tests")] \ No newline at end of file diff --git a/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj b/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj new file mode 100644 index 000000000..7c224e01e --- /dev/null +++ b/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj @@ -0,0 +1,13 @@ + + + + Discord.Net.Webhook + Discord.Webhook + A core Discord.Net library containing the Webhook client and models. + netstandard1.1 + + + + + + \ No newline at end of file diff --git a/src/Discord.Net.Webhook/DiscordWebhookClient.cs b/src/Discord.Net.Webhook/DiscordWebhookClient.cs new file mode 100644 index 000000000..9695099ee --- /dev/null +++ b/src/Discord.Net.Webhook/DiscordWebhookClient.cs @@ -0,0 +1,82 @@ +using Discord.API.Rest; +using Discord.Rest; +using System; +using System.IO; +using System.Threading.Tasks; +using System.Linq; +using Discord.Logging; + +namespace Discord.Webhook +{ + public partial class DiscordWebhookClient + { + public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } + internal readonly AsyncEvent> _logEvent = new AsyncEvent>(); + + private readonly ulong _webhookId; + internal readonly Logger _restLogger; + + internal API.DiscordRestApiClient ApiClient { get; } + internal LogManager LogManager { get; } + + /// Creates a new Webhook discord client. + public DiscordWebhookClient(ulong webhookId, string webhookToken) + : this(webhookId, webhookToken, new DiscordRestConfig()) { } + /// Creates a new Webhook discord client. + public DiscordWebhookClient(ulong webhookId, string webhookToken, DiscordRestConfig config) + { + _webhookId = webhookId; + + ApiClient = CreateApiClient(config); + LogManager = new LogManager(config.LogLevel); + LogManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); + + _restLogger = LogManager.CreateLogger("Rest"); + + ApiClient.RequestQueue.RateLimitTriggered += async (id, info) => + { + if (info == null) + await _restLogger.WarningAsync($"Preemptive Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); + else + await _restLogger.WarningAsync($"Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); + }; + ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false); + ApiClient.LoginAsync(TokenType.Webhook, webhookToken).GetAwaiter().GetResult(); + } + private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config) + => new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent); + + public async Task SendMessageAsync(string text, bool isTTS = false, Embed[] embeds = null, + string username = null, string avatarUrl = null, RequestOptions options = null) + { + var args = new CreateWebhookMessageParams(text) { IsTTS = isTTS }; + if (embeds != null) + args.Embeds = embeds.Select(x => x.ToModel()).ToArray(); + if (username != null) + args.Username = username; + if (avatarUrl != null) + args.AvatarUrl = avatarUrl; + await ApiClient.CreateWebhookMessageAsync(_webhookId, args, options).ConfigureAwait(false); + } + +#if FILESYSTEM + public async Task SendFileAsync(string filePath, string text, bool isTTS = false, + string username = null, string avatarUrl = null, RequestOptions options = null) + { + string filename = Path.GetFileName(filePath); + using (var file = File.OpenRead(filePath)) + await SendFileAsync(file, filename, text, isTTS, username, avatarUrl, options).ConfigureAwait(false); + } +#endif + public async Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, + string username = null, string avatarUrl = null, RequestOptions options = null) + { + var args = new UploadWebhookFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS }; + if (username != null) + args.Username = username; + if (avatarUrl != null) + args.AvatarUrl = username; + await ApiClient.UploadWebhookFileAsync(_webhookId, args, options).ConfigureAwait(false); + } + } +} diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index 9966d9d23..864083599 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,29 +2,39 @@ Discord.Net - 1.0.0-rc-$build$ + 1.0.1$suffix$ Discord.Net RogueException RogueException - An aynchronous API wrapper for Discord. This metapackage includes all of the optional Discord.Net components. + An asynchronous API wrapper for Discord. This metapackage includes all of the optional Discord.Net components. discord;discordapp https://github.com/RogueException/Discord.Net http://opensource.org/licenses/MIT false + + + + + + + + - - - - - + + + + + + - - - - - + + + + + + diff --git a/test.ps1 b/test.ps1 deleted file mode 100644 index b8a817743..000000000 --- a/test.ps1 +++ /dev/null @@ -1,2 +0,0 @@ -dotnet test test/Discord.Net.Tests/Discord.Net.Tests.csproj -c "Release" --no-build /p:BuildNumber="$Env:BUILD" -if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } \ No newline at end of file diff --git a/test/Discord.Net.Tests/Discord.Net.Tests.csproj b/test/Discord.Net.Tests/Discord.Net.Tests.csproj index 986605eeb..9e734641c 100644 --- a/test/Discord.Net.Tests/Discord.Net.Tests.csproj +++ b/test/Discord.Net.Tests/Discord.Net.Tests.csproj @@ -1,9 +1,9 @@ - + Exe + Discord netcoreapp1.1 $(PackageTargetFallback);portable-net45+win8+wp8+wpa81 - Discord @@ -17,10 +17,10 @@ - - - - - + + + + + diff --git a/test/Discord.Net.Tests/Net/CachedRestClient.cs b/test/Discord.Net.Tests/Net/CachedRestClient.cs index 324510688..4bc8a386a 100644 --- a/test/Discord.Net.Tests/Net/CachedRestClient.cs +++ b/test/Discord.Net.Tests/Net/CachedRestClient.cs @@ -5,11 +5,11 @@ using System; using System.Collections.Generic; using System.IO; using System.Net; +using System.Reactive.Concurrency; using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using Splat; -using System.Reactive.Concurrency; namespace Discord.Net { @@ -66,7 +66,7 @@ namespace Discord.Net _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; } - public async Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly) + public async Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly, string reason = null) { if (method != "GET") throw new InvalidOperationException("This RestClient only supports GET requests."); @@ -75,11 +75,11 @@ namespace Discord.Net var bytes = await _blobCache.DownloadUrl(uri, _headers); return new RestResponse(HttpStatusCode.OK, _headers, new MemoryStream(bytes)); } - public Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly) + public Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly, string reason = null) { throw new InvalidOperationException("This RestClient does not support payloads."); } - public Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly) + public Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly, string reason = null) { throw new InvalidOperationException("This RestClient does not support multipart requests."); }