diff --git a/.gitignore b/.gitignore index d7bf0ef19..ccd272109 100644 --- a/.gitignore +++ b/.gitignore @@ -198,6 +198,8 @@ FakesAssemblies/ #Custom project.lock.json /test/Discord.Net.Tests/config.json +/test/Discord.Net.Tests/cache.db* /docs/_build *.pyc /.editorconfig +.vscode/ \ No newline at end of file diff --git a/Discord.Net.sln b/Discord.Net.sln index 5ad44de92..6308b4444 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -1,16 +1,8 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.25914.0 +VisualStudioVersion = 15.0.26014.0 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F7F3E124-93C7-4846-AE87-9CE12BD82859}" - ProjectSection(SolutionItems) = preProject - global.json = global.json - README.md = README.md - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net", "src\Discord.Net\Discord.Net.csproj", "{496DB20A-A455-4D01-B6BC-90FE6D7C6B81}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Core", "src\Discord.Net.Core\Discord.Net.Core.csproj", "{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Impls", "Impls", "{288C363D-A636-4EAE-9AC1-4698B641B26E}" @@ -23,6 +15,20 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Commands", "src EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.WebSocket", "src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj", "{688FD1D8-7F01-4539-B2E9-F473C5D699C7}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Providers", "Providers", "{B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012}" +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}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -33,26 +39,14 @@ Global Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Debug|Any CPU.Build.0 = Debug|Any CPU - {496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Debug|x64.ActiveCfg = Debug|Any CPU - {496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Debug|x64.Build.0 = Debug|Any CPU - {496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Debug|x86.ActiveCfg = Debug|Any CPU - {496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Debug|x86.Build.0 = Debug|Any CPU - {496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Release|Any CPU.Build.0 = Debug|Any CPU - {496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Release|x64.ActiveCfg = Debug|Any CPU - {496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Release|x64.Build.0 = Debug|Any CPU - {496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Release|x86.ActiveCfg = Debug|Any CPU - {496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Release|x86.Build.0 = Debug|Any CPU {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}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|Any CPU.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 @@ -63,8 +57,8 @@ Global {BFC6DC28-0351-4573-926A-D4124244C04F}.Debug|x64.Build.0 = Debug|Any CPU {BFC6DC28-0351-4573-926A-D4124244C04F}.Debug|x86.ActiveCfg = Debug|Any CPU {BFC6DC28-0351-4573-926A-D4124244C04F}.Debug|x86.Build.0 = Debug|Any CPU - {BFC6DC28-0351-4573-926A-D4124244C04F}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {BFC6DC28-0351-4573-926A-D4124244C04F}.Release|Any CPU.Build.0 = Debug|Any CPU + {BFC6DC28-0351-4573-926A-D4124244C04F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BFC6DC28-0351-4573-926A-D4124244C04F}.Release|Any CPU.Build.0 = Release|Any CPU {BFC6DC28-0351-4573-926A-D4124244C04F}.Release|x64.ActiveCfg = Debug|Any CPU {BFC6DC28-0351-4573-926A-D4124244C04F}.Release|x64.Build.0 = Debug|Any CPU {BFC6DC28-0351-4573-926A-D4124244C04F}.Release|x86.ActiveCfg = Debug|Any CPU @@ -75,8 +69,8 @@ Global {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Debug|x64.Build.0 = Debug|Any CPU {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Debug|x86.ActiveCfg = Debug|Any CPU {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Debug|x86.Build.0 = Debug|Any CPU - {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|Any CPU.Build.0 = Debug|Any CPU + {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|Any CPU.Build.0 = Release|Any CPU {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|x64.ActiveCfg = Debug|Any CPU {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|x64.Build.0 = Debug|Any CPU {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|x86.ActiveCfg = Debug|Any CPU @@ -87,8 +81,8 @@ Global {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Debug|x64.Build.0 = Debug|Any CPU {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Debug|x86.ActiveCfg = Debug|Any CPU {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Debug|x86.Build.0 = Debug|Any CPU - {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Release|Any CPU.Build.0 = Debug|Any CPU + {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Release|Any CPU.Build.0 = Release|Any CPU {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Release|x64.ActiveCfg = Debug|Any CPU {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Release|x64.Build.0 = Debug|Any CPU {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Release|x86.ActiveCfg = Debug|Any CPU @@ -105,6 +99,66 @@ Global {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 + {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}.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 + {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}.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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -112,6 +166,10 @@ Global GlobalSection(NestedProjects) = preSolution {BFC6DC28-0351-4573-926A-D4124244C04F} = {288C363D-A636-4EAE-9AC1-4698B641B26E} {5688A353-121E-40A1-8BFA-B17B91FB48FB} = {288C363D-A636-4EAE-9AC1-4698B641B26E} + {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} EndGlobalSection EndGlobal diff --git a/README.md b/README.md index 268a6d81d..903fa76c1 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,44 @@ -# Discord.Net v1.0.0-beta2 +# Discord.Net v1.0.0-rc [![MyGet](https://img.shields.io/myget/discord-net/vpre/Discord.Net.svg)](https://www.myget.org/feed/Packages/discord-net) -[![MyGet Build Status](https://www.myget.org/BuildSource/Badge/discord-net?identifier=15bf7c42-22dd-4406-93e5-3cafc62bbc85)](https://www.myget.org/) -[![Discord](https://discordapp.com/api/guilds/81384788765712384/widget.png)](https://discord.gg/0SBTUU1wZTYLhAAW) +[![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) -An unofficial .Net API Wrapper for the Discord client (http://discordapp.com). +An unofficial .NET API Wrapper for the Discord client (http://discordapp.com). Check out the [documentation](https://discord.foxbot.me/docs/) or join the [Discord API Chat](https://discord.gg/0SBTUU1wZTVjAMPx). ## Installation ### Stable (NuGet) -Our stable builds are available from NuGet: +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.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/) +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/) +- [Discord.Net.Providers.WS4Net](https://www.nuget.org/packages/Discord.Net.Providers.WS4Net/) + ### Unstable (MyGet) -Bleeding edge builds are available using our MyGet feed (`https://www.myget.org/F/discord-net/api/v3/index.json`). These builds may break at any time - use with caution. +Nightly builds are available through our MyGet feed (`https://www.myget.org/F/discord-net/api/v3/index.json`). ## Compiling 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) The .NET Core and Docker (Preview) workload is required during Visual Studio installation. ### Using Command Line -- [.Net Core 1.1 SDK](https://www.microsoft.com/net/download/core) +- [.NET Core SDK 1.0 RC3](https://github.com/dotnet/core/blob/master/release-notes/rc3-download.md) ## Known Issues ### WebSockets (Win7 and earlier) -.Net Core 1.1 does not support WebSockets on Win7 and earlier. Track the issue [here](https://github.com/dotnet/corefx/issues/9503). +.NET Core 1.1 does not support WebSockets on Win7 and earlier. It's recommended to use the Discord.Net.Providers.WS4Net package until this is resolved. +Track the issue [here](https://github.com/dotnet/corefx/issues/9503). diff --git a/build.bat b/build.bat deleted file mode 100644 index 95a7c5d9a..000000000 --- a/build.bat +++ /dev/null @@ -1,15 +0,0 @@ -@echo Off -dotnet restore -dotnet pack "src\Discord.Net" -c "%Configuration%" -o "artifacts" --version-suffix "%PrereleaseTag%" -dotnet pack "src\Discord.Net.Core" -c "%Configuration%" -o "artifacts" --version-suffix "%PrereleaseTag%" -dotnet pack "src\Discord.Net.Commands" -c "%Configuration%" -o "artifacts" --version-suffix "%PrereleaseTag%" -dotnet pack "src\Discord.Net.Rest" -c "%Configuration%" -o "artifacts" --version-suffix "%PrereleaseTag%" -dotnet pack "src\Discord.Net.WebSocket" -c "%Configuration%" -o "artifacts" --version-suffix "%PrereleaseTag%" -dotnet pack "src\Discord.Net.Rpc" -c "%Configuration%" -o "artifacts" --version-suffix "%PrereleaseTag%" - -REM dotnet pack "src\Discord.Net\Discord.Net.csproj" -c "%Configuration%" -o "artifacts" --version-suffix "%PrereleaseTag%" -REM dotnet pack "src\Discord.Net.Core\Discord.Net.Core.csproj" -c "%Configuration%" -o "artifacts" --version-suffix "%PrereleaseTag%" -REM dotnet pack "src\Discord.Net.Commands\Discord.Net.Commands.csproj" -c "%Configuration%" -o "artifacts" --version-suffix "%PrereleaseTag%" -REM dotnet pack "src\Discord.Net.Rest\Discord.Net.Rest.csproj" -c "%Configuration%" -o "artifacts" --version-suffix "%PrereleaseTag%" -REM dotnet pack "src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" -c "%Configuration%" -o "artifacts" --version-suffix "%PrereleaseTag%" -REM dotnet pack "src\Discord.Net.Rpc\Discord.Net.Rpc.csproj" -c "%Configuration%" -o "artifacts" --version-suffix "%PrereleaseTag%" \ No newline at end of file diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 000000000..08508bbcf --- /dev/null +++ b/build.ps1 @@ -0,0 +1,4 @@ +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 468ff7bd4..f19e7c297 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -10,6 +10,6 @@ I don't really have any strict conditions for writing documentation, but just ke ### Compiling -Documentation is compiled into a static site using [DocFx](dotnet.github.io/docfx/). You **must** install a version of DocFx that supports .NET Core. The latest build of that is [2.1.0-cli-alpha](https://github.com/dotnet/docfx/releases/tag/v2.1.0-cli-alpha). +Documentation is compiled into a static site using [DocFx](https://dotnet.github.io/docfx/). We currently use version 2.8 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 diff --git a/docs/api/.manifest b/docs/api/.manifest index dbdf6b4c0..0a47304c4 100644 --- a/docs/api/.manifest +++ b/docs/api/.manifest @@ -1 +1 @@ -{"Discord.Commands":"Discord.Commands.yml","Discord.Commands.Command":"Discord.Commands.Command.yml","Discord.Commands.Command.Name":"Discord.Commands.Command.yml","Discord.Commands.Command.Description":"Discord.Commands.Command.yml","Discord.Commands.Command.Synopsis":"Discord.Commands.Command.yml","Discord.Commands.Command.Text":"Discord.Commands.Command.yml","Discord.Commands.Command.Module":"Discord.Commands.Command.yml","Discord.Commands.Command.Parameters":"Discord.Commands.Command.yml","Discord.Commands.Command.Parse(Discord.IMessage,Discord.Commands.SearchResult)":"Discord.Commands.Command.yml","Discord.Commands.Command.Execute(Discord.IMessage,Discord.Commands.ParseResult)":"Discord.Commands.Command.yml","Discord.Commands.Command.ToString":"Discord.Commands.Command.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.CastFailed":"Discord.Commands.CommandError.yml","Discord.Commands.CommandError.ObjectNotFound":"Discord.Commands.CommandError.yml","Discord.Commands.CommandError.MultipleMatches":"Discord.Commands.CommandError.yml","Discord.Commands.CommandError.Exception":"Discord.Commands.CommandError.yml","Discord.Commands.CommandParameter":"Discord.Commands.CommandParameter.yml","Discord.Commands.CommandParameter.Name":"Discord.Commands.CommandParameter.yml","Discord.Commands.CommandParameter.Description":"Discord.Commands.CommandParameter.yml","Discord.Commands.CommandParameter.IsOptional":"Discord.Commands.CommandParameter.yml","Discord.Commands.CommandParameter.IsRemainder":"Discord.Commands.CommandParameter.yml","Discord.Commands.CommandParameter.IsMultiple":"Discord.Commands.CommandParameter.yml","Discord.Commands.CommandParameter.Type":"Discord.Commands.CommandParameter.yml","Discord.Commands.CommandParameter.#ctor(System.String,System.String,System.Type,Discord.Commands.TypeReader,System.Boolean,System.Boolean,System.Boolean,System.Object)":"Discord.Commands.CommandParameter.yml","Discord.Commands.CommandParameter.Parse(Discord.IMessage,System.String)":"Discord.Commands.CommandParameter.yml","Discord.Commands.CommandParameter.ToString":"Discord.Commands.CommandParameter.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.#ctor":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.AddTypeReader``1(Discord.Commands.TypeReader)":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.AddTypeReader(System.Type,Discord.Commands.TypeReader)":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.Load(System.Object)":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.LoadAssembly(System.Reflection.Assembly,Discord.Commands.IDependencyMap)":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.Unload(Discord.Commands.Module)":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.Unload(System.Object)":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.Search(Discord.IMessage,System.Int32)":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.Search(Discord.IMessage,System.String)":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.Execute(Discord.IMessage,System.Int32)":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.Execute(Discord.IMessage,System.String)":"Discord.Commands.CommandService.yml","Discord.Commands.Module":"Discord.Commands.Module.yml","Discord.Commands.Module.Service":"Discord.Commands.Module.yml","Discord.Commands.Module.Name":"Discord.Commands.Module.yml","Discord.Commands.Module.Commands":"Discord.Commands.Module.yml","Discord.Commands.Module.ToString":"Discord.Commands.Module.yml","Discord.Commands.CommandAttribute":"Discord.Commands.CommandAttribute.yml","Discord.Commands.CommandAttribute.Text":"Discord.Commands.CommandAttribute.yml","Discord.Commands.CommandAttribute.#ctor":"Discord.Commands.CommandAttribute.yml","Discord.Commands.CommandAttribute.#ctor(System.String)":"Discord.Commands.CommandAttribute.yml","Discord.Commands.DescriptionAttribute":"Discord.Commands.DescriptionAttribute.yml","Discord.Commands.DescriptionAttribute.Text":"Discord.Commands.DescriptionAttribute.yml","Discord.Commands.DescriptionAttribute.#ctor(System.String)":"Discord.Commands.DescriptionAttribute.yml","Discord.Commands.SynopsisAttribute":"Discord.Commands.SynopsisAttribute.yml","Discord.Commands.SynopsisAttribute.Text":"Discord.Commands.SynopsisAttribute.yml","Discord.Commands.SynopsisAttribute.#ctor(System.String)":"Discord.Commands.SynopsisAttribute.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.ModuleAttribute":"Discord.Commands.ModuleAttribute.yml","Discord.Commands.ModuleAttribute.Prefix":"Discord.Commands.ModuleAttribute.yml","Discord.Commands.ModuleAttribute.AutoLoad":"Discord.Commands.ModuleAttribute.yml","Discord.Commands.ModuleAttribute.#ctor":"Discord.Commands.ModuleAttribute.yml","Discord.Commands.ModuleAttribute.#ctor(System.String)":"Discord.Commands.ModuleAttribute.yml","Discord.Commands.RemainderAttribute":"Discord.Commands.RemainderAttribute.yml","Discord.Commands.DependencyMap":"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(System.Type)":"Discord.Commands.DependencyMap.yml","Discord.Commands.DependencyMap.TryGet``1(``0@)":"Discord.Commands.DependencyMap.yml","Discord.Commands.DependencyMap.TryGet(System.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(System.Type)":"Discord.Commands.IDependencyMap.yml","Discord.Commands.IDependencyMap.TryGet(System.Type,System.Object@)":"Discord.Commands.IDependencyMap.yml","Discord.Commands.MessageExtensions":"Discord.Commands.MessageExtensions.yml","Discord.Commands.MessageExtensions.HasCharPrefix(Discord.IMessage,System.Char,System.Int32@)":"Discord.Commands.MessageExtensions.yml","Discord.Commands.MessageExtensions.HasStringPrefix(Discord.IMessage,System.String,System.Int32@)":"Discord.Commands.MessageExtensions.yml","Discord.Commands.MessageExtensions.HasMentionPrefix(Discord.IMessage,Discord.IUser,System.Int32@)":"Discord.Commands.MessageExtensions.yml","Discord.Commands.TypeReader":"Discord.Commands.TypeReader.yml","Discord.Commands.TypeReader.Read(Discord.IMessage,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.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.Values":"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.ToString":"Discord.Commands.ParseResult.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.ToString":"Discord.Commands.SearchResult.yml","Discord.Commands.TypeReaderResult":"Discord.Commands.TypeReaderResult.yml","Discord.Commands.TypeReaderResult.Value":"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.FromError(Discord.Commands.CommandError,System.String)":"Discord.Commands.TypeReaderResult.yml","Discord.Commands.TypeReaderResult.ToString":"Discord.Commands.TypeReaderResult.yml","Discord":"Discord.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.MaxMessageSize":"Discord.DiscordConfig.yml","Discord.DiscordConfig.MaxMessagesPerBatch":"Discord.DiscordConfig.yml","Discord.DiscordConfig.MaxUsersPerBatch":"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.IDiscordClient":"Discord.IDiscordClient.yml","Discord.IDiscordClient.ConnectionState":"Discord.IDiscordClient.yml","Discord.IDiscordClient.ApiClient":"Discord.IDiscordClient.yml","Discord.IDiscordClient.LogManager":"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.IDiscordClient.yml","Discord.IDiscordClient.GetPrivateChannelsAsync":"Discord.IDiscordClient.yml","Discord.IDiscordClient.GetConnectionsAsync":"Discord.IDiscordClient.yml","Discord.IDiscordClient.GetGuildAsync(System.UInt64)":"Discord.IDiscordClient.yml","Discord.IDiscordClient.GetGuildsAsync":"Discord.IDiscordClient.yml","Discord.IDiscordClient.GetGuildSummariesAsync":"Discord.IDiscordClient.yml","Discord.IDiscordClient.CreateGuildAsync(System.String,Discord.IVoiceRegion,System.IO.Stream)":"Discord.IDiscordClient.yml","Discord.IDiscordClient.GetInviteAsync(System.String)":"Discord.IDiscordClient.yml","Discord.IDiscordClient.GetUserAsync(System.UInt64)":"Discord.IDiscordClient.yml","Discord.IDiscordClient.GetUserAsync(System.String,System.String)":"Discord.IDiscordClient.yml","Discord.IDiscordClient.GetCurrentUserAsync":"Discord.IDiscordClient.yml","Discord.IDiscordClient.QueryUsersAsync(System.String,System.Int32)":"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.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.RequestOptions":"Discord.RequestOptions.yml","Discord.RequestOptions.Default":"Discord.RequestOptions.yml","Discord.RequestOptions.Timeout":"Discord.RequestOptions.yml","Discord.RequestOptions.#ctor":"Discord.RequestOptions.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.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.IDeletable.yml","Discord.IEntity`1":"Discord.IEntity`1.yml","Discord.IEntity`1.Id":"Discord.IEntity`1.yml","Discord.IEntity`1.IsAttached":"Discord.IEntity`1.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.IUpdateable.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.IChannel":"Discord.IChannel.yml","Discord.IChannel.GetUsersAsync":"Discord.IChannel.yml","Discord.IChannel.GetUserAsync(System.UInt64)":"Discord.IChannel.yml","Discord.IDMChannel":"Discord.IDMChannel.yml","Discord.IDMChannel.Recipient":"Discord.IDMChannel.yml","Discord.IDMChannel.CloseAsync":"Discord.IDMChannel.yml","Discord.IGroupChannel":"Discord.IGroupChannel.yml","Discord.IGroupChannel.AddUserAsync(Discord.IUser)":"Discord.IGroupChannel.yml","Discord.IGroupChannel.LeaveAsync":"Discord.IGroupChannel.yml","Discord.IGuildChannel":"Discord.IGuildChannel.yml","Discord.IGuildChannel.Name":"Discord.IGuildChannel.yml","Discord.IGuildChannel.Position":"Discord.IGuildChannel.yml","Discord.IGuildChannel.Guild":"Discord.IGuildChannel.yml","Discord.IGuildChannel.CreateInviteAsync(System.Nullable{System.Int32},System.Nullable{System.Int32},System.Boolean)":"Discord.IGuildChannel.yml","Discord.IGuildChannel.GetInvitesAsync":"Discord.IGuildChannel.yml","Discord.IGuildChannel.PermissionOverwrites":"Discord.IGuildChannel.yml","Discord.IGuildChannel.ModifyAsync(System.Action{Discord.API.Rest.ModifyGuildChannelParams})":"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.IGuildChannel.yml","Discord.IGuildChannel.RemovePermissionOverwriteAsync(Discord.IUser)":"Discord.IGuildChannel.yml","Discord.IGuildChannel.AddPermissionOverwriteAsync(Discord.IRole,Discord.OverwritePermissions)":"Discord.IGuildChannel.yml","Discord.IGuildChannel.AddPermissionOverwriteAsync(Discord.IUser,Discord.OverwritePermissions)":"Discord.IGuildChannel.yml","Discord.IGuildChannel.GetUsersAsync":"Discord.IGuildChannel.yml","Discord.IGuildChannel.GetUserAsync(System.UInt64)":"Discord.IGuildChannel.yml","Discord.IMessageChannel":"Discord.IMessageChannel.yml","Discord.IMessageChannel.CachedMessages":"Discord.IMessageChannel.yml","Discord.IMessageChannel.SendMessageAsync(System.String,System.Boolean)":"Discord.IMessageChannel.yml","Discord.IMessageChannel.SendFileAsync(System.String,System.String,System.Boolean)":"Discord.IMessageChannel.yml","Discord.IMessageChannel.SendFileAsync(System.IO.Stream,System.String,System.String,System.Boolean)":"Discord.IMessageChannel.yml","Discord.IMessageChannel.GetMessageAsync(System.UInt64)":"Discord.IMessageChannel.yml","Discord.IMessageChannel.GetCachedMessage(System.UInt64)":"Discord.IMessageChannel.yml","Discord.IMessageChannel.GetMessagesAsync(System.Int32)":"Discord.IMessageChannel.yml","Discord.IMessageChannel.GetMessagesAsync(System.UInt64,Discord.Direction,System.Int32)":"Discord.IMessageChannel.yml","Discord.IMessageChannel.GetPinnedMessagesAsync":"Discord.IMessageChannel.yml","Discord.IMessageChannel.DeleteMessagesAsync(System.Collections.Generic.IEnumerable{Discord.IMessage})":"Discord.IMessageChannel.yml","Discord.IMessageChannel.TriggerTypingAsync":"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(System.Action{Discord.API.Rest.ModifyTextChannelParams})":"Discord.ITextChannel.yml","Discord.IVoiceChannel":"Discord.IVoiceChannel.yml","Discord.IVoiceChannel.Bitrate":"Discord.IVoiceChannel.yml","Discord.IVoiceChannel.UserLimit":"Discord.IVoiceChannel.yml","Discord.IVoiceChannel.ModifyAsync(System.Action{Discord.API.Rest.ModifyVoiceChannelParams})":"Discord.IVoiceChannel.yml","Discord.IVoiceChannel.ConnectAsync":"Discord.IVoiceChannel.yml","Discord.DefaultMessageNotifications":"Discord.DefaultMessageNotifications.yml","Discord.DefaultMessageNotifications.MentionsOnly":"Discord.DefaultMessageNotifications.yml","Discord.DefaultMessageNotifications.AllMessages":"Discord.DefaultMessageNotifications.yml","Discord.Emoji":"Discord.Emoji.yml","Discord.Emoji.Id":"Discord.Emoji.yml","Discord.Emoji.Name":"Discord.Emoji.yml","Discord.Emoji.IsManaged":"Discord.Emoji.yml","Discord.Emoji.RequireColons":"Discord.Emoji.yml","Discord.Emoji.RoleIds":"Discord.Emoji.yml","Discord.Emoji.#ctor(Discord.API.Emoji)":"Discord.Emoji.yml","Discord.GuildEmbed":"Discord.GuildEmbed.yml","Discord.GuildEmbed.IsEnabled":"Discord.GuildEmbed.yml","Discord.GuildEmbed.ChannelId":"Discord.GuildEmbed.yml","Discord.GuildEmbed.#ctor(System.Boolean,System.Nullable{System.UInt64})":"Discord.GuildEmbed.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.IconUrl":"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(System.Action{Discord.API.Rest.ModifyGuildParams})":"Discord.IGuild.yml","Discord.IGuild.ModifyEmbedAsync(System.Action{Discord.API.Rest.ModifyGuildEmbedParams})":"Discord.IGuild.yml","Discord.IGuild.ModifyChannelsAsync(System.Collections.Generic.IEnumerable{Discord.API.Rest.ModifyGuildChannelsParams})":"Discord.IGuild.yml","Discord.IGuild.ModifyRolesAsync(System.Collections.Generic.IEnumerable{Discord.API.Rest.ModifyGuildRolesParams})":"Discord.IGuild.yml","Discord.IGuild.LeaveAsync":"Discord.IGuild.yml","Discord.IGuild.GetBansAsync":"Discord.IGuild.yml","Discord.IGuild.AddBanAsync(Discord.IUser,System.Int32)":"Discord.IGuild.yml","Discord.IGuild.AddBanAsync(System.UInt64,System.Int32)":"Discord.IGuild.yml","Discord.IGuild.RemoveBanAsync(Discord.IUser)":"Discord.IGuild.yml","Discord.IGuild.RemoveBanAsync(System.UInt64)":"Discord.IGuild.yml","Discord.IGuild.GetChannelsAsync":"Discord.IGuild.yml","Discord.IGuild.GetChannelAsync(System.UInt64)":"Discord.IGuild.yml","Discord.IGuild.CreateTextChannelAsync(System.String)":"Discord.IGuild.yml","Discord.IGuild.CreateVoiceChannelAsync(System.String)":"Discord.IGuild.yml","Discord.IGuild.GetInvitesAsync":"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.IGuild.yml","Discord.IGuild.GetUsersAsync":"Discord.IGuild.yml","Discord.IGuild.GetUserAsync(System.UInt64)":"Discord.IGuild.yml","Discord.IGuild.GetCurrentUserAsync":"Discord.IGuild.yml","Discord.IGuild.DownloadUsersAsync":"Discord.IGuild.yml","Discord.IGuild.PruneUsersAsync(System.Int32,System.Boolean)":"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.User":"Discord.IGuildIntegration.yml","Discord.IGuildIntegration.Role":"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.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.ChannelId":"Discord.IInvite.yml","Discord.IInvite.GuildId":"Discord.IInvite.yml","Discord.IInvite.AcceptAsync":"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.Direction":"Discord.Direction.yml","Discord.Direction.Before":"Discord.Direction.yml","Discord.Direction.After":"Discord.Direction.yml","Discord.Direction.Around":"Discord.Direction.yml","Discord.EmbedProvider":"Discord.EmbedProvider.yml","Discord.EmbedProvider.Name":"Discord.EmbedProvider.yml","Discord.EmbedProvider.Url":"Discord.EmbedProvider.yml","Discord.EmbedProvider.#ctor(System.String,System.String)":"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.#ctor(System.String,System.String,System.Nullable{System.Int32},System.Nullable{System.Int32})":"Discord.EmbedThumbnail.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.Provider":"Discord.IEmbed.yml","Discord.IEmbed.Thumbnail":"Discord.IEmbed.yml","Discord.IMessage":"Discord.IMessage.yml","Discord.IMessage.EditedTimestamp":"Discord.IMessage.yml","Discord.IMessage.IsTTS":"Discord.IMessage.yml","Discord.IMessage.IsPinned":"Discord.IMessage.yml","Discord.IMessage.Content":"Discord.IMessage.yml","Discord.IMessage.Timestamp":"Discord.IMessage.yml","Discord.IMessage.Type":"Discord.IMessage.yml","Discord.IMessage.Channel":"Discord.IMessage.yml","Discord.IMessage.Author":"Discord.IMessage.yml","Discord.IMessage.Attachments":"Discord.IMessage.yml","Discord.IMessage.Embeds":"Discord.IMessage.yml","Discord.IMessage.MentionedChannelIds":"Discord.IMessage.yml","Discord.IMessage.MentionedRoles":"Discord.IMessage.yml","Discord.IMessage.MentionedUsers":"Discord.IMessage.yml","Discord.IMessage.ModifyAsync(System.Action{Discord.API.Rest.ModifyMessageParams})":"Discord.IMessage.yml","Discord.IMessage.PinAsync":"Discord.IMessage.yml","Discord.IMessage.UnpinAsync":"Discord.IMessage.yml","Discord.IMessage.Resolve(System.Int32,System.Int32,Discord.UserResolveMode)":"Discord.IMessage.yml","Discord.IMessage.Resolve(Discord.UserResolveMode)":"Discord.IMessage.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.UserResolveMode":"Discord.UserResolveMode.yml","Discord.UserResolveMode.NameOnly":"Discord.UserResolveMode.yml","Discord.UserResolveMode.NameAndDiscriminator":"Discord.UserResolveMode.yml","Discord.ChannelPermission":"Discord.ChannelPermission.yml","Discord.ChannelPermission.CreateInstantInvite":"Discord.ChannelPermission.yml","Discord.ChannelPermission.ManageChannel":"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.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.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.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.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.#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)":"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.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.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.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.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.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.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.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.#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.Nullable{System.Boolean},System.Nullable{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})":"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.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.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.#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.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})":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.ToAllowList":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.ToDenyList":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.ToString":"Discord.OverwritePermissions.yml","Discord.PermissionTarget":"Discord.PermissionTarget.yml","Discord.PermissionTarget.Role":"Discord.PermissionTarget.yml","Discord.PermissionTarget.User":"Discord.PermissionTarget.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.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.Color":"Discord.IRole.yml","Discord.IRole.IsHoisted":"Discord.IRole.yml","Discord.IRole.IsManaged":"Discord.IRole.yml","Discord.IRole.Name":"Discord.IRole.yml","Discord.IRole.Permissions":"Discord.IRole.yml","Discord.IRole.Position":"Discord.IRole.yml","Discord.IRole.GuildId":"Discord.IRole.yml","Discord.IRole.ModifyAsync(System.Action{Discord.API.Rest.ModifyGuildRoleParams})":"Discord.IRole.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.#ctor(System.String)":"Discord.Game.yml","Discord.Game.ToString":"Discord.Game.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.IGroupUser.KickAsync":"Discord.IGroupUser.yml","Discord.IGroupUser.CreateDMChannelAsync":"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.Roles":"Discord.IGuildUser.yml","Discord.IGuildUser.GetPermissions(Discord.IGuildChannel)":"Discord.IGuildUser.yml","Discord.IGuildUser.KickAsync":"Discord.IGuildUser.yml","Discord.IGuildUser.ModifyAsync(System.Action{Discord.API.Rest.ModifyGuildMemberParams})":"Discord.IGuildUser.yml","Discord.IGuildUser.CreateDMChannelAsync":"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(System.Action{Discord.API.Rest.ModifyCurrentUserParams})":"Discord.ISelfUser.yml","Discord.ISelfUser.ModifyStatusAsync(System.Action{Discord.API.Rest.ModifyPresenceParams})":"Discord.ISelfUser.yml","Discord.IUser":"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.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.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.Offline":"Discord.UserStatus.yml","Discord.DiscordClientExtensions":"Discord.DiscordClientExtensions.yml","Discord.DiscordClientExtensions.GetOptimalVoiceRegionAsync(Discord.Rest.DiscordRestClient)":"Discord.DiscordClientExtensions.yml","Discord.GuildExtensions":"Discord.GuildExtensions.yml","Discord.GuildExtensions.GetTextChannelAsync(Discord.IGuild,System.UInt64)":"Discord.GuildExtensions.yml","Discord.GuildExtensions.GetVoiceChannelAsync(Discord.IGuild,System.UInt64)":"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,System.Collections.Generic.IEnumerable{Discord.IRole})":"Discord.GuildUserExtensions.yml","Discord.GuildUserExtensions.RemoveRolesAsync(Discord.IGuildUser,Discord.IRole[])":"Discord.GuildUserExtensions.yml","Discord.GuildUserExtensions.RemoveRolesAsync(Discord.IGuildUser,System.Collections.Generic.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,System.Exception)":"Discord.LogMessage.yml","Discord.LogMessage.ToString":"Discord.LogMessage.yml","Discord.LogMessage.ToString(System.Text.StringBuilder,System.Boolean,System.Boolean,System.DateTimeKind,System.Nullable{System.Int32})":"Discord.LogMessage.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.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.DiscordSocketConfig":"Discord.DiscordSocketConfig.yml","Discord.DiscordSocketConfig.GatewayEncoding":"Discord.DiscordSocketConfig.yml","Discord.DiscordSocketConfig.ShardId":"Discord.DiscordSocketConfig.yml","Discord.DiscordSocketConfig.TotalShards":"Discord.DiscordSocketConfig.yml","Discord.DiscordSocketConfig.MessageCacheSize":"Discord.DiscordSocketConfig.yml","Discord.DiscordSocketConfig.LargeThreshold":"Discord.DiscordSocketConfig.yml","Discord.DiscordSocketConfig.AudioMode":"Discord.DiscordSocketConfig.yml","Discord.DiscordSocketConfig.WebSocketProvider":"Discord.DiscordSocketConfig.yml","Discord.Audio":"Discord.Audio.yml","Discord.Audio.DiscordVoiceAPIClient":"Discord.Audio.DiscordVoiceAPIClient.yml","Discord.Audio.DiscordVoiceAPIClient.MaxBitrate":"Discord.Audio.DiscordVoiceAPIClient.yml","Discord.Audio.DiscordVoiceAPIClient.Mode":"Discord.Audio.DiscordVoiceAPIClient.yml","Discord.Audio.DiscordVoiceAPIClient.SentRequest":"Discord.Audio.DiscordVoiceAPIClient.yml","Discord.Audio.DiscordVoiceAPIClient.SentGatewayMessage":"Discord.Audio.DiscordVoiceAPIClient.yml","Discord.Audio.DiscordVoiceAPIClient.SentDiscovery":"Discord.Audio.DiscordVoiceAPIClient.yml","Discord.Audio.DiscordVoiceAPIClient.SentData":"Discord.Audio.DiscordVoiceAPIClient.yml","Discord.Audio.DiscordVoiceAPIClient.ReceivedEvent":"Discord.Audio.DiscordVoiceAPIClient.yml","Discord.Audio.DiscordVoiceAPIClient.ReceivedPacket":"Discord.Audio.DiscordVoiceAPIClient.yml","Discord.Audio.DiscordVoiceAPIClient.Disconnected":"Discord.Audio.DiscordVoiceAPIClient.yml","Discord.Audio.DiscordVoiceAPIClient.GuildId":"Discord.Audio.DiscordVoiceAPIClient.yml","Discord.Audio.DiscordVoiceAPIClient.ConnectionState":"Discord.Audio.DiscordVoiceAPIClient.yml","Discord.Audio.DiscordVoiceAPIClient.Dispose":"Discord.Audio.DiscordVoiceAPIClient.yml","Discord.Audio.DiscordVoiceAPIClient.SendAsync(Discord.API.Voice.VoiceOpCode,System.Object,Discord.RequestOptions)":"Discord.Audio.DiscordVoiceAPIClient.yml","Discord.Audio.DiscordVoiceAPIClient.SendAsync(System.Byte[],System.Int32)":"Discord.Audio.DiscordVoiceAPIClient.yml","Discord.Audio.DiscordVoiceAPIClient.SendHeartbeatAsync(Discord.RequestOptions)":"Discord.Audio.DiscordVoiceAPIClient.yml","Discord.Audio.DiscordVoiceAPIClient.SendIdentityAsync(System.UInt64,System.String,System.String)":"Discord.Audio.DiscordVoiceAPIClient.yml","Discord.Audio.DiscordVoiceAPIClient.SendSelectProtocol(System.String,System.Int32)":"Discord.Audio.DiscordVoiceAPIClient.yml","Discord.Audio.DiscordVoiceAPIClient.SendSetSpeaking(System.Boolean)":"Discord.Audio.DiscordVoiceAPIClient.yml","Discord.Audio.DiscordVoiceAPIClient.ConnectAsync(System.String)":"Discord.Audio.DiscordVoiceAPIClient.yml","Discord.Audio.DiscordVoiceAPIClient.DisconnectAsync":"Discord.Audio.DiscordVoiceAPIClient.yml","Discord.Audio.DiscordVoiceAPIClient.SendDiscoveryAsync(System.UInt32)":"Discord.Audio.DiscordVoiceAPIClient.yml","Discord.Audio.DiscordVoiceAPIClient.SetUdpEndpoint(System.Net.IPEndPoint)":"Discord.Audio.DiscordVoiceAPIClient.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.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.ApiClient":"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.CreatePCMStream(System.Int32,System.Nullable{System.Int32},Discord.Audio.OpusApplication,System.Int32)":"Discord.Audio.IAudioClient.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.OpusDecodeStream":"Discord.Audio.OpusDecodeStream.yml","Discord.Audio.OpusDecodeStream.Read(System.Byte[],System.Int32,System.Int32)":"Discord.Audio.OpusDecodeStream.yml","Discord.Audio.OpusDecodeStream.Dispose(System.Boolean)":"Discord.Audio.OpusDecodeStream.yml","Discord.Audio.OpusEncodeStream":"Discord.Audio.OpusEncodeStream.yml","Discord.Audio.OpusEncodeStream.SampleRate":"Discord.Audio.OpusEncodeStream.yml","Discord.Audio.OpusEncodeStream.Channels":"Discord.Audio.OpusEncodeStream.yml","Discord.Audio.OpusEncodeStream.Write(System.Byte[],System.Int32,System.Int32)":"Discord.Audio.OpusEncodeStream.yml","Discord.Audio.OpusEncodeStream.Dispose(System.Boolean)":"Discord.Audio.OpusEncodeStream.yml","Discord.Audio.RTPReadStream":"Discord.Audio.RTPReadStream.yml","Discord.Audio.RTPReadStream.CanRead":"Discord.Audio.RTPReadStream.yml","Discord.Audio.RTPReadStream.CanSeek":"Discord.Audio.RTPReadStream.yml","Discord.Audio.RTPReadStream.CanWrite":"Discord.Audio.RTPReadStream.yml","Discord.Audio.RTPReadStream.Read(System.Byte[],System.Int32,System.Int32)":"Discord.Audio.RTPReadStream.yml","Discord.Audio.RTPReadStream.Write(System.Byte[],System.Int32,System.Int32)":"Discord.Audio.RTPReadStream.yml","Discord.Audio.RTPReadStream.Flush":"Discord.Audio.RTPReadStream.yml","Discord.Audio.RTPReadStream.Length":"Discord.Audio.RTPReadStream.yml","Discord.Audio.RTPReadStream.Position":"Discord.Audio.RTPReadStream.yml","Discord.Audio.RTPReadStream.SetLength(System.Int64)":"Discord.Audio.RTPReadStream.yml","Discord.Audio.RTPReadStream.Seek(System.Int64,System.IO.SeekOrigin)":"Discord.Audio.RTPReadStream.yml","Discord.Audio.RTPWriteStream":"Discord.Audio.RTPWriteStream.yml","Discord.Audio.RTPWriteStream._buffer":"Discord.Audio.RTPWriteStream.yml","Discord.Audio.RTPWriteStream.CanRead":"Discord.Audio.RTPWriteStream.yml","Discord.Audio.RTPWriteStream.CanSeek":"Discord.Audio.RTPWriteStream.yml","Discord.Audio.RTPWriteStream.CanWrite":"Discord.Audio.RTPWriteStream.yml","Discord.Audio.RTPWriteStream.Write(System.Byte[],System.Int32,System.Int32)":"Discord.Audio.RTPWriteStream.yml","Discord.Audio.RTPWriteStream.Flush":"Discord.Audio.RTPWriteStream.yml","Discord.Audio.RTPWriteStream.Length":"Discord.Audio.RTPWriteStream.yml","Discord.Audio.RTPWriteStream.Position":"Discord.Audio.RTPWriteStream.yml","Discord.Audio.RTPWriteStream.Read(System.Byte[],System.Int32,System.Int32)":"Discord.Audio.RTPWriteStream.yml","Discord.Audio.RTPWriteStream.SetLength(System.Int64)":"Discord.Audio.RTPWriteStream.yml","Discord.Audio.RTPWriteStream.Seek(System.Int64,System.IO.SeekOrigin)":"Discord.Audio.RTPWriteStream.yml","Discord.Logging":"Discord.Logging.yml","Discord.Logging.ILogger":"Discord.Logging.ILogger.yml","Discord.Logging.ILogger.Level":"Discord.Logging.ILogger.yml","Discord.Logging.ILogger.LogAsync(Discord.LogSeverity,System.String,System.Exception)":"Discord.Logging.ILogger.yml","Discord.Logging.ILogger.LogAsync(Discord.LogSeverity,System.FormattableString,System.Exception)":"Discord.Logging.ILogger.yml","Discord.Logging.ILogger.LogAsync(Discord.LogSeverity,System.Exception)":"Discord.Logging.ILogger.yml","Discord.Logging.ILogger.ErrorAsync(System.String,System.Exception)":"Discord.Logging.ILogger.yml","Discord.Logging.ILogger.ErrorAsync(System.FormattableString,System.Exception)":"Discord.Logging.ILogger.yml","Discord.Logging.ILogger.ErrorAsync(System.Exception)":"Discord.Logging.ILogger.yml","Discord.Logging.ILogger.WarningAsync(System.String,System.Exception)":"Discord.Logging.ILogger.yml","Discord.Logging.ILogger.WarningAsync(System.FormattableString,System.Exception)":"Discord.Logging.ILogger.yml","Discord.Logging.ILogger.WarningAsync(System.Exception)":"Discord.Logging.ILogger.yml","Discord.Logging.ILogger.InfoAsync(System.String,System.Exception)":"Discord.Logging.ILogger.yml","Discord.Logging.ILogger.InfoAsync(System.FormattableString,System.Exception)":"Discord.Logging.ILogger.yml","Discord.Logging.ILogger.InfoAsync(System.Exception)":"Discord.Logging.ILogger.yml","Discord.Logging.ILogger.VerboseAsync(System.String,System.Exception)":"Discord.Logging.ILogger.yml","Discord.Logging.ILogger.VerboseAsync(System.FormattableString,System.Exception)":"Discord.Logging.ILogger.yml","Discord.Logging.ILogger.VerboseAsync(System.Exception)":"Discord.Logging.ILogger.yml","Discord.Logging.ILogger.DebugAsync(System.String,System.Exception)":"Discord.Logging.ILogger.yml","Discord.Logging.ILogger.DebugAsync(System.FormattableString,System.Exception)":"Discord.Logging.ILogger.yml","Discord.Logging.ILogger.DebugAsync(System.Exception)":"Discord.Logging.ILogger.yml","Discord.Logging.ILogManager":"Discord.Logging.ILogManager.yml","Discord.Logging.ILogManager.Level":"Discord.Logging.ILogManager.yml","Discord.Logging.ILogManager.LogAsync(Discord.LogSeverity,System.String,System.String,System.Exception)":"Discord.Logging.ILogManager.yml","Discord.Logging.ILogManager.LogAsync(Discord.LogSeverity,System.String,System.FormattableString,System.Exception)":"Discord.Logging.ILogManager.yml","Discord.Logging.ILogManager.LogAsync(Discord.LogSeverity,System.String,System.Exception)":"Discord.Logging.ILogManager.yml","Discord.Logging.ILogManager.ErrorAsync(System.String,System.String,System.Exception)":"Discord.Logging.ILogManager.yml","Discord.Logging.ILogManager.ErrorAsync(System.String,System.FormattableString,System.Exception)":"Discord.Logging.ILogManager.yml","Discord.Logging.ILogManager.ErrorAsync(System.String,System.Exception)":"Discord.Logging.ILogManager.yml","Discord.Logging.ILogManager.WarningAsync(System.String,System.String,System.Exception)":"Discord.Logging.ILogManager.yml","Discord.Logging.ILogManager.WarningAsync(System.String,System.FormattableString,System.Exception)":"Discord.Logging.ILogManager.yml","Discord.Logging.ILogManager.WarningAsync(System.String,System.Exception)":"Discord.Logging.ILogManager.yml","Discord.Logging.ILogManager.InfoAsync(System.String,System.String,System.Exception)":"Discord.Logging.ILogManager.yml","Discord.Logging.ILogManager.InfoAsync(System.String,System.FormattableString,System.Exception)":"Discord.Logging.ILogManager.yml","Discord.Logging.ILogManager.InfoAsync(System.String,System.Exception)":"Discord.Logging.ILogManager.yml","Discord.Logging.ILogManager.VerboseAsync(System.String,System.String,System.Exception)":"Discord.Logging.ILogManager.yml","Discord.Logging.ILogManager.VerboseAsync(System.String,System.FormattableString,System.Exception)":"Discord.Logging.ILogManager.yml","Discord.Logging.ILogManager.VerboseAsync(System.String,System.Exception)":"Discord.Logging.ILogManager.yml","Discord.Logging.ILogManager.DebugAsync(System.String,System.String,System.Exception)":"Discord.Logging.ILogManager.yml","Discord.Logging.ILogManager.DebugAsync(System.String,System.FormattableString,System.Exception)":"Discord.Logging.ILogManager.yml","Discord.Logging.ILogManager.DebugAsync(System.String,System.Exception)":"Discord.Logging.ILogManager.yml","Discord.Logging.ILogManager.CreateLogger(System.String)":"Discord.Logging.ILogManager.yml","Discord.Rest":"Discord.Rest.yml","Discord.Rest.DiscordRestClient":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.Log":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.LoggedIn":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.LoggedOut":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.ApiClient":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.LoginState":"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.LoginAsync(Discord.TokenType,System.String,System.Boolean)":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.ValidateTokenAsync(Discord.TokenType,System.String)":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.OnLoginAsync(Discord.TokenType,System.String)":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.LogoutAsync":"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,Discord.IVoiceRegion,System.IO.Stream)":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.GetUserAsync(System.UInt64)":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.GetUserAsync(System.String,System.String)":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.GetCurrentUserAsync":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.QueryUsersAsync(System.String,System.Int32)":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.GetVoiceRegionsAsync":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.GetVoiceRegionAsync(System.String)":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.Dispose":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.Discord#IDiscordClient#ConnectionState":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.Discord#IDiscordClient#LogManager":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.Discord#IDiscordClient#ConnectAsync":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.Discord#IDiscordClient#DisconnectAsync":"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.Rpc":"Discord.Rpc.yml","Discord.Rpc.DiscordRpcClient":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.ConnectionState":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.ApiClient":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.#ctor(System.String,System.String)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.#ctor(Discord.Rpc.DiscordRpcConfig)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.ValidateTokenAsync(Discord.TokenType,System.String)":"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)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.SubscribeGuild(System.UInt64,Discord.Rpc.RpcChannelEvent[])":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.UnsubscribeGuild(System.UInt64,Discord.Rpc.RpcChannelEvent[])":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.SubscribeChannel(System.UInt64,Discord.Rpc.RpcChannelEvent[])":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.UnsubscribeChannel(System.UInt64,Discord.Rpc.RpcChannelEvent[])":"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.GuildUpdated":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.VoiceStateUpdated":"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.#ctor(System.String,System.String)":"Discord.Rpc.DiscordRpcConfig.yml","Discord.Rpc.DiscordRpcConfig.ClientId":"Discord.Rpc.DiscordRpcConfig.yml","Discord.Rpc.DiscordRpcConfig.Origin":"Discord.Rpc.DiscordRpcConfig.yml","Discord.Rpc.DiscordRpcConfig.WebSocketProvider":"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.RpcGuildEvent":"Discord.Rpc.RpcGuildEvent.yml","Discord.Rpc.RpcGuildEvent.GuildStatus":"Discord.Rpc.RpcGuildEvent.yml","Discord.WebSocket":"Discord.WebSocket.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.ApiClient":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.#ctor":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.#ctor(Discord.DiscordSocketConfig)":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.OnLoginAsync(Discord.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.GetVoiceRegionAsync(System.String)":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GetGuildAsync(System.UInt64)":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GetGuildEmbedAsync(System.UInt64)":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GetGuildSummariesAsync":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GetGuildsAsync":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GetChannelAsync(System.UInt64)":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GetPrivateChannelsAsync":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GetUserAsync(System.UInt64)":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GetUserAsync(System.String,System.String)":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GetCurrentUserAsync":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.DownloadAllUsersAsync":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.DownloadUsersAsync(System.Collections.Generic.IEnumerable{Discord.IGuild})":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.DownloadUsersAsync(Discord.IGuild[])":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GetVoiceRegionsAsync":"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.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.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.Extensions":"Discord.WebSocket.Extensions.yml","Discord.WebSocket.Extensions.ChannelExtensions":"Discord.WebSocket.Extensions.ChannelExtensions.yml","Discord.WebSocket.Extensions.ChannelExtensions.GetUser(Discord.IDMChannel,System.UInt64)":"Discord.WebSocket.Extensions.ChannelExtensions.yml","Discord.WebSocket.Extensions.ChannelExtensions.GetUsers(Discord.IDMChannel)":"Discord.WebSocket.Extensions.ChannelExtensions.yml","Discord.WebSocket.Extensions.ChannelExtensions.GetUser(Discord.IGroupChannel,System.UInt64)":"Discord.WebSocket.Extensions.ChannelExtensions.yml","Discord.WebSocket.Extensions.ChannelExtensions.GetUsers(Discord.IGroupChannel)":"Discord.WebSocket.Extensions.ChannelExtensions.yml","Discord.WebSocket.Extensions.ChannelExtensions.GetUser(Discord.ITextChannel,System.UInt64)":"Discord.WebSocket.Extensions.ChannelExtensions.yml","Discord.WebSocket.Extensions.ChannelExtensions.GetUsers(Discord.ITextChannel)":"Discord.WebSocket.Extensions.ChannelExtensions.yml","Discord.WebSocket.Extensions.ChannelExtensions.GetUser(Discord.IVoiceChannel,System.UInt64)":"Discord.WebSocket.Extensions.ChannelExtensions.yml","Discord.WebSocket.Extensions.ChannelExtensions.GetUsers(Discord.IVoiceChannel)":"Discord.WebSocket.Extensions.ChannelExtensions.yml","Discord.WebSocket.Extensions.GuildExtensions":"Discord.WebSocket.Extensions.GuildExtensions.yml","Discord.WebSocket.Extensions.GuildExtensions.GetChannel(Discord.IGuild,System.UInt64)":"Discord.WebSocket.Extensions.GuildExtensions.yml","Discord.WebSocket.Extensions.GuildExtensions.GetChannels(Discord.IGuild)":"Discord.WebSocket.Extensions.GuildExtensions.yml","Discord.WebSocket.Extensions.GuildExtensions.GetTextChannel(Discord.IGuild,System.UInt64)":"Discord.WebSocket.Extensions.GuildExtensions.yml","Discord.WebSocket.Extensions.GuildExtensions.GetTextChannels(Discord.IGuild)":"Discord.WebSocket.Extensions.GuildExtensions.yml","Discord.WebSocket.Extensions.GuildExtensions.GetVoiceChannel(Discord.IGuild,System.UInt64)":"Discord.WebSocket.Extensions.GuildExtensions.yml","Discord.WebSocket.Extensions.GuildExtensions.GetVoiceChannels(Discord.IGuild)":"Discord.WebSocket.Extensions.GuildExtensions.yml","Discord.WebSocket.Extensions.GuildExtensions.GetCurrentUser(Discord.IGuild)":"Discord.WebSocket.Extensions.GuildExtensions.yml","Discord.WebSocket.Extensions.GuildExtensions.GetUser(Discord.IGuild,System.UInt64)":"Discord.WebSocket.Extensions.GuildExtensions.yml","Discord.WebSocket.Extensions.GuildExtensions.GetUsers(Discord.IGuild)":"Discord.WebSocket.Extensions.GuildExtensions.yml","Discord.WebSocket.Extensions.GuildExtensions.GetUserCount(Discord.IGuild)":"Discord.WebSocket.Extensions.GuildExtensions.yml","Discord.WebSocket.Extensions.GuildExtensions.GetCachedUserCount(Discord.IGuild)":"Discord.WebSocket.Extensions.GuildExtensions.yml"} \ No newline at end of file +{"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/guides/commands.md b/docs/guides/commands.md index 6a629f0be..8f1a34db9 100644 --- a/docs/guides/commands.md +++ b/docs/guides/commands.md @@ -3,139 +3,238 @@ [Discord.Commands](xref:Discord.Commands) provides an Attribute-based Command Parser. -### Setup +## Setup -To use Commands, you must create a -[Commands Service](xref:Discord.Commands.CommandService) -and a Command Handler. +To use Commands, you must create a [Commands Service] and a +Command Handler. -Included below is a very bare-bones Command Handler. You can extend -your Command Handler as much as you like, however the below is the +Included below is a very bare-bones Command Handler. You can extend +your Command Handler as much as you like, however the below is the bare minimum. -[!code-csharp[Barebones Command Handler](samples/command_handler.cs)] +The CommandService optionally will accept a [CommandServiceConfig], +which _does_ set a few default values for you. It is recommended to +look over the properties in [CommandServiceConfig], and their default +values. -## Commands +[!code-csharp[Command Handler](samples/command_handler.cs)] -In 1.0, Commands are no longer implemented at runtime with a builder -pattern. While a builder pattern may be provided later, commands are -created primarily with attributes. +[Command Service]: xref:Discord.Commands.CommandService +[CommandServiceConfig]: xref:Discord.Commands.CommandServiceConfig -### Basic Structure +## With Attributes -All commands belong to a Module. (See the below section for creating -modules). +In 1.0, Commands can be defined ahead of time, with attributes, or +at runtime, with builders. -All commands in a module must be defined as a `Task`. +For most bots, ahead-of-time commands should be all you need, and this +is the recommended method of defining commands. -To add parameters to your command, you simply need to add parameters -to the Task that represents the command. You are _not_ required to -accept all arguments as `String`, they will be automatically parsed -into the type you specify for the arument. See the Example Module -for an example of command parameters. +### Modules -## Modules +The first step to creating commands is to create a _module_. -Modules are an organizational pattern that allow you to write your -commands in different classes, and have them automatically loaded. +Modules are an organizational pattern that allow you to write your +commands in different classes, and have them automatically loaded. 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 ran in it. +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 whereever possible. You should **not** be implementing very much logic into your modules; outsource to a service for that. -If you are unfamiliar with Inversion of Control, it is recommended to -read the MSDN article on [IoC] and [Dependency Injection]. +If you are unfamiliar with Inversion of Control, it is recommended to +read the MSDN article on [IoC] and [Dependency Injection]. + +To begin, create a new class somewhere in your project, and +inherit the class from [ModuleBase]. This class **must** be `public`. + +>[!NOTE] +>[ModuleBase] is an _abstract_ class, meaning that you may extend it +>or override it as you see fit. Your module may inherit from any +>extension of ModuleBase. -To create a module, create a class that inherits from -@Discord.Commands.ModuleBase. +By now, your module should look like this: +[!code-csharp[Empty Module](samples/empty-module.cs)] [IoC]: https://msdn.microsoft.com/en-us/library/ff921087.aspx [Dependency Injection]: https://msdn.microsoft.com/en-us/library/ff921152.aspx +[ModuleBase]: xref:Discord.Commands.ModuleBase`1 + +### Adding Commands + +The next step to creating commands, is actually creating commands. + +To create a command, add a method to your module of type `Task`. +Typically, you will want to mark this method as `async`, although it is +not required. + +Adding parameters to a command is done by adding parameters to the +parent Task. + +For example, to take an integer as an argument, add `int arg`. To take +a user as an argument, add `IUser user`. In 1.0, a command can accept +nearly any type of argument; a full list of types that are parsed by +default can be found in the below section on _Type Readers_. + +Parameters, by default, are always required. To make a parameter +optional, give it a default value. To accept a comma-separated list, +set the parameter to `params Type[]`. + +Should a parameter include spaces, it **must** be wrapped in quotes. +For example, for a command with a parameter `string food`, you would +execute it with `!favoritefood "Key Lime Pie"`. + +If you would like a parameter to parse until the end of a command, +flag the parameter with the [RemainderAttribute]. This will allow a +user to invoke a command without wrapping a parameter in quotes. + +Finally, flag your command with the [CommandAttribute]. (You must +specify a name for this command, except for when it is part of a +module group - see below). + +[RemainderAttribute]: xref:Discord.Commands.RemainderAttribute +[CommandAttribute]: xref:Discord.Commands.CommandAttribute + +### Command Overloads + +You may add overloads of your commands, and the command parser will +automatically pick up on it. + +If, for whatever reason, you have too commands which are ambiguous to +each other, you may use the @Discord.Commands.PriorityAttribute to +specify which should be tested before the other. + +Priority's are sorted in ascending order; the higher priority will be +called first. + +### CommandContext + +Every command can access the execution context through the [Context] +property on [ModuleBase]. CommandContext allows you to access the +message, channel, guild, and user that the command was invoked from, +as well as the underlying discord client the command was invoked from. + +Different types of Contexts may be specified using the generic variant +of [ModuleBase]. When using a [SocketCommandContext], for example, +the properties on this context will already be Socket entities. You +will not need to cast them. + +To reply to messages, you may also invoke [ReplyAsync], instead of +accessing the channel through the [Context] and sending a message. + +[Context]: xref:Discord.Commands.ModuleBase`1#Discord_Commands_ModuleBase_1_Context +[SocketCommandContext]: xref:Discord.Commands.SocketCommandContext + +>![WARNING] +>Contexts should **NOT** be mixed! You cannot have one module that +>uses CommandContext, and another that uses SocketCommandContext. ### Example Module -[!code-csharp[Modules](samples/module.cs)] +At this point, your module should look comparable to this example: +[!code-csharp[Example Module](samples/module.cs)] #### Loading Modules Automatically -The Command Service can automatically discover all classes in an -Assembly that inherit @Discord.Commands.ModuleBase, and load them. +The Command Service can automatically discover all classes in an +Assembly that inherit [ModuleBase], and load them. -To have a module opt-out of auto-loading, pass `autoload: false` in -the Module attribute. +To opt a module out of auto-loading, flag it with +[DontAutoLoadAttribute] -Invoke [CommandService.AddModules] to discover modules and install them. +Invoke [CommandService.AddModulesAsync] to discover modules and +install them. -[CommandService.AddModules]: xref:Discord.Commands.CommandService#Discord_Commands_CommandService_AddModules +[DontAutoLoadAttribute]: xref:Discord.Commands.DontAutoLoadAttribute +[CommandService.AddModulesAsync]: xref:Discord_Commands_CommandService#Discord_Commands_CommandService_AddModulesAsync_Assembly_ #### Loading Modules Manually -To manually load a module, invoke [CommandService.AddModule], -by passing in the generic type of your module, and optionally +To manually load a module, invoke [CommandService.AddModuleAsync], +by passing in the generic type of your module, and optionally a dependency map. -[CommandService.AddModule]: xref:Discord.Commands.CommandService#Discord_Commands_CommandService_AddModule__1_Discord_Commands_IDependencyMap_ +[CommandService.AddModuleAsync]: xref:Discord.Commands.CommandService#Discord_Commands_CommandService_AddModuleAsync__1 ### Module Constructors -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 +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. -### Command Groups +### Module Properties -Command Groups allow you to create a module where commands are prefixed. -To create a group, create a new module and flag it with the -@Discord.Commands.GroupAttribute. +Modules with public settable properties will have them injected after module +construction. ->[!NOTE] ->Groups do not _need_ to be modules. Only classes with commands should ->inherit from ModuleBase. If you plan on using a group for strictly ->organizational purposes, there is no reason to make it a module. +### Module Groups -[!code-csharp[Groups Sample](samples/groups.cs)] +Module Groups allow you to create a module where commands are prefixed. +To create a group, flag a module with the +@Discord.Commands.GroupAttribute + +Module groups also allow you to create **nameless commands**, where the +[CommandAttribute] is configured with no name. In this case, the +command will inherit the name of the group it belongs to. + +### Submodules + +Submodules are modules that reside within another module. Typically, +submodules are used to create nested groups (although not required to +create nested groups). + +[!code-csharp[Groups and Submodules](samples/groups.cs)] + +## With Builders + +**TODO** ## Dependency Injection -The commands service is bundled with a very barebones Dependency -Injection service for your convienence. It is recommended that +The commands service is bundled with a very barebones Dependency +Injection service for your convienence. It is recommended that 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 @Discord.Commands.IDependencyMap. +The library includes @Discord.Commands.DependencyMap to help with +this, however you may create your own IDependencyMap if you wish. -Next, add the dependencies your modules will use to the map. +Next, add the dependencies your modules will use to the map. -Finally, pass the map into the `LoadAssembly` method. +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)] ### Usage in Modules -In the constructor of your module, any parameters will be filled in by +In the constructor of your module, any parameters will be filled in by the @Discord.Commands.IDependencyMap you pass into `LoadAssembly`. +Any publicly settable properties will also be filled in the same manner. + +>[!NOTE] +> Annotating a property with the [DontInject] attribute will prevent it from +being injected. + >[!NOTE] ->If you accept `CommandService` or `IDependencyMap` as a parameter in -your constructor, these parameters will be filled by the -CommandService the module was loaded from, and the DependencyMap passed -into it, respectively. +>If you accept `CommandService` or `IDependencyMap` 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 +into it, respectively. [!code-csharp[DependencyMap in Modules](samples/dependency_module.cs)] # Preconditions -Preconditions serve as a permissions system for your commands. Keep in -mind, however, that they are not limited to _just_ permissions, and +Preconditions serve as a permissions system for your commands. Keep in +mind, however, that they are not limited to _just_ permissions, and can be as complex as you want them to be. >[!NOTE] @@ -143,91 +242,79 @@ can be as complex as you want them to be. ## Bundled Preconditions -@Discord.Commands ships with two built-in preconditions, -@Discord.Commands.RequireContextAttribute and -@Discord.Commands.RequirePermissionAttribute. - -### RequireContext - -@Discord.Commands.RequireContextAttribute is a precondition that -requires your command to be executed in the specified context. - -You may require three different types of context: -* Guild -* DM -* Group +Commands ships with four bundled preconditions; you may view their +usages on their API page. -Since these are `Flags`, you may OR them together. - -[!code-csharp[RequireContext](samples/require_context.cs)] - -### RequirePermission - -@Discord.Commands.RequirePermissionAttribute is a precondition that -allows you to quickly specfiy that a user must poesess a permission -to execute a command. - -You may require either a @Discord.GuildPermission or -@Discord.ChannelPermission - -[!code-csharp[RequireContext](samples/require_permission.cs)] +- @Discord.Commands.RequireContextAttribute +- @Discord.Commands.RequireOwnerAttribute +- @Discord.Commands.RequireBotPermissionAttribute +- @Discord.Commands.RequireUserPermissionAttribute ## Custom Preconditions To write your own preconditions, create a new class that inherits from @Discord.Commands.PreconditionAttribute -In order for your precondition to function, you will need to override -`CheckPermissions`, which is a `Task`. +In order for your precondition to function, you will need to override +[CheckPermissions]. + Your IDE should provide an option to fill this in for you. -Return `PreconditionResult.FromSuccess()` if the context met the -required parameters, otherwise return `PreconditionResult.FromError()`, +Return [PreconditionResult.FromSuccess] if the context met the +required parameters, otherwise return [PreconditionResult.FromError], optionally including an error message. [!code-csharp[Custom Precondition](samples/require_owner.cs)] +[CheckPermissions]: xref:Discord.Commands.PreconditionAttribute#Discord_Commands_PreconditionAttribute_CheckPermissions_Discord_Commands_CommandContext_Discord_Commands_CommandInfo_Discord_Commands_IDependencyMap_ +[PreconditionResult.FromSuccess]: xref:Discord.Commands.PreconditionResult#Discord_Commands_PreconditionResult_FromSuccess +[PreconditionResult.FromError]: xref:Discord.Commands.PreconditionResult#Discord_Commands_PreconditionResult_FromError_System_String_ + # Type Readers -Type Readers allow you to parse different types of arguments in +Type Readers allow you to parse different types of arguments in your commands. By default, the following Types are supported arguments: -- string +- bool +- char - sbyte/byte - ushort/short - uint/int - ulong/long - float, double, decimal -- DateTime/DateTimeOffset -- IUser/IGuildUser +- string +- DateTime/DateTimeOffset/TimeSpan +- IMessage/IUserMessage - IChannel/IGuildChannel/ITextChannel/IVoiceChannel/IGroupChannel +- IUser/IGuildUser/IGroupUser - IRole -- IMessage/IUserMessage ### Creating a Type Readers -To create a TypeReader, create a new class that imports @Discord and +To create a TypeReader, create a new class that imports @Discord and @Discord.Commands. Ensure your class inherits from @Discord.Commands.TypeReader -Next, satisfy the `TypeReader` class by overriding `Task Read(CommandContext context, string input)`. +Next, satisfy the `TypeReader` class by overriding [Read]. >[!NOTE] ->In many cases, Visual Studio can fill this in for you, using the +>In many cases, Visual Studio can fill this in for you, using the >"Implement Abstract Class" IntelliSense hint. Inside this task, add whatever logic you need to parse the input string. -Finally, return a `TypeReaderResult`. If you were able to successfully -parse the input, return `TypeReaderResult.FromSuccess(parsedInput)`. +Finally, return a `TypeReaderResult`. If you were able to successfully +parse the input, return `TypeReaderResult.FromSuccess(parsedInput)`. Otherwise, return `TypeReaderResult.FromError`. +[Read]: xref:Discord.Commands.TypeReader#Discord_Commands_TypeReader_Read_Discord_Commands_CommandContext_System_String_ + #### Sample [!code-csharp[TypeReaders](samples/typereader.cs)] ### Installing TypeReaders -TypeReaders are not automatically discovered by the Command Service, -and must be explicitly added. To install a TypeReader, invoke [CommandService.AddTypeReader](xref:Discord.Commands.CommandService#Discord_Commands_CommandService_AddTypeReader__1_Discord_Commands_TypeReader_). \ No newline at end of file +TypeReaders are not automatically discovered by the Command Service, +and must be explicitly added. To install a TypeReader, invoke [CommandService.AddTypeReader](xref:Discord.Commands.CommandService#Discord_Commands_CommandService_AddTypeReader__1_Discord_Commands_TypeReader_). diff --git a/docs/guides/events.md b/docs/guides/events.md index 310b011e6..b10dc7648 100644 --- a/docs/guides/events.md +++ b/docs/guides/events.md @@ -17,7 +17,7 @@ To hook into events, you must be using the @Discord.WebSocket.DiscordSocketClien 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](Discord_WebSocket_DiscordSocketClient_Disconnected) are raised when the Gateway Socket connects or disconnects, respectively. +[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. diff --git a/docs/guides/intro.md b/docs/guides/intro.md index d02affe3d..f16bc9883 100644 --- a/docs/guides/intro.md +++ b/docs/guides/intro.md @@ -23,11 +23,19 @@ You may add the MyGet feed to Visual Studio directly from `https://www.myget.org 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. +>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. +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) @@ -37,4 +45,6 @@ For more information, go to [MSDN's Async-Await section.](https://msdn.microsoft >[!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](xref:Discord.DiscordSocketClient#ConnectAsync) method will automatically wait for the Ready event, and for all guilds to stream. To avoid this, pass `false` into `ConnectAsync`. \ No newline at end of file +>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/samples/audio_create_ffmpeg.cs b/docs/guides/samples/audio_create_ffmpeg.cs new file mode 100644 index 000000000..e24af088b --- /dev/null +++ b/docs/guides/samples/audio_create_ffmpeg.cs @@ -0,0 +1,11 @@ +private Process CreateStream(string path) +{ + var ffmpeg = new ProcessStartInfo + { + FileName = "ffmpeg", + Arguments = $"-i {path} -ac 2 -f s16le -ar 48000 pipe:1", + UseShellExecute = false, + RedirectStandardOutput = true, + }; + return Process.Start(ffmpeg); +} \ No newline at end of file diff --git a/docs/guides/samples/audio_ffmpeg.cs b/docs/guides/samples/audio_ffmpeg.cs new file mode 100644 index 000000000..877050caf --- /dev/null +++ b/docs/guides/samples/audio_ffmpeg.cs @@ -0,0 +1,9 @@ +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); + await output.CopyToAsync(discord); + await discord.FlushAsync(); +} \ No newline at end of file diff --git a/docs/guides/samples/command_handler.cs b/docs/guides/samples/command_handler.cs index 3ef3bfc6e..71869415b 100644 --- a/docs/guides/samples/command_handler.cs +++ b/docs/guides/samples/command_handler.cs @@ -8,6 +8,7 @@ public class Program { private CommandService commands; private DiscordSocketClient client; + private DependencyMap map; static void Main(string[] args) => new Program().Start().GetAwaiter().GetResult(); @@ -18,6 +19,8 @@ public class Program string token = "bot token here"; + map = new DependencyMap(); + await InstallCommands(); await client.LoginAsync(TokenType.Bot, token); @@ -25,13 +28,12 @@ public class Program 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. - await commands.LoadAssembly(Assembly.GetEntryAssembly()); + await commands.AddModulesAsync(Assembly.GetEntryAssembly()); } public async Task HandleCommand(SocketMessage messageParam) { @@ -41,16 +43,14 @@ public class Program // 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)) - { - // 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.Execute(context, argPos); - if (!result.IsSuccess) - await msg.Channel.SendMessageAsync(result.ErrorReason); - } + 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); + if (!result.IsSuccess) + await context.Channel.SendMessageAsync(result.ErrorReason); } -} \ No newline at end of file +} diff --git a/docs/guides/samples/dependency_map_setup.cs b/docs/guides/samples/dependency_map_setup.cs index af791990e..aa39150e7 100644 --- a/docs/guides/samples/dependency_map_setup.cs +++ b/docs/guides/samples/dependency_map_setup.cs @@ -7,18 +7,13 @@ public class Commands { public async Task Install(DiscordSocketClient client) { - var commands = new CommandService(); - var map = new DependencyMap(); - map.Add(client); - map.Add(commands); - await commands.LoadAssembly(Assembly.GetCurrentAssembly(), map); + // Here, we will inject the Dependency Map 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)); + // ... + await _commands.AddModulesAsync(Assembly.GetEntryAssembly()); } - // In ConfigureServices, we will inject the Dependency Map with - // all of the services our client will use. - public Task ConfigureServices(IDependencyMap map) - { - map.Add(new NotificationService(map)); - map.Add(new DatabaseService(map)); - } - // ... -} \ No newline at end of file +} diff --git a/docs/guides/samples/dependency_module.cs b/docs/guides/samples/dependency_module.cs index be7c980aa..561b0f6ac 100644 --- a/docs/guides/samples/dependency_module.cs +++ b/docs/guides/samples/dependency_module.cs @@ -2,28 +2,39 @@ using Discord; using Discord.Commands; using Discord.WebSocket; -[Module] -public class ModuleA +public class ModuleA : ModuleBase { - private DiscordSocketClient client; - private ISelfUser self; + private readonly DatabaseService _database; - public ModuleA(IDiscordClient c, ISelfUser s) + // Dependencies can be injected via the constructor + public ModuleA(DatabaseService database) { - if (!(c is DiscordSocketClient)) throw new InvalidOperationException("This module requires a DiscordSocketClient"); - client = c as DiscordSocketClient; - self = s; + _database = database; + } + + public async Task ReadFromDb() + { + var x = _database.getX(); + await ReplyAsync(x); } } public class ModuleB { - private IDiscordClient client; - private CommandService commands; - - public ModuleB(CommandService c, IDependencyMap m) + + // Public settable properties will be injected + public AnnounceService { get; set; } + + // Public properties without setters will not + public CommandService Commands { get; } + + // Public properties annotated with [DontInject] will not + [DontInject] + public NotificationService { get; set; } + + public ModuleB(CommandService commands) { - commands = c; - client = m.Get(); + Commands = commands; } -} \ No newline at end of file + +} diff --git a/docs/guides/samples/empty-module.cs b/docs/guides/samples/empty-module.cs new file mode 100644 index 000000000..cac9922b5 --- /dev/null +++ b/docs/guides/samples/empty-module.cs @@ -0,0 +1,6 @@ +using Discord.Commands; + +public class InfoModule : ModuleBase +{ + +} \ No newline at end of file diff --git a/docs/guides/samples/faq/send_message.cs b/docs/guides/samples/faq/send_message.cs index ed4ff9b8a..d7ecf5131 100644 --- a/docs/guides/samples/faq/send_message.cs +++ b/docs/guides/samples/faq/send_message.cs @@ -1,6 +1,6 @@ public async Task SendMessageToChannel(ulong ChannelId) { - var channel = _client.GetChannel(ChannelId) as ISocketMessageChannel; + 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 index 8025dd7fd..18906c53b 100644 --- a/docs/guides/samples/faq/status.cs +++ b/docs/guides/samples/faq/status.cs @@ -1,5 +1,5 @@ public async Task ModifyStatus() { - await _client.SetStatus(UserStatus.Idle); - await _client.SetGame("Type !help for help"); + await _client.SetStatusAsync(UserStatus.Idle); + await _client.SetGameAsync("Type !help for help"); } diff --git a/docs/guides/samples/first-steps.cs b/docs/guides/samples/first-steps.cs index f811f186b..3f1377ed7 100644 --- a/docs/guides/samples/first-steps.cs +++ b/docs/guides/samples/first-steps.cs @@ -1,35 +1,128 @@ +using System; +using System.Reflection; +using System.Threading.Tasks; using Discord; +using Discord.Commands; using Discord.WebSocket; class Program { - // Convert our sync-main to an async main method - static void Main(string[] args) => new Program().Run().GetAwaiter().GetResult(); + private readonly DiscordSocketClient _client; + + // Keep the CommandService and IDependencyMap around for use with commands. + private readonly IDependencyMap _map = new DependencyMap(); + private readonly CommandService _commands = new CommandService(); - // Create a DiscordClient with WebSocket support - private DiscordSocketClient client; + // Program entry point + static void Main(string[] args) + { + // Call the Program constructor, followed by the + // MainAsync method and wait until it finishes (which should be never). + new Program().MainAsync().GetAwaiter().GetResult(); + } + + private Program() + { + _client = new DiscordSocketClient(new DiscordSocketConfig + { + // How much logging do you want to see? + LogLevel = LogSeverity.Info, + + // If you or another service needs to do anything with messages + // (eg. checking Reactions), you should probably + // set the MessageCacheSize here. + //MessageCacheSize = 50, + + // If your platform doesn't have native websockets, + // add Discord.Net.Providers.WS4Net from NuGet, + // add the `using` at the top, and uncomment this line: + //WebSocketProvider = WS4NetProvider.Instance + }); + } - public async Task Run() + // Create a named logging handler, so it can be re-used by addons + // that ask for a Func. + private static Task Logger(LogMessage message) { - client = new DiscordSocketClient(); + var cc = Console.ForegroundColor; + switch (message.Severity) + { + case LogSeverity.Critical: + case LogSeverity.Error: + Console.ForegroundColor = ConsoleColor.Red; + break; + case LogSeverity.Warning: + Console.ForegroundColor = ConsoleColor.Yellow; + break; + case LogSeverity.Info: + Console.ForegroundColor = ConsoleColor.White; + break; + case LogSeverity.Verbose: + case LogSeverity.Debug: + Console.ForegroundColor = ConsoleColor.DarkGray; + break; + } + Console.WriteLine($"{DateTime.Now,-19} [{message.Severity,8}] {message.Source}: {message.Message}"); + Console.ForegroundColor = cc; + return Task.CompletedTask; + } + + private async Task MainAsync() + { + // Subscribe the logging handler. + _client.Log += Logger; + + // Centralize the logic for commands into a seperate method. + await InitCommands(); + + // Login and connect. + await _client.LoginAsync(TokenType.Bot, /* */); + await _client.ConnectAsync(); - // Place the token of your bot account here - string token = "aaabbbccc"; - - // Hook into the MessageReceived event on DiscordSocketClient - client.MessageReceived += async (message) => - { // Check to see if the Message Content is "!ping" - if (message.Content == "!ping") - // Send 'pong' back to the channel the message was sent in - await message.Channel.SendMessageAsync("pong"); - }; - - // Configure the client to use a Bot token, and use our token - await client.LoginAsync(TokenType.Bot, token); - // Connect the client to Discord's gateway - await client.ConnectAsync(); - - // Block this task until the program is exited. + // Wait infinitely so your bot actually stays connected. await Task.Delay(-1); } + + private async Task InitCommands() + { + // Repeat this for all the service classes + // and other dependencies that your commands might need. + _map.Add(new SomeServiceClass()); + + // Either search the program and add all Module classes that can be found: + 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; + } + + private async Task CmdHandler(SocketMessage arg) + { + // Bail out if it's a System Message. + var msg = arg as SocketUserMessage; + if (msg == null) return; + + // Create a number to track where the prefix ends and the command begins + int pos = 0; + // Replace the '!' with whatever character + // 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) */) + { + // Create a Command Context + var context = new SocketCommandContext(msg.Discord, 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); + + // Uncomment the following lines if you want the bot + // to send a message if it failed (not advised for most situations). + //if (!result.IsSuccess && result.Error != CommandError.UnknownCommand) + // await msg.Channel.SendMessageAsync(result.ErrorReason); + } + } } \ No newline at end of file diff --git a/docs/guides/samples/groups.cs b/docs/guides/samples/groups.cs index b04148d26..db6456c87 100644 --- a/docs/guides/samples/groups.cs +++ b/docs/guides/samples/groups.cs @@ -1,15 +1,18 @@ [Group("admin")] public class AdminModule : ModuleBase { - [Group("mod")] - public class ModerationGroup : ModuleBase + [Group("clean")] + public class CleanModule : ModuleBase { - // ~admin mod ban foxbot#0282 - [Command("ban")] - public async Task Ban(IGuildUser user) { } - } + // ~admin clean 15 + [Command] + public async Task Default(int count = 10) => Messages(count); - // ~admin clean 100 - [Command("clean")] - public async Task Clean(int count = 100) { } + // ~admin clean messages 15 + [Command("messages")] + public async Task Messages(int count = 10) { } + } + // ~admin ban foxbot#0282 + [Command("ban")] + public async Task Ban(IGuildUser user) { } } \ No newline at end of file diff --git a/docs/guides/samples/joining_audio.cs b/docs/guides/samples/joining_audio.cs index 52248757f..0cc36978a 100644 --- a/docs/guides/samples/joining_audio.cs +++ b/docs/guides/samples/joining_audio.cs @@ -1,15 +1,10 @@ -// Create an IAudioClient, and store it for later use -private IAudioClient _audio; - -// Create a Join command, that will join the parameter or the user's current voice channel [Command("join")] -public async Task JoinChannel(IUserMessage msg, - IVoiceChannel channel = null) +public async Task JoinChannel(IVoiceChannel channel = null) { // Get the audio channel channel = channel ?? (msg.Author as IGuildUser)?.VoiceChannel; if (channel == null) { await msg.Channel.SendMessageAsync("User must be in a voice channel, or a voice channel must be passed as an argument."); return; } - // Get the IAudioClient by calling the JoinAsync method - _audio = await channel.JoinAsync(); + // For the next step with transmitting audio, you would want to pass this Audio Client in to a service. + var audioClient = await channel.ConnectAsync(); } \ No newline at end of file diff --git a/docs/guides/samples/logging.cs b/docs/guides/samples/logging.cs index 451480761..fd72daf2b 100644 --- a/docs/guides/samples/logging.cs +++ b/docs/guides/samples/logging.cs @@ -3,18 +3,26 @@ using Discord.Rest; public class Program { - // Note: This is the light client, it only supports REST calls. - private DiscordClient _client; + private DiscordSocketClient _client; static void Main(string[] args) => new Program().Start().GetAwaiter().GetResult(); public async Task Start() { - _client = new DiscordClient(new DiscordConfig() { + _client = new DiscordSocketClient(new DiscordSocketConfig() { LogLevel = LogSeverity.Info }); - _client.Log += (message) => Console.WriteLine($"{message.ToString()}"); + _client.Log += Log; - await _client.LoginAsync(TokenType.Bot, "bot token"); + await _client.LoginAsync(TokenType.Bot, "bot token"); + await _client.ConnectAsync(); + + await Task.Delay(-1); } -} \ No newline at end of file + + private Task Log(LogMessage message) + { + Console.WriteLine(message.ToString()); + return Task.CompletedTask; + } +} diff --git a/docs/guides/samples/module.cs b/docs/guides/samples/module.cs index 66d2907df..403acba06 100644 --- a/docs/guides/samples/module.cs +++ b/docs/guides/samples/module.cs @@ -7,7 +7,7 @@ public class Info : ModuleBase { // ~say hello -> hello [Command("say"), Summary("Echos a message.")] - public async Task Say([Unparsed, Summary("The text to echo")] string echo) + public async Task Say([Remainder, Summary("The text to echo")] string echo) { // ReplyAsync is a method on ModuleBase await ReplyAsync(echo); @@ -39,4 +39,4 @@ public class Sample : ModuleBase var userInfo = user ?? Context.Client.CurrentUser; await ReplyAsync($"{userInfo.Username}#{userInfo.Discriminator}"); } -} \ No newline at end of file +} diff --git a/docs/guides/samples/require_context.cs b/docs/guides/samples/require_context.cs deleted file mode 100644 index 0bc558e4a..000000000 --- a/docs/guides/samples/require_context.cs +++ /dev/null @@ -1,10 +0,0 @@ -public class InfoModule : ModuleBase -{ - // Constrain this command to Guilds - [RequireContext(ContextType.Guild)] - public async Task Whois(IGuildUser user) { } - - // Constrain this command to either Guilds or DMs - [RequireContext(ContextType.Guild | ContextType.DM)] - public async Task Info() { } -} \ No newline at end of file diff --git a/docs/guides/samples/require_owner.cs b/docs/guides/samples/require_owner.cs index 670d92c19..567b3d2af 100644 --- a/docs/guides/samples/require_owner.cs +++ b/docs/guides/samples/require_owner.cs @@ -1,10 +1,10 @@ -// Defining the Precondition +// (Note: This precondition is obsolete, it is recommended to use the RequireOwnerAttribute that is bundled with Discord.Commands) // Inherit from PreconditionAttribute public class RequireOwnerAttribute : PreconditionAttribute { // Override the CheckPermissions method - public override Task CheckPermissions(CommandContext context, CommandInfo command, IDependencyMap map) + public async override Task CheckPermissions(CommandContext context, CommandInfo command, IDependencyMap map) { // Get the ID of the bot's owner var ownerId = (await map.Get().GetApplicationInfoAsync()).Owner.Id; @@ -15,4 +15,4 @@ public class RequireOwnerAttribute : PreconditionAttribute else return PreconditionResult.FromError("You must be the owner of the bot to run this command."); } -} \ No newline at end of file +} diff --git a/docs/guides/samples/require_permission.cs b/docs/guides/samples/require_permission.cs deleted file mode 100644 index 56a1ff744..000000000 --- a/docs/guides/samples/require_permission.cs +++ /dev/null @@ -1,6 +0,0 @@ -public class AdminModule : ModuleBase -{ - [Command("ban")] - [RequirePermission(GuildPermission.BanMembers)] - public async Task Ban(IGuildUser target) { } -} \ No newline at end of file diff --git a/docs/guides/samples/typereader.cs b/docs/guides/samples/typereader.cs index 69db3b991..b21e6c15a 100644 --- a/docs/guides/samples/typereader.cs +++ b/docs/guides/samples/typereader.cs @@ -1,3 +1,4 @@ +// Note: This example is obsolete, a boolean type reader is bundled with Discord.Commands using Discord; using Discord.Commands; diff --git a/docs/guides/voice.md b/docs/guides/voice.md index 2606a5f3e..1f09069f5 100644 --- a/docs/guides/voice.md +++ b/docs/guides/voice.md @@ -7,22 +7,105 @@ ## Installation -To use Audio, you must first configure your `DiscordSocketClient` with Audio support. +To use Audio, you must first configure your [DiscordSocketClient] +with Audio support. -In your @Discord.DiscordSocketConfig, set `AudioMode` to the appropriate @Discord.Audio.AudioMode for your bot. For most bots, you will only need to use `AudioMode.Outgoing`. +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 -Audio requires two native libraries, `libsodium` and `opus`. Both of these libraries must be placed in the runtime directory of your bot (for .NET 4.6, the directory where your exe is located; for .NET core, directory where your project.json is located) +Audio requires two native libraries, `libsodium` and `opus`. +Both of these libraries must be placed in the runtime directory of your +bot. (When developing on .NET Framework, this would be `bin/debug`, +when developing on .NET Core, this is where you execute `dotnet run` +from; typically the same directory as your csproj). + +For Windows Users, precompiled binaries are available for your +convienence [here](https://discord.foxbot.me/binaries/) -For Windows Users, precompiled binaries are available for your convienence [here](https://discord.foxbot.me/binaries/) +For Linux Users, you will need to compile [Sodium] and [Opus] from +source, or install them from your package manager. -For Linux Users, you will need to compile from source. [Sodium Source Code](https://download.libsodium.org/libsodium/releases/), [Opus Source Code](http://downloads.xiph.org/releases/opus/). +[Sodium]: https://download.libsodium.org/libsodium/releases/ +[Opus]: http://downloads.xiph.org/releases/opus/ ## Joining a Channel -Joining Voice Channels is relatively straight-forward, and is a requirement for sending or receiving audio. This will also allow us to create an @Discord.Audio.IAudioClient, which will be used later to send or receive audio. +Joining a channel is the first step to sending audio, and will return +an [IAudioClient] to send data with. + +To join a channel, simply await [ConnectAsync] on any instance of an +@Discord.IVoiceChannel. [!code-csharp[Joining a Channel](samples/joining_audio.cs)] -The client will sustain a connection to this channel until it is kicked, disconnected from Discord, or told to disconnect. \ No newline at end of file +The client will sustain a connection to this channel until it is +kicked, disconnected from Discord, or told to disconnect. + +It should be noted that voice connections are created on a per-guild +basis; only one audio connection may be open by the bot in a single +guild. To switch channels within a guild, invoke [ConnectAsync] on +another voice channel in the guild. + +[IAudioClient]: xref:Discord.Audio.IAudioClient +[ConnectAsync]: xref:Discord.IVoiceChannel#Discord_IVoiceChannel_ConnectAsync + +## Transmitting Audio + +### With FFmpeg + +[FFmpeg] is an open source, highly versatile AV-muxing tool. This is +the recommended method of transmitting audio. + +Before you begin, you will need to have a version of FFmpeg downloaded +and placed somewhere in your PATH (or alongside the bot, in the same +location as libsodium and opus). Windows binaries are available on +[FFmpeg's download page]. + +[FFmpeg]: https://ffmpeg.org/ +[FFmpeg's download page]: https://ffmpeg.org/download.html + +First, you will need to create a Process that starts FFmpeg. An +example of how to do this is included below, though it is important +that you return PCM at 48000hz. + +>[!NOTE] +>As of the time of this writing, Discord.Audio struggles significantly +>with processing audio that is already opus-encoded; you will need to +>use the PCM write streams. + +[!code-csharp[Creating FFmpeg](samples/audio_create_ffmpeg.cs)] + +Next, to transmit audio from FFmpeg to Discord, you will need to +pull an [AudioOutStream] from your [IAudioClient]. Since we're using +PCM audio, use [IAudioClient.CreatePCMStream]. + +The sample rate argument doesn't particularly matter, so long as it is +a valid rate (120, 240, 480, 960, 1920, or 2880). For the sake of +simplicity, I recommend using 1920. + +Channels should be left at `2`, unless you specified a different value +for `-ac 2` when creating FFmpeg. + +[AudioOutStream]: xref:Discord.Audio.AudioOutStream +[IAudioClient.CreatePCMStream]: xref:Discord.Audio.IAudioClient#Discord_Audio_IAudioClient_CreatePCMStream_System_Int32_System_Int32_System_Nullable_System_Int32__System_Int32_ + +Finally, audio will need to be piped from FFmpeg's stdout into your +AudioOutStream. This step can be as complex as you'd like it to be, but +for the majority of cases, you can just use [Stream.CopyToAsync], as +shown below. + +[Stream.CopyToAsync]: https://msdn.microsoft.com/en-us/library/hh159084(v=vs.110).aspx + +If you are implementing a queue for sending songs, it's likely that +you will want to wait for audio to stop playing before continuing on +to the next song. You can await `AudioOutStream.FlushAsync` to wait for +the audio client's internal buffer to clear out. + +[!code-csharp[Sending Audio](samples/audio_ffmpeg.cs)] \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 6f5f4a1eb..3f0393d4f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,13 @@ # Discord.Net Documentation -Refer to [Guides](guides/intro.md) for tutorials on using Discord.Net, or the [API documentation](api/index.md) to review individual objects in the library. +Discord.Net is an asynchronous, multiplatform .NET Library used to interface with the [Discord API](https://discordapp.com/). -**Todo:** Put something meaningful here. \ No newline at end of file +If this is your first time using Discord.Net, you should refer to the [Intro](guides/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: + - [Discord API Guild](https://discord.gg/discord-api) - Look for `#dotnet_discord-net` + - [GitHub](https://github.com/RogueException/Discord.Net/tree/dev) + - [NuGet](https://www.nuget.org/packages/Discord.Net/) + - [MyGet Feed](https://www.myget.org/feed/Packages/discord-net) - Addons and nightly builds \ No newline at end of file diff --git a/pack.ps1 b/pack.ps1 new file mode 100644 index 000000000..0f84ea309 --- /dev/null +++ b/pack.ps1 @@ -0,0 +1,17 @@ +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/AssemblyInfo.cs b/src/Discord.Net.Analyzers/AssemblyInfo.cs new file mode 100644 index 000000000..5e9efa5bc --- /dev/null +++ b/src/Discord.Net.Analyzers/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.Analyzers/ConfigureAwaitAnalyzer.cs b/src/Discord.Net.Analyzers/ConfigureAwaitAnalyzer.cs new file mode 100644 index 000000000..97382f353 --- /dev/null +++ b/src/Discord.Net.Analyzers/ConfigureAwaitAnalyzer.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace RegexAnalyzer +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class ConfigureAwaitAnalyzer : DiagnosticAnalyzer + { + public const string DiagnosticId = "ConfigureAwait"; + internal const string Title = "ConfigureAwait was not specified"; + internal const string MessageFormat = "ConfigureAwait error {0}"; + internal const string Description = "ConfigureAwait(false) should be used."; + internal const string Category = "Usage"; + internal static DiagnosticDescriptor Rule = + new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, + Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.InvocationExpression); + } + + private void AnalyzeNode(SyntaxNodeAnalysisContext context) + { + /*var invocationExpr = (InvocationExpressionSyntax)context.Node; + var memberAccessExpr = invocationExpr.Expression as MemberAccessExpressionSyntax; + if (memberAccessExpr?.Name.ToString() != "Match") return; + var memberSymbol = context.SemanticModel.GetSymbolInfo(memberAccessExpr).Symbol as IMethodSymbol; + if (!memberSymbol?.ToString().StartsWith("System.Text.RegularExpressions.Regex.Match") ?? true) return; + var argumentList = invocationExpr.ArgumentList as ArgumentListSyntax; + if ((argumentList?.Arguments.Count ?? 0) < 2) return; + var regexLiteral = argumentList.Arguments[1].Expression as LiteralExpressionSyntax; + if (regexLiteral == null) return; + var regexOpt = context.SemanticModel.GetConstantValue(regexLiteral); + if (!regexOpt.HasValue) return; + var regex = regexOpt.Value as string; + if (regex == null) return; + try + { + System.Text.RegularExpressions.Regex.Match("", regex); + } + catch (ArgumentException e) + { + var diagnostic = Diagnostic.Create(Rule, regexLiteral.GetLocation(), e.Message); + context.ReportDiagnostic(diagnostic); + }*/ + } + } +} \ 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 new file mode 100644 index 000000000..0612e423f --- /dev/null +++ b/src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj @@ -0,0 +1,32 @@ + + + 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/AliasAttribute.cs b/src/Discord.Net.Commands/Attributes/AliasAttribute.cs index 9aa1371f6..6e115bd60 100644 --- a/src/Discord.Net.Commands/Attributes/AliasAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/AliasAttribute.cs @@ -3,7 +3,7 @@ using System; namespace Discord.Commands { /// Provides aliases for a command. - [AttributeUsage(AttributeTargets.Method)] + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class AliasAttribute : Attribute { /// The aliases which have been defined for the command. diff --git a/src/Discord.Net.Commands/Attributes/CommandAttribute.cs b/src/Discord.Net.Commands/Attributes/CommandAttribute.cs index baac75ff9..5ae6092eb 100644 --- a/src/Discord.Net.Commands/Attributes/CommandAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/CommandAttribute.cs @@ -6,7 +6,7 @@ namespace Discord.Commands public class CommandAttribute : Attribute { public string Text { get; } - public RunMode RunMode { get; set; } = RunMode.Sync; + public RunMode RunMode { get; set; } = RunMode.Default; public CommandAttribute() { diff --git a/src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs b/src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs new file mode 100644 index 000000000..bd966e129 --- /dev/null +++ b/src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace Discord.Commands { + + [AttributeUsage(AttributeTargets.Property)] + public class DontInjectAttribute : Attribute { + } + +} diff --git a/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs b/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs new file mode 100644 index 000000000..37f685c95 --- /dev/null +++ b/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs @@ -0,0 +1,22 @@ +using System; + +using System.Reflection; + +namespace Discord.Commands +{ + [AttributeUsage(AttributeTargets.Parameter)] + public class OverrideTypeReaderAttribute : Attribute + { + private static readonly TypeInfo _typeReaderTypeInfo = typeof(TypeReader).GetTypeInfo(); + + public Type TypeReader { get; } + + public OverrideTypeReaderAttribute(Type overridenTypeReader) + { + if (!_typeReaderTypeInfo.IsAssignableFrom(overridenTypeReader.GetTypeInfo())) + throw new ArgumentException($"{nameof(overridenTypeReader)} must inherit from {nameof(TypeReader)}"); + + TypeReader = overridenTypeReader; + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs b/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs new file mode 100644 index 000000000..168d15e5f --- /dev/null +++ b/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading.Tasks; + +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); + } +} \ 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 067c8e93b..7755d459b 100644 --- a/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs @@ -6,6 +6,6 @@ namespace Discord.Commands [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = true)] public abstract class PreconditionAttribute : Attribute { - public abstract Task CheckPermissions(CommandContext context, CommandInfo command, IDependencyMap map); + public abstract Task CheckPermissions(ICommandContext context, CommandInfo command, IDependencyMap map); } } diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs new file mode 100644 index 000000000..520cfa6fd --- /dev/null +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs @@ -0,0 +1,73 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + /// + /// This attribute requires that the bot has a specified permission in the channel a command is invoked in. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class RequireBotPermissionAttribute : PreconditionAttribute + { + public GuildPermission? GuildPermission { get; } + public ChannelPermission? ChannelPermission { get; } + + /// + /// Require that the bot account has a specified GuildPermission + /// + /// This precondition will always fail if the command is being invoked in a private channel. + /// The GuildPermission that the bot must have. Multiple permissions can be specified by ORing the permissions together. + public RequireBotPermissionAttribute(GuildPermission permission) + { + GuildPermission = permission; + ChannelPermission = null; + } + /// + /// Require that the bot account has a specified ChannelPermission. + /// + /// The ChannelPermission that the bot must have. Multiple permissions can be specified by ORing the permissions together. + /// + /// + /// [Command("permission")] + /// [RequireBotPermission(ChannelPermission.ManageMessages)] + /// public async Task Purge() + /// { + /// } + /// + /// + public RequireBotPermissionAttribute(ChannelPermission permission) + { + ChannelPermission = permission; + GuildPermission = null; + } + + public override async Task CheckPermissions(ICommandContext context, CommandInfo command, IDependencyMap map) + { + var guildUser = await context.Guild.GetCurrentUserAsync(); + + 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}"); + } + + if (ChannelPermission.HasValue) + { + var guildChannel = context.Channel as IGuildChannel; + + ChannelPermissions perms; + if (guildChannel != null) + perms = guildUser.GetPermissions(guildChannel); + else + perms = ChannelPermissions.All(guildChannel); + + if (!perms.Has(ChannelPermission.Value)) + return PreconditionResult.FromError($"Command 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 1cd32e72e..42d835c30 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs @@ -11,17 +11,33 @@ namespace Discord.Commands Group = 0x04 } + /// + /// Require that the command be invoked in a specified context. + /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class RequireContextAttribute : PreconditionAttribute { public ContextType Contexts { get; } + /// + /// Require that the command be invoked in a specified context. + /// + /// The type of context the command can be invoked in. Multiple contexts can be specified by ORing the contexts together. + /// + /// + /// [Command("private_only")] + /// [RequireContext(ContextType.DM | ContextType.Group)] + /// public async Task PrivateOnly() + /// { + /// } + /// + /// public RequireContextAttribute(ContextType contexts) { Contexts = contexts; } - public override Task CheckPermissions(CommandContext context, CommandInfo command, IDependencyMap map) + public override Task CheckPermissions(ICommandContext context, CommandInfo command, IDependencyMap map) { bool isValid = false; diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs new file mode 100644 index 000000000..cfedcad23 --- /dev/null +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs @@ -0,0 +1,20 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + /// + /// Require that the command is invoked by the owner of the bot. + /// + /// This precondition will only work if the bot is a bot account. + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class RequireOwnerAttribute : PreconditionAttribute + { + public override async Task CheckPermissions(ICommandContext context, CommandInfo command, IDependencyMap map) + { + 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"); + } + } +} diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequirePermissionAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs similarity index 53% rename from src/Discord.Net.Commands/Attributes/Preconditions/RequirePermissionAttribute.cs rename to src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs index 26aeac5ec..c5b79c5b9 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequirePermissionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs @@ -3,24 +3,46 @@ using System.Threading.Tasks; namespace Discord.Commands { + /// + /// This attribute requires that the user invoking the command has a specified permission. + /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] - public class RequirePermissionAttribute : PreconditionAttribute + public class RequireUserPermissionAttribute : PreconditionAttribute { public GuildPermission? GuildPermission { get; } public ChannelPermission? ChannelPermission { get; } - public RequirePermissionAttribute(GuildPermission permission) + /// + /// Require that the user invoking the command has a specified GuildPermission + /// + /// This precondition will always fail if the command is being invoked in a private channel. + /// The GuildPermission that the user must have. Multiple permissions can be specified by ORing the permissions together. + public RequireUserPermissionAttribute(GuildPermission permission) { GuildPermission = permission; ChannelPermission = null; } - public RequirePermissionAttribute(ChannelPermission permission) + /// + /// Require that the user invoking the command has a specified ChannelPermission. + /// + /// The ChannelPermission that the user must have. Multiple permissions can be specified by ORing the permissions together. + /// + /// + /// [Command("permission")] + /// [RequireUserPermission(ChannelPermission.ReadMessageHistory | ChannelPermission.ReadMessages)] + /// public async Task HasPermission() + /// { + /// await ReplyAsync("You can read messages and the message history!"); + /// } + /// + /// + public RequireUserPermissionAttribute(ChannelPermission permission) { ChannelPermission = permission; GuildPermission = null; } - public override Task CheckPermissions(CommandContext context, CommandInfo command, IDependencyMap map) + public override Task CheckPermissions(ICommandContext context, CommandInfo command, IDependencyMap map) { var guildUser = context.User as IGuildUser; diff --git a/src/Discord.Net.Commands/Builders/CommandBuilder.cs b/src/Discord.Net.Commands/Builders/CommandBuilder.cs index 9b983fd1f..27f991b16 100644 --- a/src/Discord.Net.Commands/Builders/CommandBuilder.cs +++ b/src/Discord.Net.Commands/Builders/CommandBuilder.cs @@ -12,7 +12,7 @@ namespace Discord.Commands.Builders 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; } @@ -34,7 +34,7 @@ namespace Discord.Commands.Builders _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)); @@ -72,7 +72,12 @@ namespace Discord.Commands.Builders public CommandBuilder AddAliases(params string[] aliases) { - _aliases.AddRange(aliases); + for (int i = 0; i < aliases.Length; i++) + { + var alias = aliases[i] ?? ""; + if (!_aliases.Contains(alias)) + _aliases.Add(alias); + } return this; } public CommandBuilder AddPrecondition(PreconditionAttribute precondition) @@ -80,6 +85,13 @@ namespace Discord.Commands.Builders _preconditions.Add(precondition); return this; } + public CommandBuilder AddParameter(string name, Action createFunc) + { + var param = new ParameterBuilder(this, name, typeof(T)); + createFunc(param); + _parameters.Add(param); + return this; + } public CommandBuilder AddParameter(string name, Type type, Action createFunc) { var param = new ParameterBuilder(this, name, type); diff --git a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs index 083de8e81..45c0034f2 100644 --- a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs @@ -58,9 +58,14 @@ namespace Discord.Commands.Builders return this; } - public ModuleBuilder AddAlias(params string[] newAliases) + public ModuleBuilder AddAliases(params string[] aliases) { - _aliases.AddRange(newAliases); + for (int i = 0; i < aliases.Length; i++) + { + var alias = aliases[i] ?? ""; + if (!_aliases.Contains(alias)) + _aliases.Add(alias); + } return this; } public ModuleBuilder AddPrecondition(PreconditionAttribute precondition) @@ -68,7 +73,7 @@ namespace Discord.Commands.Builders _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); @@ -97,13 +102,17 @@ namespace Discord.Commands.Builders return this; } - public ModuleInfo Build(CommandService service) + private ModuleInfo BuildImpl(CommandService service, ModuleInfo parent = null) { //Default name to first alias if (Name == null) Name = _aliases[0]; - return new ModuleInfo(this, service); + return new ModuleInfo(this, service, parent); } + + public ModuleInfo Build(CommandService service) => BuildImpl(service); + + internal ModuleInfo Build(CommandService service, ModuleInfo parent) => BuildImpl(service, parent); } } diff --git a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs index 9884f18db..82850b091 100644 --- a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -10,7 +10,7 @@ namespace Discord.Commands { internal static class ModuleClassBuilder { - private static readonly TypeInfo _moduleTypeInfo = typeof(ModuleBase).GetTypeInfo(); + private static readonly TypeInfo _moduleTypeInfo = typeof(IModuleBase).GetTypeInfo(); public static IEnumerable Search(Assembly assembly) { @@ -28,8 +28,8 @@ namespace Discord.Commands public static Dictionary Build(CommandService service, params TypeInfo[] validTypes) => Build(validTypes, service); public static Dictionary Build(IEnumerable validTypes, CommandService service) { - if (!validTypes.Any()) - throw new InvalidOperationException("Could not find any valid modules from the given selection"); + /*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); @@ -65,7 +65,8 @@ namespace Discord.Commands if (builtTypes.Contains(typeInfo)) continue; - builder.AddModule((module) => { + builder.AddModule((module) => + { BuildModule(module, typeInfo, service); BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service); }); @@ -88,17 +89,20 @@ namespace Discord.Commands else if (attribute is RemarksAttribute) builder.Remarks = (attribute as RemarksAttribute).Text; else if (attribute is AliasAttribute) - builder.AddAlias((attribute as AliasAttribute).Aliases); + builder.AddAliases((attribute as AliasAttribute).Aliases); else if (attribute is GroupAttribute) { var groupAttr = attribute as GroupAttribute; builder.Name = builder.Name ?? groupAttr.Prefix; - builder.AddAlias(groupAttr.Prefix); + builder.AddAliases(groupAttr.Prefix); } else if (attribute is PreconditionAttribute) builder.AddPrecondition(attribute as PreconditionAttribute); } + //Check for unspecified info + if (builder.Aliases.Count == 0) + builder.AddAliases(""); if (builder.Name == null) builder.Name = typeInfo.Name; @@ -106,7 +110,8 @@ namespace Discord.Commands foreach (var method in validCommands) { - builder.AddCommand((command) => { + builder.AddCommand((command) => + { BuildCommand(command, typeInfo, method, service); }); } @@ -115,7 +120,7 @@ namespace Discord.Commands private static void BuildCommand(CommandBuilder builder, TypeInfo typeInfo, MethodInfo method, CommandService service) { var attributes = method.GetCustomAttributes(); - + foreach (var attribute in attributes) { // TODO: C#7 type switch @@ -140,25 +145,33 @@ namespace Discord.Commands builder.AddPrecondition(attribute as PreconditionAttribute); } + if (builder.Name == null) + builder.Name = method.Name; + var parameters = method.GetParameters(); int pos = 0, count = parameters.Length; foreach (var paramInfo in parameters) { - builder.AddParameter((parameter) => { + builder.AddParameter((parameter) => + { BuildParameter(parameter, paramInfo, pos++, count, service); }); } - var createInstance = ReflectionUtils.CreateBuilder(typeInfo, service); + var createInstance = ReflectionUtils.CreateBuilder(typeInfo, service); - builder.Callback = (ctx, args, map) => { + builder.Callback = (ctx, args, map) => + { var instance = createInstance(map); - instance.Context = ctx; + instance.SetContext(ctx); try { - return method.Invoke(instance, args) as Task ?? Task.CompletedTask; + instance.BeforeExecute(); + return method.Invoke(instance, args) as Task ?? Task.Delay(0); } - finally{ + finally + { + instance.AfterExecute(); (instance as IDisposable)?.Dispose(); } }; @@ -179,6 +192,10 @@ namespace Discord.Commands // 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; @@ -193,23 +210,37 @@ namespace Discord.Commands } } - var reader = service.GetTypeReader(paramType); - if (reader == null) + builder.ParameterType = paramType; + + if (builder.TypeReader == null) { - var paramTypeInfo = paramType.GetTypeInfo(); - if (paramTypeInfo.IsEnum) - { - reader = EnumTypeReader.GetReader(paramType); - service.AddTypeReader(paramType, reader); - } + var readers = service.GetTypeReaders(paramType); + TypeReader reader = null; + + if (readers != null) + reader = readers.FirstOrDefault().Value; else - { - throw new InvalidOperationException($"{paramType.FullName} is not supported as a command parameter, are you missing a TypeReader?"); - } + reader = service.GetDefaultTypeReader(paramType); + + builder.TypeReader = reader; + } + } + + private static TypeReader GetTypeReader(CommandService service, Type paramType, Type typeReaderType) + { + var readers = service.GetTypeReaders(paramType); + TypeReader reader = null; + if (readers != null) + { + if (readers.TryGetValue(typeReaderType, out reader)) + return reader; } - builder.ParameterType = paramType; - builder.TypeReader = reader; + //We dont have a cached type reader, create one + reader = ReflectionUtils.CreateObject(typeReaderType.GetTypeInfo(), service, DependencyMap.Empty); + service.AddTypeReader(paramType, reader); + + return reader; } private static bool IsValidModuleDefinition(TypeInfo typeInfo) diff --git a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs index 801a10080..c9801f458 100644 --- a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs @@ -1,10 +1,15 @@ using System; +using System.Linq; using System.Reflection; +using System.Collections.Generic; + namespace Discord.Commands.Builders { public class ParameterBuilder { + private readonly List _preconditions; + public CommandBuilder Command { get; } public string Name { get; internal set; } public Type ParameterType { get; internal set; } @@ -16,16 +21,20 @@ namespace Discord.Commands.Builders public object DefaultValue { get; set; } public string Summary { get; set; } + public IReadOnlyList Preconditions => _preconditions; + //Automatic internal ParameterBuilder(CommandBuilder command) { + _preconditions = new List(); + Command = command; } //User-defined internal ParameterBuilder(CommandBuilder command, string name, Type type) : this(command) { - Preconditions.NotNull(name, nameof(name)); + Discord.Preconditions.NotNull(name, nameof(name)); Name = name; SetType(type); @@ -33,7 +42,14 @@ namespace Discord.Commands.Builders internal void SetType(Type type) { - TypeReader = Command.Module.Service.GetTypeReader(type); + var readers = Command.Module.Service.GetTypeReaders(type); + if (readers != null) + TypeReader = readers.FirstOrDefault().Value; + else + TypeReader = Command.Module.Service.GetDefaultTypeReader(type); + + if (TypeReader == null) + throw new InvalidOperationException($"{type} does not have a TypeReader registered for it"); if (type.GetTypeInfo().IsValueType) DefaultValue = Activator.CreateInstance(type); @@ -49,7 +65,7 @@ namespace Discord.Commands.Builders } public ParameterBuilder WithDefault(object defaultValue) { - DefaultValue = defaultValue; + DefaultValue = defaultValue; return this; } public ParameterBuilder WithIsOptional(bool isOptional) @@ -68,6 +84,12 @@ namespace Discord.Commands.Builders return this; } + public ParameterBuilder AddPrecondition(ParameterPreconditionAttribute precondition) + { + _preconditions.Add(precondition); + return this; + } + internal ParameterInfo Build(CommandInfo info) { if (TypeReader == null) diff --git a/src/Discord.Net.Commands/CommandContext.cs b/src/Discord.Net.Commands/CommandContext.cs index 50b41f02e..05bde56b1 100644 --- a/src/Discord.Net.Commands/CommandContext.cs +++ b/src/Discord.Net.Commands/CommandContext.cs @@ -1,6 +1,6 @@ namespace Discord.Commands { - public struct CommandContext + public class CommandContext : ICommandContext { public IDiscordClient Client { get; } public IGuild Guild { get; } @@ -9,15 +9,7 @@ public IUserMessage Message { get; } public bool IsPrivate => Channel is IPrivateChannel; - - public CommandContext(IDiscordClient client, IGuild guild, IMessageChannel channel, IUser user, IUserMessage msg) - { - Client = client; - Guild = guild; - Channel = channel; - User = user; - Message = msg; - } + public CommandContext(IDiscordClient client, IUserMessage msg) { Client = client; diff --git a/src/Discord.Net.Commands/CommandMatch.cs b/src/Discord.Net.Commands/CommandMatch.cs new file mode 100644 index 000000000..6e78b8509 --- /dev/null +++ b/src/Discord.Net.Commands/CommandMatch.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + public struct CommandMatch + { + public CommandInfo Command { get; } + public string Alias { get; } + + public CommandMatch(CommandInfo command, string alias) + { + Command = command; + 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); + } +} diff --git a/src/Discord.Net.Commands/CommandParser.cs b/src/Discord.Net.Commands/CommandParser.cs index 53ea1330f..5b4ba2480 100644 --- a/src/Discord.Net.Commands/CommandParser.cs +++ b/src/Discord.Net.Commands/CommandParser.cs @@ -13,7 +13,7 @@ namespace Discord.Commands QuotedParameter } - public static async Task ParseArgs(CommandInfo command, CommandContext context, string input, int startPos) + public static async Task ParseArgs(CommandInfo command, ICommandContext context, string input, int startPos) { ParameterInfo curParam = null; StringBuilder argBuilder = new StringBuilder(input.Length); diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 3c3760908..2c7955028 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -14,56 +14,46 @@ namespace Discord.Commands public class CommandService { private readonly SemaphoreSlim _moduleLock; - private readonly ConcurrentDictionary _typedModuleDefs; - private readonly ConcurrentDictionary _typeReaders; - private readonly ConcurrentBag _moduleDefs; + private readonly ConcurrentDictionary _typedModuleDefs; + private readonly ConcurrentDictionary> _typeReaders; + private readonly ConcurrentDictionary _defaultTypeReaders; + private readonly ImmutableList> _entityTypeReaders; //TODO: Candidate for C#7 Tuple + private readonly HashSet _moduleDefs; private readonly CommandMap _map; - public IEnumerable Modules => _typedModuleDefs.Select(x => x.Value); - public IEnumerable Commands => _typedModuleDefs.SelectMany(x => x.Value.Commands); + internal readonly bool _caseSensitive; + internal readonly char _separatorChar; + internal readonly RunMode _defaultRunMode; - public CommandService() + 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 CommandService() : this(new CommandServiceConfig()) { } + public CommandService(CommandServiceConfig config) { + _caseSensitive = config.CaseSensitiveCommands; + _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"); + _moduleLock = new SemaphoreSlim(1, 1); _typedModuleDefs = new ConcurrentDictionary(); - _moduleDefs = new ConcurrentBag(); - _map = new CommandMap(); - _typeReaders = new ConcurrentDictionary - { - [typeof(bool)] = new SimpleTypeReader(), - [typeof(char)] = new SimpleTypeReader(), - [typeof(string)] = new SimpleTypeReader(), - [typeof(byte)] = new SimpleTypeReader(), - [typeof(sbyte)] = new SimpleTypeReader(), - [typeof(ushort)] = new SimpleTypeReader(), - [typeof(short)] = new SimpleTypeReader(), - [typeof(uint)] = new SimpleTypeReader(), - [typeof(int)] = new SimpleTypeReader(), - [typeof(ulong)] = new SimpleTypeReader(), - [typeof(long)] = new SimpleTypeReader(), - [typeof(float)] = new SimpleTypeReader(), - [typeof(double)] = new SimpleTypeReader(), - [typeof(decimal)] = new SimpleTypeReader(), - [typeof(DateTime)] = new SimpleTypeReader(), - [typeof(DateTimeOffset)] = new SimpleTypeReader(), - - [typeof(IMessage)] = new MessageTypeReader(), - [typeof(IUserMessage)] = new MessageTypeReader(), - [typeof(IChannel)] = new ChannelTypeReader(), - [typeof(IDMChannel)] = new ChannelTypeReader(), - [typeof(IGroupChannel)] = new ChannelTypeReader(), - [typeof(IGuildChannel)] = new ChannelTypeReader(), - [typeof(IMessageChannel)] = new ChannelTypeReader(), - [typeof(IPrivateChannel)] = new ChannelTypeReader(), - [typeof(ITextChannel)] = new ChannelTypeReader(), - [typeof(IVoiceChannel)] = new ChannelTypeReader(), - - [typeof(IRole)] = new RoleTypeReader(), - - [typeof(IUser)] = new UserTypeReader(), - [typeof(IGroupUser)] = new UserTypeReader(), - [typeof(IGuildUser)] = new UserTypeReader(), - }; + _moduleDefs = new HashSet(); + _map = new CommandMap(this); + _typeReaders = new ConcurrentDictionary>(); + + _defaultTypeReaders = new ConcurrentDictionary(); + foreach (var type in PrimitiveParsers.SupportedTypes) + _defaultTypeReaders[type] = PrimitiveTypeReader.Create(type); + + var entityTypeReaders = ImmutableList.CreateBuilder>(); + entityTypeReaders.Add(new Tuple(typeof(IMessage), typeof(MessageTypeReader<>))); + entityTypeReaders.Add(new Tuple(typeof(IChannel), typeof(ChannelTypeReader<>))); + entityTypeReaders.Add(new Tuple(typeof(IRole), typeof(RoleTypeReader<>))); + entityTypeReaders.Add(new Tuple(typeof(IUser), typeof(UserTypeReader<>))); + _entityTypeReaders = entityTypeReaders.ToImmutable(); } //Modules @@ -99,7 +89,7 @@ namespace Discord.Commands throw new InvalidOperationException($"Could not build the module {typeof(T).FullName}, did you pass an invalid type?"); _typedModuleDefs[module.Key] = module.Value; - + return LoadModuleInternal(module.Value); } finally @@ -137,7 +127,7 @@ namespace Discord.Commands foreach (var submodule in module.Submodules) LoadModuleInternal(submodule); - + return module; } @@ -162,7 +152,7 @@ namespace Discord.Commands _typedModuleDefs.TryGetValue(typeof(T), out module); if (module == default(ModuleInfo)) return false; - + return RemoveModuleInternal(module); } finally @@ -172,10 +162,9 @@ namespace Discord.Commands } private bool RemoveModuleInternal(ModuleInfo module) { - var defsRemove = module; - if (!_moduleDefs.TryTake(out defsRemove)) + if (!_moduleDefs.Remove(module)) return false; - + foreach (var cmd in module.Commands) _map.RemoveCommand(cmd); @@ -190,26 +179,56 @@ namespace Discord.Commands //Type Readers public void AddTypeReader(TypeReader reader) { - _typeReaders[typeof(T)] = reader; + var readers = _typeReaders.GetOrAdd(typeof(T), x => new ConcurrentDictionary()); + readers[reader.GetType()] = reader; } public void AddTypeReader(Type type, TypeReader reader) { - _typeReaders[type] = reader; + var readers = _typeReaders.GetOrAdd(type, x=> new ConcurrentDictionary()); + readers[reader.GetType()] = reader; } - internal TypeReader GetTypeReader(Type type) + internal IDictionary GetTypeReaders(Type type) + { + ConcurrentDictionary definedTypeReaders; + if (_typeReaders.TryGetValue(type, out definedTypeReaders)) + return definedTypeReaders; + return null; + } + internal TypeReader GetDefaultTypeReader(Type type) { TypeReader reader; - if (_typeReaders.TryGetValue(type, out reader)) + if (_defaultTypeReaders.TryGetValue(type, out reader)) + return reader; + var typeInfo = type.GetTypeInfo(); + + //Is this an enum? + if (typeInfo.IsEnum) + { + reader = EnumTypeReader.GetReader(type); + _defaultTypeReaders[type] = reader; return reader; + } + + //Is this an entity? + for (int i = 0; i < _entityTypeReaders.Count; i++) + { + if (type == _entityTypeReaders[i].Item1 || typeInfo.ImplementedInterfaces.Contains(_entityTypeReaders[i].Item1)) + { + reader = Activator.CreateInstance(_entityTypeReaders[i].Item2.MakeGenericType(type)) as TypeReader; + _defaultTypeReaders[type] = reader; + return reader; + } + } return null; } //Execution - public SearchResult Search(CommandContext context, int argPos) => Search(context, context.Message.Content.Substring(argPos)); - public SearchResult Search(CommandContext context, string input) + public SearchResult Search(ICommandContext context, int argPos) + => Search(context, context.Message.Content.Substring(argPos)); + public SearchResult Search(ICommandContext context, string input) { - string lowerInput = input.ToLowerInvariant(); - var matches = _map.GetCommands(input).OrderByDescending(x => x.Priority).ToImmutableArray(); + 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); @@ -217,9 +236,9 @@ namespace Discord.Commands return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command."); } - public Task ExecuteAsync(CommandContext context, int argPos, IDependencyMap dependencyMap = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) + 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(CommandContext context, string input, IDependencyMap dependencyMap = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) + public async Task ExecuteAsync(ICommandContext context, string input, IDependencyMap dependencyMap = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) { dependencyMap = dependencyMap ?? DependencyMap.Empty; @@ -264,9 +283,9 @@ namespace Discord.Commands } } - return await commands[i].Execute(context, parseResult, dependencyMap).ConfigureAwait(false); + return await commands[i].ExecuteAsync(context, parseResult, dependencyMap).ConfigureAwait(false); } - + return SearchResult.FromError(CommandError.UnknownCommand, "This input does not match any overload."); } } diff --git a/src/Discord.Net.Commands/CommandServiceConfig.cs b/src/Discord.Net.Commands/CommandServiceConfig.cs new file mode 100644 index 000000000..037e315c7 --- /dev/null +++ b/src/Discord.Net.Commands/CommandServiceConfig.cs @@ -0,0 +1,12 @@ +namespace Discord.Commands +{ + public class CommandServiceConfig + { + /// The default RunMode commands should have, if one is not specified on the Command attribute or builder. + public RunMode DefaultRunMode { get; set; } = RunMode.Sync; + + public char SeparatorChar { get; set; } = ' '; + /// Should commands be case-sensitive? + public bool CaseSensitiveCommands { get; set; } = false; + } +} diff --git a/src/Discord.Net.Commands/Dependencies/DependencyMap.cs b/src/Discord.Net.Commands/Dependencies/DependencyMap.cs index ba5995f26..f5adf1a8c 100644 --- a/src/Discord.Net.Commands/Dependencies/DependencyMap.cs +++ b/src/Discord.Net.Commands/Dependencies/DependencyMap.cs @@ -5,27 +5,59 @@ namespace Discord.Commands { public class DependencyMap : IDependencyMap { - private Dictionary map; + private Dictionary> map; public static DependencyMap Empty => new DependencyMap(); public DependencyMap() { - map = new Dictionary(); + map = new Dictionary>(); } - public void Add(T obj) + /// + 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, obj); + 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; @@ -35,6 +67,7 @@ namespace Discord.Commands return result; } + /// public bool TryGet(out T result) { object untypedResult; @@ -49,9 +82,17 @@ namespace Discord.Commands return false; } } + /// public bool TryGet(Type t, out object result) { - return map.TryGetValue(t, out 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 index 784a9bc56..a55a9e4c5 100644 --- a/src/Discord.Net.Commands/Dependencies/IDependencyMap.cs +++ b/src/Discord.Net.Commands/Dependencies/IDependencyMap.cs @@ -4,12 +4,86 @@ namespace Discord.Commands { public interface IDependencyMap { - void Add(T obj); + /// + /// 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 63ffafd2c..452b52f21 100644 --- a/src/Discord.Net.Commands/Discord.Net.Commands.csproj +++ b/src/Discord.Net.Commands/Discord.Net.Commands.csproj @@ -1,41 +1,26 @@ - - + - A Discord.Net extension adding support for bot commands. - 1.0.0-beta2 - netstandard1.3 + 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 - $(PackageTargetFallback);dotnet5.4;dnxcore50;portable-net45+win8 + Discord.Commands + true - - - - - - - - - 1.0.0-alpha-20161104-2 - All - - - - - False - - $(DefineConstants);RELEASE $(NoWarn);CS1573;CS1591 true true - \ No newline at end of file diff --git a/src/Discord.Net.Commands/IModuleBase.cs b/src/Discord.Net.Commands/IModuleBase.cs new file mode 100644 index 000000000..fda768b53 --- /dev/null +++ b/src/Discord.Net.Commands/IModuleBase.cs @@ -0,0 +1,11 @@ +namespace Discord.Commands +{ + internal interface IModuleBase + { + void SetContext(ICommandContext context); + + void BeforeExecute(); + + void AfterExecute(); + } +} diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index 4d546b6fa..031d37581 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -7,15 +7,17 @@ using System.Threading.Tasks; using System.Reflection; using Discord.Commands.Builders; +using System.Diagnostics; namespace Discord.Commands { + [DebuggerDisplay("{Name,nq}")] public class CommandInfo { 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; } @@ -37,13 +39,21 @@ namespace Discord.Commands Summary = builder.Summary; Remarks = builder.Remarks; - RunMode = builder.RunMode; + RunMode = (builder.RunMode == RunMode.Default ? service._defaultRunMode : builder.RunMode); Priority = builder.Priority; - if (module.Aliases.Count != 0) - Aliases = module.Aliases.Permutate(builder.Aliases, (first, second) => first + " " + second).ToImmutableArray(); - else - Aliases = builder.Aliases.ToImmutableArray(); + Aliases = module.Aliases + .Permutate(builder.Aliases, (first, second) => + { + if (first == "") + return second; + else if (second == "") + return first; + else + return first + service._separatorChar + second; + }) + .Select(x => service._caseSensitive ? x : x.ToLowerInvariant()) + .ToImmutableArray(); Preconditions = builder.Preconditions.ToImmutableArray(); @@ -53,7 +63,7 @@ namespace Discord.Commands _action = builder.Callback; } - public async Task CheckPreconditionsAsync(CommandContext context, IDependencyMap map = null) + public async Task CheckPreconditionsAsync(ICommandContext context, IDependencyMap map = null) { if (map == null) map = DependencyMap.Empty; @@ -74,30 +84,19 @@ namespace Discord.Commands return PreconditionResult.FromSuccess(); } - - public async Task ParseAsync(CommandContext context, SearchResult searchResult, PreconditionResult? preconditionResult = null) + + public async Task ParseAsync(ICommandContext context, int startIndex, SearchResult searchResult, PreconditionResult? preconditionResult = null) { if (!searchResult.IsSuccess) return ParseResult.FromError(searchResult); if (preconditionResult != null && !preconditionResult.Value.IsSuccess) return ParseResult.FromError(preconditionResult.Value); - string input = searchResult.Text; - var matchingAliases = Aliases.Where(alias => input.StartsWith(alias)); - - string matchingAlias = ""; - foreach (string alias in matchingAliases) - { - if (alias.Length > matchingAlias.Length) - matchingAlias = alias; - } - - input = input.Substring(matchingAlias.Length); - + string input = searchResult.Text.Substring(startIndex); return await CommandParser.ParseArgs(this, context, input, 0).ConfigureAwait(false); } - public Task Execute(CommandContext context, ParseResult parseResult, IDependencyMap map) + public Task ExecuteAsync(ICommandContext context, ParseResult parseResult, IDependencyMap map) { if (!parseResult.IsSuccess) return Task.FromResult(ExecuteResult.FromError(parseResult)); @@ -120,14 +119,22 @@ namespace Discord.Commands return ExecuteAsync(context, argList, paramList, map); } - public async Task ExecuteAsync(CommandContext context, IEnumerable argList, IEnumerable paramList, IDependencyMap map) + public async Task ExecuteAsync(ICommandContext context, IEnumerable argList, IEnumerable paramList, IDependencyMap map) { if (map == null) map = DependencyMap.Empty; try { - var args = GenerateArgs(argList, paramList); + object[] args = GenerateArgs(argList, paramList); + + foreach (var parameter in Parameters) + { + var result = await parameter.CheckPreconditionsAsync(context, args, map).ConfigureAwait(false); + if (!result.IsSuccess) + return ExecuteResult.FromError(result); + } + switch (RunMode) { case RunMode.Sync: //Always sync diff --git a/src/Discord.Net.Commands/Info/ModuleInfo.cs b/src/Discord.Net.Commands/Info/ModuleInfo.cs index 64fa29ea2..a2094df65 100644 --- a/src/Discord.Net.Commands/Info/ModuleInfo.cs +++ b/src/Discord.Net.Commands/Info/ModuleInfo.cs @@ -14,61 +14,61 @@ namespace Discord.Commands public string Remarks { get; } public IReadOnlyList Aliases { get; } - public IEnumerable Commands { get; } + public IReadOnlyList Commands { get; } public IReadOnlyList Preconditions { get; } public IReadOnlyList Submodules { get; } + public ModuleInfo Parent { get; } + public bool IsSubmodule => Parent != null; - internal ModuleInfo(ModuleBuilder builder, CommandService service) + internal ModuleInfo(ModuleBuilder builder, CommandService service, ModuleInfo parent = null) { Service = service; Name = builder.Name; Summary = builder.Summary; Remarks = builder.Remarks; + Parent = parent; - Aliases = BuildAliases(builder).ToImmutableArray(); - Commands = builder.Commands.Select(x => x.Build(this, service)); + Aliases = BuildAliases(builder, service).ToImmutableArray(); + Commands = builder.Commands.Select(x => x.Build(this, service)).ToImmutableArray(); Preconditions = BuildPreconditions(builder).ToImmutableArray(); Submodules = BuildSubmodules(builder, service).ToImmutableArray(); } - private static IEnumerable BuildAliases(ModuleBuilder builder) + private static IEnumerable BuildAliases(ModuleBuilder builder, CommandService service) { - IEnumerable result = null; + var result = builder.Aliases.ToList(); + var builderQueue = new Queue(); - Stack builderStack = new Stack(); - builderStack.Push(builder); + var parent = builder; + while ((parent = parent.Parent) != null) + builderQueue.Enqueue(parent); - ModuleBuilder parent = builder.Parent; - while (parent != null) + while (builderQueue.Count > 0) { - builderStack.Push(parent); - parent = parent.Parent; - } - - while (builderStack.Count() > 0) - { - ModuleBuilder level = builderStack.Pop(); // get the topmost builder - if (result == null) - result = level.Aliases.ToList(); // create a shallow copy so we don't overwrite the builder unexpectedly - else if (result.Count() > level.Aliases.Count) - result = result.Permutate(level.Aliases, (first, second) => first + " " + second); - else - result = level.Aliases.Permutate(result, (second, first) => first + " " + second); + var level = builderQueue.Dequeue(); + // permute in reverse because we want to *prefix* our aliases + result = level.Aliases.Permutate(result, (first, second) => + { + if (first == "") + return second; + else if (second == "") + return first; + else + return first + service._separatorChar + second; + }).ToList(); } return result; } - private static List BuildSubmodules(ModuleBuilder parent, CommandService service) + private List BuildSubmodules(ModuleBuilder parent, CommandService service) { var result = new List(); foreach (var submodule in parent.Modules) - { - result.Add(submodule.Build(service)); - } + result.Add(submodule.Build(service, this)); return result; } @@ -87,4 +87,4 @@ namespace Discord.Commands return result; } } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Commands/Info/ParameterInfo.cs b/src/Discord.Net.Commands/Info/ParameterInfo.cs index 18c5e653c..a0cdf03d7 100644 --- a/src/Discord.Net.Commands/Info/ParameterInfo.cs +++ b/src/Discord.Net.Commands/Info/ParameterInfo.cs @@ -1,5 +1,6 @@ using System; -using System.Linq; +using System.Collections.Generic; +using System.Collections.Immutable; using System.Threading.Tasks; using Discord.Commands.Builders; @@ -10,6 +11,17 @@ namespace Discord.Commands { private readonly TypeReader _reader; + public CommandInfo Command { get; } + public string Name { get; } + public string Summary { get; } + public bool IsOptional { get; } + public bool IsRemainder { get; } + public bool IsMultiple { get; } + public Type Type { get; } + public object DefaultValue { get; } + + public IReadOnlyList Preconditions { get; } + internal ParameterInfo(ParameterBuilder builder, CommandInfo command, CommandService service) { Command = command; @@ -23,19 +35,32 @@ namespace Discord.Commands Type = builder.ParameterType; DefaultValue = builder.DefaultValue; + Preconditions = builder.Preconditions.ToImmutableArray(); + _reader = builder.TypeReader; } - public CommandInfo Command { get; } - public string Name { get; } - public string Summary { get; } - public bool IsOptional { get; } - public bool IsRemainder { get; } - public bool IsMultiple { get; } - public Type Type { get; } - public object DefaultValue { get; } + public async Task CheckPreconditionsAsync(ICommandContext context, object[] args, IDependencyMap map = null) + { + if (map == null) + map = DependencyMap.Empty; + + int position = 0; + for(position = 0; position < Command.Parameters.Count; position++) + if (Command.Parameters[position] == this) + break; + + foreach (var precondition in Preconditions) + { + var result = await precondition.CheckPermissions(context, this, args[position], map).ConfigureAwait(false); + if (!result.IsSuccess) + return result; + } + + return PreconditionResult.FromSuccess(); + } - public async Task Parse(CommandContext context, string input) + public async Task Parse(ICommandContext context, string input) { return await _reader.Read(context, input).ConfigureAwait(false); } diff --git a/src/Discord.Net.Commands/Map/CommandMap.cs b/src/Discord.Net.Commands/Map/CommandMap.cs index 3a5239878..bcff800d3 100644 --- a/src/Discord.Net.Commands/Map/CommandMap.cs +++ b/src/Discord.Net.Commands/Map/CommandMap.cs @@ -4,36 +4,30 @@ namespace Discord.Commands { internal class CommandMap { + private readonly CommandService _service; private readonly CommandMapNode _root; private static readonly string[] _blankAliases = new[] { "" }; - public CommandMap() + public CommandMap(CommandService service) { + _service = service; _root = new CommandMapNode(""); } public void AddCommand(CommandInfo command) { - foreach (string text in GetAliases(command)) - _root.AddCommand(text, 0, command); + foreach (string text in command.Aliases) + _root.AddCommand(_service, text, 0, command); } public void RemoveCommand(CommandInfo command) { - foreach (string text in GetAliases(command)) - _root.RemoveCommand(text, 0, command); + foreach (string text in command.Aliases) + _root.RemoveCommand(_service, text, 0, command); } - public IEnumerable GetCommands(string text) + public IEnumerable GetCommands(string text) { - return _root.GetCommands(text, 0); - } - - private IReadOnlyList GetAliases(CommandInfo command) - { - var aliases = command.Aliases; - if (aliases.Count == 0) - return _blankAliases; - return aliases; + return _root.GetCommands(_service, text, 0, text != ""); } } } diff --git a/src/Discord.Net.Commands/Map/CommandMapNode.cs b/src/Discord.Net.Commands/Map/CommandMapNode.cs index a86c0643d..863409207 100644 --- a/src/Discord.Net.Commands/Map/CommandMapNode.cs +++ b/src/Discord.Net.Commands/Map/CommandMapNode.cs @@ -7,7 +7,7 @@ namespace Discord.Commands { internal class CommandMapNode { - private static readonly char[] _whitespaceChars = new char[] { ' ', '\r', '\n' }; + private static readonly char[] _whitespaceChars = new[] { ' ', '\r', '\n' }; private readonly ConcurrentDictionary _nodes; private readonly string _name; @@ -23,9 +23,9 @@ namespace Discord.Commands _commands = ImmutableArray.Create(); } - public void AddCommand(string text, int index, CommandInfo command) + public void AddCommand(CommandService service, string text, int index, CommandInfo command) { - int nextSpace = NextWhitespace(text, index); + int nextSegment = NextSegment(text, index, service._separatorChar); string name; lock (_lockObj) @@ -38,19 +38,20 @@ namespace Discord.Commands } else { - if (nextSpace == -1) + if (nextSegment == -1) name = text.Substring(index); else - name = text.Substring(index, nextSpace - index); + name = text.Substring(index, nextSegment - index); - var nextNode = _nodes.GetOrAdd(name, x => new CommandMapNode(x)); - nextNode.AddCommand(nextSpace == -1 ? "" : text, nextSpace + 1, command); + string fullName = _name == "" ? name : _name + service._separatorChar + name; + var nextNode = _nodes.GetOrAdd(name, x => new CommandMapNode(fullName)); + nextNode.AddCommand(service, nextSegment == -1 ? "" : text, nextSegment + 1, command); } } } - public void RemoveCommand(string text, int index, CommandInfo command) + public void RemoveCommand(CommandService service, string text, int index, CommandInfo command) { - int nextSpace = NextWhitespace(text, index); + int nextSegment = NextSegment(text, index, service._separatorChar); string name; lock (_lockObj) @@ -59,15 +60,15 @@ namespace Discord.Commands _commands = _commands.Remove(command); else { - if (nextSpace == -1) + if (nextSegment == -1) name = text.Substring(index); else - name = text.Substring(index, nextSpace - index); + name = text.Substring(index, nextSegment - index); CommandMapNode nextNode; if (_nodes.TryGetValue(name, out nextNode)) { - nextNode.RemoveCommand(nextSpace == -1 ? "" : text, nextSpace + 1, command); + nextNode.RemoveCommand(service, nextSegment == -1 ? "" : text, nextSegment + 1, command); if (nextNode.IsEmpty) _nodes.TryRemove(name, out nextNode); } @@ -75,39 +76,58 @@ namespace Discord.Commands } } - public IEnumerable GetCommands(string text, int index) + public IEnumerable GetCommands(CommandService service, string text, int index, bool visitChildren = true) { - int nextSpace = NextWhitespace(text, index); - string name; - var commands = _commands; for (int i = 0; i < commands.Length; i++) - yield return _commands[i]; + yield return new CommandMatch(_commands[i], _name); - if (text != "") + if (visitChildren) { - if (nextSpace == -1) + string name; + CommandMapNode nextNode; + + //Search for next segment + int nextSegment = NextSegment(text, index, service._separatorChar); + if (nextSegment == -1) name = text.Substring(index); else - name = text.Substring(index, nextSpace - index); - - CommandMapNode nextNode; + name = text.Substring(index, nextSegment - index); if (_nodes.TryGetValue(name, out nextNode)) { - foreach (var cmd in nextNode.GetCommands(nextSpace == -1 ? "" : text, nextSpace + 1)) + foreach (var cmd in nextNode.GetCommands(service, nextSegment == -1 ? "" : text, nextSegment + 1, true)) yield return cmd; } + + //Check if this is the last command segment before args + nextSegment = NextSegment(text, index, _whitespaceChars, service._separatorChar); + if (nextSegment != -1) + { + name = text.Substring(index, nextSegment - index); + if (_nodes.TryGetValue(name, out nextNode)) + { + foreach (var cmd in nextNode.GetCommands(service, nextSegment == -1 ? "" : text, nextSegment + 1, false)) + yield return cmd; + } + } } } - private static int NextWhitespace(string text, int startIndex) + private static int NextSegment(string text, int startIndex, char separator) + { + return text.IndexOf(separator, startIndex); + } + private static int NextSegment(string text, int startIndex, char[] separators, char except) { int lowest = int.MaxValue; - for (int i = 0; i < _whitespaceChars.Length; i++) + for (int i = 0; i < separators.Length; i++) { - int index = text.IndexOf(_whitespaceChars[i], startIndex); - if (index != -1 && index < lowest) - lowest = index; + if (separators[i] != except) + { + int index = text.IndexOf(separators[i], startIndex); + if (index != -1 && index < lowest) + lowest = index; + } } return (lowest != int.MaxValue) ? lowest : -1; } diff --git a/src/Discord.Net.Commands/ModuleBase.cs b/src/Discord.Net.Commands/ModuleBase.cs index 96dedcb23..a38ffce06 100644 --- a/src/Discord.Net.Commands/ModuleBase.cs +++ b/src/Discord.Net.Commands/ModuleBase.cs @@ -1,14 +1,39 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; namespace Discord.Commands { - public abstract class ModuleBase + public abstract class ModuleBase : ModuleBase { } + + public abstract class ModuleBase : IModuleBase + where T : class, ICommandContext { - public CommandContext Context { get; internal set; } + public T Context { get; private set; } - protected virtual async Task ReplyAsync(string message, bool isTTS = false, EmbedBuilder embed = null, RequestOptions options = null) + protected virtual async Task ReplyAsync(string message, bool isTTS = false, Embed embed = null, RequestOptions options = null) { return await Context.Channel.SendMessageAsync(message, isTTS, embed, options).ConfigureAwait(false); } + + protected virtual void BeforeExecute() + { + } + + protected virtual void AfterExecute() + { + } + + //IModuleBase + 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; + } + + void IModuleBase.BeforeExecute() => BeforeExecute(); + + void IModuleBase.AfterExecute() => AfterExecute(); } } diff --git a/src/Discord.Net.Commands/PrimitiveParsers.cs b/src/Discord.Net.Commands/PrimitiveParsers.cs index 5e3dcd68a..623ddafa7 100644 --- a/src/Discord.Net.Commands/PrimitiveParsers.cs +++ b/src/Discord.Net.Commands/PrimitiveParsers.cs @@ -8,9 +8,11 @@ namespace Discord.Commands internal static class PrimitiveParsers { - private static readonly IReadOnlyDictionary _parsers; + private static readonly Lazy> _parsers = new Lazy>(CreateParsers); - static PrimitiveParsers() + public static IEnumerable SupportedTypes = _parsers.Value.Keys; + + static IReadOnlyDictionary CreateParsers() { var parserBuilder = ImmutableDictionary.CreateBuilder(); parserBuilder[typeof(bool)] = (TryParseDelegate)bool.TryParse; @@ -27,16 +29,17 @@ namespace Discord.Commands parserBuilder[typeof(decimal)] = (TryParseDelegate)decimal.TryParse; parserBuilder[typeof(DateTime)] = (TryParseDelegate)DateTime.TryParse; 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; }; - _parsers = parserBuilder.ToImmutable(); + return parserBuilder.ToImmutable(); } - public static TryParseDelegate Get() => (TryParseDelegate)_parsers[typeof(T)]; - public static Delegate Get(Type type) => _parsers[type]; + public static TryParseDelegate Get() => (TryParseDelegate)_parsers.Value[typeof(T)]; + public static Delegate Get(Type type) => _parsers.Value[type]; } } diff --git a/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs b/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs index e05c02abb..08821c62f 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(CommandContext context, string input) + public override async Task Read(ICommandContext context, string input) { if (context.Guild != null) { diff --git a/src/Discord.Net.Commands/Readers/EnumTypeReader.cs b/src/Discord.Net.Commands/Readers/EnumTypeReader.cs index 81870ecaf..7b2ff505a 100644 --- a/src/Discord.Net.Commands/Readers/EnumTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/EnumTypeReader.cs @@ -44,7 +44,7 @@ namespace Discord.Commands _enumsByValue = byValueBuilder.ToImmutable(); } - public override Task Read(CommandContext context, string input) + public override Task Read(ICommandContext context, string input) { T baseValue; object enumValue; diff --git a/src/Discord.Net.Commands/Readers/MessageTypeReader.cs b/src/Discord.Net.Commands/Readers/MessageTypeReader.cs index 57bfc21cd..9baa1901a 100644 --- a/src/Discord.Net.Commands/Readers/MessageTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/MessageTypeReader.cs @@ -6,7 +6,7 @@ namespace Discord.Commands internal class MessageTypeReader : TypeReader where T : class, IMessage { - public override async Task Read(CommandContext context, string input) + public override async Task Read(ICommandContext context, string input) { ulong id; diff --git a/src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs b/src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs new file mode 100644 index 000000000..aa4c7c7a4 --- /dev/null +++ b/src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs @@ -0,0 +1,32 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + internal static class PrimitiveTypeReader + { + public static TypeReader Create(Type type) + { + type = typeof(PrimitiveTypeReader<>).MakeGenericType(type); + return Activator.CreateInstance(type) as TypeReader; + } + } + + internal class PrimitiveTypeReader : TypeReader + { + private readonly TryParseDelegate _tryParse; + + public PrimitiveTypeReader() + { + _tryParse = PrimitiveParsers.Get(); + } + + public override Task Read(ICommandContext context, string input) + { + T value; + if (_tryParse(input, out value)) + return Task.FromResult(TypeReaderResult.FromSuccess(value)); + 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 66b76b7e7..48544eeda 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(CommandContext context, string input) + public override Task Read(ICommandContext context, string input) { ulong id; diff --git a/src/Discord.Net.Commands/Readers/SimpleTypeReader.cs b/src/Discord.Net.Commands/Readers/SimpleTypeReader.cs deleted file mode 100644 index ad939e59d..000000000 --- a/src/Discord.Net.Commands/Readers/SimpleTypeReader.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Threading.Tasks; - -namespace Discord.Commands -{ - internal class SimpleTypeReader : TypeReader - { - private readonly TryParseDelegate _tryParse; - - public SimpleTypeReader() - { - _tryParse = PrimitiveParsers.Get(); - } - - public override Task Read(CommandContext context, string input) - { - T value; - if (_tryParse(input, out value)) - return Task.FromResult(TypeReaderResult.FromSuccess(value)); - return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, $"Failed to parse {typeof(T).Name}")); - } - } -} diff --git a/src/Discord.Net.Commands/Readers/TypeReader.cs b/src/Discord.Net.Commands/Readers/TypeReader.cs index 23562cb16..d53491e92 100644 --- a/src/Discord.Net.Commands/Readers/TypeReader.cs +++ b/src/Discord.Net.Commands/Readers/TypeReader.cs @@ -4,6 +4,6 @@ namespace Discord.Commands { public abstract class TypeReader { - public abstract Task Read(CommandContext context, string input); + public abstract Task Read(ICommandContext context, string input); } } diff --git a/src/Discord.Net.Commands/Readers/UserTypeReader.cs b/src/Discord.Net.Commands/Readers/UserTypeReader.cs index 31bdd0b58..a5f92a277 100644 --- a/src/Discord.Net.Commands/Readers/UserTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/UserTypeReader.cs @@ -10,7 +10,7 @@ namespace Discord.Commands internal class UserTypeReader : TypeReader where T : class, IUser { - public override async Task Read(CommandContext context, string input) + public override async Task Read(ICommandContext context, string input) { var results = new Dictionary(); IReadOnlyCollection channelUsers = (await context.Channel.GetUsersAsync(CacheMode.CacheOnly).Flatten().ConfigureAwait(false)).ToArray(); //TODO: must be a better way? @@ -46,13 +46,13 @@ namespace Discord.Commands ushort discriminator; if (ushort.TryParse(input.Substring(index + 1), out discriminator)) { - var channelUser = channelUsers.Where(x => x.DiscriminatorValue == discriminator && - string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); - AddResult(results, channelUser as T, channelUser.Username == username ? 0.85f : 0.75f); + 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.Where(x => x.DiscriminatorValue == discriminator && - string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); - AddResult(results, guildUser as T, guildUser.Username == username ? 0.80f : 0.70f); + var guildUser = channelUsers.FirstOrDefault(x => x.DiscriminatorValue == discriminator && + string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase)); + AddResult(results, guildUser as T, guildUser?.Username == username ? 0.80f : 0.70f); } } diff --git a/src/Discord.Net.Commands/Results/ParseResult.cs b/src/Discord.Net.Commands/Results/ParseResult.cs index 0f024cb44..d4a9af521 100644 --- a/src/Discord.Net.Commands/Results/ParseResult.cs +++ b/src/Discord.Net.Commands/Results/ParseResult.cs @@ -14,10 +14,10 @@ namespace Discord.Commands public bool IsSuccess => !Error.HasValue; - private ParseResult(IReadOnlyList argValues, IReadOnlyList paramValue, CommandError? error, string errorReason) + private ParseResult(IReadOnlyList argValues, IReadOnlyList paramValues, CommandError? error, string errorReason) { ArgValues = argValues; - ParamValues = paramValue; + ParamValues = paramValues; Error = error; ErrorReason = errorReason; } diff --git a/src/Discord.Net.Commands/Results/SearchResult.cs b/src/Discord.Net.Commands/Results/SearchResult.cs index 17942b61a..87d900d4d 100644 --- a/src/Discord.Net.Commands/Results/SearchResult.cs +++ b/src/Discord.Net.Commands/Results/SearchResult.cs @@ -7,14 +7,14 @@ namespace Discord.Commands public struct SearchResult : IResult { public string Text { get; } - public IReadOnlyList Commands { get; } + public IReadOnlyList Commands { get; } public CommandError? Error { get; } public string ErrorReason { get; } public bool IsSuccess => !Error.HasValue; - private SearchResult(string text, IReadOnlyList commands, CommandError? error, string errorReason) + private SearchResult(string text, IReadOnlyList commands, CommandError? error, string errorReason) { Text = text; Commands = commands; @@ -22,7 +22,7 @@ namespace Discord.Commands ErrorReason = errorReason; } - public static SearchResult FromSuccess(string text, IReadOnlyList commands) + public static SearchResult FromSuccess(string text, IReadOnlyList commands) => new SearchResult(text, commands, null, null); public static SearchResult FromError(CommandError error, string reason) => new SearchResult(null, null, error, reason); diff --git a/src/Discord.Net.Commands/RunMode.cs b/src/Discord.Net.Commands/RunMode.cs index 0799f825c..2bb5dbbf6 100644 --- a/src/Discord.Net.Commands/RunMode.cs +++ b/src/Discord.Net.Commands/RunMode.cs @@ -2,6 +2,7 @@ { public enum RunMode { + Default, Sync, Mixed, Async diff --git a/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs index 27ea601bf..1333b9640 100644 --- a/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs +++ b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs @@ -19,6 +19,9 @@ namespace Discord.Commands 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) => { @@ -27,28 +30,40 @@ namespace Discord.Commands for (int i = 0; i < parameters.Length; i++) { var parameter = parameters[i]; - object arg; - if (map == null || !map.TryGet(parameter.ParameterType, out arg)) - { - if (parameter.ParameterType == typeof(CommandService)) - arg = service; - else if (parameter.ParameterType == typeof(IDependencyMap)) - arg = map; - else - throw new InvalidOperationException($"Failed to create \"{typeInfo.FullName}\", dependency \"{parameter.ParameterType.Name}\" was not found."); - } - args[i] = arg; + args[i] = GetMember(parameter.ParameterType, map, service, typeInfo); } + T obj; try { - return (T)constructor.Invoke(args); + obj = (T)constructor.Invoke(args); } catch (Exception ex) { throw new Exception($"Failed to create \"{typeInfo.FullName}\"", ex); } + + foreach(var property in properties) + { + property.SetValue(obj, GetMember(property.PropertyType, map, service, typeInfo)); + } + return obj; }; } + + internal static object GetMember(Type targetType, IDependencyMap map, CommandService service, TypeInfo baseType) + { + object arg; + if (map == null || !map.TryGet(targetType, out arg)) + { + 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."); + } + return arg; + } } } diff --git a/src/Discord.Net.Commands/project.json b/src/Discord.Net.Commands/project.json deleted file mode 100644 index a87591c03..000000000 --- a/src/Discord.Net.Commands/project.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "version": "1.0.0-*", - "description": "A Discord.Net extension adding support for bot commands.", - "authors": [ "RogueException" ], - - "packOptions": { - "tags": [ "discord", "discordapp" ], - "licenseUrl": "http://opensource.org/licenses/MIT", - "projectUrl": "https://github.com/RogueException/Discord.Net", - "repository": { - "type": "git", - "url": "git://github.com/RogueException/Discord.Net" - } - }, - - "configurations": { - "Release": { - "buildOptions": { - "define": [ "RELEASE" ], - "nowarn": [ "CS1573", "CS1591" ], - "optimize": true, - "warningsAsErrors": true, - "xmlDoc": true - } - } - }, - - "dependencies": { - "Discord.Net.Core": { - "target": "project" - } - }, - - "frameworks": { - "netstandard1.3": { - "imports": [ - "dotnet5.4", - "dnxcore50", - "portable-net45+win8" - ] - } - } -} \ No newline at end of file diff --git a/src/Discord.Net.Core/AssemblyInfo.cs b/src/Discord.Net.Core/AssemblyInfo.cs index 8563c4035..c75729acf 100644 --- a/src/Discord.Net.Core/AssemblyInfo.cs +++ b/src/Discord.Net.Core/AssemblyInfo.cs @@ -1,5 +1,6 @@ using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("Discord.Net.Relay")] [assembly: InternalsVisibleTo("Discord.Net.Rest")] [assembly: InternalsVisibleTo("Discord.Net.Rpc")] [assembly: InternalsVisibleTo("Discord.Net.WebSocket")] diff --git a/src/Discord.Net.Core/Audio/AudioApplication.cs b/src/Discord.Net.Core/Audio/AudioApplication.cs new file mode 100644 index 000000000..276d934b2 --- /dev/null +++ b/src/Discord.Net.Core/Audio/AudioApplication.cs @@ -0,0 +1,9 @@ +namespace Discord.Audio +{ + public enum AudioApplication : int + { + Voice, + Music, + Mixed + } +} \ No newline at end of file diff --git a/src/Discord.Net.Core/Audio/AudioInStream.cs b/src/Discord.Net.Core/Audio/AudioInStream.cs new file mode 100644 index 000000000..a6b5c5e6b --- /dev/null +++ b/src/Discord.Net.Core/Audio/AudioInStream.cs @@ -0,0 +1,41 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Audio +{ + public abstract class AudioInStream : Stream + { + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => true; + + public abstract Task ReadFrameAsync(CancellationToken cancelToken); + + 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 long Length { get { throw new NotSupportedException(); } } + public override long Position + { + get { throw new NotSupportedException(); } + set { 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/AudioOutStream.cs b/src/Discord.Net.Core/Audio/AudioOutStream.cs new file mode 100644 index 000000000..2b4b012ee --- /dev/null +++ b/src/Discord.Net.Core/Audio/AudioOutStream.cs @@ -0,0 +1,41 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Audio +{ + public abstract class AudioOutStream : Stream + { + 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/IAudioClient.cs b/src/Discord.Net.Core/Audio/IAudioClient.cs index 3cfdfa856..bea44fcf4 100644 --- a/src/Discord.Net.Core/Audio/IAudioClient.cs +++ b/src/Discord.Net.Core/Audio/IAudioClient.cs @@ -1,10 +1,9 @@ using System; -using System.IO; using System.Threading.Tasks; namespace Discord.Audio { - public interface IAudioClient + public interface IAudioClient : IDisposable { event Func Connected; event Func Disconnected; @@ -15,9 +14,33 @@ namespace Discord.Audio /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. int Latency { get; } - Task DisconnectAsync(); + Task StopAsync(); - Stream CreateOpusStream(int samplesPerFrame, int bufferSize = 4000); - Stream CreatePCMStream(int samplesPerFrame, int? bitrate = null, OpusApplication application = OpusApplication.MusicOrMixed, int bufferSize = 4000); + /// + /// 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); } } diff --git a/src/Discord.Net.Core/Audio/RTPFrame.cs b/src/Discord.Net.Core/Audio/RTPFrame.cs new file mode 100644 index 000000000..5005870f4 --- /dev/null +++ b/src/Discord.Net.Core/Audio/RTPFrame.cs @@ -0,0 +1,16 @@ +namespace Discord.Audio +{ + public struct RTPFrame + { + public readonly ushort Sequence; + public readonly uint Timestamp; + public readonly byte[] Payload; + + public RTPFrame(ushort sequence, uint timestamp, byte[] payload) + { + Sequence = sequence; + Timestamp = timestamp; + Payload = payload; + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Core/API/CDN.cs b/src/Discord.Net.Core/CDN.cs similarity index 87% rename from src/Discord.Net.Core/API/CDN.cs rename to src/Discord.Net.Core/CDN.cs index e4fcbc8c4..b7a5346ea 100644 --- a/src/Discord.Net.Core/API/CDN.cs +++ b/src/Discord.Net.Core/CDN.cs @@ -1,11 +1,11 @@ -namespace Discord.API +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) - => avatarId != null ? $"{DiscordConfig.CDNUrl}avatars/{userId}/{avatarId}.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 GetGuildIconUrl(ulong guildId, string iconId) => iconId != null ? $"{DiscordConfig.CDNUrl}icons/{guildId}/{iconId}.jpg" : null; public static string GetGuildSplashUrl(ulong guildId, string splashId) diff --git a/src/Discord.Net.Core/Commands/ICommandContext.cs b/src/Discord.Net.Core/Commands/ICommandContext.cs new file mode 100644 index 000000000..ac1424339 --- /dev/null +++ b/src/Discord.Net.Core/Commands/ICommandContext.cs @@ -0,0 +1,11 @@ +namespace Discord.Commands +{ + public interface ICommandContext + { + IDiscordClient Client { get; } + IGuild Guild { get; } + IMessageChannel Channel { get; } + IUser User { get; } + IUserMessage Message { get; } + } +} diff --git a/src/Discord.Net.Core/Discord.Net.Core.csproj b/src/Discord.Net.Core/Discord.Net.Core.csproj index b2d9fc870..9f75d7327 100644 --- a/src/Discord.Net.Core/Discord.Net.Core.csproj +++ b/src/Discord.Net.Core/Discord.Net.Core.csproj @@ -1,60 +1,36 @@ - - + - A .Net API wrapper and bot framework for Discord. - 1.0.0-beta2 - netstandard1.3 + 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 - $(PackageTargetFallback);dotnet5.4;dnxcore50;portable-net45+win8 + Discord + true - - - + + + + + + + + + + + - - - - 1.0.0-alpha-20161104-2 - All - - - 4.3.0 - - - 9.0.1 - - - 4.3.0 - - - 1.3.0 - - - 3.1.0 - - - 4.3.0 - - - 4.3.0 - All - - - - - False - - $(DefineConstants);RELEASE $(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 b35f0d745..78a5b0e1e 100644 --- a/src/Discord.Net.Core/DiscordConfig.cs +++ b/src/Discord.Net.Core/DiscordConfig.cs @@ -19,7 +19,13 @@ namespace Discord public const int MaxMessagesPerBatch = 100; public const int MaxUsersPerBatch = 1000; + /// 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. public LogSeverity LogLevel { get; set; } = LogSeverity.Info; + + /// Gets or sets whether the initial log entry should be printed. + internal bool DisplayInitialLog { get; set; } = true; } } diff --git a/src/Discord.Net.Core/Entities/Channels/BulkGuildChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/BulkGuildChannelProperties.cs new file mode 100644 index 000000000..2358b2e2e --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/BulkGuildChannelProperties.cs @@ -0,0 +1,20 @@ +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/Messages/Direction.cs b/src/Discord.Net.Core/Entities/Channels/Direction.cs similarity index 100% rename from src/Discord.Net.Core/Entities/Messages/Direction.cs rename to src/Discord.Net.Core/Entities/Channels/Direction.cs diff --git a/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs new file mode 100644 index 000000000..0ea196a4a --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs @@ -0,0 +1,30 @@ +namespace Discord +{ + /// + /// Modify an IGuildChannel with the specified changes. + /// + /// + /// + /// await (Context.Channel as ITextChannel)?.ModifyAsync(x => + /// { + /// x.Name = "do-not-enter"; + /// }); + /// + /// + public class GuildChannelProperties + { + /// + /// Set the channel to this name + /// + /// + /// When modifying an ITextChannel, the Name MUST be alphanumeric with dashes. + /// It must match the following RegEx: [a-z0-9-_]{2,100} + /// + /// A BadRequest will be thrown if the name does not match the above RegEx. + public Optional Name { get; set; } + /// + /// Move the channel to the following position. This is 0-based! + /// + public Optional Position { get; set; } + } +} diff --git a/src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs b/src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs index 9b8074efb..6c9507299 100644 --- a/src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs @@ -1,6 +1,6 @@ namespace Discord { - public interface IAudioChannel + public interface IAudioChannel : IChannel { } } diff --git a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs index 2e9272729..3d08a8c51 100644 --- a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs @@ -1,5 +1,4 @@ -using Discord.API.Rest; -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -21,12 +20,12 @@ namespace Discord /// The time (in seconds) until the invite expires. Set to null to never expire. /// The max amount of times this invite may be used. Set to null to have unlimited uses. /// If true, a user accepting this invite will be kicked from the guild after closing their client. - Task CreateInviteAsync(int? maxAge = 1800, int? maxUses = default(int?), bool isTemporary = false, RequestOptions options = null); + Task CreateInviteAsync(int? maxAge = 1800, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null); /// Returns a collection of all invites to this channel. Task> GetInvitesAsync(RequestOptions options = null); /// Modifies this guild channel. - Task ModifyAsync(Action func, RequestOptions options = null); + Task ModifyAsync(Action func, RequestOptions options = null); /// Gets the permission overwrite for a specific role, or null if one does not exist. OverwritePermissions? GetPermissionOverwrite(IRole role); diff --git a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs index 41bc79511..9c9c63929 100644 --- a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs @@ -8,9 +8,11 @@ namespace Discord public interface IMessageChannel : IChannel { /// Sends a message to this message channel. - Task SendMessageAsync(string text, bool isTTS = false, EmbedBuilder embed = null, RequestOptions options = null); + Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null); +#if NETSTANDARD1_3 /// 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 /// Sends a file to this text channel, with an optional caption. Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, RequestOptions options = null); diff --git a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs index 7ecaf6d7b..038faf6bc 100644 --- a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs @@ -1,5 +1,4 @@ -using Discord.API.Rest; -using System; +using System; using System.Threading.Tasks; namespace Discord @@ -10,6 +9,6 @@ namespace Discord string Topic { get; } /// Modifies this text channel. - Task ModifyAsync(Action func, RequestOptions options = null); + Task ModifyAsync(Action func, RequestOptions options = null); } } \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs b/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs index d1be73072..80c90e4bd 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.API.Rest; -using Discord.Audio; +using Discord.Audio; using System; using System.Threading.Tasks; @@ -9,11 +8,11 @@ namespace Discord { /// Gets the bitrate, in bits per second, clients in this voice channel are requested to use. int Bitrate { get; } - /// Gets the max amount of users allowed to be connected to this channel at one time. A value of 0 represents no limit. - int UserLimit { get; } + /// Gets the max amount of users allowed to be connected to this channel at one time. + int? UserLimit { get; } /// Modifies this voice channel. - Task ModifyAsync(Action func, RequestOptions options = null); + Task ModifyAsync(Action func, RequestOptions options = null); /// Connects to this voice channel. Task ConnectAsync(); } diff --git a/src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs new file mode 100644 index 000000000..2461a09f2 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs @@ -0,0 +1,11 @@ +namespace Discord +{ + /// + public class TextChannelProperties : GuildChannelProperties + { + /// + /// What the topic of the channel should be set to. + /// + public Optional Topic { get; set; } + } +} diff --git a/src/Discord.Net.Core/Entities/Channels/VoiceChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/VoiceChannelProperties.cs new file mode 100644 index 000000000..81dd8063e --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/VoiceChannelProperties.cs @@ -0,0 +1,15 @@ +namespace Discord +{ + /// + public class VoiceChannelProperties : GuildChannelProperties + { + /// + /// The bitrate of the voice connections in this channel. Must be greater than 8000 + /// + public Optional Bitrate { get; set; } + /// + /// The maximum number of users that can be present in a channel. + /// + public Optional UserLimit { get; set; } + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildEmbedProperties.cs b/src/Discord.Net.Core/Entities/Guilds/GuildEmbedProperties.cs new file mode 100644 index 000000000..a2b2ec4fc --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/GuildEmbedProperties.cs @@ -0,0 +1,21 @@ +namespace Discord +{ + /// + /// Modify the widget of an IGuild with the specified parameters + /// + public class GuildEmbedProperties + { + /// + /// Should the widget be enabled? + /// + public Optional Enabled { get; set; } + /// + /// What channel should the invite place users in, if not null. + /// + public Optional Channel { get; set; } + /// + /// What channel should the invite place users in, if not null. + /// + public Optional ChannelId { get; set; } + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildEmoji.cs b/src/Discord.Net.Core/Entities/Guilds/GuildEmoji.cs index 8b2bbd9c2..e925991eb 100644 --- a/src/Discord.Net.Core/Entities/Guilds/GuildEmoji.cs +++ b/src/Discord.Net.Core/Entities/Guilds/GuildEmoji.cs @@ -1,7 +1,5 @@ using System.Collections.Generic; -using System.Collections.Immutable; using System.Diagnostics; -using Model = Discord.API.Emoji; namespace Discord { @@ -14,7 +12,7 @@ namespace Discord public bool RequireColons { get; } public IReadOnlyList RoleIds { get; } - private GuildEmoji(ulong id, string name, bool isManaged, bool requireColons, IReadOnlyList roleIds) + internal GuildEmoji(ulong id, string name, bool isManaged, bool requireColons, IReadOnlyList roleIds) { Id = id; Name = name; @@ -22,10 +20,6 @@ namespace Discord RequireColons = requireColons; RoleIds = roleIds; } - internal static GuildEmoji Create(Model model) - { - return new GuildEmoji(model.Id, model.Name, model.Managed, model.RequireColons, ImmutableArray.Create(model.Roles)); - } public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id})"; diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildIntegrationProperties.cs b/src/Discord.Net.Core/Entities/Guilds/GuildIntegrationProperties.cs new file mode 100644 index 000000000..f329e78e6 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/GuildIntegrationProperties.cs @@ -0,0 +1,9 @@ +namespace Discord +{ + public class GuildIntegrationProperties + { + public Optional ExpireBehavior { get; set; } + public Optional ExpireGracePeriod { get; set; } + public Optional EnableEmoticons { get; set; } + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs b/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs new file mode 100644 index 000000000..ebec03e0b --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs @@ -0,0 +1,71 @@ +namespace Discord +{ + /// + /// Modify an IGuild with the specified changes + /// + /// + /// + /// await Context.Guild.ModifyAsync(async x => + /// { + /// x.Name = "aaaaaah"; + /// x.RegionId = (await Context.Client.GetOptimalVoiceRegionAsync()).Id; + /// }); + /// + /// + /// + public class GuildProperties + { + public Optional Username { get; set; } + /// + /// The name of the Guild + /// + public Optional Name { get; set; } + /// + /// The region for the Guild's voice connections + /// + public Optional Region { get; set; } + /// + /// The ID of the region for the Guild's voice connections + /// + public Optional RegionId { get; set; } + /// + /// What verification level new users need to achieve before speaking + /// + public Optional VerificationLevel { get; set; } + /// + /// The default message notification state for the guild + /// + public Optional DefaultMessageNotifications { get; set; } + /// + /// How many seconds before a user is sent to AFK. This value MUST be one of: (60, 300, 900, 1800, 3600). + /// + public Optional AfkTimeout { get; set; } + /// + /// The icon of the guild + /// + public Optional Icon { get; set; } + /// + /// The guild's splash image + /// + /// + /// The guild must be partnered for this value to have any effect. + /// + public Optional Splash { get; set; } + /// + /// The IVoiceChannel where AFK users should be sent. + /// + public Optional AfkChannel { get; set; } + /// + /// The ID of the IVoiceChannel where AFK users should be sent. + /// + public Optional AfkChannelId { get; set; } + /// + /// The owner of this guild. + /// + public Optional Owner { get; set; } + /// + /// The ID of the owner of this guild. + /// + public Optional OwnerId { get; set; } + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 413b5da62..2ce9b48d0 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -1,5 +1,4 @@ -using Discord.API.Rest; -using Discord.Audio; +using Discord.Audio; using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -53,13 +52,13 @@ namespace Discord IReadOnlyCollection Roles { get; } /// Modifies this guild. - Task ModifyAsync(Action func, RequestOptions options = null); + Task ModifyAsync(Action func, RequestOptions options = null); /// Modifies this guild's embed. - Task ModifyEmbedAsync(Action func, RequestOptions options = null); + Task ModifyEmbedAsync(Action func, RequestOptions options = null); /// Bulk modifies the channels of this guild. - Task ModifyChannelsAsync(IEnumerable args, RequestOptions options = null); + Task ModifyChannelsAsync(IEnumerable args, RequestOptions options = null); /// Bulk modifies the roles of this guild. - Task ModifyRolesAsync(IEnumerable args, RequestOptions options = null); + Task ModifyRolesAsync(IEnumerable args, RequestOptions options = null); /// Leaves this guild. If you are the owner, use Delete instead. Task LeaveAsync(RequestOptions options = null); @@ -78,6 +77,13 @@ namespace Discord Task> GetChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// Gets the channel in this guild with the provided id, or null if not found. Task GetChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task> GetTextChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task GetTextChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task> GetVoiceChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + 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); /// Creates a new text channel. Task CreateTextChannelAsync(string name, RequestOptions options = null); /// Creates a new voice channel. @@ -100,6 +106,8 @@ namespace Discord Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// Gets the current user for this guild. Task GetCurrentUserAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// Gets the owner of this guild. + Task GetOwnerAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// Downloads all users for this guild if the current list is incomplete. Task DownloadUsersAsync(); /// Removes all users from this guild if they have not logged on in a provided number of days or, if simulate is true, returns the number of users that would be removed. diff --git a/src/Discord.Net.Core/Entities/Permissions/PermissionTarget.cs b/src/Discord.Net.Core/Entities/Guilds/PermissionTarget.cs similarity index 100% rename from src/Discord.Net.Core/Entities/Permissions/PermissionTarget.cs rename to src/Discord.Net.Core/Entities/Guilds/PermissionTarget.cs diff --git a/src/Discord.Net.Core/Entities/Image.cs b/src/Discord.Net.Core/Entities/Image.cs new file mode 100644 index 000000000..59fe8bbdb --- /dev/null +++ b/src/Discord.Net.Core/Entities/Image.cs @@ -0,0 +1,32 @@ +using System.IO; +namespace Discord +{ + /// + /// An image that will be uploaded to Discord. + /// + public struct Image + { + public Stream Stream { get; } + /// + /// Create the image with a Stream. + /// + /// This must be some type of stream with the contents of a file in it. + public Image(Stream stream) + { + Stream = stream; + } +#if NETSTANDARD1_3 + /// + /// Create the image from a file path. + /// + /// + /// This file path is NOT validated, and is passed directly into a + /// + /// The path to the file. + public Image(string path) + { + Stream = File.OpenRead(path); + } +#endif + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/Embed.cs b/src/Discord.Net.Core/Entities/Messages/Embed.cs new file mode 100644 index 000000000..ebde05d4c --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/Embed.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Immutable; +using System.Diagnostics; + +namespace Discord +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class Embed : IEmbed + { + public string Type { get; } + + public string Description { get; internal set; } + public string Url { get; internal set; } + public string Title { get; internal set; } + public DateTimeOffset? Timestamp { get; internal set; } + public Color? Color { get; internal set; } + public EmbedImage? Image { get; internal set; } + public EmbedVideo? Video { get; internal set; } + public EmbedAuthor? Author { get; internal set; } + public EmbedFooter? Footer { get; internal set; } + public EmbedProvider? Provider { get; internal set; } + public EmbedThumbnail? Thumbnail { get; internal set; } + public ImmutableArray Fields { get; internal set; } + + internal Embed(string type) + { + Type = type; + Fields = ImmutableArray.Create(); + } + internal Embed(string type, + string title, + string description, + string url, + DateTimeOffset? timestamp, + Color? color, + EmbedImage? image, + EmbedVideo? video, + EmbedAuthor? author, + EmbedFooter? footer, + EmbedProvider? provider, + EmbedThumbnail? thumbnail, + ImmutableArray fields) + { + Type = type; + Title = title; + Description = description; + Url = url; + Color = color; + Timestamp = timestamp; + Image = image; + Video = video; + Author = author; + Footer = footer; + Provider = provider; + Thumbnail = thumbnail; + Fields = fields; + } + + 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 b0ed0f08f..142e36832 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs @@ -1,27 +1,22 @@ using System.Diagnostics; -using Model = Discord.API.EmbedAuthor; namespace Discord { [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct EmbedAuthor { - public string Name { get; set; } - public string Url { get; set; } - public string IconUrl { get; set; } - public string ProxyIconUrl { get; set; } + public string Name { get; internal set; } + public string Url { get; internal set; } + public string IconUrl { get; internal set; } + public string ProxyIconUrl { get; internal set; } - private EmbedAuthor(string name, string url, string iconUrl, string proxyIconUrl) + internal EmbedAuthor(string name, string url, string iconUrl, string proxyIconUrl) { Name = name; Url = url; IconUrl = iconUrl; ProxyIconUrl = proxyIconUrl; } - internal static EmbedAuthor Create(Model model) - { - return new EmbedAuthor(model.Name, model.Url, model.IconUrl, model.ProxyIconUrl); - } private string DebuggerDisplay => $"{Name} ({Url})"; public override string ToString() => Name; diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs deleted file mode 100644 index 5c0c72e8f..000000000 --- a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs +++ /dev/null @@ -1,181 +0,0 @@ -using System; -using System.Collections.Generic; -using Embed = Discord.API.Embed; -using Field = Discord.API.EmbedField; -using Author = Discord.API.EmbedAuthor; -using Footer = Discord.API.EmbedFooter; - -namespace Discord -{ - public class EmbedBuilder - { - private readonly Embed _model; - private readonly List _fields; - - public EmbedBuilder() - { - _model = new Embed(); - _model.Type = "rich"; - _fields = new List(); - } - - public string Title { get { return _model.Title; } set { _model.Title = value; } } - public string Description { get { return _model.Description; } set { _model.Description = value; } } - public string Url { get { return _model.Url; } set { _model.Url = value; } } - public Color? Color { get { return _model.Color.HasValue ? new Color(_model.Color.Value) : (Color?)null; } set { _model.Color = value?.RawValue; } } - public EmbedAuthorBuilder Author { get; set; } - public EmbedFooterBuilder Footer { get; set; } - - public EmbedBuilder WithTitle(string title) - { - Title = title; - return this; - } - public EmbedBuilder WithDescription(string description) - { - Description = description; - return this; - } - public EmbedBuilder WithUrl(string url) - { - Url = url; - return this; - } - public EmbedBuilder WithColor(Color color) - { - Color = color; - return this; - } - - public EmbedBuilder WithAuthor(EmbedAuthorBuilder author) - { - Author = author; - return this; - } - public EmbedBuilder WithAuthor(Action action) - { - var author = new EmbedAuthorBuilder(); - action(author); - Author = author; - return this; - } - public EmbedBuilder WithFooter(EmbedFooterBuilder footer) - { - Footer = footer; - return this; - } - public EmbedBuilder WithFooter(Action action) - { - var footer = new EmbedFooterBuilder(); - action(footer); - Footer = footer; - return this; - } - - public EmbedBuilder AddField(Action action) - { - var field = new EmbedFieldBuilder(); - action(field); - _fields.Add(field.ToModel()); - return this; - } - - internal Embed Build() - { - _model.Author = Author?.ToModel(); - _model.Footer = Footer?.ToModel(); - _model.Fields = _fields.ToArray(); - return _model; - } - } - - public class EmbedFieldBuilder - { - private Field _model; - - public string Name { get { return _model.Name; } set { _model.Name = value; } } - public string Value { get { return _model.Value; } set { _model.Value = value; } } - public bool IsInline { get { return _model.Inline; } set { _model.Inline = value; } } - - public EmbedFieldBuilder() - { - _model = new Field(); - } - - public EmbedFieldBuilder WithName(string name) - { - Name = name; - return this; - } - public EmbedFieldBuilder WithValue(string value) - { - Value = value; - return this; - } - public EmbedFieldBuilder WithIsInline(bool isInline) - { - IsInline = isInline; - return this; - } - - internal Field ToModel() => _model; - } - - public class EmbedAuthorBuilder - { - private Author _model; - - public string Name { get { return _model.Name; } set { _model.Name = value; } } - public string Url { get { return _model.Url; } set { _model.Url = value; } } - public string IconUrl { get { return _model.IconUrl; } set { _model.IconUrl = value; } } - - public EmbedAuthorBuilder() - { - _model = new Author(); - } - - public EmbedAuthorBuilder WithName(string name) - { - Name = name; - return this; - } - public EmbedAuthorBuilder WithUrl(string url) - { - Url = url; - return this; - } - public EmbedAuthorBuilder WithIconUrl(string iconUrl) - { - IconUrl = iconUrl; - return this; - } - - internal Author ToModel() => _model; - } - - public class EmbedFooterBuilder - { - private Footer _model; - - public string Text { get { return _model.Text; } set { _model.Text = value; } } - public string IconUrl { get { return _model.IconUrl; } set { _model.IconUrl = value; } } - - public EmbedFooterBuilder() - { - _model = new Footer(); - } - - public EmbedFooterBuilder WithText(string text) - { - Text = text; - return this; - } - public EmbedFooterBuilder WithIconUrl(string iconUrl) - { - IconUrl = iconUrl; - return this; - } - - internal Footer ToModel() => _model; - } -} diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedField.cs b/src/Discord.Net.Core/Entities/Messages/EmbedField.cs index 257074e41..f7c1f8348 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedField.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedField.cs @@ -1,25 +1,20 @@ using System.Diagnostics; -using Model = Discord.API.EmbedField; namespace Discord { [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct EmbedField { - public string Name { get; set; } - public string Value { get; set; } - public bool Inline { get; set; } + public string Name { get; internal set; } + public string Value { get; internal set; } + public bool Inline { get; internal set; } - private EmbedField(string name, string value, bool inline) + internal EmbedField(string name, string value, bool inline) { Name = name; Value = value; Inline = inline; } - internal static EmbedField Create(Model model) - { - return new EmbedField(model.Name, model.Value, model.Inline); - } private string DebuggerDisplay => $"{Name} ({Value}"; public override string ToString() => Name; diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs b/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs index a69e4b077..33582070a 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs @@ -1,25 +1,20 @@ using System.Diagnostics; -using Model = Discord.API.EmbedFooter; namespace Discord { [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct EmbedFooter { - public string Text { get; set; } - public string IconUrl { get; set; } - public string ProxyUrl { get; set; } + public string Text { get; internal set; } + public string IconUrl { get; internal set; } + public string ProxyUrl { get; internal set; } - private EmbedFooter(string text, string iconUrl, string proxyUrl) + internal EmbedFooter(string text, string iconUrl, string proxyUrl) { Text = text; IconUrl = iconUrl; ProxyUrl = proxyUrl; } - internal static EmbedFooter Create(Model model) - { - return new EmbedFooter(model.Text, model.IconUrl, model.ProxyIconUrl); - } private string DebuggerDisplay => $"{Text} ({IconUrl})"; public override string ToString() => Text; diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs b/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs new file mode 100644 index 000000000..fa4847721 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs @@ -0,0 +1,24 @@ +using System.Diagnostics; + +namespace Discord +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public struct EmbedImage + { + public string Url { get; } + public string ProxyUrl { get; } + public int? Height { get; } + public int? Width { get; } + + internal EmbedImage(string url, string proxyUrl, int? height, int? width) + { + Url = url; + ProxyUrl = proxyUrl; + Height = height; + Width = width; + } + + private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})"; + public override string ToString() => Url; + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs b/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs index 64b13e8e3..943ac5b52 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs @@ -1,5 +1,4 @@ using System.Diagnostics; -using Model = Discord.API.EmbedProvider; namespace Discord { @@ -9,15 +8,11 @@ namespace Discord public string Name { get; } public string Url { get; } - private EmbedProvider(string name, string url) + internal EmbedProvider(string name, string url) { Name = name; Url = url; } - internal static EmbedProvider Create(Model model) - { - return new EmbedProvider(model.Name, model.Url); - } private string DebuggerDisplay => $"{Name} ({Url})"; public override string ToString() => Name; diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs b/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs index 6a5fc4163..4e125bf2a 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs @@ -1,5 +1,4 @@ using System.Diagnostics; -using Model = Discord.API.EmbedThumbnail; namespace Discord { @@ -11,21 +10,15 @@ namespace Discord public int? Height { get; } public int? Width { get; } - private EmbedThumbnail(string url, string proxyUrl, int? height, int? width) + internal EmbedThumbnail(string url, string proxyUrl, int? height, int? width) { Url = url; ProxyUrl = proxyUrl; Height = height; Width = width; } - internal static EmbedThumbnail Create(Model model) - { - return new EmbedThumbnail(model.Url, model.ProxyUrl, - model.Height.IsSpecified ? model.Height.Value : (int?)null, - model.Width.IsSpecified ? model.Width.Value : (int?)null); - } - private string DebuggerDisplay => $"{ToString()} ({Url})"; - public override string ToString() => Width != null && Height != null ? $"{Width}x{Height}" : "0x0"; + private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})"; + public override string ToString() => Url; } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs b/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs new file mode 100644 index 000000000..eaf6f4a4c --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs @@ -0,0 +1,22 @@ +using System.Diagnostics; + +namespace Discord +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public struct EmbedVideo + { + public string Url { get; } + public int? Height { get; } + public int? Width { get; } + + internal EmbedVideo(string url, int? height, int? width) + { + Url = url; + Height = height; + Width = width; + } + + private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})"; + public override string ToString() => Url; + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/Emoji.cs b/src/Discord.Net.Core/Entities/Messages/Emoji.cs index 612e99f29..f0a0489e2 100644 --- a/src/Discord.Net.Core/Entities/Messages/Emoji.cs +++ b/src/Discord.Net.Core/Entities/Messages/Emoji.cs @@ -1,5 +1,4 @@ -using Discord.API; -using System; +using System; using System.Diagnostics; using System.Globalization; @@ -8,12 +7,12 @@ namespace Discord [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct Emoji { - public ulong Id { get; } + public ulong? Id { get; } public string Name { get; } - public string Url => CDN.GetEmojiUrl(Id); + public string Url => Id != null ? CDN.GetEmojiUrl(Id.Value) : null; - internal Emoji(ulong id, string name) + internal Emoji(ulong? id, string name) { Id = id; Name = name; diff --git a/src/Discord.Net.Core/Entities/Messages/IEmbed.cs b/src/Discord.Net.Core/Entities/Messages/IEmbed.cs index c46b22d17..5eef5ec9b 100644 --- a/src/Discord.Net.Core/Entities/Messages/IEmbed.cs +++ b/src/Discord.Net.Core/Entities/Messages/IEmbed.cs @@ -1,4 +1,5 @@ -using System.Collections.Immutable; +using System; +using System.Collections.Immutable; namespace Discord { @@ -8,7 +9,10 @@ namespace Discord string Type { get; } string Title { get; } string Description { get; } + DateTimeOffset? Timestamp { get; } Color? Color { get; } + EmbedImage? Image { get; } + EmbedVideo? Video { get; } EmbedAuthor? Author { get; } EmbedFooter? Footer { get; } EmbedProvider? Provider { get; } diff --git a/src/Discord.Net.Core/Entities/Messages/IReaction.cs b/src/Discord.Net.Core/Entities/Messages/IReaction.cs new file mode 100644 index 000000000..7145fce7f --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/IReaction.cs @@ -0,0 +1,7 @@ +namespace Discord +{ + public interface IReaction + { + Emoji Emoji { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs index a9dc4735c..73d402041 100644 --- a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs @@ -1,5 +1,5 @@ -using Discord.API.Rest; -using System; +using System; +using System.Collections.Generic; using System.Threading.Tasks; namespace Discord @@ -7,12 +7,27 @@ namespace Discord public interface IUserMessage : IMessage { /// Modifies this message. - Task ModifyAsync(Action func, RequestOptions options = null); + Task ModifyAsync(Action func, RequestOptions options = null); /// Adds this message to its channel's pinned messages. Task PinAsync(RequestOptions options = null); /// Removes this message from its channel's pinned messages. Task UnpinAsync(RequestOptions options = null); - + + /// Returns all reactions included in this message. + 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); + /// 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); + /// Removes all reactions from this message. + Task RemoveAllReactionsAsync(RequestOptions options = null); + Task> GetReactionUsersAsync(string emoji, int limit = 100, ulong? afterUserId = null, RequestOptions options = null); + /// Transforms this message's text into a human readable form by resolving its tags. string Resolve( TagHandling userHandling = TagHandling.Name, diff --git a/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs b/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs new file mode 100644 index 000000000..b3f3a9c89 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs @@ -0,0 +1,37 @@ +namespace Discord +{ + /// + /// Modify a message with the specified parameters. + /// + /// + /// The content of a message can be cleared with String.Empty; if and only if an Embed is present. + /// + /// + /// + /// var message = await ReplyAsync("abc"); + /// await message.ModifyAsync(x => + /// { + /// x.Content = ""; + /// x.Embed = new EmbedBuilder() + /// .WithColor(new Color(40, 40, 120)) + /// .WithAuthor(a => a.Name = "foxbot") + /// .WithTitle("Embed!") + /// .WithDescription("This is an embed."); + /// }); + /// + /// + public class MessageProperties + { + /// + /// The content of the message + /// + /// + /// This must be less than 2000 characters. + /// + public Optional Content { get; set; } + /// + /// The embed the message should display + /// + public Optional Embed { get; set; } + } +} diff --git a/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs index 5bedfbfae..a93f02497 100644 --- a/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs @@ -11,6 +11,7 @@ //ManageGuild = 5, //Text + AddReactions = 6, ReadMessages = 10, SendMessages = 11, SendTTSMessages = 12, @@ -33,7 +34,7 @@ //ChangeNickname = 26, //ManageNicknames = 27, ManagePermissions = 28, - //ManageWebhooks = 29, + ManageWebhooks = 29, //ManageEmojis = 30 } } diff --git a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs index bada89a32..2824a1426 100644 --- a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs @@ -10,7 +10,7 @@ namespace Discord //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("00010000000001111111110000010001", 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. @@ -35,6 +35,8 @@ namespace Discord /// If True, a user may create, delete and modify this channel. public bool ManageChannel => Permissions.GetValue(RawValue, ChannelPermission.ManageChannel); + /// If true, a user may add reactions. + public bool AddReactions => Permissions.GetValue(RawValue, ChannelPermission.AddReactions); /// If True, a user may join channels. public bool ReadMessages => Permissions.GetValue(RawValue, ChannelPermission.ReadMessages); /// If True, a user may send messages. @@ -69,20 +71,24 @@ namespace Discord /// If True, a user may adjust permissions. This also implictly grants all other permissions. public bool ManagePermissions => Permissions.GetValue(RawValue, ChannelPermission.ManagePermissions); + /// If True, a user may edit the webhooks for this channel. + public bool ManageWebhooks => Permissions.GetValue(RawValue, ChannelPermission.ManageWebhooks); /// 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, bool? useExternalEmojis = null, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, - bool? moveMembers = null, bool? useVoiceActivation = null, bool? managePermissions = null) + bool? moveMembers = null, bool? useVoiceActivation = null, bool? managePermissions = null, bool? manageWebhooks = null) { ulong value = initialValue; Permissions.SetValue(ref value, createInstantInvite, ChannelPermission.CreateInstantInvite); Permissions.SetValue(ref value, manageChannel, ChannelPermission.ManageChannel); + Permissions.SetValue(ref value, addReactions, ChannelPermission.AddReactions); Permissions.SetValue(ref value, readMessages, ChannelPermission.ReadMessages); Permissions.SetValue(ref value, sendMessages, ChannelPermission.SendMessages); Permissions.SetValue(ref value, sendTTSMessages, ChannelPermission.SendTTSMessages); @@ -99,30 +105,32 @@ namespace Discord Permissions.SetValue(ref value, moveMembers, ChannelPermission.MoveMembers); Permissions.SetValue(ref value, useVoiceActivation, ChannelPermission.UseVAD); Permissions.SetValue(ref value, managePermissions, ChannelPermission.ManagePermissions); + Permissions.SetValue(ref value, manageWebhooks, ChannelPermission.ManageWebhooks); RawValue = value; } /// 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) - : this(0, createInstantInvite, manageChannel, readMessages, sendMessages, sendTTSMessages, manageMessages, - embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, - speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, managePermissions) - { } + 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) { } /// 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) - => new ChannelPermissions(RawValue, createInstantInvite, manageChannel, readMessages, sendMessages, sendTTSMessages, manageMessages, + 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, - speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, managePermissions); + 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/GuildPermission.cs b/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs index e74a4da49..3975c1b8b 100644 --- a/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs +++ b/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs @@ -11,6 +11,7 @@ ManageGuild = 5, //Text + AddReactions = 6, ReadMessages = 10, SendMessages = 11, SendTTSMessages = 12, diff --git a/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs index 5941fde97..e7461915c 100644 --- a/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs @@ -11,7 +11,7 @@ namespace Discord 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("01111111111100111111110000111111", 2)); + public static readonly GuildPermissions All = new GuildPermissions(Convert.ToUInt64("01111111111100111111110001111111", 2)); /// Gets a packed value representing all the permissions in this GuildPermissions. public ulong RawValue { get; } @@ -28,7 +28,9 @@ namespace Discord public bool ManageChannels => Permissions.GetValue(RawValue, GuildPermission.ManageChannels); /// If True, a user may adjust guild properties. public bool ManageGuild => Permissions.GetValue(RawValue, GuildPermission.ManageGuild); - + + /// If true, a user may add reactions. + public bool AddReactions => Permissions.GetValue(RawValue, GuildPermission.AddReactions); /// If True, a user may join channels. public bool ReadMessages => Permissions.GetValue(RawValue, GuildPermission.ReadMessages); /// If True, a user may send messages. @@ -67,7 +69,7 @@ namespace Discord public bool ManageNicknames => Permissions.GetValue(RawValue, GuildPermission.ManageNicknames); /// If True, a user may adjust roles. public bool ManageRoles => Permissions.GetValue(RawValue, GuildPermission.ManageRoles); - /// If True, a user may edit the emojis for this guild. + /// If True, a user may edit the webhooks for this guild. public bool ManageWebhooks => Permissions.GetValue(RawValue, GuildPermission.ManageWebhooks); /// If True, a user may edit the emojis for this guild. public bool ManageEmojis => Permissions.GetValue(RawValue, GuildPermission.ManageEmojis); @@ -76,7 +78,8 @@ namespace Discord public GuildPermissions(ulong rawValue) { RawValue = rawValue; } private GuildPermissions(ulong initialValue, bool? createInstantInvite = null, bool? kickMembers = null, - bool? banMembers = null, bool? administrator = null, bool? manageChannel = null, bool? manageGuild = null, + bool? banMembers = null, bool? administrator = null, bool? manageChannel = null, bool? manageGuild = 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? userExternalEmojis = null, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, @@ -91,6 +94,7 @@ namespace Discord Permissions.SetValue(ref value, administrator, GuildPermission.Administrator); Permissions.SetValue(ref value, manageChannel, GuildPermission.ManageChannels); Permissions.SetValue(ref value, manageGuild, GuildPermission.ManageGuild); + Permissions.SetValue(ref value, addReactions, GuildPermission.AddReactions); Permissions.SetValue(ref value, readMessages, GuildPermission.ReadMessages); Permissions.SetValue(ref value, sendMessages, GuildPermission.SendMessages); Permissions.SetValue(ref value, sendTTSMessages, GuildPermission.SendTTSMessages); @@ -118,28 +122,29 @@ namespace Discord /// Creates a new GuildPermissions with the provided permissions. public GuildPermissions(bool createInstantInvite = false, bool kickMembers = false, bool banMembers = false, bool administrator = false, bool manageChannels = false, bool manageGuild = 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? changeNickname = false, bool? manageNicknames = false, bool manageRoles = false, bool manageWebhooks = false, bool manageEmojis = false) - : this(0, createInstantInvite, manageRoles, kickMembers, banMembers, manageChannels, manageGuild, readMessages, - sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, mentionEveryone, useExternalEmojis, connect, - speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, changeNickname, manageNicknames, manageRoles, - manageWebhooks, manageEmojis) { } + : this(0, createInstantInvite, manageRoles, kickMembers, banMembers, manageChannels, manageGuild, addReactions, + readMessages, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, mentionEveryone, useExternalEmojis, connect, + manageWebhooks, manageEmojis) { } /// Creates a new GuildPermissions from this one, changing the provided non-null permissions. public GuildPermissions Modify(bool? createInstantInvite = null, bool? kickMembers = null, bool? banMembers = null, bool? administrator = null, bool? manageChannels = null, bool? manageGuild = 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 = null, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, bool? moveMembers = null, bool? useVoiceActivation = null, bool? changeNickname = null, bool? manageNicknames = null, bool? manageRoles = null, bool? manageWebhooks = null, bool? manageEmojis = null) - => new GuildPermissions(RawValue, createInstantInvite, manageRoles, kickMembers, banMembers, manageChannels, manageGuild, readMessages, - sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, mentionEveryone, useExternalEmojis, connect, - speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, changeNickname, manageNicknames, manageRoles, - manageWebhooks, manageEmojis); + => new GuildPermissions(RawValue, createInstantInvite, manageRoles, kickMembers, banMembers, manageChannels, manageGuild, addReactions, + readMessages, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, mentionEveryone, useExternalEmojis, connect, + speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, changeNickname, manageNicknames, manageRoles, + manageWebhooks, manageEmojis); public bool Has(GuildPermission permission) => Permissions.GetValue(RawValue, permission); diff --git a/src/Discord.Net.Core/Entities/Permissions/Overwrite.cs b/src/Discord.Net.Core/Entities/Permissions/Overwrite.cs index ff5b00623..bda67a870 100644 --- a/src/Discord.Net.Core/Entities/Permissions/Overwrite.cs +++ b/src/Discord.Net.Core/Entities/Permissions/Overwrite.cs @@ -1,6 +1,4 @@ -using Model = Discord.API.Overwrite; - -namespace Discord +namespace Discord { public struct Overwrite { @@ -18,8 +16,5 @@ namespace Discord TargetType = targetType; Permissions = permissions; } - - public Overwrite(Model model) - : this(model.TargetId, model.TargetType, new OverwritePermissions(model.Allow, model.Deny)) { } } } diff --git a/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs b/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs index 2fd0c2d93..c3f8b2bab 100644 --- a/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs @@ -24,6 +24,8 @@ namespace Discord public PermValue CreateInstantInvite => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.CreateInstantInvite); /// If Allowed, a user may create, delete and modify this channel. public PermValue ManageChannel => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ManageChannel); + /// If Allowed, a user may add reactions. + public PermValue AddReactions => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.AddReactions); /// If Allowed, a user may join channels. public PermValue ReadMessages => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ReadMessages); /// If Allowed, a user may send messages. @@ -58,6 +60,8 @@ namespace Discord /// If Allowed, a user may adjust permissions. This also implictly grants all other permissions. public PermValue ManagePermissions => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ManagePermissions); + /// If True, a user may edit the webhooks for this channel. + public PermValue ManageWebhooks => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ManageWebhooks); /// Creates a new OverwritePermissions with the provided allow and deny packed values. public OverwritePermissions(ulong allowValue, ulong denyValue) @@ -67,13 +71,16 @@ namespace Discord } private OverwritePermissions(ulong allowValue, ulong denyValue, PermValue? createInstantInvite = null, PermValue? manageChannel = null, + PermValue? addReactions = null, PermValue? readMessages = null, PermValue? sendMessages = null, PermValue? sendTTSMessages = null, PermValue? manageMessages = null, PermValue? embedLinks = null, PermValue? attachFiles = null, PermValue? readMessageHistory = null, PermValue? mentionEveryone = null, PermValue? useExternalEmojis = null, PermValue? connect = null, PermValue? speak = null, PermValue? muteMembers = null, - PermValue? deafenMembers = null, PermValue? moveMembers = null, PermValue? useVoiceActivation = null, PermValue? managePermissions = null) + PermValue? deafenMembers = null, PermValue? moveMembers = null, PermValue? useVoiceActivation = null, PermValue? managePermissions = null, + PermValue? manageWebhooks = null) { Permissions.SetValue(ref allowValue, ref denyValue, createInstantInvite, ChannelPermission.CreateInstantInvite); Permissions.SetValue(ref allowValue, ref denyValue, manageChannel, ChannelPermission.ManageChannel); + Permissions.SetValue(ref allowValue, ref denyValue, addReactions, ChannelPermission.AddReactions); Permissions.SetValue(ref allowValue, ref denyValue, readMessages, ChannelPermission.ReadMessages); Permissions.SetValue(ref allowValue, ref denyValue, sendMessages, ChannelPermission.SendMessages); Permissions.SetValue(ref allowValue, ref denyValue, sendTTSMessages, ChannelPermission.SendTTSMessages); @@ -90,30 +97,33 @@ namespace Discord Permissions.SetValue(ref allowValue, ref denyValue, moveMembers, ChannelPermission.MoveMembers); Permissions.SetValue(ref allowValue, ref denyValue, useVoiceActivation, ChannelPermission.UseVAD); Permissions.SetValue(ref allowValue, ref denyValue, managePermissions, ChannelPermission.ManagePermissions); + Permissions.SetValue(ref allowValue, ref denyValue, manageWebhooks, ChannelPermission.ManageWebhooks); AllowValue = allowValue; DenyValue = denyValue; } /// Creates a new ChannelPermissions with the provided permissions. - public OverwritePermissions(PermValue createInstantInvite = PermValue.Inherit, PermValue manageChannel = PermValue.Inherit, + public OverwritePermissions(PermValue createInstantInvite = PermValue.Inherit, PermValue manageChannel = PermValue.Inherit, + PermValue addReactions = PermValue.Inherit, PermValue readMessages = PermValue.Inherit, PermValue sendMessages = PermValue.Inherit, PermValue sendTTSMessages = PermValue.Inherit, PermValue manageMessages = PermValue.Inherit, PermValue embedLinks = PermValue.Inherit, PermValue attachFiles = PermValue.Inherit, PermValue readMessageHistory = PermValue.Inherit, PermValue mentionEveryone = PermValue.Inherit, PermValue useExternalEmojis = PermValue.Inherit, PermValue connect = PermValue.Inherit, PermValue speak = PermValue.Inherit, PermValue muteMembers = PermValue.Inherit, PermValue deafenMembers = PermValue.Inherit, - PermValue moveMembers = PermValue.Inherit, PermValue useVoiceActivation = PermValue.Inherit, PermValue managePermissions = PermValue.Inherit) - : this(0, 0, createInstantInvite, manageChannel, readMessages, sendMessages, sendTTSMessages, manageMessages, + PermValue moveMembers = PermValue.Inherit, PermValue useVoiceActivation = PermValue.Inherit, PermValue managePermissions = PermValue.Inherit, PermValue manageWebhooks = PermValue.Inherit) + : this(0, 0, createInstantInvite, manageChannel, addReactions, readMessages, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, - moveMembers, useVoiceActivation, managePermissions) { } + moveMembers, useVoiceActivation, managePermissions, manageWebhooks) { } /// Creates a new OverwritePermissions from this one, changing the provided non-null permissions. - public OverwritePermissions Modify(PermValue? createInstantInvite = null, PermValue? manageChannel = null, + public OverwritePermissions Modify(PermValue? createInstantInvite = null, PermValue? manageChannel = null, + PermValue? addReactions = null, PermValue? readMessages = null, PermValue? sendMessages = null, PermValue? sendTTSMessages = null, PermValue? manageMessages = null, PermValue? embedLinks = null, PermValue? attachFiles = null, PermValue? readMessageHistory = null, PermValue? mentionEveryone = null, PermValue? useExternalEmojis = null, PermValue? connect = null, PermValue? speak = null, PermValue? muteMembers = null, PermValue? deafenMembers = null, - PermValue? moveMembers = null, PermValue? useVoiceActivation = null, PermValue? managePermissions = null) - => new OverwritePermissions(AllowValue, DenyValue, createInstantInvite, manageChannel, readMessages, sendMessages, sendTTSMessages, manageMessages, + PermValue? moveMembers = null, PermValue? useVoiceActivation = null, PermValue? managePermissions = null, PermValue? manageWebhooks = null) + => new OverwritePermissions(AllowValue, DenyValue, createInstantInvite, manageChannel, addReactions, readMessages, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, - moveMembers, useVoiceActivation, managePermissions); + moveMembers, useVoiceActivation, managePermissions, manageWebhooks); public List ToAllowList() { diff --git a/src/Discord.Net.Core/Entities/Roles/BulkRoleProperties.cs b/src/Discord.Net.Core/Entities/Roles/BulkRoleProperties.cs new file mode 100644 index 000000000..eacb6689d --- /dev/null +++ b/src/Discord.Net.Core/Entities/Roles/BulkRoleProperties.cs @@ -0,0 +1,15 @@ +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 563917959..ead46fd8a 100644 --- a/src/Discord.Net.Core/Entities/Roles/Color.cs +++ b/src/Discord.Net.Core/Entities/Roles/Color.cs @@ -32,6 +32,12 @@ namespace Discord } 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]"); + if (g < 0.0f || g > 1.0f) + throw new ArgumentOutOfRangeException(nameof(g), "A float 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]"); RawValue = ((uint)(r * 255.0f) << 16) | ((uint)(g * 255.0f) << 8) | diff --git a/src/Discord.Net.Core/Entities/Roles/IRole.cs b/src/Discord.Net.Core/Entities/Roles/IRole.cs index 2e3d63702..c40e0d716 100644 --- a/src/Discord.Net.Core/Entities/Roles/IRole.cs +++ b/src/Discord.Net.Core/Entities/Roles/IRole.cs @@ -1,5 +1,4 @@ -using Discord.API.Rest; -using System; +using System; using System.Threading.Tasks; namespace Discord @@ -25,6 +24,6 @@ namespace Discord int Position { get; } ///// Modifies this role. - Task ModifyAsync(Action func, RequestOptions options = null); + Task ModifyAsync(Action func, RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Roles/RoleProperties.cs b/src/Discord.Net.Core/Entities/Roles/RoleProperties.cs new file mode 100644 index 000000000..8950a2634 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Roles/RoleProperties.cs @@ -0,0 +1,58 @@ +namespace Discord +{ + /// + /// Modify an IRole with the specified parameters + /// + /// + /// + /// await role.ModifyAsync(x => + /// { + /// x.Color = new Color(180, 15, 40); + /// x.Hoist = true; + /// }); + /// + /// + /// + public class RoleProperties + { + /// + /// The name of the role + /// + /// + /// If this role is the EveryoneRole, this value may not be set. + /// + public Optional Name { get; set; } + /// + /// The role's GuildPermissions + /// + public Optional Permissions { get; set; } + /// + /// The position of the role. This is 0-based! + /// + /// + /// If this role is the EveryoneRole, this value may not be set. + /// + public Optional Position { get; set; } + /// + /// The color of the Role. + /// + /// + /// If this role is the EveryoneRole, this value may not be set. + /// + public Optional Color { get; set; } + /// + /// Whether or not this role should be displayed independently in the userlist. + /// + /// + /// If this role is the EveryoneRole, this value may not be set. + /// + public Optional Hoist { get; set; } + /// + /// Whether or not this role can be mentioned. + /// + /// + /// If this role is the EveryoneRole, this value may not be set. + /// + public Optional Mentionable { get; set; } + } +} diff --git a/src/Discord.Net.Core/Entities/Users/AvatarFormat.cs b/src/Discord.Net.Core/Entities/Users/AvatarFormat.cs new file mode 100644 index 000000000..29c17cede --- /dev/null +++ b/src/Discord.Net.Core/Entities/Users/AvatarFormat.cs @@ -0,0 +1,10 @@ +namespace Discord +{ + public enum AvatarFormat + { + WebP, + Png, + Jpeg, + Gif, + } +} diff --git a/src/Discord.Net.Core/Entities/Users/Game.cs b/src/Discord.Net.Core/Entities/Users/Game.cs index 5bed84ddb..3405b0dd4 100644 --- a/src/Discord.Net.Core/Entities/Users/Game.cs +++ b/src/Discord.Net.Core/Entities/Users/Game.cs @@ -1,5 +1,4 @@ using System.Diagnostics; -using Model = Discord.API.Game; namespace Discord { @@ -18,12 +17,6 @@ namespace Discord } private Game(string name) : this(name, null, StreamType.NotStreaming) { } - internal static Game Create(Model model) - { - return new Game(model.Name, - model.StreamUrl.GetValueOrDefault(null), - model.StreamType.GetValueOrDefault(null) ?? StreamType.NotStreaming); - } public override string ToString() => Name; private string DebuggerDisplay => StreamUrl != null ? $"{Name} ({StreamUrl})" : Name; diff --git a/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs b/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs new file mode 100644 index 000000000..5ceffef0e --- /dev/null +++ b/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; + +namespace Discord +{ + /// + /// Modify an IGuildUser with the following parameters. + /// + /// + /// + /// await (Context.User as IGuildUser)?.ModifyAsync(x => + /// { + /// x.Nickname = $"festive {Context.User.Username}"; + /// }); + /// + /// + /// + public class GuildUserProperties + { + /// + /// Should the user be guild-muted in a voice channel? + /// + /// + /// If this value is set to true, no user will be able to hear this user speak in the guild. + /// + public Optional Mute { get; set; } + /// + /// Should the user be guild-deafened in a voice channel? + /// + /// + /// If this value is set to true, this user will not be able to hear anyone speak in the guild. + /// + public Optional Deaf { get; set; } + /// + /// Should the user have a nickname set? + /// + /// + /// To clear the user's nickname, this value can be set to null. + /// + public Optional Nickname { get; set; } + /// + /// What roles should the user have? + /// + /// + /// 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: + /// + public Optional> RoleIds { get; set; } + /// + /// Move a user to a voice channel. + /// + /// + /// This user MUST already be in a Voice Channel for this to work. + /// + public Optional Channel { get; set; } + /// + /// Move a user to a voice channel. + /// + /// + /// This user MUST already be in a Voice Channel for this to work. + /// + public Optional ChannelId { get; set; } + } +} diff --git a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs index b48c76a37..79e8f5dcc 100644 --- a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs @@ -1,5 +1,4 @@ -using Discord.API.Rest; -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -12,6 +11,7 @@ namespace Discord DateTimeOffset? JoinedAt { get; } /// Gets the nickname for this user. string Nickname { get; } + /// Gets the guild-level permissions for this user. GuildPermissions GuildPermissions { get; } /// Gets the guild for this user. @@ -27,6 +27,6 @@ namespace Discord /// Kicks this user from this guild. Task KickAsync(RequestOptions options = null); /// Modifies this user's properties in this guild. - Task ModifyAsync(Action func, RequestOptions options = null); + Task ModifyAsync(Action func, RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Users/ISelfUser.cs b/src/Discord.Net.Core/Entities/Users/ISelfUser.cs index 782ea22c7..7b91d4e3a 100644 --- a/src/Discord.Net.Core/Entities/Users/ISelfUser.cs +++ b/src/Discord.Net.Core/Entities/Users/ISelfUser.cs @@ -1,4 +1,3 @@ -using Discord.API.Rest; using System; using System.Threading.Tasks; @@ -13,6 +12,6 @@ namespace Discord /// Returns true if this user has enabled MFA on their account. bool IsMfaEnabled { get; } - Task ModifyAsync(Action func, RequestOptions options = null); + Task ModifyAsync(Action func, RequestOptions options = null); } } \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Users/IUser.cs b/src/Discord.Net.Core/Entities/Users/IUser.cs index c02f8aeca..b7e807d8e 100644 --- a/src/Discord.Net.Core/Entities/Users/IUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IUser.cs @@ -7,7 +7,7 @@ namespace Discord /// Gets the id of this user's avatar. string AvatarId { get; } /// Gets the url to this user's avatar. - string AvatarUrl { get; } + string GetAvatarUrl(AvatarFormat format = AvatarFormat.Png, ushort size = 128); /// Gets the per-username unique id for this user. string Discriminator { get; } /// Gets the per-username unique id for this user. diff --git a/src/Discord.Net.Core/Entities/Users/SelfUserProperties.cs b/src/Discord.Net.Core/Entities/Users/SelfUserProperties.cs new file mode 100644 index 000000000..9c4162780 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Users/SelfUserProperties.cs @@ -0,0 +1,26 @@ +namespace Discord +{ + /// + /// Modify the current user with the specified arguments + /// + /// + /// + /// await Context.Client.CurrentUser.ModifyAsync(x => + /// { + /// x.Avatar = new Image(File.OpenRead("avatar.jpg")); + /// }); + /// + /// + /// + public class SelfUserProperties + { + /// + /// Your username + /// + public Optional Username { get; set; } + /// + /// Your avatar + /// + public Optional Avatar { get; set; } + } +} diff --git a/src/Discord.Net.Core/Extensions/CollectionExtensions.cs b/src/Discord.Net.Core/Extensions/CollectionExtensions.cs index 8eebac817..e5d6025c2 100644 --- a/src/Discord.Net.Core/Extensions/CollectionExtensions.cs +++ b/src/Discord.Net.Core/Extensions/CollectionExtensions.cs @@ -8,24 +8,30 @@ namespace Discord { internal static class CollectionExtensions { - public static IReadOnlyCollection ToReadOnlyCollection(this IReadOnlyDictionary source) - => new ConcurrentDictionaryWrapper(source.Select(x => x.Value), () => source.Count); + //public static IReadOnlyCollection ToReadOnlyCollection(this IReadOnlyCollection source) + // => new CollectionWrapper(source, () => source.Count); + public static IReadOnlyCollection ToReadOnlyCollection(this ICollection source) + => new CollectionWrapper(source, () => source.Count); + //public static IReadOnlyCollection ToReadOnlyCollection(this IReadOnlyDictionary source) + // => new CollectionWrapper(source.Select(x => x.Value), () => source.Count); + public static IReadOnlyCollection ToReadOnlyCollection(this IDictionary source) + => new CollectionWrapper(source.Select(x => x.Value), () => source.Count); public static IReadOnlyCollection ToReadOnlyCollection(this IEnumerable query, IReadOnlyCollection source) - => new ConcurrentDictionaryWrapper(query, () => source.Count); + => new CollectionWrapper(query, () => source.Count); public static IReadOnlyCollection ToReadOnlyCollection(this IEnumerable query, Func countFunc) - => new ConcurrentDictionaryWrapper(query, countFunc); + => new CollectionWrapper(query, countFunc); } [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - internal struct ConcurrentDictionaryWrapper : IReadOnlyCollection + internal struct CollectionWrapper : IReadOnlyCollection { private readonly IEnumerable _query; private readonly Func _countFunc; //It's okay that this count is affected by race conditions - we're wrapping a concurrent collection and that's to be expected public int Count => _countFunc(); - - public ConcurrentDictionaryWrapper(IEnumerable query, Func countFunc) + + public CollectionWrapper(IEnumerable query, Func countFunc) { _query = query; _countFunc = countFunc; diff --git a/src/Discord.Net.Core/Extensions/GuildExtensions.cs b/src/Discord.Net.Core/Extensions/GuildExtensions.cs deleted file mode 100644 index ea8e58e2b..000000000 --- a/src/Discord.Net.Core/Extensions/GuildExtensions.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Discord -{ - public static class GuildExtensions - { - public static async Task GetTextChannelAsync(this IGuild guild, ulong id) - => await guild.GetChannelAsync(id).ConfigureAwait(false) as ITextChannel; - public static async Task> GetTextChannelsAsync(this IGuild guild) - => (await guild.GetChannelsAsync().ConfigureAwait(false)).Select(x => x as ITextChannel).Where(x => x != null); - - public static async Task GetVoiceChannelAsync(this IGuild guild, ulong id) - => await guild.GetChannelAsync(id).ConfigureAwait(false) as IVoiceChannel; - public static async Task> GetVoiceChannelsAsync(this IGuild guild) - => (await guild.GetChannelsAsync().ConfigureAwait(false)).Select(x => x as IVoiceChannel).Where(x => x != null); - - public static async Task GetAFKChannelAsync(this IGuild guild) - { - var afkId = guild.AFKChannelId; - if (afkId.HasValue) - return await guild.GetChannelAsync(afkId.Value).ConfigureAwait(false) as IVoiceChannel; - return null; - } - public static async Task GetDefaultChannelAsync(this IGuild guild) - => await guild.GetChannelAsync(guild.DefaultChannelId).ConfigureAwait(false) as ITextChannel; - public static async Task GetEmbedChannelAsync(this IGuild guild) - { - var embedId = guild.EmbedChannelId; - if (embedId.HasValue) - return await guild.GetChannelAsync(embedId.Value).ConfigureAwait(false) as IVoiceChannel; - return null; - } - public static async Task GetOwnerAsync(this IGuild guild) - => await guild.GetUserAsync(guild.OwnerId).ConfigureAwait(false); - } -} diff --git a/src/Discord.Net.Core/Extensions/GuildUserExtensions.cs b/src/Discord.Net.Core/Extensions/GuildUserExtensions.cs index 855551c3d..9d152adf9 100644 --- a/src/Discord.Net.Core/Extensions/GuildUserExtensions.cs +++ b/src/Discord.Net.Core/Extensions/GuildUserExtensions.cs @@ -6,7 +6,6 @@ namespace Discord { public static class GuildUserExtensions { - //TODO: Should we remove Add/Remove? Encourages race conditions. public static Task AddRolesAsync(this IGuildUser user, params IRole[] roles) => ChangeRolesAsync(user, add: roles); public static Task AddRolesAsync(this IGuildUser user, IEnumerable roles) diff --git a/src/Discord.Net.Core/Format.cs b/src/Discord.Net.Core/Format.cs index 0039836d8..aa822f99e 100644 --- a/src/Discord.Net.Core/Format.cs +++ b/src/Discord.Net.Core/Format.cs @@ -3,7 +3,7 @@ public static class Format { // Characters which need escaping - private static string[] SensitiveCharacters = { "*", "_", "~", "`", "\\" }; + private static string[] SensitiveCharacters = { "\\", "*", "_", "~", "`" }; /// Returns a markdown-formatted string with bold formatting. public static string Bold(string text) => $"**{text}**"; diff --git a/src/Discord.Net.Core/IDiscordClient.cs b/src/Discord.Net.Core/IDiscordClient.cs index 21491341f..620f5bde8 100644 --- a/src/Discord.Net.Core/IDiscordClient.cs +++ b/src/Discord.Net.Core/IDiscordClient.cs @@ -1,4 +1,3 @@ -using Discord.API; using System; using System.Collections.Generic; using System.IO; @@ -9,16 +8,17 @@ namespace Discord public interface IDiscordClient : IDisposable { ConnectionState ConnectionState { get; } - DiscordRestApiClient ApiClient { get; } ISelfUser CurrentUser { get; } - Task ConnectAsync(); - Task DisconnectAsync(); + Task StartAsync(); + Task StopAsync(); Task GetApplicationInfoAsync(); 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> GetConnectionsAsync(); diff --git a/src/Discord.Net.Core/Logging/LogManager.cs b/src/Discord.Net.Core/Logging/LogManager.cs index 104e02835..21f956b99 100644 --- a/src/Discord.Net.Core/Logging/LogManager.cs +++ b/src/Discord.Net.Core/Logging/LogManager.cs @@ -6,7 +6,7 @@ namespace Discord.Logging internal class LogManager { public LogSeverity Level { get; } - public Logger ClientLogger { get; } + private Logger ClientLogger { get; } public event Func Message { add { _messageEvent.Add(value); } remove { _messageEvent.Remove(value); } } private readonly AsyncEvent> _messageEvent = new AsyncEvent>(); @@ -17,56 +17,68 @@ namespace Discord.Logging ClientLogger = new Logger(this, "Discord"); } + public async Task LogAsync(LogSeverity severity, string source, Exception ex) + { + if (severity <= Level) + await _messageEvent.InvokeAsync(new LogMessage(severity, source, null, ex)).ConfigureAwait(false); + } public async Task LogAsync(LogSeverity severity, string source, string message, Exception ex = null) { if (severity <= Level) await _messageEvent.InvokeAsync(new LogMessage(severity, source, message, ex)).ConfigureAwait(false); } +#if NETSTANDARD1_3 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); } - public async Task LogAsync(LogSeverity severity, string source, Exception ex) - { - if (severity <= Level) - await _messageEvent.InvokeAsync(new LogMessage(severity, source, null, ex)).ConfigureAwait(false); - } +#endif + public Task ErrorAsync(string source, Exception ex) + => LogAsync(LogSeverity.Error, source, ex); public Task ErrorAsync(string source, string message, Exception ex = null) => LogAsync(LogSeverity.Error, source, message, ex); +#if NETSTANDARD1_3 public Task ErrorAsync(string source, FormattableString message, Exception ex = null) => LogAsync(LogSeverity.Error, source, message, ex); - public Task ErrorAsync(string source, Exception ex) - => LogAsync(LogSeverity.Error, source, ex); +#endif + public Task WarningAsync(string source, Exception ex) + => LogAsync(LogSeverity.Warning, source, ex); public Task WarningAsync(string source, string message, Exception ex = null) => LogAsync(LogSeverity.Warning, source, message, ex); +#if NETSTANDARD1_3 public Task WarningAsync(string source, FormattableString message, Exception ex = null) => LogAsync(LogSeverity.Warning, source, message, ex); - public Task WarningAsync(string source, Exception ex) - => LogAsync(LogSeverity.Warning, source, ex); +#endif + public Task InfoAsync(string source, Exception ex) + => LogAsync(LogSeverity.Info, source, ex); public Task InfoAsync(string source, string message, Exception ex = null) => LogAsync(LogSeverity.Info, source, message, ex); +#if NETSTANDARD1_3 public Task InfoAsync(string source, FormattableString message, Exception ex = null) => LogAsync(LogSeverity.Info, source, message, ex); - public Task InfoAsync(string source, Exception ex) - => LogAsync(LogSeverity.Info, source, ex); +#endif + public Task VerboseAsync(string source, Exception ex) + => LogAsync(LogSeverity.Verbose, source, ex); public Task VerboseAsync(string source, string message, Exception ex = null) => LogAsync(LogSeverity.Verbose, source, message, ex); +#if NETSTANDARD1_3 public Task VerboseAsync(string source, FormattableString message, Exception ex = null) => LogAsync(LogSeverity.Verbose, source, message, ex); - public Task VerboseAsync(string source, Exception ex) - => LogAsync(LogSeverity.Verbose, source, ex); +#endif + public Task DebugAsync(string source, Exception ex) + => LogAsync(LogSeverity.Debug, source, ex); public Task DebugAsync(string source, string message, Exception ex = null) => LogAsync(LogSeverity.Debug, source, message, ex); +#if NETSTANDARD1_3 public Task DebugAsync(string source, FormattableString message, Exception ex = null) => LogAsync(LogSeverity.Debug, source, message, ex); - public Task DebugAsync(string source, Exception ex) - => LogAsync(LogSeverity.Debug, source, ex); +#endif public Logger CreateLogger(string name) => new Logger(this, name); diff --git a/src/Discord.Net.Core/Logging/LogMessage.cs b/src/Discord.Net.Core/Logging/LogMessage.cs index 9c3dfcfea..d1b3782be 100644 --- a/src/Discord.Net.Core/Logging/LogMessage.cs +++ b/src/Discord.Net.Core/Logging/LogMessage.cs @@ -19,7 +19,7 @@ namespace Discord } public override string ToString() => ToString(null); - public string ToString(StringBuilder builder = null, bool fullException = true, bool prependTimestamp = true, DateTimeKind timestampKind = DateTimeKind.Local, int? padSource = 9) + public string ToString(StringBuilder builder = null, bool fullException = true, bool prependTimestamp = true, DateTimeKind timestampKind = DateTimeKind.Local, int? padSource = 11) { string sourceName = Source; string message = Message; @@ -87,8 +87,11 @@ namespace Discord } if (exMessage != null) { - builder.Append(':'); - builder.AppendLine(); + if (!string.IsNullOrEmpty(Message)) + { + builder.Append(':'); + builder.AppendLine(); + } builder.Append(exMessage); } diff --git a/src/Discord.Net.Core/Logging/Logger.cs b/src/Discord.Net.Core/Logging/Logger.cs index c871c0b26..cff69a84c 100644 --- a/src/Discord.Net.Core/Logging/Logger.cs +++ b/src/Discord.Net.Core/Logging/Logger.cs @@ -20,42 +20,54 @@ 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 public Task LogAsync(LogSeverity severity, FormattableString message, Exception exception = null) => _manager.LogAsync(severity, Name, message, exception); +#endif + public Task ErrorAsync(Exception exception) + => _manager.ErrorAsync(Name, exception); public Task ErrorAsync(string message, Exception exception = null) => _manager.ErrorAsync(Name, message, exception); +#if NETSTANDARD1_3 public Task ErrorAsync(FormattableString message, Exception exception = null) => _manager.ErrorAsync(Name, message, exception); - public Task ErrorAsync(Exception exception) - => _manager.ErrorAsync(Name, exception); +#endif + public Task WarningAsync(Exception exception) + => _manager.WarningAsync(Name, exception); public Task WarningAsync(string message, Exception exception = null) => _manager.WarningAsync(Name, message, exception); +#if NETSTANDARD1_3 public Task WarningAsync(FormattableString message, Exception exception = null) => _manager.WarningAsync(Name, message, exception); - public Task WarningAsync(Exception exception) - => _manager.WarningAsync(Name, exception); +#endif + public Task InfoAsync(Exception exception) + => _manager.InfoAsync(Name, exception); public Task InfoAsync(string message, Exception exception = null) => _manager.InfoAsync(Name, message, exception); +#if NETSTANDARD1_3 public Task InfoAsync(FormattableString message, Exception exception = null) => _manager.InfoAsync(Name, message, exception); - public Task InfoAsync(Exception exception) - => _manager.InfoAsync(Name, exception); +#endif + public Task VerboseAsync(Exception exception) + => _manager.VerboseAsync(Name, exception); public Task VerboseAsync(string message, Exception exception = null) => _manager.VerboseAsync(Name, message, exception); +#if NETSTANDARD1_3 public Task VerboseAsync(FormattableString message, Exception exception = null) => _manager.VerboseAsync(Name, message, exception); - public Task VerboseAsync(Exception exception) - => _manager.VerboseAsync(Name, exception); +#endif + public Task DebugAsync(Exception exception) + => _manager.DebugAsync(Name, exception); public Task DebugAsync(string message, Exception exception = null) => _manager.DebugAsync(Name, message, exception); +#if NETSTANDARD1_3 public Task DebugAsync(FormattableString message, Exception exception = null) => _manager.DebugAsync(Name, message, exception); - public Task DebugAsync(Exception exception) - => _manager.DebugAsync(Name, exception); +#endif } } diff --git a/src/Discord.Net.Core/Net/Converters/NullableUInt64Converter.cs b/src/Discord.Net.Core/Net/Converters/NullableUInt64Converter.cs deleted file mode 100644 index a2e409292..000000000 --- a/src/Discord.Net.Core/Net/Converters/NullableUInt64Converter.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Globalization; - -namespace Discord.Net.Converters -{ - internal class NullableUInt64Converter : JsonConverter - { - public static readonly NullableUInt64Converter Instance = new NullableUInt64Converter(); - - public override bool CanConvert(Type objectType) => true; - public override bool CanRead => true; - public override bool CanWrite => true; - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - object value = reader.Value; - if (value != null) - return ulong.Parse((string)value, NumberStyles.None, CultureInfo.InvariantCulture); - else - return null; - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - if (value != null) - writer.WriteValue(((ulong?)value).Value.ToString(CultureInfo.InvariantCulture)); - else - writer.WriteNull(); - } - } -} diff --git a/src/Discord.Net.Core/Net/HttpException.cs b/src/Discord.Net.Core/Net/HttpException.cs index d18d81abf..4141979a0 100644 --- a/src/Discord.Net.Core/Net/HttpException.cs +++ b/src/Discord.Net.Core/Net/HttpException.cs @@ -5,14 +5,36 @@ namespace Discord.Net { public class HttpException : Exception { - public HttpStatusCode StatusCode { get; } + public HttpStatusCode HttpCode { get; } + public int? DiscordCode { get; } public string Reason { get; } - public HttpException(HttpStatusCode statusCode, string reason = null) - : base($"The server responded with error {(int)statusCode} ({statusCode}){(reason != null ? $": \"{reason}\"" : "")}") + public HttpException(HttpStatusCode httpCode, int? discordCode = null, string reason = null) + : base(CreateMessage(httpCode, discordCode, reason)) { - StatusCode = statusCode; + HttpCode = httpCode; + DiscordCode = discordCode; Reason = reason; } + + private static string CreateMessage(HttpStatusCode httpCode, int? discordCode = null, string reason = null) + { + string msg; + if (discordCode != null) + { + if (reason != null) + msg = $"The server responded with error {(int)discordCode}: {reason}"; + else + msg = $"The server responded with error {(int)discordCode}: {httpCode}"; + } + else + { + if (reason != null) + msg = $"The server responded with error {(int)httpCode}: {reason}"; + else + msg = $"The server responded with error {(int)httpCode}: {httpCode}"; + } + return msg; + } } } diff --git a/src/Discord.Net.Core/Net/Udp/IUdpSocket.cs b/src/Discord.Net.Core/Net/Udp/IUdpSocket.cs new file mode 100644 index 000000000..8da948d1a --- /dev/null +++ b/src/Discord.Net.Core/Net/Udp/IUdpSocket.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.Udp +{ + public interface IUdpSocket + { + event Func ReceivedDatagram; + + void SetCancelToken(CancellationToken cancelToken); + void SetDestination(string host, int port); + + Task StartAsync(); + Task StopAsync(); + + Task SendAsync(byte[] data, int index, int count); + } +} diff --git a/src/Discord.Net.Core/Net/Udp/UdpSocketProvider.cs b/src/Discord.Net.Core/Net/Udp/UdpSocketProvider.cs new file mode 100644 index 000000000..07fbd4f57 --- /dev/null +++ b/src/Discord.Net.Core/Net/Udp/UdpSocketProvider.cs @@ -0,0 +1,4 @@ +namespace Discord.Net.Udp +{ + public delegate IUdpSocket UdpSocketProvider(); +} diff --git a/src/Discord.Net.Core/Net/WebSocketException.cs b/src/Discord.Net.Core/Net/WebSocketClosedException.cs similarity index 100% rename from src/Discord.Net.Core/Net/WebSocketException.cs rename to src/Discord.Net.Core/Net/WebSocketClosedException.cs diff --git a/src/Discord.Net.Core/RequestOptions.cs b/src/Discord.Net.Core/RequestOptions.cs index b82ec29c8..4f5910c53 100644 --- a/src/Discord.Net.Core/RequestOptions.cs +++ b/src/Discord.Net.Core/RequestOptions.cs @@ -1,11 +1,18 @@ -namespace Discord +using System.Threading; + +namespace Discord { public class RequestOptions { public static RequestOptions Default => new RequestOptions(); - /// The max time, in milliseconds, to wait for this request to complete. If null, a request will not time out. If a rate limit has been triggered for this request's bucket and will not be unpaused in time, this request will fail immediately. + /// + /// The max time, in milliseconds, to wait for this request to complete. If null, a request will not time out. + /// If a rate limit has been triggered for this request's bucket and will not be unpaused in time, this request will fail immediately. + /// public int? Timeout { get; set; } + public CancellationToken CancelToken { get; set; } = CancellationToken.None; + public RetryMode? RetryMode { get; set; } public bool HeaderOnly { get; internal set; } internal bool IgnoreState { get; set; } @@ -13,7 +20,7 @@ internal bool IsClientBucket { get; set; } internal static RequestOptions CreateOrClone(RequestOptions options) - { + { if (options == null) return new RequestOptions(); else diff --git a/src/Discord.Net.Core/RetryMode.cs b/src/Discord.Net.Core/RetryMode.cs new file mode 100644 index 000000000..65ae75fc3 --- /dev/null +++ b/src/Discord.Net.Core/RetryMode.cs @@ -0,0 +1,22 @@ +using System; + +namespace Discord +{ + /// Specifies how a request should act in the case of an error. + [Flags] + public enum RetryMode + { + /// If a request fails, an exception is thrown immediately. + AlwaysFail = 0x0, + /// Retry if a request timed out. + RetryTimeouts = 0x1, + // /// Retry if a request failed due to a network error. + //RetryErrors = 0x2, + /// Retry if a request failed due to a ratelimit. + RetryRatelimit = 0x4, + /// Retry if a request failed due to an HTTP error 502. + Retry502 = 0x8, + /// Continuously retry a request until it times out, its cancel token is triggered, or the server responds with a non-502 error. + AlwaysRetry = RetryTimeouts | /*RetryErrors |*/ RetryRatelimit | Retry502, + } +} diff --git a/src/Discord.Net.Core/Utils/Cacheable.cs b/src/Discord.Net.Core/Utils/Cacheable.cs new file mode 100644 index 000000000..10b61be90 --- /dev/null +++ b/src/Discord.Net.Core/Utils/Cacheable.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Contains an entity that may be cached. + /// + /// The type of entity that is cached + /// The type of this entity's ID + public struct Cacheable + where TEntity : IEntity + where TId : IEquatable + { + /// + /// Is this entity cached? + /// + public bool HasValue { get; } + /// + /// The ID of this entity. + /// + public TId Id { get; } + /// + /// The entity, if it could be pulled from cache. + /// + /// + /// This value is not guaranteed to be set; in cases where the entity cannot be pulled from cache, it is null. + /// + public TEntity Value { get; } + private Func> DownloadFunc { get; } + + internal Cacheable(TEntity value, TId id, bool hasValue , Func> downloadFunc) + { + Value = value; + Id = id; + HasValue = hasValue; + DownloadFunc = downloadFunc; + } + + /// + /// Downloads this entity to cache. + /// + /// An awaitable Task containing the downloaded entity. + /// Thrown when used from a user account. + /// Thrown when the message is deleted. + public async Task DownloadAsync() + { + return await DownloadFunc(); + } + + /// + /// Returns the cached entity if it exists; otherwise downloads it. + /// + /// An awaitable Task containing a cached or downloaded entity. + /// Thrown when used from a user account. + /// Thrown when the message is deleted and is not in cache. + public async Task GetOrDownloadAsync() => HasValue ? Value : await DownloadAsync(); + } +} \ No newline at end of file diff --git a/src/Discord.Net.Core/Utils/DateTimeUtils.cs b/src/Discord.Net.Core/Utils/DateTimeUtils.cs index aa127fe29..fc9ef4b7b 100644 --- a/src/Discord.Net.Core/Utils/DateTimeUtils.cs +++ b/src/Discord.Net.Core/Utils/DateTimeUtils.cs @@ -2,14 +2,60 @@ namespace Discord { -internal static class DateTimeUtils -{ - public static DateTimeOffset FromSnowflake(ulong value) - => DateTimeOffset.FromUnixTimeMilliseconds((long)((value >> 22) + 1420070400000UL)); + 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; +#endif - public static DateTimeOffset FromTicks(long ticks) - => new DateTimeOffset(ticks, TimeSpan.Zero); - public static DateTimeOffset? FromTicks(long? ticks) - => ticks != null ? new DateTimeOffset(ticks.Value, TimeSpan.Zero) : (DateTimeOffset?)null; -} + 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) + => ticks != null ? new DateTimeOffset(ticks.Value, TimeSpan.Zero) : (DateTimeOffset?)null; + + public static DateTimeOffset FromUnixSeconds(long seconds) + { +#if NETSTANDARD1_3 + return DateTimeOffset.FromUnixTimeSeconds(seconds); +#else + long ticks = seconds * TimeSpan.TicksPerSecond + UnixEpochTicks; + return new DateTimeOffset(ticks, TimeSpan.Zero); +#endif + } + public static DateTimeOffset FromUnixMilliseconds(long seconds) + { +#if NETSTANDARD1_3 + return DateTimeOffset.FromUnixTimeMilliseconds(seconds); +#else + long ticks = seconds * TimeSpan.TicksPerMillisecond + UnixEpochTicks; + return new DateTimeOffset(ticks, TimeSpan.Zero); +#endif + } + + public static long ToUnixSeconds(DateTimeOffset dto) + { +#if NETSTANDARD1_3 + return dto.ToUnixTimeSeconds(); +#else + long seconds = dto.UtcDateTime.Ticks / TimeSpan.TicksPerSecond; + return seconds - UnixEpochSeconds; +#endif + } + public static long ToUnixMilliseconds(DateTimeOffset dto) + { +#if NETSTANDARD1_3 + return dto.ToUnixTimeMilliseconds(); +#else + long seconds = dto.UtcDateTime.Ticks / TimeSpan.TicksPerMillisecond; + return seconds - UnixEpochSeconds; +#endif + } + } } diff --git a/src/Discord.Net.Core/Utils/Permissions.cs b/src/Discord.Net.Core/Utils/Permissions.cs index 390c142de..a6c545da0 100644 --- a/src/Discord.Net.Core/Utils/Permissions.cs +++ b/src/Discord.Net.Core/Utils/Permissions.cs @@ -118,6 +118,7 @@ namespace Discord //Start with this user's guild permissions resolvedPermissions = guildPermissions; + //Give/Take Role permissions OverwritePermissions? perms; var roleIds = user.RoleIds; if (roleIds.Count > 0) @@ -128,15 +129,17 @@ namespace Discord perms = channel.GetPermissionOverwrite(guild.GetRole(roleId)); if (perms != null) { - deniedPermissions |= perms.Value.DenyValue; allowedPermissions |= perms.Value.AllowValue; + deniedPermissions |= perms.Value.DenyValue; } } - resolvedPermissions = (resolvedPermissions & ~deniedPermissions) | allowedPermissions; + resolvedPermissions = (resolvedPermissions | allowedPermissions) & ~deniedPermissions; } + + //Give/Take User permissions perms = channel.GetPermissionOverwrite(user); if (perms != null) - resolvedPermissions = (resolvedPermissions & ~perms.Value.DenyValue) | perms.Value.AllowValue; + resolvedPermissions = (resolvedPermissions | perms.Value.AllowValue) & ~perms.Value.DenyValue; //TODO: C#7 Typeswitch candidate var textChannel = channel as ITextChannel; diff --git a/src/Discord.Net.Core/Utils/Preconditions.cs b/src/Discord.Net.Core/Utils/Preconditions.cs index 44d4e381b..65af6e49b 100644 --- a/src/Discord.Net.Core/Utils/Preconditions.cs +++ b/src/Discord.Net.Core/Utils/Preconditions.cs @@ -42,7 +42,7 @@ namespace Discord if (obj.Value == null) throw CreateNotNullException(name, msg); if (obj.Value.Trim().Length == 0) throw CreateNotEmptyException(name, msg); } - } + } private static ArgumentException CreateNotEmptyException(string name, string msg) { @@ -181,5 +181,13 @@ namespace Discord if (msg == null) return new ArgumentException($"Value must be less than {value}", name); else return new ArgumentException(msg, name); } + + // Bulk Delete + public static void YoungerThanTwoWeeks(ulong[] collection, string name) + { + var minimum = DateTimeUtils.ToSnowflake(DateTimeOffset.Now.Subtract(TimeSpan.FromMilliseconds(1209540000))); + 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."); + } } } diff --git a/src/Discord.Net.Core/Extensions/RoleExtensions.cs b/src/Discord.Net.Core/Utils/RoleUtils.cs similarity index 79% rename from src/Discord.Net.Core/Extensions/RoleExtensions.cs rename to src/Discord.Net.Core/Utils/RoleUtils.cs index bd6856501..444afe62e 100644 --- a/src/Discord.Net.Core/Extensions/RoleExtensions.cs +++ b/src/Discord.Net.Core/Utils/RoleUtils.cs @@ -1,8 +1,8 @@ namespace Discord { - internal static class RoleExtensions + internal static class RoleUtils { - internal static int Compare(this IRole left, IRole right) + public static int Compare(IRole left, IRole right) { if (left == null) return -1; diff --git a/src/Discord.Net.Core/project.json b/src/Discord.Net.Core/project.json deleted file mode 100644 index 6a62b9474..000000000 --- a/src/Discord.Net.Core/project.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "version": "1.0.0-*", - "description": "A .Net API wrapper and bot framework for Discord.", - "authors": [ "RogueException" ], - - "packOptions": { - "tags": [ "discord", "discordapp" ], - "licenseUrl": "http://opensource.org/licenses/MIT", - "projectUrl": "https://github.com/RogueException/Discord.Net", - "repository": { - "type": "git", - "url": "git://github.com/RogueException/Discord.Net" - } - }, - - "configurations": { - "Release": { - "buildOptions": { - "define": [ "RELEASE" ], - "nowarn": [ "CS1573", "CS1591" ], - "optimize": true, - "warningsAsErrors": true, - "xmlDoc": true - } - } - }, - - "dependencies": { - "Microsoft.Win32.Primitives": "4.3.0", - "Newtonsoft.Json": "9.0.1", - "System.Collections.Concurrent": "4.3.0", - "System.Collections.Immutable": "1.3.0", - "System.Interactive.Async": "3.1.0", - "System.Net.Http": "4.3.0", - "System.Net.WebSockets.Client": { - "version": "4.3.0", - "type": "build" - } - }, - - "frameworks": { - "netstandard1.3": { - "imports": [ - "dotnet5.4", - "dnxcore50", - "portable-net45+win8" - ] - } - } -} diff --git a/src/Discord.Net.DebugTools/Discord.Net.DebugTools.csproj b/src/Discord.Net.DebugTools/Discord.Net.DebugTools.csproj new file mode 100644 index 000000000..829951d19 --- /dev/null +++ b/src/Discord.Net.DebugTools/Discord.Net.DebugTools.csproj @@ -0,0 +1,31 @@ + + + 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 + + + + + + + + + + + $(NoWarn);CS1573;CS1591 + true + true + + \ No newline at end of file diff --git a/src/Discord.Net.DebugTools/UnstableUdpClient.cs b/src/Discord.Net.DebugTools/UnstableUdpClient.cs new file mode 100644 index 000000000..297c689cf --- /dev/null +++ b/src/Discord.Net.DebugTools/UnstableUdpClient.cs @@ -0,0 +1,142 @@ +using Discord.Net.Udp; +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.Providers.UnstableUdpSocket +{ + internal class UnstableUdpSocket : IUdpSocket, IDisposable + { + private const double FailureRate = 0.10; //10% + + public event Func ReceivedDatagram; + + private readonly SemaphoreSlim _lock; + private readonly Random _rand; + private UdpClient _udp; + private IPEndPoint _destination; + private CancellationTokenSource _cancelTokenSource; + private CancellationToken _cancelToken, _parentToken; + private Task _task; + private bool _isDisposed; + + public UnstableUdpSocket() + { + _lock = new SemaphoreSlim(1, 1); + _rand = new Random(); + _cancelTokenSource = new CancellationTokenSource(); + } + 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 UdpClient(0); + + _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.Dispose(); } + 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 (!UnstableCheck()) + return; + + 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); + } + } + + private bool UnstableCheck() + { + return _rand.NextDouble() > FailureRate; + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.DebugTools/UnstableUdpClientProvider.cs b/src/Discord.Net.DebugTools/UnstableUdpClientProvider.cs new file mode 100644 index 000000000..e78514602 --- /dev/null +++ b/src/Discord.Net.DebugTools/UnstableUdpClientProvider.cs @@ -0,0 +1,9 @@ +using Discord.Net.Udp; + +namespace Discord.Net.Providers.UnstableUdpSocket +{ + public static class UnstableUdpSocketProvider + { + public static readonly UdpSocketProvider Instance = () => new UnstableUdpSocket(); + } +} diff --git a/src/Discord.Net.DebugTools/UnstableWebSocketClient.cs b/src/Discord.Net.DebugTools/UnstableWebSocketClient.cs new file mode 100644 index 000000000..a0f28ba0a --- /dev/null +++ b/src/Discord.Net.DebugTools/UnstableWebSocketClient.cs @@ -0,0 +1,256 @@ +using Discord.Net.WebSockets; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.Providers.UnstableWebSocket +{ + internal class UnstableWebSocketClient : IWebSocketClient, IDisposable + { + public const int ReceiveChunkSize = 16 * 1024; //16KB + public const int SendChunkSize = 4 * 1024; //4KB + private const int HR_TIMEOUT = -2147012894; + private const double FailureRate = 0.10; //10% + + public event Func BinaryMessage; + public event Func TextMessage; + public event Func Closed; + + private readonly SemaphoreSlim _lock; + private readonly Dictionary _headers; + private readonly Random _rand; + private ClientWebSocket _client; + private Task _task; + private CancellationTokenSource _cancelTokenSource; + private CancellationToken _cancelToken, _parentToken; + private bool _isDisposed, _isDisconnecting; + + public UnstableWebSocketClient() + { + _lock = new SemaphoreSlim(1, 1); + _rand = new Random(); + _cancelTokenSource = new CancellationTokenSource(); + _cancelToken = CancellationToken.None; + _parentToken = CancellationToken.None; + _headers = new Dictionary(); + } + private void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + DisconnectInternalAsync(true).GetAwaiter().GetResult(); + _isDisposed = true; + } + } + public void Dispose() + { + Dispose(true); + } + + public async Task ConnectAsync(string host) + { + await _lock.WaitAsync().ConfigureAwait(false); + try + { + await ConnectInternalAsync(host).ConfigureAwait(false); + } + finally + { + _lock.Release(); + } + } + private async Task ConnectInternalAsync(string host) + { + await DisconnectInternalAsync().ConfigureAwait(false); + + _cancelTokenSource = new CancellationTokenSource(); + _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; + + _client = new ClientWebSocket(); + _client.Options.Proxy = null; + _client.Options.KeepAliveInterval = TimeSpan.Zero; + foreach (var header in _headers) + { + if (header.Value != null) + _client.Options.SetRequestHeader(header.Key, header.Value); + } + + await _client.ConnectAsync(new Uri(host), _cancelToken).ConfigureAwait(false); + _task = RunAsync(_cancelToken); + } + + public async Task DisconnectAsync() + { + await _lock.WaitAsync().ConfigureAwait(false); + try + { + await DisconnectInternalAsync().ConfigureAwait(false); + } + finally + { + _lock.Release(); + } + } + private async Task DisconnectInternalAsync(bool isDisposing = false) + { + try { _cancelTokenSource.Cancel(false); } catch { } + + _isDisconnecting = true; + try + { + await (_task ?? Task.Delay(0)).ConfigureAwait(false); + _task = null; + } + finally { _isDisconnecting = false; } + + if (_client != null) + { + if (!isDisposing) + { + try { await _client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", new CancellationToken()); } + catch { } + } + try { _client.Dispose(); } + catch { } + + _client = null; + } + } + private async Task OnClosed(Exception ex) + { + if (_isDisconnecting) + return; //Ignore, this disconnect was requested. + + System.Diagnostics.Debug.WriteLine("OnClosed - " + ex.Message); + await _lock.WaitAsync().ConfigureAwait(false); + try + { + await DisconnectInternalAsync(false); + } + finally + { + _lock.Release(); + } + await Closed(ex); + } + + public void SetHeader(string key, string value) + { + _headers[key] = value; + } + public void SetCancelToken(CancellationToken cancelToken) + { + _parentToken = cancelToken; + _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; + } + + public async Task SendAsync(byte[] data, int index, int count, bool isText) + { + await _lock.WaitAsync().ConfigureAwait(false); + try + { + if (!UnstableCheck()) + return; + + if (_client == null) return; + + int frameCount = (int)Math.Ceiling((double)count / SendChunkSize); + + for (int i = 0; i < frameCount; i++, index += SendChunkSize) + { + bool isLast = i == (frameCount - 1); + + int frameSize; + if (isLast) + frameSize = count - (i * SendChunkSize); + else + frameSize = SendChunkSize; + + var type = isText ? WebSocketMessageType.Text : WebSocketMessageType.Binary; + await _client.SendAsync(new ArraySegment(data, index, count), type, isLast, _cancelToken).ConfigureAwait(false); + } + } + finally + { + _lock.Release(); + } + } + + private async Task RunAsync(CancellationToken cancelToken) + { + var buffer = new ArraySegment(new byte[ReceiveChunkSize]); + + try + { + while (!cancelToken.IsCancellationRequested) + { + WebSocketReceiveResult socketResult = await _client.ReceiveAsync(buffer, cancelToken).ConfigureAwait(false); + byte[] result; + int resultCount; + + if (socketResult.MessageType == WebSocketMessageType.Close) + throw new WebSocketClosedException((int)socketResult.CloseStatus, socketResult.CloseStatusDescription); + + if (!socketResult.EndOfMessage) + { + //This is a large message (likely just READY), lets create a temporary expandable stream + using (var stream = new MemoryStream()) + { + stream.Write(buffer.Array, 0, socketResult.Count); + do + { + if (cancelToken.IsCancellationRequested) return; + socketResult = await _client.ReceiveAsync(buffer, cancelToken).ConfigureAwait(false); + stream.Write(buffer.Array, 0, socketResult.Count); + } + while (socketResult == null || !socketResult.EndOfMessage); + + //Use the internal buffer if we can get it + resultCount = (int)stream.Length; + ArraySegment streamBuffer; + if (stream.TryGetBuffer(out streamBuffer)) + result = streamBuffer.Array; + else + result = stream.ToArray(); + } + } + else + { + //Small message + resultCount = socketResult.Count; + result = buffer.Array; + } + + if (socketResult.MessageType == WebSocketMessageType.Text) + { + string text = Encoding.UTF8.GetString(result, 0, resultCount); + await TextMessage(text).ConfigureAwait(false); + } + else + await BinaryMessage(result, 0, resultCount).ConfigureAwait(false); + } + } + catch (Win32Exception ex) when (ex.HResult == HR_TIMEOUT) + { + var _ = OnClosed(new Exception("Connection timed out.", ex)); + } + catch (OperationCanceledException) { } + catch (Exception ex) + { + //This cannot be awaited otherwise we'll deadlock when DiscordApiClient waits for this task to complete. + var _ = OnClosed(ex); + } + } + + private bool UnstableCheck() + { + return _rand.NextDouble() > FailureRate; + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.DebugTools/UnstableWebSocketClientProvider.cs b/src/Discord.Net.DebugTools/UnstableWebSocketClientProvider.cs new file mode 100644 index 000000000..9619e8882 --- /dev/null +++ b/src/Discord.Net.DebugTools/UnstableWebSocketClientProvider.cs @@ -0,0 +1,9 @@ +using Discord.Net.WebSockets; + +namespace Discord.Net.Providers.UnstableWebSocket +{ + public static class UnstableWebSocketProvider + { + public static readonly WebSocketProvider Instance = () => new UnstableWebSocketClient(); + } +} diff --git a/src/Discord.Net.Providers.UdpClient/Discord.Net.Providers.UdpClient.csproj b/src/Discord.Net.Providers.UdpClient/Discord.Net.Providers.UdpClient.csproj new file mode 100644 index 000000000..984cd8f9c --- /dev/null +++ b/src/Discord.Net.Providers.UdpClient/Discord.Net.Providers.UdpClient.csproj @@ -0,0 +1,26 @@ + + + 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 new file mode 100644 index 000000000..459feb335 --- /dev/null +++ b/src/Discord.Net.Providers.UdpClient/UDPClient.cs @@ -0,0 +1,128 @@ +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 new file mode 100644 index 000000000..6bdf9eb63 --- /dev/null +++ b/src/Discord.Net.Providers.UdpClient/UDPClientProvider.cs @@ -0,0 +1,9 @@ +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 new file mode 100644 index 000000000..62adfc0b1 --- /dev/null +++ b/src/Discord.Net.Providers.WS4Net/Discord.Net.Providers.WS4Net.csproj @@ -0,0 +1,30 @@ + + + 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 + + + + + + + + + $(NoWarn);CS1573;CS1591 + true + true + + \ No newline at end of file diff --git a/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs b/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs new file mode 100644 index 000000000..93d6a83d6 --- /dev/null +++ b/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs @@ -0,0 +1,166 @@ +using Discord.Net.WebSockets; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using WebSocket4Net; +using WS4NetSocket = WebSocket4Net.WebSocket; + +namespace Discord.Net.Providers.WS4Net +{ + internal class WS4NetClient : IWebSocketClient, IDisposable + { + public event Func BinaryMessage; + public event Func TextMessage; + public event Func Closed; + + private readonly SemaphoreSlim _lock; + private readonly Dictionary _headers; + private WS4NetSocket _client; + private CancellationTokenSource _cancelTokenSource; + private CancellationToken _cancelToken, _parentToken; + private ManualResetEventSlim _waitUntilConnect; + private bool _isDisposed; + + public WS4NetClient() + { + _headers = new Dictionary(); + _lock = new SemaphoreSlim(1, 1); + _cancelTokenSource = new CancellationTokenSource(); + _cancelToken = CancellationToken.None; + _parentToken = CancellationToken.None; + _waitUntilConnect = new ManualResetEventSlim(); + } + private void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + DisconnectInternalAsync(true).GetAwaiter().GetResult(); + _isDisposed = true; + } + } + public void Dispose() + { + Dispose(true); + } + + public async Task ConnectAsync(string host) + { + await _lock.WaitAsync().ConfigureAwait(false); + try + { + await ConnectInternalAsync(host).ConfigureAwait(false); + } + finally + { + _lock.Release(); + } + } + private async Task ConnectInternalAsync(string host) + { + await DisconnectInternalAsync().ConfigureAwait(false); + + _cancelTokenSource = new CancellationTokenSource(); + _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; + + _client = new WS4NetSocket(host, customHeaderItems: _headers.ToList()) + { + EnableAutoSendPing = false, + NoDelay = true, + Proxy = null + }; + + _client.MessageReceived += OnTextMessage; + _client.DataReceived += OnBinaryMessage; + _client.Opened += OnConnected; + _client.Closed += OnClosed; + + _client.Open(); + _waitUntilConnect.Wait(_cancelToken); + } + + public async Task DisconnectAsync() + { + await _lock.WaitAsync().ConfigureAwait(false); + try + { + await DisconnectInternalAsync().ConfigureAwait(false); + } + finally + { + _lock.Release(); + } + } + private Task DisconnectInternalAsync(bool isDisposing = false) + { + _cancelTokenSource.Cancel(); + if (_client == null) + return Task.Delay(0); + + if (_client.State == WebSocketState.Open) + { + try { _client.Close(1000, ""); } + catch { } + } + + _client.MessageReceived -= OnTextMessage; + _client.DataReceived -= OnBinaryMessage; + _client.Opened -= OnConnected; + _client.Closed -= OnClosed; + + try { _client.Dispose(); } + catch { } + _client = null; + + _waitUntilConnect.Reset(); + return Task.Delay(0); + } + + public void SetHeader(string key, string value) + { + _headers[key] = value; + } + public void SetCancelToken(CancellationToken cancelToken) + { + _parentToken = cancelToken; + _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; + } + + public async Task SendAsync(byte[] data, int index, int count, bool isText) + { + await _lock.WaitAsync(_cancelToken).ConfigureAwait(false); + try + { + if (isText) + _client.Send(Encoding.UTF8.GetString(data, index, count)); + else + _client.Send(data, index, count); + } + finally + { + _lock.Release(); + } + } + + private void OnTextMessage(object sender, MessageReceivedEventArgs e) + { + TextMessage(e.Message).GetAwaiter().GetResult(); + } + private void OnBinaryMessage(object sender, DataReceivedEventArgs e) + { + BinaryMessage(e.Data, 0, e.Data.Count()).GetAwaiter().GetResult(); + } + private void OnConnected(object sender, object e) + { + _waitUntilConnect.Set(); + } + private void OnClosed(object sender, object e) + { + var ex = (e as SuperSocket.ClientEngine.ErrorEventArgs)?.Exception ?? new Exception("Unexpected close"); + Closed(ex).GetAwaiter().GetResult(); + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Providers.WS4Net/WS4NetProvider.cs b/src/Discord.Net.Providers.WS4Net/WS4NetProvider.cs new file mode 100644 index 000000000..166e767d0 --- /dev/null +++ b/src/Discord.Net.Providers.WS4Net/WS4NetProvider.cs @@ -0,0 +1,9 @@ +using Discord.Net.WebSockets; + +namespace Discord.Net.Providers.WS4Net +{ + public static class WS4NetProvider + { + public static readonly WebSocketProvider Instance = () => new WS4NetClient(); + } +} diff --git a/src/Discord.Net.Relay/ApplicationBuilderExtensions.cs b/src/Discord.Net.Relay/ApplicationBuilderExtensions.cs new file mode 100644 index 000000000..2a1e759c0 --- /dev/null +++ b/src/Discord.Net.Relay/ApplicationBuilderExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Builder; +using System; + +namespace Discord.Relay +{ + public static class ApplicationBuilderExtensions + { + public static void UseDiscordRelay(this IApplicationBuilder app, Action configAction = null) + { + var server = new RelayServer(configAction); + server.StartAsync(); + app.Use(async (context, next) => + { + if (context.WebSockets.IsWebSocketRequest) + await server.AcceptAsync(context); + await next(); + }); + } + } +} diff --git a/src/Discord.Net.Relay/AssemblyInfo.cs b/src/Discord.Net.Relay/AssemblyInfo.cs new file mode 100644 index 000000000..5e9efa5bc --- /dev/null +++ b/src/Discord.Net.Relay/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.Relay/Discord.Net.Relay.csproj b/src/Discord.Net.Relay/Discord.Net.Relay.csproj new file mode 100644 index 000000000..8fee12d14 --- /dev/null +++ b/src/Discord.Net.Relay/Discord.Net.Relay.csproj @@ -0,0 +1,32 @@ + + + 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.Relay/RelayConnection.cs b/src/Discord.Net.Relay/RelayConnection.cs new file mode 100644 index 000000000..ffce74f9c --- /dev/null +++ b/src/Discord.Net.Relay/RelayConnection.cs @@ -0,0 +1,79 @@ +using Discord.API; +using Discord.API.Gateway; +using Discord.Logging; +using System; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; +using WebSocketClient = System.Net.WebSockets.WebSocket; + +namespace Discord.Relay +{ + public class RelayConnection + { + private readonly RelayServer _server; + private readonly WebSocketClient _socket; + private readonly CancellationTokenSource _cancelToken; + private readonly byte[] _inBuffer, _outBuffer; + private readonly Logger _logger; + + internal RelayConnection(RelayServer server, WebSocketClient socket, int id) + { + _server = server; + _socket = socket; + _cancelToken = new CancellationTokenSource(); + _inBuffer = new byte[4000]; + _outBuffer = new byte[4000]; + _logger = server.LogManager.CreateLogger($"Client #{id}"); + } + + internal async Task RunAsync() + { + await _logger.InfoAsync($"Connected"); + var token = _cancelToken.Token; + try + { + var segment = new ArraySegment(_inBuffer); + + //Send HELLO + await SendAsync(GatewayOpCode.Hello, new HelloEvent { HeartbeatInterval = 15000 }).ConfigureAwait(false); + + while (_socket.State == WebSocketState.Open) + { + var result = await _socket.ReceiveAsync(segment, token).ConfigureAwait(false); + if (result.MessageType == WebSocketMessageType.Close) + await _logger.WarningAsync($"Received Close {result.CloseStatus} ({result.CloseStatusDescription ?? "No Reason"})").ConfigureAwait(false); + else + await _logger.InfoAsync($"Received {result.Count} bytes"); + } + } + catch (OperationCanceledException) + { + try { await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None).ConfigureAwait(false); } + catch { } + } + catch (Exception ex) + { + try { await _socket.CloseAsync(WebSocketCloseStatus.InternalServerError, ex.Message, CancellationToken.None).ConfigureAwait(false); } + catch { } + } + finally + { + await _logger.InfoAsync($"Disconnected"); + } + } + + internal void Stop() + { + _cancelToken.Cancel(); + } + + private async Task SendAsync(GatewayOpCode opCode, object payload) + { + var frame = new SocketFrame { Operation = (int)opCode, Payload = payload }; + var bytes = _server.Serialize(frame, _outBuffer); + var segment = new ArraySegment(_outBuffer, 0, bytes); + await _socket.SendAsync(segment, WebSocketMessageType.Text, true, CancellationToken.None).ConfigureAwait(false); + } + } +} diff --git a/src/Discord.Net.Relay/RelayServer.cs b/src/Discord.Net.Relay/RelayServer.cs new file mode 100644 index 000000000..4082191fb --- /dev/null +++ b/src/Discord.Net.Relay/RelayServer.cs @@ -0,0 +1,103 @@ +using Discord.API; +using Discord.Logging; +using Discord.Net.Rest; +using Discord.Net.WebSockets; +using Discord.Rest; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using WebSocketClient = System.Net.WebSockets.WebSocket; + +namespace Discord.Relay +{ + public class RelayServer + { + public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } + internal readonly AsyncEvent> _logEvent = new AsyncEvent>(); + + private readonly HashSet _connections; + private readonly SemaphoreSlim _lock; + private readonly JsonSerializer _serializer; + private readonly DiscordSocketApiClient _discord; + private int _nextId; + + internal LogManager LogManager { get; } + + internal RelayServer(Action configAction) + { + _connections = new HashSet(); + _lock = new SemaphoreSlim(1, 1); + _serializer = new JsonSerializer(); + _discord = new DiscordSocketApiClient( + DefaultRestClientProvider.Instance, + DefaultWebSocketProvider.Instance, + DiscordRestConfig.UserAgent); + configAction?.Invoke(this); + + LogManager = new LogManager(LogSeverity.Debug); + LogManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); + } + + internal async Task AcceptAsync(HttpContext context) + { + WebSocketClient socket; + try + { + socket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false); + } + catch { return; } + + var _ = Task.Run(async () => + { + var conn = new RelayConnection(this, socket, Interlocked.Increment(ref _nextId)); + await AddConnection(conn).ConfigureAwait(false); + try + { + await conn.RunAsync().ConfigureAwait(false); + } + finally { await RemoveConnection(conn).ConfigureAwait(false); } + }); + } + + internal void StartAsync() + { + Task.Run(async () => + { + await _discord.ConnectAsync().ConfigureAwait(false); + }); + } + + internal async Task AddConnection(RelayConnection conn) + { + await _lock.WaitAsync().ConfigureAwait(false); + try + { + _connections.Add(conn); + } + finally { _lock.Release(); } + } + internal async Task RemoveConnection(RelayConnection conn) + { + await _lock.WaitAsync().ConfigureAwait(false); + try + { + _connections.Remove(conn); + } + finally { _lock.Release(); } + } + + internal int Serialize(object obj, byte[] buffer) + { + using (var stream = new MemoryStream(buffer)) + using (var writer = new StreamWriter(stream)) + { + _serializer.Serialize(writer, obj); + return (int)stream.Position; + } + } + } +} diff --git a/src/Discord.Net.Core/API/Common/Application.cs b/src/Discord.Net.Rest/API/Common/Application.cs similarity index 95% rename from src/Discord.Net.Core/API/Common/Application.cs rename to src/Discord.Net.Rest/API/Common/Application.cs index e72c6ce79..ca4c443f1 100644 --- a/src/Discord.Net.Core/API/Common/Application.cs +++ b/src/Discord.Net.Rest/API/Common/Application.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API { - public class Application + internal class Application { [JsonProperty("description")] public string Description { get; set; } diff --git a/src/Discord.Net.Core/API/Common/Attachment.cs b/src/Discord.Net.Rest/API/Common/Attachment.cs similarity index 95% rename from src/Discord.Net.Core/API/Common/Attachment.cs rename to src/Discord.Net.Rest/API/Common/Attachment.cs index 9ab2a798b..4a651d9fa 100644 --- a/src/Discord.Net.Core/API/Common/Attachment.cs +++ b/src/Discord.Net.Rest/API/Common/Attachment.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API { - public class Attachment + internal class Attachment { [JsonProperty("id")] public ulong Id { get; set; } diff --git a/src/Discord.Net.Core/API/Common/Ban.cs b/src/Discord.Net.Rest/API/Common/Ban.cs similarity index 91% rename from src/Discord.Net.Core/API/Common/Ban.cs rename to src/Discord.Net.Rest/API/Common/Ban.cs index c1f667e65..202004f53 100644 --- a/src/Discord.Net.Core/API/Common/Ban.cs +++ b/src/Discord.Net.Rest/API/Common/Ban.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API { - public class Ban + internal class Ban { [JsonProperty("user")] public User User { get; set; } diff --git a/src/Discord.Net.Core/API/Common/Channel.cs b/src/Discord.Net.Rest/API/Common/Channel.cs similarity index 98% rename from src/Discord.Net.Core/API/Common/Channel.cs rename to src/Discord.Net.Rest/API/Common/Channel.cs index 5b02e6806..56a24a1f4 100644 --- a/src/Discord.Net.Core/API/Common/Channel.cs +++ b/src/Discord.Net.Rest/API/Common/Channel.cs @@ -4,7 +4,7 @@ using System; namespace Discord.API { - public class Channel + internal class Channel { //Shared [JsonProperty("id")] diff --git a/src/Discord.Net.Core/API/Common/Connection.cs b/src/Discord.Net.Rest/API/Common/Connection.cs similarity index 94% rename from src/Discord.Net.Core/API/Common/Connection.cs rename to src/Discord.Net.Rest/API/Common/Connection.cs index 0d218d6a2..ad0a76ac1 100644 --- a/src/Discord.Net.Core/API/Common/Connection.cs +++ b/src/Discord.Net.Rest/API/Common/Connection.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; namespace Discord.API { - public class Connection + internal class Connection { [JsonProperty("id")] public string Id { get; set; } diff --git a/src/Discord.Net.Core/API/Common/Embed.cs b/src/Discord.Net.Rest/API/Common/Embed.cs similarity index 75% rename from src/Discord.Net.Core/API/Common/Embed.cs rename to src/Discord.Net.Rest/API/Common/Embed.cs index ed4715237..f6325efbb 100644 --- a/src/Discord.Net.Core/API/Common/Embed.cs +++ b/src/Discord.Net.Rest/API/Common/Embed.cs @@ -1,9 +1,10 @@ #pragma warning disable CS1591 +using System; using Newtonsoft.Json; namespace Discord.API { - public class Embed + internal class Embed { [JsonProperty("title")] public string Title { get; set; } @@ -15,12 +16,18 @@ namespace Discord.API public string Url { get; set; } [JsonProperty("color")] public uint? Color { get; set; } + [JsonProperty("timestamp")] + public DateTimeOffset? Timestamp { get; set; } [JsonProperty("author")] public Optional Author { get; set; } [JsonProperty("footer")] public Optional Footer { get; set; } + [JsonProperty("video")] + public Optional Video { get; set; } [JsonProperty("thumbnail")] public Optional Thumbnail { get; set; } + [JsonProperty("image")] + public Optional Image { get; set; } [JsonProperty("provider")] public Optional Provider { get; set; } [JsonProperty("fields")] diff --git a/src/Discord.Net.Core/API/Common/EmbedAuthor.cs b/src/Discord.Net.Rest/API/Common/EmbedAuthor.cs similarity index 92% rename from src/Discord.Net.Core/API/Common/EmbedAuthor.cs rename to src/Discord.Net.Rest/API/Common/EmbedAuthor.cs index 973f7d5ea..e69fee6eb 100644 --- a/src/Discord.Net.Core/API/Common/EmbedAuthor.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedAuthor.cs @@ -2,7 +2,7 @@ namespace Discord.API { - public class EmbedAuthor + internal class EmbedAuthor { [JsonProperty("name")] public string Name { get; set; } diff --git a/src/Discord.Net.Core/API/Common/EmbedField.cs b/src/Discord.Net.Rest/API/Common/EmbedField.cs similarity index 90% rename from src/Discord.Net.Core/API/Common/EmbedField.cs rename to src/Discord.Net.Rest/API/Common/EmbedField.cs index 12aa0137a..6ce810f1a 100644 --- a/src/Discord.Net.Core/API/Common/EmbedField.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedField.cs @@ -2,7 +2,7 @@ namespace Discord.API { - public class EmbedField + internal class EmbedField { [JsonProperty("name")] public string Name { get; set; } diff --git a/src/Discord.Net.Core/API/Common/EmbedFooter.cs b/src/Discord.Net.Rest/API/Common/EmbedFooter.cs similarity index 90% rename from src/Discord.Net.Core/API/Common/EmbedFooter.cs rename to src/Discord.Net.Rest/API/Common/EmbedFooter.cs index 2ad22cae7..27048972e 100644 --- a/src/Discord.Net.Core/API/Common/EmbedFooter.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedFooter.cs @@ -2,7 +2,7 @@ namespace Discord.API { - public class EmbedFooter + internal class EmbedFooter { [JsonProperty("text")] public string Text { get; set; } diff --git a/src/Discord.Net.Core/API/Common/EmbedThumbnail.cs b/src/Discord.Net.Rest/API/Common/EmbedImage.cs similarity index 92% rename from src/Discord.Net.Core/API/Common/EmbedThumbnail.cs rename to src/Discord.Net.Rest/API/Common/EmbedImage.cs index 59cbd9e6a..a5ef748f8 100644 --- a/src/Discord.Net.Core/API/Common/EmbedThumbnail.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedImage.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API { - public class EmbedThumbnail + internal class EmbedImage { [JsonProperty("url")] public string Url { get; set; } diff --git a/src/Discord.Net.Core/API/Common/EmbedProvider.cs b/src/Discord.Net.Rest/API/Common/EmbedProvider.cs similarity index 87% rename from src/Discord.Net.Core/API/Common/EmbedProvider.cs rename to src/Discord.Net.Rest/API/Common/EmbedProvider.cs index c23998628..8c46b10dc 100644 --- a/src/Discord.Net.Core/API/Common/EmbedProvider.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedProvider.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API { - public class EmbedProvider + internal class EmbedProvider { [JsonProperty("name")] public string Name { get; set; } diff --git a/src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs b/src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs new file mode 100644 index 000000000..f22953a25 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs @@ -0,0 +1,17 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class EmbedThumbnail + { + [JsonProperty("url")] + public string Url { get; set; } + [JsonProperty("proxy_url")] + public string ProxyUrl { get; set; } + [JsonProperty("height")] + public Optional Height { get; set; } + [JsonProperty("width")] + public Optional Width { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/EmbedVideo.cs b/src/Discord.Net.Rest/API/Common/EmbedVideo.cs new file mode 100644 index 000000000..09e933784 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/EmbedVideo.cs @@ -0,0 +1,15 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class EmbedVideo + { + [JsonProperty("url")] + public string Url { get; set; } + [JsonProperty("height")] + public Optional Height { get; set; } + [JsonProperty("width")] + public Optional Width { get; set; } + } +} diff --git a/src/Discord.Net.Core/API/Common/Emoji.cs b/src/Discord.Net.Rest/API/Common/Emoji.cs similarity index 87% rename from src/Discord.Net.Core/API/Common/Emoji.cs rename to src/Discord.Net.Rest/API/Common/Emoji.cs index 032ae51eb..bd9c4d466 100644 --- a/src/Discord.Net.Core/API/Common/Emoji.cs +++ b/src/Discord.Net.Rest/API/Common/Emoji.cs @@ -3,10 +3,10 @@ using Newtonsoft.Json; namespace Discord.API { - public class Emoji + internal class Emoji { [JsonProperty("id")] - public ulong Id { get; set; } + public ulong? Id { get; set; } [JsonProperty("name")] public string Name { get; set; } [JsonProperty("roles")] diff --git a/src/Discord.Net.Core/API/Common/Game.cs b/src/Discord.Net.Rest/API/Common/Game.cs similarity index 96% rename from src/Discord.Net.Core/API/Common/Game.cs rename to src/Discord.Net.Rest/API/Common/Game.cs index df5c78880..a499d83b0 100644 --- a/src/Discord.Net.Core/API/Common/Game.cs +++ b/src/Discord.Net.Rest/API/Common/Game.cs @@ -5,7 +5,7 @@ using System.Runtime.Serialization; namespace Discord.API { - public class Game + internal class Game { [JsonProperty("name")] public string Name { get; set; } diff --git a/src/Discord.Net.Core/API/Common/Guild.cs b/src/Discord.Net.Rest/API/Common/Guild.cs similarity index 98% rename from src/Discord.Net.Core/API/Common/Guild.cs rename to src/Discord.Net.Rest/API/Common/Guild.cs index 567b34714..b69ba1293 100644 --- a/src/Discord.Net.Core/API/Common/Guild.cs +++ b/src/Discord.Net.Rest/API/Common/Guild.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API { - public class Guild + internal class Guild { [JsonProperty("id")] public ulong Id { get; set; } diff --git a/src/Discord.Net.Core/API/Common/GuildEmbed.cs b/src/Discord.Net.Rest/API/Common/GuildEmbed.cs similarity index 89% rename from src/Discord.Net.Core/API/Common/GuildEmbed.cs rename to src/Discord.Net.Rest/API/Common/GuildEmbed.cs index 3cf4176eb..ff8b8e180 100644 --- a/src/Discord.Net.Core/API/Common/GuildEmbed.cs +++ b/src/Discord.Net.Rest/API/Common/GuildEmbed.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API { - public class GuildEmbed + internal class GuildEmbed { [JsonProperty("enabled")] public bool Enabled { get; set; } diff --git a/src/Discord.Net.Core/API/Common/GuildMember.cs b/src/Discord.Net.Rest/API/Common/GuildMember.cs similarity index 84% rename from src/Discord.Net.Core/API/Common/GuildMember.cs rename to src/Discord.Net.Rest/API/Common/GuildMember.cs index 116a2b29a..daba36d23 100644 --- a/src/Discord.Net.Core/API/Common/GuildMember.cs +++ b/src/Discord.Net.Rest/API/Common/GuildMember.cs @@ -4,7 +4,7 @@ using System; namespace Discord.API { - public class GuildMember + internal class GuildMember { [JsonProperty("user")] public User User { get; set; } @@ -13,7 +13,7 @@ namespace Discord.API [JsonProperty("roles")] public ulong[] Roles { get; set; } [JsonProperty("joined_at")] - public DateTimeOffset JoinedAt { get; set; } + public Optional JoinedAt { get; set; } [JsonProperty("deaf")] public bool Deaf { get; set; } [JsonProperty("mute")] diff --git a/src/Discord.Net.Core/API/Common/Integration.cs b/src/Discord.Net.Rest/API/Common/Integration.cs similarity index 96% rename from src/Discord.Net.Core/API/Common/Integration.cs rename to src/Discord.Net.Rest/API/Common/Integration.cs index c27649f97..821359975 100644 --- a/src/Discord.Net.Core/API/Common/Integration.cs +++ b/src/Discord.Net.Rest/API/Common/Integration.cs @@ -4,7 +4,7 @@ using System; namespace Discord.API { - public class Integration + internal class Integration { [JsonProperty("id")] public ulong Id { get; set; } diff --git a/src/Discord.Net.Core/API/Common/IntegrationAccount.cs b/src/Discord.Net.Rest/API/Common/IntegrationAccount.cs similarity index 86% rename from src/Discord.Net.Core/API/Common/IntegrationAccount.cs rename to src/Discord.Net.Rest/API/Common/IntegrationAccount.cs index d1ab761b6..22831e795 100644 --- a/src/Discord.Net.Core/API/Common/IntegrationAccount.cs +++ b/src/Discord.Net.Rest/API/Common/IntegrationAccount.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API { - public class IntegrationAccount + internal class IntegrationAccount { [JsonProperty("id")] public ulong Id { get; set; } diff --git a/src/Discord.Net.Core/API/Common/Invite.cs b/src/Discord.Net.Rest/API/Common/Invite.cs similarity index 92% rename from src/Discord.Net.Core/API/Common/Invite.cs rename to src/Discord.Net.Rest/API/Common/Invite.cs index 646e30a59..67a318c5a 100644 --- a/src/Discord.Net.Core/API/Common/Invite.cs +++ b/src/Discord.Net.Rest/API/Common/Invite.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API { - public class Invite + internal class Invite { [JsonProperty("code")] public string Code { get; set; } diff --git a/src/Discord.Net.Core/API/Common/InviteChannel.cs b/src/Discord.Net.Rest/API/Common/InviteChannel.cs similarity index 90% rename from src/Discord.Net.Core/API/Common/InviteChannel.cs rename to src/Discord.Net.Rest/API/Common/InviteChannel.cs index 4780573b3..ca9699067 100644 --- a/src/Discord.Net.Core/API/Common/InviteChannel.cs +++ b/src/Discord.Net.Rest/API/Common/InviteChannel.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API { - public class InviteChannel + internal class InviteChannel { [JsonProperty("id")] public ulong Id { get; set; } diff --git a/src/Discord.Net.Core/API/Common/InviteGuild.cs b/src/Discord.Net.Rest/API/Common/InviteGuild.cs similarity index 91% rename from src/Discord.Net.Core/API/Common/InviteGuild.cs rename to src/Discord.Net.Rest/API/Common/InviteGuild.cs index f358625b3..3d6d7cd74 100644 --- a/src/Discord.Net.Core/API/Common/InviteGuild.cs +++ b/src/Discord.Net.Rest/API/Common/InviteGuild.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API { - public class InviteGuild + internal class InviteGuild { [JsonProperty("id")] public ulong Id { get; set; } diff --git a/src/Discord.Net.Core/API/Common/InviteMetadata.cs b/src/Discord.Net.Rest/API/Common/InviteMetadata.cs similarity index 93% rename from src/Discord.Net.Core/API/Common/InviteMetadata.cs rename to src/Discord.Net.Rest/API/Common/InviteMetadata.cs index a9603869d..586307523 100644 --- a/src/Discord.Net.Core/API/Common/InviteMetadata.cs +++ b/src/Discord.Net.Rest/API/Common/InviteMetadata.cs @@ -4,7 +4,7 @@ using System; namespace Discord.API { - public class InviteMetadata : Invite + internal class InviteMetadata : Invite { [JsonProperty("inviter")] public User Inviter { get; set; } diff --git a/src/Discord.Net.Core/API/Common/Message.cs b/src/Discord.Net.Rest/API/Common/Message.cs similarity index 92% rename from src/Discord.Net.Core/API/Common/Message.cs rename to src/Discord.Net.Rest/API/Common/Message.cs index 2c19780b1..9a7629b96 100644 --- a/src/Discord.Net.Core/API/Common/Message.cs +++ b/src/Discord.Net.Rest/API/Common/Message.cs @@ -4,7 +4,7 @@ using System; namespace Discord.API { - public class Message + internal class Message { [JsonProperty("id")] public ulong Id { get; set; } @@ -36,5 +36,7 @@ namespace Discord.API public Optional Embeds { get; set; } [JsonProperty("pinned")] public Optional Pinned { get; set; } + [JsonProperty("reactions")] + public Optional Reactions { get; set; } } } diff --git a/src/Discord.Net.Core/API/Common/Overwrite.cs b/src/Discord.Net.Rest/API/Common/Overwrite.cs similarity index 93% rename from src/Discord.Net.Core/API/Common/Overwrite.cs rename to src/Discord.Net.Rest/API/Common/Overwrite.cs index c72e03395..1ba836127 100644 --- a/src/Discord.Net.Core/API/Common/Overwrite.cs +++ b/src/Discord.Net.Rest/API/Common/Overwrite.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API { - public class Overwrite + internal class Overwrite { [JsonProperty("id")] public ulong TargetId { get; set; } diff --git a/src/Discord.Net.Core/API/Common/Presence.cs b/src/Discord.Net.Rest/API/Common/Presence.cs similarity index 95% rename from src/Discord.Net.Core/API/Common/Presence.cs rename to src/Discord.Net.Rest/API/Common/Presence.cs index 86ccd60bc..2902b7ce3 100644 --- a/src/Discord.Net.Core/API/Common/Presence.cs +++ b/src/Discord.Net.Rest/API/Common/Presence.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API { - public class Presence + internal class Presence { [JsonProperty("user")] public User User { get; set; } diff --git a/src/Discord.Net.Rest/API/Common/Reaction.cs b/src/Discord.Net.Rest/API/Common/Reaction.cs new file mode 100644 index 000000000..4d368ab2d --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/Reaction.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class Reaction + { + [JsonProperty("count")] + public int Count { get; set; } + [JsonProperty("me")] + public bool Me { get; set; } + [JsonProperty("emoji")] + public Emoji Emoji { get; set; } + } +} diff --git a/src/Discord.Net.Core/API/Common/ReadState.cs b/src/Discord.Net.Rest/API/Common/ReadState.cs similarity index 92% rename from src/Discord.Net.Core/API/Common/ReadState.cs rename to src/Discord.Net.Rest/API/Common/ReadState.cs index 7e1cacf56..6ea6e4bd0 100644 --- a/src/Discord.Net.Core/API/Common/ReadState.cs +++ b/src/Discord.Net.Rest/API/Common/ReadState.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API { - public class ReadState + internal class ReadState { [JsonProperty("id")] public ulong Id { get; set; } diff --git a/src/Discord.Net.Core/API/Common/Relationship.cs b/src/Discord.Net.Rest/API/Common/Relationship.cs similarity index 90% rename from src/Discord.Net.Core/API/Common/Relationship.cs rename to src/Discord.Net.Rest/API/Common/Relationship.cs index 877d0cf2a..ecbb96f80 100644 --- a/src/Discord.Net.Core/API/Common/Relationship.cs +++ b/src/Discord.Net.Rest/API/Common/Relationship.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API { - public class Relationship + internal class Relationship { [JsonProperty("id")] public ulong Id { get; set; } diff --git a/src/Discord.Net.Core/API/Common/RelationshipType.cs b/src/Discord.Net.Rest/API/Common/RelationshipType.cs similarity index 82% rename from src/Discord.Net.Core/API/Common/RelationshipType.cs rename to src/Discord.Net.Rest/API/Common/RelationshipType.cs index 94f0f73b4..0ed99f396 100644 --- a/src/Discord.Net.Core/API/Common/RelationshipType.cs +++ b/src/Discord.Net.Rest/API/Common/RelationshipType.cs @@ -1,7 +1,7 @@ #pragma warning disable CS1591 namespace Discord.API { - public enum RelationshipType + internal enum RelationshipType { Friend = 1, Blocked = 2, diff --git a/src/Discord.Net.Core/API/Common/Role.cs b/src/Discord.Net.Rest/API/Common/Role.cs similarity index 96% rename from src/Discord.Net.Core/API/Common/Role.cs rename to src/Discord.Net.Rest/API/Common/Role.cs index 6a3659489..856a8695f 100644 --- a/src/Discord.Net.Core/API/Common/Role.cs +++ b/src/Discord.Net.Rest/API/Common/Role.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API { - public class Role + internal class Role { [JsonProperty("id")] public ulong Id { get; set; } diff --git a/src/Discord.Net.Core/API/Common/User.cs b/src/Discord.Net.Rest/API/Common/User.cs similarity index 97% rename from src/Discord.Net.Core/API/Common/User.cs rename to src/Discord.Net.Rest/API/Common/User.cs index e8674a95c..d49d24623 100644 --- a/src/Discord.Net.Core/API/Common/User.cs +++ b/src/Discord.Net.Rest/API/Common/User.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API { - public class User + internal class User { [JsonProperty("id")] public ulong Id { get; set; } diff --git a/src/Discord.Net.Core/API/Common/UserGuild.cs b/src/Discord.Net.Rest/API/Common/UserGuild.cs similarity index 94% rename from src/Discord.Net.Core/API/Common/UserGuild.cs rename to src/Discord.Net.Rest/API/Common/UserGuild.cs index 7d7905dae..f4f763885 100644 --- a/src/Discord.Net.Core/API/Common/UserGuild.cs +++ b/src/Discord.Net.Rest/API/Common/UserGuild.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API { - public class UserGuild + internal class UserGuild { [JsonProperty("id")] public ulong Id { get; set; } diff --git a/src/Discord.Net.Core/API/Common/VoiceRegion.cs b/src/Discord.Net.Rest/API/Common/VoiceRegion.cs similarity index 94% rename from src/Discord.Net.Core/API/Common/VoiceRegion.cs rename to src/Discord.Net.Rest/API/Common/VoiceRegion.cs index aef07cc78..5f31e8f64 100644 --- a/src/Discord.Net.Core/API/Common/VoiceRegion.cs +++ b/src/Discord.Net.Rest/API/Common/VoiceRegion.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API { - public class VoiceRegion + internal class VoiceRegion { [JsonProperty("id")] public string Id { get; set; } diff --git a/src/Discord.Net.Core/API/Common/VoiceState.cs b/src/Discord.Net.Rest/API/Common/VoiceState.cs similarity index 96% rename from src/Discord.Net.Core/API/Common/VoiceState.cs rename to src/Discord.Net.Rest/API/Common/VoiceState.cs index 2039f25f5..563a5f95b 100644 --- a/src/Discord.Net.Core/API/Common/VoiceState.cs +++ b/src/Discord.Net.Rest/API/Common/VoiceState.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API { - public class VoiceState + internal class VoiceState { [JsonProperty("guild_id")] public ulong? GuildId { get; set; } diff --git a/src/Discord.Net.Core/API/EntityOrId.cs b/src/Discord.Net.Rest/API/EntityOrId.cs similarity index 90% rename from src/Discord.Net.Core/API/EntityOrId.cs rename to src/Discord.Net.Rest/API/EntityOrId.cs index 01ccdcb22..9bcda260a 100644 --- a/src/Discord.Net.Core/API/EntityOrId.cs +++ b/src/Discord.Net.Rest/API/EntityOrId.cs @@ -1,6 +1,6 @@ namespace Discord.API { - public struct EntityOrId + internal struct EntityOrId { public ulong Id { get; } public T Object { get; } diff --git a/src/Discord.Net.Core/API/Image.cs b/src/Discord.Net.Rest/API/Image.cs similarity index 93% rename from src/Discord.Net.Core/API/Image.cs rename to src/Discord.Net.Rest/API/Image.cs index 5442bd30f..b2357a0a6 100644 --- a/src/Discord.Net.Core/API/Image.cs +++ b/src/Discord.Net.Rest/API/Image.cs @@ -2,7 +2,7 @@ namespace Discord.API { - public struct Image + internal struct Image { public Stream Stream { get; } public string Hash { get; } diff --git a/src/Discord.Net.Core/API/Int53Attribute.cs b/src/Discord.Net.Rest/API/Int53Attribute.cs similarity index 71% rename from src/Discord.Net.Core/API/Int53Attribute.cs rename to src/Discord.Net.Rest/API/Int53Attribute.cs index 3e9139b9d..70ef2f185 100644 --- a/src/Discord.Net.Core/API/Int53Attribute.cs +++ b/src/Discord.Net.Rest/API/Int53Attribute.cs @@ -4,5 +4,5 @@ using System; namespace Discord.API { [AttributeUsage(AttributeTargets.Property)] - public class Int53Attribute : Attribute { } + internal class Int53Attribute : Attribute { } } diff --git a/src/Discord.Net.Core/Net/Rest/MultipartFile.cs b/src/Discord.Net.Rest/API/Net/MultipartFile.cs similarity index 89% rename from src/Discord.Net.Core/Net/Rest/MultipartFile.cs rename to src/Discord.Net.Rest/API/Net/MultipartFile.cs index f7244afd7..604852e90 100644 --- a/src/Discord.Net.Core/Net/Rest/MultipartFile.cs +++ b/src/Discord.Net.Rest/API/Net/MultipartFile.cs @@ -2,7 +2,7 @@ namespace Discord.Net.Rest { - struct MultipartFile + internal struct MultipartFile { public Stream Stream { get; } public string Filename { get; } diff --git a/src/Discord.Net.Core/API/Rest/CreateChannelInviteParams.cs b/src/Discord.Net.Rest/API/Rest/CreateChannelInviteParams.cs similarity index 76% rename from src/Discord.Net.Core/API/Rest/CreateChannelInviteParams.cs rename to src/Discord.Net.Rest/API/Rest/CreateChannelInviteParams.cs index 8a619a8b7..db79bc314 100644 --- a/src/Discord.Net.Core/API/Rest/CreateChannelInviteParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateChannelInviteParams.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Discord.API.Rest { [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class CreateChannelInviteParams + internal class CreateChannelInviteParams { [JsonProperty("max_age")] public Optional MaxAge { get; set; } @@ -12,5 +12,7 @@ namespace Discord.API.Rest public Optional MaxUses { get; set; } [JsonProperty("temporary")] public Optional IsTemporary { get; set; } + [JsonProperty("unique")] + public Optional IsUnique { get; set; } } } diff --git a/src/Discord.Net.Core/API/Rest/CreateDMChannelParams.cs b/src/Discord.Net.Rest/API/Rest/CreateDMChannelParams.cs similarity index 89% rename from src/Discord.Net.Core/API/Rest/CreateDMChannelParams.cs rename to src/Discord.Net.Rest/API/Rest/CreateDMChannelParams.cs index 83fe76e98..f32796e02 100644 --- a/src/Discord.Net.Core/API/Rest/CreateDMChannelParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateDMChannelParams.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Discord.API.Rest { [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class CreateDMChannelParams + internal class CreateDMChannelParams { [JsonProperty("recipient_id")] public ulong RecipientId { get; } diff --git a/src/Discord.Net.Core/API/Rest/CreateGuildBanParams.cs b/src/Discord.Net.Rest/API/Rest/CreateGuildBanParams.cs similarity index 77% rename from src/Discord.Net.Core/API/Rest/CreateGuildBanParams.cs rename to src/Discord.Net.Rest/API/Rest/CreateGuildBanParams.cs index 724112bc0..0c148fe70 100644 --- a/src/Discord.Net.Core/API/Rest/CreateGuildBanParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateGuildBanParams.cs @@ -1,7 +1,7 @@ #pragma warning disable CS1591 namespace Discord.API.Rest { - public class CreateGuildBanParams + internal class CreateGuildBanParams { public Optional DeleteMessageDays { get; set; } } diff --git a/src/Discord.Net.Core/API/Rest/CreateGuildChannelParams.cs b/src/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs similarity index 92% rename from src/Discord.Net.Core/API/Rest/CreateGuildChannelParams.cs rename to src/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs index f0e06e3d2..bae677148 100644 --- a/src/Discord.Net.Core/API/Rest/CreateGuildChannelParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Discord.API.Rest { [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class CreateGuildChannelParams + internal class CreateGuildChannelParams { [JsonProperty("name")] public string Name { get; } diff --git a/src/Discord.Net.Core/API/Rest/CreateGuildIntegrationParams.cs b/src/Discord.Net.Rest/API/Rest/CreateGuildIntegrationParams.cs similarity index 89% rename from src/Discord.Net.Core/API/Rest/CreateGuildIntegrationParams.cs rename to src/Discord.Net.Rest/API/Rest/CreateGuildIntegrationParams.cs index 0d6e3a654..1053a0ed3 100644 --- a/src/Discord.Net.Core/API/Rest/CreateGuildIntegrationParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateGuildIntegrationParams.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Discord.API.Rest { [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class CreateGuildIntegrationParams + internal class CreateGuildIntegrationParams { [JsonProperty("id")] public ulong Id { get; } diff --git a/src/Discord.Net.Core/API/Rest/CreateGuildParams.cs b/src/Discord.Net.Rest/API/Rest/CreateGuildParams.cs similarity index 93% rename from src/Discord.Net.Core/API/Rest/CreateGuildParams.cs rename to src/Discord.Net.Rest/API/Rest/CreateGuildParams.cs index 4bc18c28b..cda6caedf 100644 --- a/src/Discord.Net.Core/API/Rest/CreateGuildParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateGuildParams.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Discord.API.Rest { [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class CreateGuildParams + internal class CreateGuildParams { [JsonProperty("name")] public string Name { get; } diff --git a/src/Discord.Net.Core/API/Rest/CreateMessageParams.cs b/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs similarity index 92% rename from src/Discord.Net.Core/API/Rest/CreateMessageParams.cs rename to src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs index a0dbb59dd..d77bff8ca 100644 --- a/src/Discord.Net.Core/API/Rest/CreateMessageParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs @@ -1,11 +1,10 @@ #pragma warning disable CS1591 -using System; using Newtonsoft.Json; namespace Discord.API.Rest { [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class CreateMessageParams + internal class CreateMessageParams { [JsonProperty("content")] public string Content { get; } diff --git a/src/Discord.Net.Core/API/Rest/DeleteMessagesParams.cs b/src/Discord.Net.Rest/API/Rest/DeleteMessagesParams.cs similarity index 89% rename from src/Discord.Net.Core/API/Rest/DeleteMessagesParams.cs rename to src/Discord.Net.Rest/API/Rest/DeleteMessagesParams.cs index 09b9a2bf1..ca9d8c26e 100644 --- a/src/Discord.Net.Core/API/Rest/DeleteMessagesParams.cs +++ b/src/Discord.Net.Rest/API/Rest/DeleteMessagesParams.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Discord.API.Rest { [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class DeleteMessagesParams + internal class DeleteMessagesParams { [JsonProperty("messages")] public ulong[] MessageIds { get; } diff --git a/src/Discord.Net.Rest/API/Rest/GetBotGatewayResponse.cs b/src/Discord.Net.Rest/API/Rest/GetBotGatewayResponse.cs new file mode 100644 index 000000000..111fcf3db --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/GetBotGatewayResponse.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + internal class GetBotGatewayResponse + { + [JsonProperty("url")] + public string Url { get; set; } + [JsonProperty("shards")] + public int Shards { get; set; } + } +} diff --git a/src/Discord.Net.Core/API/Rest/GetChannelMessagesParams.cs b/src/Discord.Net.Rest/API/Rest/GetChannelMessagesParams.cs similarity index 85% rename from src/Discord.Net.Core/API/Rest/GetChannelMessagesParams.cs rename to src/Discord.Net.Rest/API/Rest/GetChannelMessagesParams.cs index 2d00833ca..ea5327667 100644 --- a/src/Discord.Net.Core/API/Rest/GetChannelMessagesParams.cs +++ b/src/Discord.Net.Rest/API/Rest/GetChannelMessagesParams.cs @@ -1,7 +1,7 @@ #pragma warning disable CS1591 namespace Discord.API.Rest { - public class GetChannelMessagesParams + internal class GetChannelMessagesParams { public Optional Limit { get; set; } public Optional RelativeDirection { get; set; } diff --git a/src/Discord.Net.Core/API/Rest/GetGatewayResponse.cs b/src/Discord.Net.Rest/API/Rest/GetGatewayResponse.cs similarity index 81% rename from src/Discord.Net.Core/API/Rest/GetGatewayResponse.cs rename to src/Discord.Net.Rest/API/Rest/GetGatewayResponse.cs index 1ca03b73b..ce3630170 100644 --- a/src/Discord.Net.Core/API/Rest/GetGatewayResponse.cs +++ b/src/Discord.Net.Rest/API/Rest/GetGatewayResponse.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Rest { - public class GetGatewayResponse + internal class GetGatewayResponse { [JsonProperty("url")] public string Url { get; set; } diff --git a/src/Discord.Net.Core/API/Rest/GetGuildMembersParams.cs b/src/Discord.Net.Rest/API/Rest/GetGuildMembersParams.cs similarity index 81% rename from src/Discord.Net.Core/API/Rest/GetGuildMembersParams.cs rename to src/Discord.Net.Rest/API/Rest/GetGuildMembersParams.cs index 2bd34ddcb..66023cb43 100644 --- a/src/Discord.Net.Core/API/Rest/GetGuildMembersParams.cs +++ b/src/Discord.Net.Rest/API/Rest/GetGuildMembersParams.cs @@ -1,7 +1,7 @@ #pragma warning disable CS1591 namespace Discord.API.Rest { - public class GetGuildMembersParams + internal class GetGuildMembersParams { public Optional Limit { get; set; } public Optional AfterUserId { get; set; } diff --git a/src/Discord.Net.Core/API/Rest/GetGuildPruneCountResponse.cs b/src/Discord.Net.Rest/API/Rest/GetGuildPruneCountResponse.cs similarity index 79% rename from src/Discord.Net.Core/API/Rest/GetGuildPruneCountResponse.cs rename to src/Discord.Net.Rest/API/Rest/GetGuildPruneCountResponse.cs index 3f832caee..4af85acfa 100644 --- a/src/Discord.Net.Core/API/Rest/GetGuildPruneCountResponse.cs +++ b/src/Discord.Net.Rest/API/Rest/GetGuildPruneCountResponse.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Rest { - public class GetGuildPruneCountResponse + internal class GetGuildPruneCountResponse { [JsonProperty("pruned")] public int Pruned { get; set; } diff --git a/src/Discord.Net.Rest/API/Rest/GetReactionUsersParams.cs b/src/Discord.Net.Rest/API/Rest/GetReactionUsersParams.cs new file mode 100644 index 000000000..d70da5632 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/GetReactionUsersParams.cs @@ -0,0 +1,8 @@ +namespace Discord.API.Rest +{ + internal class GetReactionUsersParams + { + public Optional Limit { get; set; } + public Optional AfterUserId { get; set; } + } +} diff --git a/src/Discord.Net.Core/API/Rest/GuildPruneParams.cs b/src/Discord.Net.Rest/API/Rest/GuildPruneParams.cs similarity index 89% rename from src/Discord.Net.Core/API/Rest/GuildPruneParams.cs rename to src/Discord.Net.Rest/API/Rest/GuildPruneParams.cs index 9cff46992..6a98d3758 100644 --- a/src/Discord.Net.Core/API/Rest/GuildPruneParams.cs +++ b/src/Discord.Net.Rest/API/Rest/GuildPruneParams.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Discord.API.Rest { [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class GuildPruneParams + internal class GuildPruneParams { [JsonProperty("days")] public int Days { get; } diff --git a/src/Discord.Net.Core/API/Rest/ModifyChannelPermissionsParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyChannelPermissionsParams.cs similarity index 91% rename from src/Discord.Net.Core/API/Rest/ModifyChannelPermissionsParams.cs rename to src/Discord.Net.Rest/API/Rest/ModifyChannelPermissionsParams.cs index 8676b22e7..0fe5f7e5a 100644 --- a/src/Discord.Net.Core/API/Rest/ModifyChannelPermissionsParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyChannelPermissionsParams.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Discord.API.Rest { [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class ModifyChannelPermissionsParams + internal class ModifyChannelPermissionsParams { [JsonProperty("type")] public string Type { get; } diff --git a/src/Discord.Net.Core/API/Rest/ModifyCurrentUserNickParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyCurrentUserNickParams.cs similarity index 88% rename from src/Discord.Net.Core/API/Rest/ModifyCurrentUserNickParams.cs rename to src/Discord.Net.Rest/API/Rest/ModifyCurrentUserNickParams.cs index ca7ad2bd3..ba44e34cf 100644 --- a/src/Discord.Net.Core/API/Rest/ModifyCurrentUserNickParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyCurrentUserNickParams.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Discord.API.Rest { [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class ModifyCurrentUserNickParams + internal class ModifyCurrentUserNickParams { [JsonProperty("nick")] public string Nickname { get; } diff --git a/src/Discord.Net.Core/API/Rest/ModifyCurrentUserParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyCurrentUserParams.cs similarity index 75% rename from src/Discord.Net.Core/API/Rest/ModifyCurrentUserParams.cs rename to src/Discord.Net.Rest/API/Rest/ModifyCurrentUserParams.cs index d11ef2b77..7ba27c3a5 100644 --- a/src/Discord.Net.Core/API/Rest/ModifyCurrentUserParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyCurrentUserParams.cs @@ -4,11 +4,11 @@ using Newtonsoft.Json; namespace Discord.API.Rest { [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class ModifyCurrentUserParams + internal class ModifyCurrentUserParams { [JsonProperty("username")] public Optional Username { get; set; } [JsonProperty("avatar")] - public Optional Avatar { get; set; } + public Optional Avatar { get; set; } } } diff --git a/src/Discord.Net.Core/API/Rest/ModifyGuildChannelParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs similarity index 88% rename from src/Discord.Net.Core/API/Rest/ModifyGuildChannelParams.cs rename to src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs index 6d6ee4c24..b4add2ac9 100644 --- a/src/Discord.Net.Core/API/Rest/ModifyGuildChannelParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Discord.API.Rest { [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class ModifyGuildChannelParams + internal class ModifyGuildChannelParams { [JsonProperty("name")] public Optional Name { get; set; } diff --git a/src/Discord.Net.Core/API/Rest/ModifyGuildChannelsParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelsParams.cs similarity index 90% rename from src/Discord.Net.Core/API/Rest/ModifyGuildChannelsParams.cs rename to src/Discord.Net.Rest/API/Rest/ModifyGuildChannelsParams.cs index 8ac3299fa..2bbb58ea6 100644 --- a/src/Discord.Net.Core/API/Rest/ModifyGuildChannelsParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelsParams.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Discord.API.Rest { [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class ModifyGuildChannelsParams + internal class ModifyGuildChannelsParams { [JsonProperty("id")] public ulong Id { get; set; } diff --git a/src/Discord.Net.Core/API/Rest/ModifyGuildEmbedParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildEmbedParams.cs similarity index 89% rename from src/Discord.Net.Core/API/Rest/ModifyGuildEmbedParams.cs rename to src/Discord.Net.Rest/API/Rest/ModifyGuildEmbedParams.cs index f362f8cd7..487744c65 100644 --- a/src/Discord.Net.Core/API/Rest/ModifyGuildEmbedParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildEmbedParams.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Discord.API.Rest { [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class ModifyGuildEmbedParams + internal class ModifyGuildEmbedParams { [JsonProperty("enabled")] public Optional Enabled { get; set; } diff --git a/src/Discord.Net.Core/API/Rest/ModifyGuildIntegrationParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildIntegrationParams.cs similarity index 90% rename from src/Discord.Net.Core/API/Rest/ModifyGuildIntegrationParams.cs rename to src/Discord.Net.Rest/API/Rest/ModifyGuildIntegrationParams.cs index 3a5526c96..0a1b4f9fa 100644 --- a/src/Discord.Net.Core/API/Rest/ModifyGuildIntegrationParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildIntegrationParams.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Discord.API.Rest { [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class ModifyGuildIntegrationParams + internal class ModifyGuildIntegrationParams { [JsonProperty("expire_behavior")] public Optional ExpireBehavior { get; set; } diff --git a/src/Discord.Net.Core/API/Rest/ModifyGuildMemberParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildMemberParams.cs similarity index 93% rename from src/Discord.Net.Core/API/Rest/ModifyGuildMemberParams.cs rename to src/Discord.Net.Rest/API/Rest/ModifyGuildMemberParams.cs index 17a8e2da1..159670afb 100644 --- a/src/Discord.Net.Core/API/Rest/ModifyGuildMemberParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildMemberParams.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Discord.API.Rest { [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class ModifyGuildMemberParams + internal class ModifyGuildMemberParams { [JsonProperty("mute")] public Optional Mute { get; set; } diff --git a/src/Discord.Net.Core/API/Rest/ModifyGuildParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs similarity index 96% rename from src/Discord.Net.Core/API/Rest/ModifyGuildParams.cs rename to src/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs index f72ff2c96..2c7d84087 100644 --- a/src/Discord.Net.Core/API/Rest/ModifyGuildParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Discord.API.Rest { [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class ModifyGuildParams + internal class ModifyGuildParams { [JsonProperty("username")] public Optional Username { get; set; } diff --git a/src/Discord.Net.Core/API/Rest/ModifyGuildRoleParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildRoleParams.cs similarity index 81% rename from src/Discord.Net.Core/API/Rest/ModifyGuildRoleParams.cs rename to src/Discord.Net.Rest/API/Rest/ModifyGuildRoleParams.cs index d1226b534..c3c20706b 100644 --- a/src/Discord.Net.Core/API/Rest/ModifyGuildRoleParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildRoleParams.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Discord.API.Rest { [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class ModifyGuildRoleParams + internal class ModifyGuildRoleParams { [JsonProperty("name")] public Optional Name { get; set; } @@ -16,5 +16,7 @@ namespace Discord.API.Rest public Optional Color { get; set; } [JsonProperty("hoist")] public Optional Hoist { get; set; } + [JsonProperty("mentionable")] + public Optional Mentionable { get; set; } } } diff --git a/src/Discord.Net.Core/API/Rest/ModifyGuildRolesParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildRolesParams.cs similarity index 82% rename from src/Discord.Net.Core/API/Rest/ModifyGuildRolesParams.cs rename to src/Discord.Net.Rest/API/Rest/ModifyGuildRolesParams.cs index 2350a8c47..38c3fb646 100644 --- a/src/Discord.Net.Core/API/Rest/ModifyGuildRolesParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildRolesParams.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Discord.API.Rest { [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class ModifyGuildRolesParams : ModifyGuildRoleParams + internal class ModifyGuildRolesParams : ModifyGuildRoleParams { [JsonProperty("id")] public ulong Id { get; } diff --git a/src/Discord.Net.Core/API/Rest/ModifyMessageParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyMessageParams.cs similarity index 67% rename from src/Discord.Net.Core/API/Rest/ModifyMessageParams.cs rename to src/Discord.Net.Rest/API/Rest/ModifyMessageParams.cs index 4901ddc9d..fdff4de15 100644 --- a/src/Discord.Net.Core/API/Rest/ModifyMessageParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyMessageParams.cs @@ -4,9 +4,11 @@ using Newtonsoft.Json; namespace Discord.API.Rest { [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class ModifyMessageParams + internal class ModifyMessageParams { [JsonProperty("content")] public Optional Content { get; set; } + [JsonProperty("embed")] + public Optional Embed { get; set; } } } diff --git a/src/Discord.Net.Core/API/Rest/ModifyTextChannelParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyTextChannelParams.cs similarity index 78% rename from src/Discord.Net.Core/API/Rest/ModifyTextChannelParams.cs rename to src/Discord.Net.Rest/API/Rest/ModifyTextChannelParams.cs index 3546cee95..311336ec3 100644 --- a/src/Discord.Net.Core/API/Rest/ModifyTextChannelParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyTextChannelParams.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Discord.API.Rest { [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class ModifyTextChannelParams : ModifyGuildChannelParams + internal class ModifyTextChannelParams : ModifyGuildChannelParams { [JsonProperty("topic")] public Optional Topic { get; set; } diff --git a/src/Discord.Net.Core/API/Rest/ModifyVoiceChannelParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyVoiceChannelParams.cs similarity index 82% rename from src/Discord.Net.Core/API/Rest/ModifyVoiceChannelParams.cs rename to src/Discord.Net.Rest/API/Rest/ModifyVoiceChannelParams.cs index 8b5af9d8e..ce36eb11f 100644 --- a/src/Discord.Net.Core/API/Rest/ModifyVoiceChannelParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyVoiceChannelParams.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Discord.API.Rest { [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class ModifyVoiceChannelParams : ModifyGuildChannelParams + internal class ModifyVoiceChannelParams : ModifyGuildChannelParams { [JsonProperty("bitrate")] public Optional Bitrate { get; set; } diff --git a/src/Discord.Net.Core/API/Rest/UploadFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs similarity index 96% rename from src/Discord.Net.Core/API/Rest/UploadFileParams.cs rename to src/Discord.Net.Rest/API/Rest/UploadFileParams.cs index bbd798900..30bfc7f9a 100644 --- a/src/Discord.Net.Core/API/Rest/UploadFileParams.cs +++ b/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs @@ -5,7 +5,7 @@ using System.IO; namespace Discord.API.Rest { - public class UploadFileParams + internal class UploadFileParams { public Stream File { get; } diff --git a/src/Discord.Net.Rest/BaseDiscordClient.cs b/src/Discord.Net.Rest/BaseDiscordClient.cs index a92a44882..a9d71e440 100644 --- a/src/Discord.Net.Rest/BaseDiscordClient.cs +++ b/src/Discord.Net.Rest/BaseDiscordClient.cs @@ -11,19 +11,18 @@ namespace Discord.Rest public abstract class BaseDiscordClient : IDiscordClient { public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } - private readonly AsyncEvent> _logEvent = new AsyncEvent>(); + internal readonly AsyncEvent> _logEvent = new AsyncEvent>(); public event Func LoggedIn { add { _loggedInEvent.Add(value); } remove { _loggedInEvent.Remove(value); } } private readonly AsyncEvent> _loggedInEvent = new AsyncEvent>(); public event Func LoggedOut { add { _loggedOutEvent.Add(value); } remove { _loggedOutEvent.Remove(value); } } private readonly AsyncEvent> _loggedOutEvent = new AsyncEvent>(); - internal readonly Logger _restLogger, _queueLogger; - internal readonly SemaphoreSlim _connectionLock; - private bool _isFirstLogin; - private bool _isDisposed; + internal readonly Logger _restLogger; + private readonly SemaphoreSlim _stateLock; + private bool _isFirstLogin, _isDisposed; - public API.DiscordRestApiClient ApiClient { get; } + internal API.DiscordRestApiClient ApiClient { get; } internal LogManager LogManager { get; } public LoginState LoginState { get; private set; } public ISelfUser CurrentUser { get; protected set; } @@ -35,17 +34,16 @@ namespace Discord.Rest LogManager = new LogManager(config.LogLevel); LogManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); - _connectionLock = new SemaphoreSlim(1, 1); + _stateLock = new SemaphoreSlim(1, 1); _restLogger = LogManager.CreateLogger("Rest"); - _queueLogger = LogManager.CreateLogger("Queue"); - _isFirstLogin = true; + _isFirstLogin = config.DisplayInitialLog; ApiClient.RequestQueue.RateLimitTriggered += async (id, info) => { if (info == null) - await _queueLogger.WarningAsync($"Preemptive Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); + await _restLogger.WarningAsync($"Preemptive Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); else - await _queueLogger.WarningAsync($"Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); + 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); } @@ -53,12 +51,12 @@ namespace Discord.Rest /// public async Task LoginAsync(TokenType tokenType, string token, bool validateToken = true) { - await _connectionLock.WaitAsync().ConfigureAwait(false); + await _stateLock.WaitAsync().ConfigureAwait(false); try { await LoginInternalAsync(tokenType, token).ConfigureAwait(false); } - finally { _connectionLock.Release(); } + finally { _stateLock.Release(); } } private async Task LoginInternalAsync(TokenType tokenType, string token) { @@ -86,17 +84,17 @@ namespace Discord.Rest await _loggedInEvent.InvokeAsync().ConfigureAwait(false); } - protected virtual Task OnLoginAsync(TokenType tokenType, string token) { return Task.CompletedTask; } + internal virtual Task OnLoginAsync(TokenType tokenType, string token) { return Task.Delay(0); } /// public async Task LogoutAsync() { - await _connectionLock.WaitAsync().ConfigureAwait(false); + await _stateLock.WaitAsync().ConfigureAwait(false); try { await LogoutInternalAsync().ConfigureAwait(false); } - finally { _connectionLock.Release(); } + finally { _stateLock.Release(); } } private async Task LogoutInternalAsync() { @@ -111,7 +109,7 @@ namespace Discord.Rest await _loggedOutEvent.InvokeAsync().ConfigureAwait(false); } - protected virtual Task OnLogoutAsync() { return Task.CompletedTask; } + internal virtual Task OnLogoutAsync() { return Task.Delay(0); } internal virtual void Dispose(bool disposing) { @@ -139,6 +137,10 @@ namespace Discord.Rest => Task.FromResult(null); Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode) => Task.FromResult>(ImmutableArray.Create()); + Task> IDiscordClient.GetDMChannelsAsync(CacheMode mode) + => Task.FromResult>(ImmutableArray.Create()); + Task> IDiscordClient.GetGroupChannelsAsync(CacheMode mode) + => Task.FromResult>(ImmutableArray.Create()); Task> IDiscordClient.GetConnectionsAsync() => Task.FromResult>(ImmutableArray.Create()); @@ -162,8 +164,9 @@ namespace Discord.Rest Task IDiscordClient.GetVoiceRegionAsync(string id) => Task.FromResult(null); - Task IDiscordClient.ConnectAsync() { throw new NotSupportedException(); } - Task IDiscordClient.DisconnectAsync() { throw new NotSupportedException(); } - + Task IDiscordClient.StartAsync() + => Task.Delay(0); + Task IDiscordClient.StopAsync() + => Task.Delay(0); } } diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs index e7ef55033..456362be6 100644 --- a/src/Discord.Net.Rest/ClientHelper.cs +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -24,10 +24,24 @@ namespace Discord.Rest return RestChannel.Create(client, model); return null; } - public static async Task> GetPrivateChannelsAsync(BaseDiscordClient client) + public static async Task> GetPrivateChannelsAsync(BaseDiscordClient client) { var models = await client.ApiClient.GetMyPrivateChannelsAsync().ConfigureAwait(false); - return models.Select(x => RestDMChannel.Create(client, x)).ToImmutableArray(); + return models.Select(x => RestChannel.CreatePrivate(client, x)).ToImmutableArray(); + } + public static async Task> GetDMChannelsAsync(BaseDiscordClient client) + { + var models = await client.ApiClient.GetMyPrivateChannelsAsync().ConfigureAwait(false); + return models + .Where(x => x.Type == ChannelType.DM) + .Select(x => RestDMChannel.Create(client, x)).ToImmutableArray(); + } + public static async Task> GetGroupChannelsAsync(BaseDiscordClient client) + { + var models = await client.ApiClient.GetMyPrivateChannelsAsync().ConfigureAwait(false); + return models + .Where(x => x.Type == ChannelType.Group) + .Select(x => RestGroupChannel.Create(client, x)).ToImmutableArray(); } public static async Task> GetConnectionsAsync(BaseDiscordClient client) diff --git a/src/Discord.Net.Rest/Discord.Net.Rest.csproj b/src/Discord.Net.Rest/Discord.Net.Rest.csproj index 6bc40571f..b7495f273 100644 --- a/src/Discord.Net.Rest/Discord.Net.Rest.csproj +++ b/src/Discord.Net.Rest/Discord.Net.Rest.csproj @@ -1,44 +1,29 @@ - - + - A core Discord.Net library containing the REST client and models. - 1.0.0-beta2 - netstandard1.3 + 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 - $(PackageTargetFallback);dotnet5.4;dnxcore50;portable-net45+win8 + Discord.Rest + true - - - - - - - - - 1.0.0-alpha-20161104-2 - All - - - 4.3.0 - + + - - - False - - $(DefineConstants);RELEASE $(NoWarn);CS1573;CS1591 true true - \ No newline at end of file diff --git a/src/Discord.Net.Core/API/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs similarity index 88% rename from src/Discord.Net.Core/API/DiscordRestApiClient.cs rename to src/Discord.Net.Rest/DiscordRestApiClient.cs index 02b52ec21..3c837da6e 100644 --- a/src/Discord.Net.Core/API/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -22,7 +22,7 @@ using System.Threading.Tasks; namespace Discord.API { - public class DiscordRestApiClient : IDisposable + internal class DiscordRestApiClient : IDisposable { private static readonly ConcurrentDictionary> _bucketIdGenerators = new ConcurrentDictionary>(); @@ -31,38 +31,40 @@ namespace Discord.API protected readonly JsonSerializer _serializer; protected readonly SemaphoreSlim _stateLock; - private readonly RestClientProvider _restClientProvider; - private readonly string _userAgent; + private readonly RestClientProvider RestClientProvider; - protected string _authToken; protected bool _isDisposed; private CancellationTokenSource _loginCancelToken; - private IRestClient _restClient; + + public RetryMode DefaultRetryMode { get; } + public string UserAgent { get; } + internal RequestQueue RequestQueue { get; } public LoginState LoginState { get; private set; } public TokenType AuthTokenType { get; private set; } - public User CurrentUser { get; private set; } - public RequestQueue RequestQueue { get; private set; } - internal bool FetchCurrentUser { get; set; } + internal string AuthToken { get; private set; } + internal IRestClient RestClient { get; private set; } + internal ulong? CurrentUserId { get; set;} - public DiscordRestApiClient(RestClientProvider restClientProvider, string userAgent, JsonSerializer serializer = null, RequestQueue requestQueue = null) + public DiscordRestApiClient(RestClientProvider restClientProvider, string userAgent, RetryMode defaultRetryMode = RetryMode.AlwaysRetry, + JsonSerializer serializer = null) { - _restClientProvider = restClientProvider; - _userAgent = userAgent; - _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; - RequestQueue = requestQueue; - FetchCurrentUser = true; + RestClientProvider = restClientProvider; + UserAgent = userAgent; + DefaultRetryMode = defaultRetryMode; + _serializer = serializer ?? new JsonSerializer { DateFormatString = "yyyy-MM-ddTHH:mm:ssZ", ContractResolver = new DiscordContractResolver() }; + RequestQueue = new RequestQueue(); _stateLock = new SemaphoreSlim(1, 1); SetBaseUrl(DiscordConfig.ClientAPIUrl); } internal void SetBaseUrl(string baseUrl) { - _restClient = _restClientProvider(baseUrl); - _restClient.SetHeader("accept", "*/*"); - _restClient.SetHeader("user-agent", _userAgent); - _restClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, _authToken)); + RestClient = RestClientProvider(baseUrl); + RestClient.SetHeader("accept", "*/*"); + RestClient.SetHeader("user-agent", UserAgent); + RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken)); } internal static string GetPrefixedToken(TokenType tokenType, string token) { @@ -85,7 +87,7 @@ namespace Discord.API if (disposing) { _loginCancelToken?.Dispose(); - (_restClient as IDisposable)?.Dispose(); + (RestClient as IDisposable)?.Dispose(); } _isDisposed = true; } @@ -112,16 +114,13 @@ namespace Discord.API _loginCancelToken = new CancellationTokenSource(); AuthTokenType = TokenType.User; - _authToken = null; + AuthToken = null; await RequestQueue.SetCancelTokenAsync(_loginCancelToken.Token).ConfigureAwait(false); - _restClient.SetCancelToken(_loginCancelToken.Token); + RestClient.SetCancelToken(_loginCancelToken.Token); AuthTokenType = tokenType; - _authToken = token; - _restClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, _authToken)); - - if (FetchCurrentUser) - CurrentUser = await GetMyUserAsync(new RequestOptions { IgnoreState = true }).ConfigureAwait(false); + AuthToken = token; + RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken)); LoginState = LoginState.LoggedIn; } @@ -154,14 +153,14 @@ namespace Discord.API await RequestQueue.ClearAsync().ConfigureAwait(false); await RequestQueue.SetCancelTokenAsync(CancellationToken.None).ConfigureAwait(false); - _restClient.SetCancelToken(CancellationToken.None); + RestClient.SetCancelToken(CancellationToken.None); - CurrentUser = null; + CurrentUserId = null; LoginState = LoginState.LoggedOut; } - internal virtual Task ConnectInternalAsync() => Task.CompletedTask; - internal virtual Task DisconnectInternalAsync() => Task.CompletedTask; + internal virtual Task ConnectInternalAsync() => Task.Delay(0); + internal virtual Task DisconnectInternalAsync() => Task.Delay(0); //Core internal Task SendAsync(string method, Expression> endpointExpr, BucketIds ids, @@ -175,7 +174,7 @@ namespace Discord.API options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; options.IsClientBucket = AuthTokenType == TokenType.User; - var request = new RestRequest(_restClient, method, endpoint, options); + var request = new RestRequest(RestClient, method, endpoint, options); await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); } @@ -191,7 +190,7 @@ namespace Discord.API options.IsClientBucket = AuthTokenType == TokenType.User; var json = payload != null ? SerializeJson(payload) : null; - var request = new JsonRestRequest(_restClient, method, endpoint, json, options); + var request = new JsonRestRequest(RestClient, method, endpoint, json, options); await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); } @@ -206,7 +205,7 @@ namespace Discord.API options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; options.IsClientBucket = AuthTokenType == TokenType.User; - var request = new MultipartRestRequest(_restClient, method, endpoint, multipartArgs, options); + var request = new MultipartRestRequest(RestClient, method, endpoint, multipartArgs, options); await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); } @@ -220,7 +219,7 @@ namespace Discord.API options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; options.IsClientBucket = AuthTokenType == TokenType.User; - var request = new RestRequest(_restClient, method, endpoint, options); + var request = new RestRequest(RestClient, method, endpoint, options); return DeserializeJson(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false)); } @@ -235,7 +234,7 @@ namespace Discord.API options.IsClientBucket = AuthTokenType == TokenType.User; var json = payload != null ? SerializeJson(payload) : null; - var request = new JsonRestRequest(_restClient, method, endpoint, json, options); + var request = new JsonRestRequest(RestClient, method, endpoint, json, options); return DeserializeJson(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false)); } @@ -249,7 +248,7 @@ namespace Discord.API options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; options.IsClientBucket = AuthTokenType == TokenType.User; - var request = new MultipartRestRequest(_restClient, method, endpoint, multipartArgs, options); + var request = new MultipartRestRequest(RestClient, method, endpoint, multipartArgs, options); return DeserializeJson(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false)); } @@ -257,6 +256,8 @@ namespace Discord.API { if (!request.Options.IgnoreState) CheckState(); + if (request.Options.RetryMode == null) + request.Options.RetryMode = DefaultRetryMode; var stopwatch = Stopwatch.StartNew(); var responseStream = await RequestQueue.SendAsync(request).ConfigureAwait(false); @@ -286,7 +287,7 @@ namespace Discord.API var ids = new BucketIds(channelId: channelId); return await SendAsync("GET", () => $"channels/{channelId}", ids, options: options).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } + catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; } } public async Task GetChannelAsync(ulong guildId, ulong channelId, RequestOptions options = null) { @@ -302,7 +303,7 @@ namespace Discord.API return null; return model; } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } + catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; } } public async Task> GetGuildChannelsAsync(ulong guildId, RequestOptions options = null) { @@ -331,7 +332,7 @@ namespace Discord.API var ids = new BucketIds(channelId: channelId); return await SendAsync("DELETE", () => $"channels/{channelId}", ids, options: options).ConfigureAwait(false); } - public async Task ModifyGuildChannelAsync(ulong channelId, ModifyGuildChannelParams args, RequestOptions options = null) + public async Task ModifyGuildChannelAsync(ulong channelId, Rest.ModifyGuildChannelParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotNull(args, nameof(args)); @@ -342,7 +343,7 @@ namespace Discord.API var ids = new BucketIds(channelId: channelId); return await SendJsonAsync("PATCH", () => $"channels/{channelId}", args, ids, options: options).ConfigureAwait(false); } - public async Task ModifyGuildChannelAsync(ulong channelId, ModifyTextChannelParams args, RequestOptions options = null) + public async Task ModifyGuildChannelAsync(ulong channelId, Rest.ModifyTextChannelParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotNull(args, nameof(args)); @@ -353,12 +354,12 @@ namespace Discord.API var ids = new BucketIds(channelId: channelId); return await SendJsonAsync("PATCH", () => $"channels/{channelId}", args, ids, options: options).ConfigureAwait(false); } - public async Task ModifyGuildChannelAsync(ulong channelId, ModifyVoiceChannelParams args, RequestOptions options = null) + public async Task ModifyGuildChannelAsync(ulong channelId, Rest.ModifyVoiceChannelParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotNull(args, nameof(args)); - Preconditions.GreaterThan(args.Bitrate, 0, nameof(args.Bitrate)); - Preconditions.AtLeast(args.UserLimit, 0, nameof(args.Bitrate)); + Preconditions.AtLeast(args.Bitrate, 8000, nameof(args.Bitrate)); + Preconditions.AtLeast(args.UserLimit, 0, nameof(args.UserLimit)); Preconditions.AtLeast(args.Position, 0, nameof(args.Position)); Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name)); options = RequestOptions.CreateOrClone(options); @@ -366,7 +367,7 @@ namespace Discord.API var ids = new BucketIds(channelId: channelId); return await SendJsonAsync("PATCH", () => $"channels/{channelId}", args, ids, options: options).ConfigureAwait(false); } - public async Task ModifyGuildChannelsAsync(ulong guildId, IEnumerable args, RequestOptions options = null) + public async Task ModifyGuildChannelsAsync(ulong guildId, IEnumerable args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotNull(args, nameof(args)); @@ -378,7 +379,7 @@ namespace Discord.API case 0: return; case 1: - await ModifyGuildChannelAsync(channels[0].Id, new ModifyGuildChannelParams { Position = channels[0].Position }).ConfigureAwait(false); + await ModifyGuildChannelAsync(channels[0].Id, new Rest.ModifyGuildChannelParams { Position = channels[0].Position }).ConfigureAwait(false); break; default: var ids = new BucketIds(guildId: guildId); @@ -399,7 +400,7 @@ namespace Discord.API var ids = new BucketIds(channelId: channelId); return await SendAsync("GET", () => $"channels/{channelId}/messages/{messageId}", ids, options: options).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } + catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; } } public async Task> GetChannelMessagesAsync(ulong channelId, GetChannelMessagesParams args, RequestOptions options = null) { @@ -483,6 +484,7 @@ namespace Discord.API Preconditions.NotNull(args, nameof(args)); Preconditions.NotNull(args.MessageIds, nameof(args.MessageIds)); Preconditions.AtMost(args.MessageIds.Length, 100, nameof(args.MessageIds.Length)); + Preconditions.YoungerThanTwoWeeks(args.MessageIds, nameof(args.MessageIds)); options = RequestOptions.CreateOrClone(options); switch (args.MessageIds.Length) @@ -498,14 +500,15 @@ namespace Discord.API break; } } - public async Task ModifyMessageAsync(ulong channelId, ulong messageId, ModifyMessageParams args, RequestOptions options = null) + public async Task ModifyMessageAsync(ulong channelId, ulong messageId, Rest.ModifyMessageParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotEqual(messageId, 0, nameof(messageId)); Preconditions.NotNull(args, nameof(args)); if (args.Content.IsSpecified) { - Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); + if (!args.Embed.IsSpecified) + Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); if (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)); } @@ -514,6 +517,59 @@ namespace Discord.API var ids = new BucketIds(channelId: channelId); return await SendJsonAsync("PATCH", () => $"channels/{channelId}/messages/{messageId}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } + public async Task AddReactionAsync(ulong channelId, ulong messageId, string emoji, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + Preconditions.NotNullOrWhitespace(emoji, nameof(emoji)); + + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + + await SendAsync("PUT", () => $"channels/{channelId}/messages/{messageId}/reactions/{emoji}/@me", ids, options: options).ConfigureAwait(false); + } + public async Task RemoveReactionAsync(ulong channelId, ulong messageId, ulong userId, string emoji, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + Preconditions.NotNullOrWhitespace(emoji, nameof(emoji)); + + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + + await SendAsync("DELETE", () => $"channels/{channelId}/messages/{messageId}/reactions/{emoji}/{userId}", ids, options: options).ConfigureAwait(false); + } + public async Task RemoveAllReactionsAsync(ulong channelId, ulong messageId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + + await SendAsync("DELETE", () => $"channels/{channelId}/messages/{messageId}/reactions", ids, options: options).ConfigureAwait(false); + } + public async Task> GetReactionUsersAsync(ulong channelId, ulong messageId, string emoji, GetReactionUsersParams args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + Preconditions.NotNullOrWhitespace(emoji, nameof(emoji)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.GreaterThan(args.Limit, 0, nameof(args.Limit)); + Preconditions.AtMost(args.Limit, DiscordConfig.MaxUsersPerBatch, nameof(args.Limit)); + Preconditions.GreaterThan(args.AfterUserId, 0, nameof(args.AfterUserId)); + options = RequestOptions.CreateOrClone(options); + + int limit = args.Limit.GetValueOrDefault(int.MaxValue); + ulong afterUserId = args.AfterUserId.GetValueOrDefault(0); + + var ids = new BucketIds(channelId: channelId); + Expression> endpoint = () => $"channels/{channelId}/messages/{messageId}/reactions/{emoji}"; + return await SendAsync>("GET", endpoint, ids, options: options).ConfigureAwait(false); + } public async Task AckMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); @@ -614,7 +670,7 @@ namespace Discord.API var ids = new BucketIds(guildId: guildId); return await SendAsync("GET", () => $"guilds/{guildId}", ids, options: options).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } + catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; } } public async Task CreateGuildAsync(CreateGuildParams args, RequestOptions options = null) { @@ -641,7 +697,7 @@ namespace Discord.API var ids = new BucketIds(guildId: guildId); return await SendAsync("DELETE", () => $"users/@me/guilds/{guildId}", ids, options: options).ConfigureAwait(false); } - public async Task ModifyGuildAsync(ulong guildId, ModifyGuildParams args, RequestOptions options = null) + public async Task ModifyGuildAsync(ulong guildId, Rest.ModifyGuildParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotNull(args, nameof(args)); @@ -717,9 +773,9 @@ namespace Discord.API var ids = new BucketIds(guildId: guildId); return await SendAsync("GET", () => $"guilds/{guildId}/embed", ids, options: options).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } + catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; } } - public async Task ModifyGuildEmbedAsync(ulong guildId, ModifyGuildEmbedParams args, RequestOptions options = null) + public async Task ModifyGuildEmbedAsync(ulong guildId, Rest.ModifyGuildEmbedParams args, RequestOptions options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(guildId, 0, nameof(guildId)); @@ -757,7 +813,7 @@ namespace Discord.API var ids = new BucketIds(guildId: guildId); return await SendAsync("DELETE", () => $"guilds/{guildId}/integrations/{integrationId}", ids, options: options).ConfigureAwait(false); } - public async Task ModifyGuildIntegrationAsync(ulong guildId, ulong integrationId, ModifyGuildIntegrationParams args, RequestOptions options = null) + public async Task ModifyGuildIntegrationAsync(ulong guildId, ulong integrationId, Rest.ModifyGuildIntegrationParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(integrationId, 0, nameof(integrationId)); @@ -797,7 +853,7 @@ namespace Discord.API { return await SendAsync("GET", () => $"invites/{inviteId}", new BucketIds(), options: options).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } + catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; } } public async Task> GetGuildInvitesAsync(ulong guildId, RequestOptions options = null) { @@ -853,7 +909,7 @@ namespace Discord.API var ids = new BucketIds(guildId: guildId); return await SendAsync("GET", () => $"guilds/{guildId}/members/{userId}", ids, options: options).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } + catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; } } public async Task> GetGuildMembersAsync(ulong guildId, GetGuildMembersParams args, RequestOptions options = null) { @@ -880,18 +936,18 @@ namespace Discord.API var ids = new BucketIds(guildId: guildId); await SendAsync("DELETE", () => $"guilds/{guildId}/members/{userId}", ids, options: options).ConfigureAwait(false); } - public async Task ModifyGuildMemberAsync(ulong guildId, ulong userId, ModifyGuildMemberParams args, RequestOptions options = null) + public async Task ModifyGuildMemberAsync(ulong guildId, ulong userId, Rest.ModifyGuildMemberParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(userId, 0, nameof(userId)); Preconditions.NotNull(args, nameof(args)); options = RequestOptions.CreateOrClone(options); - bool isCurrentUser = userId == CurrentUser.Id; + bool isCurrentUser = userId == CurrentUserId; if (isCurrentUser && args.Nickname.IsSpecified) { - var nickArgs = new ModifyCurrentUserNickParams(args.Nickname.Value ?? ""); + var nickArgs = new Rest.ModifyCurrentUserNickParams(args.Nickname.Value ?? ""); await ModifyMyNickAsync(guildId, nickArgs).ConfigureAwait(false); args.Nickname = Optional.Create(); //Remove } @@ -928,7 +984,7 @@ namespace Discord.API var ids = new BucketIds(guildId: guildId); await SendAsync("DELETE", () => $"guilds/{guildId}/roles/{roleId}", ids, options: options).ConfigureAwait(false); } - public async Task ModifyGuildRoleAsync(ulong guildId, ulong roleId, ModifyGuildRoleParams args, RequestOptions options = null) + public async Task ModifyGuildRoleAsync(ulong guildId, ulong roleId, Rest.ModifyGuildRoleParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(roleId, 0, nameof(roleId)); @@ -941,7 +997,7 @@ namespace Discord.API var ids = new BucketIds(guildId: guildId); return await SendJsonAsync("PATCH", () => $"guilds/{guildId}/roles/{roleId}", args, ids, options: options).ConfigureAwait(false); } - public async Task> ModifyGuildRolesAsync(ulong guildId, IEnumerable args, RequestOptions options = null) + public async Task> ModifyGuildRolesAsync(ulong guildId, IEnumerable args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotNull(args, nameof(args)); @@ -970,7 +1026,7 @@ namespace Discord.API { return await SendAsync("GET", () => $"users/{userId}", new BucketIds(), options: options).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } + catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; } } //Relationships @@ -1027,7 +1083,7 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); return await SendAsync("GET", () => "oauth2/applications/@me", new BucketIds(), options: options).ConfigureAwait(false); } - public async Task ModifySelfAsync(ModifyCurrentUserParams args, RequestOptions options = null) + public async Task ModifySelfAsync(Rest.ModifyCurrentUserParams args, RequestOptions options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotNullOrEmpty(args.Username, nameof(args.Username)); @@ -1035,7 +1091,7 @@ namespace Discord.API return await SendJsonAsync("PATCH", () => "users/@me", args, new BucketIds(), options: options).ConfigureAwait(false); } - public async Task ModifyMyNickAsync(ulong guildId, ModifyCurrentUserNickParams args, RequestOptions options = null) + public async Task ModifyMyNickAsync(ulong guildId, Rest.ModifyCurrentUserNickParams args, RequestOptions options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotNull(args.Nickname, nameof(args.Nickname)); diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index f36c0fb06..0ff1a4821 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -1,5 +1,4 @@ -using Discord.Net.Queue; -using System.Collections.Generic; +using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Threading.Tasks; @@ -8,30 +7,49 @@ namespace Discord.Rest { public class DiscordRestClient : BaseDiscordClient, IDiscordClient { + private RestApplication _applicationInfo; + public new RestSelfUser CurrentUser => base.CurrentUser as RestSelfUser; public DiscordRestClient() : this(new DiscordRestConfig()) { } public DiscordRestClient(DiscordRestConfig config) : base(config, CreateApiClient(config)) { } private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config) - => new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent, requestQueue: new RequestQueue()); + => new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent); + internal override void Dispose(bool disposing) + { + if (disposing) + ApiClient.Dispose(); + } - protected override Task OnLoginAsync(TokenType tokenType, string token) + internal override async Task OnLoginAsync(TokenType tokenType, string token) + { + var user = await ApiClient.GetMyUserAsync(new RequestOptions { RetryMode = RetryMode.AlwaysRetry }).ConfigureAwait(false); + ApiClient.CurrentUserId = user.Id; + base.CurrentUser = RestSelfUser.Create(this, user); + } + internal override Task OnLogoutAsync() { - base.CurrentUser = RestSelfUser.Create(this, ApiClient.CurrentUser); - return Task.CompletedTask; + _applicationInfo = null; + return Task.Delay(0); } /// - public Task GetApplicationInfoAsync() - => ClientHelper.GetApplicationInfoAsync(this); + public async Task GetApplicationInfoAsync() + { + return _applicationInfo ?? (_applicationInfo = await ClientHelper.GetApplicationInfoAsync(this)); + } /// public Task GetChannelAsync(ulong id) => ClientHelper.GetChannelAsync(this, id); /// - public Task> GetPrivateChannelsAsync() + public Task> GetPrivateChannelsAsync() => ClientHelper.GetPrivateChannelsAsync(this); + public Task> GetDMChannelsAsync() + => ClientHelper.GetDMChannelsAsync(this); + public Task> GetGroupChannelsAsync() + => ClientHelper.GetGroupChannelsAsync(this); /// public Task> GetConnectionsAsync() @@ -89,6 +107,20 @@ namespace Discord.Rest else return ImmutableArray.Create(); } + async Task> IDiscordClient.GetDMChannelsAsync(CacheMode mode) + { + if (mode == CacheMode.AllowDownload) + return await GetDMChannelsAsync().ConfigureAwait(false); + else + return ImmutableArray.Create(); + } + async Task> IDiscordClient.GetGroupChannelsAsync(CacheMode mode) + { + if (mode == CacheMode.AllowDownload) + return await GetGroupChannelsAsync().ConfigureAwait(false); + else + return ImmutableArray.Create(); + } async Task> IDiscordClient.GetConnectionsAsync() => await GetConnectionsAsync().ConfigureAwait(false); diff --git a/src/Discord.Net.Rest/DiscordRestConfig.cs b/src/Discord.Net.Rest/DiscordRestConfig.cs index 33a3cb4e8..c3cd70683 100644 --- a/src/Discord.Net.Rest/DiscordRestConfig.cs +++ b/src/Discord.Net.Rest/DiscordRestConfig.cs @@ -10,6 +10,6 @@ namespace Discord.Rest internal const int WebSocketQueueInterval = 100; /// Gets or sets the provider used to generate new REST connections. - public RestClientProvider RestClientProvider { get; set; } = url => new DefaultRestClient(url); + 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 99edc6f48..07bdfe0eb 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -19,28 +19,46 @@ namespace Discord.Rest await client.ApiClient.DeleteChannelAsync(channel.Id, options).ConfigureAwait(false); } public static async Task ModifyAsync(IGuildChannel channel, BaseDiscordClient client, - Action func, + Action func, RequestOptions options) { - var args = new ModifyGuildChannelParams(); + var args = new GuildChannelProperties(); func(args); - return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, args, options).ConfigureAwait(false); + var apiArgs = new API.Rest.ModifyGuildChannelParams + { + Name = args.Name, + Position = args.Position + }; + return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); } public static async Task ModifyAsync(ITextChannel channel, BaseDiscordClient client, - Action func, + Action func, RequestOptions options) { - var args = new ModifyTextChannelParams(); + var args = new TextChannelProperties(); func(args); - return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, args, options).ConfigureAwait(false); + var apiArgs = new API.Rest.ModifyTextChannelParams + { + Name = args.Name, + Position = args.Position, + Topic = args.Topic + }; + return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); } public static async Task ModifyAsync(IVoiceChannel channel, BaseDiscordClient client, - Action func, + Action func, RequestOptions options) { - var args = new ModifyVoiceChannelParams(); + var args = new VoiceChannelProperties(); func(args); - return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, args, options).ConfigureAwait(false); + var apiArgs = new API.Rest.ModifyVoiceChannelParams + { + Bitrate = args.Bitrate, + Name = args.Name, + Position = args.Position, + UserLimit = args.UserLimit.IsSpecified ? (args.UserLimit.Value ?? 0) : Optional.Create() + }; + return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); } //Invites @@ -51,13 +69,17 @@ namespace Discord.Rest return models.Select(x => RestInviteMetadata.Create(client, null, channel, x)).ToImmutableArray(); } public static async Task CreateInviteAsync(IGuildChannel channel, BaseDiscordClient client, - int? maxAge, int? maxUses, bool isTemporary, RequestOptions options) + int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) { - var args = new CreateChannelInviteParams { IsTemporary = isTemporary }; + var args = new CreateChannelInviteParams { IsTemporary = isTemporary, IsUnique = isUnique }; if (maxAge.HasValue) args.MaxAge = maxAge.Value; + else + args.MaxAge = 0; if (maxUses.HasValue) args.MaxUses = maxUses.Value; + else + args.MaxUses = 0; var model = await client.ApiClient.CreateChannelInviteAsync(channel.Id, args, options).ConfigureAwait(false); return RestInviteMetadata.Create(client, null, channel, model); } @@ -132,13 +154,14 @@ namespace Discord.Rest } public static async Task SendMessageAsync(IMessageChannel channel, BaseDiscordClient client, - string text, bool isTTS, EmbedBuilder embed, RequestOptions options) + string text, bool isTTS, Embed embed, RequestOptions options) { - var args = new CreateMessageParams(text) { IsTTS = isTTS, Embed = embed?.Build() }; + var args = new CreateMessageParams(text) { IsTTS = isTTS, Embed = embed?.ToModel() }; var model = await client.ApiClient.CreateMessageAsync(channel.Id, args, options).ConfigureAwait(false); return RestUserMessage.Create(client, channel, client.CurrentUser, model); } +#if NETSTANDARD1_3 public static async Task SendFileAsync(IMessageChannel channel, BaseDiscordClient client, string filePath, string text, bool isTTS, RequestOptions options) { @@ -146,6 +169,7 @@ namespace Discord.Rest using (var file = File.OpenRead(filePath)) return await SendFileAsync(channel, client, file, filename, text, isTTS, options).ConfigureAwait(false); } +#endif public static async Task SendFileAsync(IMessageChannel channel, BaseDiscordClient client, Stream stream, string filename, string text, bool isTTS, RequestOptions options) { diff --git a/src/Discord.Net.Core/Entities/Channels/ChannelType.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelType.cs similarity index 100% rename from src/Discord.Net.Core/Entities/Channels/ChannelType.cs rename to src/Discord.Net.Rest/Entities/Channels/ChannelType.cs diff --git a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs index 554104d4d..2c006834c 100644 --- a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs @@ -7,9 +7,11 @@ namespace Discord.Rest public interface IRestMessageChannel : IMessageChannel { /// Sends a message to this message channel. - new Task SendMessageAsync(string text, bool isTTS = false, EmbedBuilder embed = null, RequestOptions options = null); + new Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null); +#if NETSTANDARD1_3 /// 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 /// Sends a file to this text channel, with an optional caption. new Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, RequestOptions options = null); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs index 573cfef72..75b331499 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs @@ -63,10 +63,12 @@ namespace Discord.Rest public Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - public Task SendMessageAsync(string text, bool isTTS = false, EmbedBuilder embed = null, RequestOptions options = null) + 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 public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); +#endif public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); @@ -122,11 +124,13 @@ namespace Discord.Rest async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); +#if NETSTANDARD1_3 async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); +#endif async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, EmbedBuilder embed, RequestOptions options) + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs index e2a015c75..a4b49b118 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs @@ -76,10 +76,12 @@ namespace Discord.Rest public Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - public Task SendMessageAsync(string text, bool isTTS = false, EmbedBuilder embed = null, RequestOptions options = null) + 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 public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); +#endif public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); @@ -132,11 +134,13 @@ namespace Discord.Rest async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); +#if NETSTANDARD1_3 async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); +#endif async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, EmbedBuilder embed, RequestOptions options) + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs index 0e6e55772..114c886c4 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs @@ -1,5 +1,4 @@ -using Discord.API.Rest; -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -45,7 +44,7 @@ namespace Discord.Rest var overwrites = model.PermissionOverwrites.Value; var newOverwrites = ImmutableArray.CreateBuilder(overwrites.Length); for (int i = 0; i < overwrites.Length; i++) - newOverwrites.Add(new Overwrite(overwrites[i])); + newOverwrites.Add(overwrites[i].ToEntity()); _overwrites = newOverwrites.ToImmutable(); } @@ -54,7 +53,7 @@ namespace Discord.Rest var model = await Discord.ApiClient.GetChannelAsync(GuildId, Id, options).ConfigureAwait(false); Update(model); } - public async Task ModifyAsync(Action func, RequestOptions options = null) + public async Task ModifyAsync(Action func, RequestOptions options = null) { var model = await ChannelHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); Update(model); @@ -83,12 +82,12 @@ namespace Discord.Rest public async Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions perms, RequestOptions options = null) { await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, user, perms, options).ConfigureAwait(false); - _overwrites = _overwrites.Add(new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = user.Id, TargetType = PermissionTarget.User })); + _overwrites = _overwrites.Add(new Overwrite(user.Id, PermissionTarget.User, new OverwritePermissions(perms.AllowValue, perms.DenyValue))); } public async Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions perms, RequestOptions options = null) { await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, role, perms, options).ConfigureAwait(false); - _overwrites.Add(new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = role.Id, TargetType = PermissionTarget.Role })); + _overwrites = _overwrites.Add(new Overwrite(role.Id, PermissionTarget.Role, new OverwritePermissions(perms.AllowValue, perms.DenyValue))); } public async Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null) { @@ -119,8 +118,8 @@ namespace Discord.Rest public async Task> GetInvitesAsync(RequestOptions options = null) => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); - public async Task CreateInviteAsync(int? maxAge = 3600, int? maxUses = null, bool isTemporary = true, RequestOptions options = null) - => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, options).ConfigureAwait(false); + public async Task CreateInviteAsync(int? maxAge = 3600, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); public override string ToString() => Name; @@ -137,8 +136,8 @@ namespace Discord.Rest async Task> IGuildChannel.GetInvitesAsync(RequestOptions options) => await GetInvitesAsync(options).ConfigureAwait(false); - async Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, RequestOptions options) - => await CreateInviteAsync(maxAge, maxUses, isTemporary, options).ConfigureAwait(false); + async Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) + => await CreateInviteAsync(maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IRole role) => GetPermissionOverwrite(role); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index 991b30283..2687312a7 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -1,5 +1,4 @@ -using Discord.API.Rest; -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -33,7 +32,7 @@ namespace Discord.Rest Topic = model.Topic.Value; } - public async Task ModifyAsync(Action func, RequestOptions options = null) + public async Task ModifyAsync(Action func, RequestOptions options = null) { var model = await ChannelHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); Update(model); @@ -55,10 +54,12 @@ namespace Discord.Rest public Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - public Task SendMessageAsync(string text, bool isTTS = false, EmbedBuilder embed = null, RequestOptions options = null) + 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 public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); +#endif public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); @@ -104,11 +105,13 @@ namespace Discord.Rest async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); +#if NETSTANDARD1_3 async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); +#endif async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, EmbedBuilder embed, RequestOptions options) + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs index a19a5cb38..e5330f29e 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs @@ -1,5 +1,4 @@ -using Discord.API.Rest; -using Discord.Audio; +using Discord.Audio; using System; using System.Collections.Generic; using System.Diagnostics; @@ -13,7 +12,7 @@ namespace Discord.Rest public class RestVoiceChannel : RestGuildChannel, IVoiceChannel, IRestAudioChannel { public int Bitrate { get; private set; } - public int UserLimit { get; private set; } + public int? UserLimit { get; private set; } internal RestVoiceChannel(BaseDiscordClient discord, IGuild guild, ulong id) : base(discord, guild, id) @@ -30,10 +29,10 @@ namespace Discord.Rest base.Update(model); Bitrate = model.Bitrate.Value; - UserLimit = model.UserLimit.Value; + UserLimit = model.UserLimit.Value != 0 ? model.UserLimit.Value : (int?)null; } - public async Task ModifyAsync(Action func, RequestOptions options = null) + public async Task ModifyAsync(Action func, RequestOptions options = null) { var model = await ChannelHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); Update(model); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestVirtualMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs similarity index 97% rename from src/Discord.Net.Rest/Entities/Channels/RestVirtualMessageChannel.cs rename to src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs index 6127eaf65..664e9c9fc 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestVirtualMessageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs @@ -33,10 +33,12 @@ namespace Discord.Rest public Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - public Task SendMessageAsync(string text, bool isTTS, EmbedBuilder embed = null, RequestOptions options = null) + public Task SendMessageAsync(string text, bool isTTS, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); +#if NETSTANDARD1_3 public Task SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); +#endif public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); @@ -82,11 +84,13 @@ namespace Discord.Rest async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options); +#if NETSTANDARD1_3 async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options); +#endif async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) => await SendFileAsync(stream, filename, text, isTTS, options); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, EmbedBuilder embed, RequestOptions options) + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) => await SendMessageAsync(text, isTTS, embed, options); IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 7aaa19304..195ae27d0 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using EmbedModel = Discord.API.GuildEmbed; using Model = Discord.API.Guild; using RoleModel = Discord.API.Role; +using ImageModel = Discord.API.Image; namespace Discord.Rest { @@ -14,65 +15,111 @@ namespace Discord.Rest { //General public static async Task ModifyAsync(IGuild guild, BaseDiscordClient client, - Action func, RequestOptions options) + Action func, RequestOptions options) { if (func == null) throw new NullReferenceException(nameof(func)); - var args = new ModifyGuildParams(); + var args = new GuildProperties(); func(args); - if (args.Splash.IsSpecified && guild.SplashId != null) - args.Splash = new API.Image(guild.SplashId); - if (args.Icon.IsSpecified && guild.IconId != null) - args.Icon = new API.Image(guild.IconId); + var apiArgs = new API.Rest.ModifyGuildParams + { + AfkChannelId = args.AfkChannelId, + AfkTimeout = args.AfkTimeout, + DefaultMessageNotifications = args.DefaultMessageNotifications, + Icon = args.Icon.IsSpecified ? args.Icon.Value?.ToModel() : Optional.Create(), + Name = args.Name, + Splash = args.Splash.IsSpecified ? args.Splash.Value?.ToModel() : Optional.Create(), + Username = args.Username, + VerificationLevel = args.VerificationLevel + }; + + if (args.AfkChannel.IsSpecified) + apiArgs.AfkChannelId = args.AfkChannel.Value.Id; + else if (args.AfkChannelId.IsSpecified) + apiArgs.AfkChannelId = args.AfkChannelId.Value; + + if (args.Owner.IsSpecified) + apiArgs.OwnerId = args.Owner.Value.Id; + else if (args.OwnerId.IsSpecified) + apiArgs.OwnerId = args.OwnerId.Value; - return await client.ApiClient.ModifyGuildAsync(guild.Id, args, options).ConfigureAwait(false); + if (args.Region.IsSpecified) + apiArgs.RegionId = args.Region.Value.Id; + else if (args.RegionId.IsSpecified) + apiArgs.RegionId = args.RegionId.Value; + + if (!apiArgs.Splash.IsSpecified && guild.SplashId != null) + apiArgs.Splash = new ImageModel(guild.SplashId); + if (!apiArgs.Icon.IsSpecified && guild.IconId != null) + apiArgs.Icon = new ImageModel(guild.IconId); + + return await client.ApiClient.ModifyGuildAsync(guild.Id, apiArgs, options).ConfigureAwait(false); } public static async Task ModifyEmbedAsync(IGuild guild, BaseDiscordClient client, - Action func, RequestOptions options) + Action func, RequestOptions options) { if (func == null) throw new NullReferenceException(nameof(func)); - var args = new ModifyGuildEmbedParams(); + var args = new GuildEmbedProperties(); func(args); - return await client.ApiClient.ModifyGuildEmbedAsync(guild.Id, args, options).ConfigureAwait(false); + var apiArgs = new API.Rest.ModifyGuildEmbedParams + { + Enabled = args.Enabled + }; + + if (args.Channel.IsSpecified) + apiArgs.ChannelId = args.Channel.Value?.Id; + else if (args.ChannelId.IsSpecified) + apiArgs.ChannelId = args.ChannelId.Value; + + return await client.ApiClient.ModifyGuildEmbedAsync(guild.Id, apiArgs, options).ConfigureAwait(false); } public static async Task ModifyChannelsAsync(IGuild guild, BaseDiscordClient client, - IEnumerable args, RequestOptions options) + IEnumerable args, RequestOptions options) { - await client.ApiClient.ModifyGuildChannelsAsync(guild.Id, args, options).ConfigureAwait(false); + 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) + IEnumerable args, RequestOptions options) { - return await client.ApiClient.ModifyGuildRolesAsync(guild.Id, args, options).ConfigureAwait(false); + 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 + }); + return await client.ApiClient.ModifyGuildRolesAsync(guild.Id, apiArgs, options).ConfigureAwait(false); } - public static async Task LeaveAsync(IGuild guild, BaseDiscordClient client, + public static async Task LeaveAsync(IGuild guild, BaseDiscordClient client, RequestOptions options) { await client.ApiClient.LeaveGuildAsync(guild.Id, options).ConfigureAwait(false); } - public static async Task DeleteAsync(IGuild guild, BaseDiscordClient client, + public static async Task DeleteAsync(IGuild guild, BaseDiscordClient client, RequestOptions options) { await client.ApiClient.DeleteGuildAsync(guild.Id, options).ConfigureAwait(false); } //Bans - public static async Task> GetBansAsync(IGuild guild, BaseDiscordClient client, + public static async Task> GetBansAsync(IGuild guild, BaseDiscordClient client, RequestOptions options) { var models = await client.ApiClient.GetGuildBansAsync(guild.Id, options).ConfigureAwait(false); return models.Select(x => RestBan.Create(client, x)).ToImmutableArray(); } - - public static async Task AddBanAsync(IGuild guild, BaseDiscordClient client, + + public static async Task AddBanAsync(IGuild guild, BaseDiscordClient client, ulong userId, int pruneDays, RequestOptions options) { var args = new CreateGuildBanParams { DeleteMessageDays = pruneDays }; await client.ApiClient.CreateGuildBanAsync(guild.Id, userId, args, options).ConfigureAwait(false); - } - public static async Task RemoveBanAsync(IGuild guild, BaseDiscordClient client, + } + public static async Task RemoveBanAsync(IGuild guild, BaseDiscordClient client, ulong userId, RequestOptions options) { await client.ApiClient.RemoveGuildBanAsync(guild.Id, userId, options).ConfigureAwait(false); @@ -87,7 +134,7 @@ namespace Discord.Rest return RestGuildChannel.Create(client, guild, model); return null; } - public static async Task> GetChannelsAsync(IGuild guild, BaseDiscordClient client, + public static async Task> GetChannelsAsync(IGuild guild, BaseDiscordClient client, RequestOptions options) { var models = await client.ApiClient.GetGuildChannelsAsync(guild.Id, options).ConfigureAwait(false); @@ -113,7 +160,7 @@ namespace Discord.Rest } //Integrations - public static async Task> GetIntegrationsAsync(IGuild guild, BaseDiscordClient client, + public static async Task> GetIntegrationsAsync(IGuild guild, BaseDiscordClient client, RequestOptions options) { var models = await client.ApiClient.GetGuildIntegrationsAsync(guild.Id, options).ConfigureAwait(false); @@ -128,7 +175,7 @@ namespace Discord.Rest } //Invites - public static async Task> GetInvitesAsync(IGuild guild, BaseDiscordClient client, + public static async Task> GetInvitesAsync(IGuild guild, BaseDiscordClient client, RequestOptions options) { var models = await client.ApiClient.GetGuildInvitesAsync(guild.Id, options).ConfigureAwait(false); @@ -147,8 +194,8 @@ namespace Discord.Rest await role.ModifyAsync(x => { x.Name = name; - x.Permissions = (permissions ?? role.Permissions).RawValue; - x.Color = (color ?? Color.Default).RawValue; + x.Permissions = (permissions ?? role.Permissions); + x.Color = (color ?? Color.Default); x.Hoist = isHoisted; }, options).ConfigureAwait(false); @@ -164,7 +211,7 @@ namespace Discord.Rest return RestGuildUser.Create(client, guild, model); return null; } - public static async Task GetCurrentUserAsync(IGuild guild, BaseDiscordClient client, + public static async Task GetCurrentUserAsync(IGuild guild, BaseDiscordClient client, RequestOptions options) { return await GetUserAsync(guild, client, client.CurrentUser.Id, options).ConfigureAwait(false); diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index a85107831..0622df6ce 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -1,5 +1,4 @@ -using Discord.API.Rest; -using Discord.Audio; +using Discord.Audio; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -35,8 +34,8 @@ namespace Discord.Rest public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); public ulong DefaultChannelId => Id; - public string IconUrl => API.CDN.GetGuildIconUrl(Id, IconId); - public string SplashUrl => API.CDN.GetGuildSplashUrl(Id, SplashId); + public string IconUrl => CDN.GetGuildIconUrl(Id, IconId); + public string SplashUrl => CDN.GetGuildSplashUrl(Id, SplashId); public RestRole EveryoneRole => GetRole(Id); public IReadOnlyCollection Roles => _roles.ToReadOnlyCollection(); @@ -72,7 +71,7 @@ namespace Discord.Rest { var emojis = ImmutableArray.CreateBuilder(model.Emojis.Length); for (int i = 0; i < model.Emojis.Length; i++) - emojis.Add(GuildEmoji.Create(model.Emojis[i])); + emojis.Add(model.Emojis[i].ToEntity()); _emojis = emojis.ToImmutableArray(); } else @@ -105,22 +104,22 @@ namespace Discord.Rest public Task DeleteAsync(RequestOptions options = null) => GuildHelper.DeleteAsync(this, Discord, options); - public async Task ModifyAsync(Action func, RequestOptions options = null) + public async Task ModifyAsync(Action func, RequestOptions options = null) { var model = await GuildHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); Update(model); } - public async Task ModifyEmbedAsync(Action func, RequestOptions options = null) + public async Task ModifyEmbedAsync(Action func, RequestOptions options = null) { 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 ModifyChannelsAsync(IEnumerable args, RequestOptions options = null) { var arr = args.ToArray(); await GuildHelper.ModifyChannelsAsync(this, Discord, arr, options); } - public async Task ModifyRolesAsync(IEnumerable args, RequestOptions options = null) + public async Task ModifyRolesAsync(IEnumerable args, RequestOptions options = null) { var models = await GuildHelper.ModifyRolesAsync(this, Discord, args, options).ConfigureAwait(false); foreach (var model in models) @@ -152,7 +151,53 @@ namespace Discord.Rest public Task> GetChannelsAsync(RequestOptions options = null) => GuildHelper.GetChannelsAsync(this, Discord, options); public Task GetChannelAsync(ulong id, RequestOptions options = null) - => GuildHelper.GetChannelAsync(this, Discord, id, options); + => GuildHelper.GetChannelAsync(this, Discord, id, options); + public async Task GetTextChannelAsync(ulong id, RequestOptions options = null) + { + var channel = await GuildHelper.GetChannelAsync(this, Discord, id, options).ConfigureAwait(false); + return channel as RestTextChannel; + } + public async Task> GetTextChannelsAsync(RequestOptions options = null) + { + var channels = await GuildHelper.GetChannelsAsync(this, Discord, options).ConfigureAwait(false); + return channels.Select(x => x as RestTextChannel).Where(x => x != null).ToImmutableArray(); + } + public async Task GetVoiceChannelAsync(ulong id, RequestOptions options = null) + { + var channel = await GuildHelper.GetChannelAsync(this, Discord, id, options).ConfigureAwait(false); + return channel as RestVoiceChannel; + } + public async Task> GetVoiceChannelsAsync(RequestOptions options = null) + { + var channels = await GuildHelper.GetChannelsAsync(this, Discord, options).ConfigureAwait(false); + return channels.Select(x => x as RestVoiceChannel).Where(x => x != null).ToImmutableArray(); + } + + public async Task GetAFKChannelAsync(RequestOptions options = null) + { + var afkId = AFKChannelId; + if (afkId.HasValue) + { + var channel = await GuildHelper.GetChannelAsync(this, Discord, afkId.Value, options).ConfigureAwait(false); + return channel as RestVoiceChannel; + } + return null; + } + public async Task GetDefaultChannelAsync(RequestOptions options = null) + { + var channel = await GuildHelper.GetChannelAsync(this, Discord, DefaultChannelId, options).ConfigureAwait(false); + return channel as RestTextChannel; + } + 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 null; + } public Task CreateTextChannelAsync(string name, RequestOptions options = null) => GuildHelper.CreateTextChannelAsync(this, Discord, name, options); public Task CreateVoiceChannelAsync(string name, RequestOptions options = null) @@ -192,6 +237,8 @@ namespace Discord.Rest => GuildHelper.GetUserAsync(this, Discord, id, options); public Task GetCurrentUserAsync(RequestOptions options = null) => GuildHelper.GetUserAsync(this, Discord, Discord.CurrentUser.Id, options); + public Task GetOwnerAsync(RequestOptions options = null) + => GuildHelper.GetUserAsync(this, Discord, OwnerId, options); public Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null) => GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options); @@ -222,6 +269,55 @@ namespace Discord.Rest else return null; } + async Task> IGuild.GetTextChannelsAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetTextChannelsAsync(options).ConfigureAwait(false); + else + return ImmutableArray.Create(); + } + async Task IGuild.GetTextChannelAsync(ulong id, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetTextChannelAsync(id, options).ConfigureAwait(false); + else + return null; + } + async Task> IGuild.GetVoiceChannelsAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetVoiceChannelsAsync(options).ConfigureAwait(false); + else + return ImmutableArray.Create(); + } + async Task IGuild.GetVoiceChannelAsync(ulong id, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetVoiceChannelAsync(id, options).ConfigureAwait(false); + else + return null; + } + async Task IGuild.GetAFKChannelAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetAFKChannelAsync(options).ConfigureAwait(false); + else + return null; + } + async Task IGuild.GetDefaultChannelAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetDefaultChannelAsync(options).ConfigureAwait(false); + else + return null; + } + async Task IGuild.GetEmbedChannelAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetEmbedChannelAsync(options).ConfigureAwait(false); + else + return null; + } async Task IGuild.CreateTextChannelAsync(string name, RequestOptions options) => await CreateTextChannelAsync(name, options).ConfigureAwait(false); async Task IGuild.CreateVoiceChannelAsync(string name, RequestOptions options) @@ -254,6 +350,13 @@ namespace Discord.Rest else return null; } + async Task IGuild.GetOwnerAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetOwnerAsync(options).ConfigureAwait(false); + else + return null; + } async Task> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs index fc2bfd8b2..eadda53f2 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs @@ -1,5 +1,4 @@ -using Discord.API.Rest; -using System; +using System; using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.Integration; @@ -37,7 +36,7 @@ namespace Discord.Rest return entity; } - public void Update(Model model) + internal void Update(Model model) { Name = model.Name; Type = model.Type; @@ -55,13 +54,19 @@ namespace Discord.Rest { await Discord.ApiClient.DeleteGuildIntegrationAsync(GuildId, Id).ConfigureAwait(false); } - public async Task ModifyAsync(Action func) + public async Task ModifyAsync(Action func) { if (func == null) throw new NullReferenceException(nameof(func)); - var args = new ModifyGuildIntegrationParams(); + var args = new GuildIntegrationProperties(); func(args); - var model = await Discord.ApiClient.ModifyGuildIntegrationAsync(GuildId, Id, args).ConfigureAwait(false); + var apiArgs = new API.Rest.ModifyGuildIntegrationParams + { + EnableEmoticons = args.EnableEmoticons, + ExpireBehavior = args.ExpireBehavior, + ExpireGracePeriod = args.ExpireGracePeriod + }; + var model = await Discord.ApiClient.ModifyGuildIntegrationAsync(GuildId, Id, apiArgs).ConfigureAwait(false); Update(model); } diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs index 69c2c9362..12601b72e 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs @@ -15,7 +15,7 @@ namespace Discord.Rest public GuildPermissions Permissions { get; private set; } public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); - public string IconUrl => API.CDN.GetGuildIconUrl(Id, _iconId); + public string IconUrl => CDN.GetGuildIconUrl(Id, _iconId); internal RestUserGuild(BaseDiscordClient discord, ulong id) : base(discord, id) @@ -28,7 +28,7 @@ namespace Discord.Rest return entity; } - public void Update(Model model) + internal void Update(Model model) { _iconId = model.Icon; IsOwner = model.Owner; diff --git a/src/Discord.Net.Rest/Entities/Invites/InviteHelper.cs b/src/Discord.Net.Rest/Entities/Invites/InviteHelper.cs index 8ec428178..80a49e34e 100644 --- a/src/Discord.Net.Rest/Entities/Invites/InviteHelper.cs +++ b/src/Discord.Net.Rest/Entities/Invites/InviteHelper.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; -using Model = Discord.API.Invite; namespace Discord.Rest { diff --git a/src/Discord.Net.Rest/Entities/Messages/Embed.cs b/src/Discord.Net.Rest/Entities/Messages/Embed.cs deleted file mode 100644 index 540a39ea2..000000000 --- a/src/Discord.Net.Rest/Entities/Messages/Embed.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Linq; -using Model = Discord.API.Embed; - -namespace Discord -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class Embed : IEmbed - { - public string Description { get; } - public string Url { get; } - public string Title { get; } - public string Type { get; } - public Color? Color { get; } - public EmbedAuthor? Author { get; } - public EmbedFooter? Footer { get; } - public EmbedProvider? Provider { get; } - public EmbedThumbnail? Thumbnail { get; } - public ImmutableArray Fields { get; } - - internal Embed(string type, - string title, - string description, - string url, - Color? color, - EmbedAuthor? author, - EmbedFooter? footer, - EmbedProvider? provider, - EmbedThumbnail? thumbnail, - ImmutableArray fields) - { - Type = type; - Title = title; - Description = description; - Url = url; - Color = color; - Author = author; - Footer = footer; - Provider = provider; - Thumbnail = thumbnail; - Fields = fields; - } - internal static Embed Create(Model model) - { - return new Embed(model.Type, model.Title, model.Description, model.Url, - model.Color.HasValue ? new Color(model.Color.Value) : (Color?)null, - model.Author.IsSpecified ? EmbedAuthor.Create(model.Author.Value) : (EmbedAuthor?)null, - model.Footer.IsSpecified ? EmbedFooter.Create(model.Footer.Value) : (EmbedFooter?)null, - model.Provider.IsSpecified ? EmbedProvider.Create(model.Provider.Value) : (EmbedProvider?)null, - model.Thumbnail.IsSpecified ? EmbedThumbnail.Create(model.Thumbnail.Value) : (EmbedThumbnail?)null, - model.Fields.IsSpecified ? model.Fields.Value.Select(x => EmbedField.Create(x)).ToImmutableArray() : ImmutableArray.Create()); - } - - public override string ToString() => Title; - private string DebuggerDisplay => $"{Title} ({Type})"; - } -} diff --git a/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs new file mode 100644 index 000000000..8890df683 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs @@ -0,0 +1,213 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Discord +{ + public class EmbedBuilder + { + private readonly Embed _embed; + private readonly List _fields; + + public EmbedBuilder() + { + _embed = new Embed("rich"); + _fields = new List(); + } + + 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 EmbedAuthorBuilder Author { get; set; } + public EmbedFooterBuilder Footer { get; set; } + + public EmbedBuilder WithTitle(string title) + { + Title = title; + return this; + } + public EmbedBuilder WithDescription(string description) + { + Description = description; + return this; + } + public EmbedBuilder WithUrl(string url) + { + Url = url; + return this; + } + public EmbedBuilder WithThumbnailUrl(string thumbnailUrl) + { + ThumbnailUrl = thumbnailUrl; + return this; + } + public EmbedBuilder WithImageUrl(string imageUrl) + { + ImageUrl = imageUrl; + return this; + } + public EmbedBuilder WithCurrentTimestamp() + { + Timestamp = DateTimeOffset.UtcNow; + return this; + } + public EmbedBuilder WithTimestamp(DateTimeOffset dateTimeOffset) + { + Timestamp = dateTimeOffset; + return this; + } + public EmbedBuilder WithColor(Color color) + { + Color = color; + return this; + } + + public EmbedBuilder WithAuthor(EmbedAuthorBuilder author) + { + Author = author; + return this; + } + public EmbedBuilder WithAuthor(Action action) + { + var author = new EmbedAuthorBuilder(); + action(author); + Author = author; + return this; + } + public EmbedBuilder WithFooter(EmbedFooterBuilder footer) + { + Footer = footer; + return this; + } + public EmbedBuilder WithFooter(Action action) + { + var footer = new EmbedFooterBuilder(); + action(footer); + Footer = footer; + return this; + } + + public EmbedBuilder AddField(EmbedFieldBuilder field) + { + _fields.Add(field); + return this; + } + public EmbedBuilder AddField(Action action) + { + var field = new EmbedFieldBuilder(); + action(field); + _fields.Add(field); + return this; + } + + public Embed Build() + { + _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()); + _embed.Fields = fields.ToImmutable(); + return _embed; + } + public static implicit operator Embed(EmbedBuilder builder) => builder?.Build(); + } + + public class EmbedFieldBuilder + { + 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 EmbedFieldBuilder() + { + _field = new EmbedField(); + } + + public EmbedFieldBuilder WithName(string name) + { + Name = name; + return this; + } + public EmbedFieldBuilder WithValue(object value) + { + Value = value; + return this; + } + public EmbedFieldBuilder WithIsInline(bool isInline) + { + IsInline = isInline; + return this; + } + + public EmbedField Build() + => _field; + } + + public class EmbedAuthorBuilder + { + 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 EmbedAuthorBuilder() + { + _author = new EmbedAuthor(); + } + + public EmbedAuthorBuilder WithName(string name) + { + Name = name; + return this; + } + public EmbedAuthorBuilder WithUrl(string url) + { + Url = url; + return this; + } + public EmbedAuthorBuilder WithIconUrl(string iconUrl) + { + IconUrl = iconUrl; + return this; + } + + public EmbedAuthor Build() + => _author; + } + + public class EmbedFooterBuilder + { + 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 EmbedFooterBuilder() + { + _footer = new EmbedFooter(); + } + + public EmbedFooterBuilder WithText(string text) + { + Text = text; + return this; + } + public EmbedFooterBuilder WithIconUrl(string iconUrl) + { + IconUrl = iconUrl; + return this; + } + + public EmbedFooter Build() + => _footer; + } +} diff --git a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs index 358f6f5a9..4f8d52263 100644 --- a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -10,12 +10,17 @@ namespace Discord.Rest { internal static class MessageHelper { - public static async Task ModifyAsync(IMessage msg, BaseDiscordClient client, Action func, + public static async Task ModifyAsync(IMessage msg, BaseDiscordClient client, Action func, RequestOptions options) { - var args = new ModifyMessageParams(); + var args = new MessageProperties(); func(args); - return await client.ApiClient.ModifyMessageAsync(msg.Channel.Id, msg.Id, args, options).ConfigureAwait(false); + var apiArgs = new API.Rest.ModifyMessageParams + { + Content = args.Content, + Embed = args.Embed.IsSpecified ? args.Embed.Value.ToModel() : Optional.Create() + }; + return await client.ApiClient.ModifyMessageAsync(msg.Channel.Id, msg.Id, apiArgs, options).ConfigureAwait(false); } public static async Task DeleteAsync(IMessage msg, BaseDiscordClient client, RequestOptions options) @@ -23,6 +28,34 @@ 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) + { + await client.ApiClient.AddReactionAsync(msg.Channel.Id, msg.Id, emoji, 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) + { + await client.ApiClient.RemoveReactionAsync(msg.Channel.Id, msg.Id, user.Id, emoji, options).ConfigureAwait(false); + } + + public static async Task RemoveAllReactionsAsync(IMessage msg, BaseDiscordClient client, RequestOptions options) + { + await client.ApiClient.RemoveAllReactionsAsync(msg.Channel.Id, msg.Id, options); + } + + public static async Task> GetReactionUsersAsync(IMessage msg, string emoji, + Action func, BaseDiscordClient client, RequestOptions options) + { + 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(); + } + public static async Task PinAsync(IMessage msg, BaseDiscordClient client, RequestOptions options) { @@ -36,7 +69,7 @@ namespace Discord.Rest public static ImmutableArray ParseTags(string text, IMessageChannel channel, IGuild guild, ImmutableArray userMentions) { - var tags = new SortedList(); + var tags = ImmutableArray.CreateBuilder(); int index = 0; while (true) @@ -61,27 +94,27 @@ namespace Discord.Rest break; } } - tags.Add(index, new Tag(TagType.UserMention, index, content.Length, id, mentionedUser)); + tags.Add(new Tag(TagType.UserMention, index, content.Length, id, mentionedUser)); } else if (MentionUtils.TryParseChannel(content, out id)) { IChannel mentionedChannel = null; if (guild != null) mentionedChannel = guild.GetChannelAsync(id, CacheMode.CacheOnly).GetAwaiter().GetResult(); - tags.Add(index, new Tag(TagType.ChannelMention, index, content.Length, id, mentionedChannel)); + tags.Add(new Tag(TagType.ChannelMention, index, content.Length, id, mentionedChannel)); } else if (MentionUtils.TryParseRole(content, out id)) { IRole mentionedRole = null; if (guild != null) mentionedRole = guild.GetRole(id); - tags.Add(index, new Tag(TagType.RoleMention, index, content.Length, id, mentionedRole)); + tags.Add(new Tag(TagType.RoleMention, index, content.Length, id, mentionedRole)); } else { Emoji emoji; if (Emoji.TryParse(content, out emoji)) - tags.Add(index, new Tag(TagType.Emoji, index, content.Length, id, emoji)); + tags.Add(new Tag(TagType.Emoji, index, content.Length, id, emoji)); } index = endIndex + 1; } @@ -92,7 +125,7 @@ namespace Discord.Rest index = text.IndexOf("@everyone", index); if (index == -1) break; - tags.Add(index, new Tag(TagType.EveryoneMention, index, "@everyone".Length, 0, null)); + tags.Add(new Tag(TagType.EveryoneMention, index, "@everyone".Length, 0, null)); index++; } @@ -102,11 +135,11 @@ namespace Discord.Rest index = text.IndexOf("@here", index); if (index == -1) break; - tags.Add(index, new Tag(TagType.HereMention, index, "@here".Length, 0, null)); + tags.Add(new Tag(TagType.HereMention, index, "@here".Length, 0, null)); index++; } - return tags.Values.ToImmutableArray(); + return tags.ToImmutable(); } public static ImmutableArray FilterTagsByKey(TagType type, ImmutableArray tags) { diff --git a/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs b/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs new file mode 100644 index 000000000..933833d56 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs @@ -0,0 +1,22 @@ +using Model = Discord.API.Reaction; + +namespace Discord.Rest +{ + public class RestReaction : IReaction + { + public Emoji Emoji { get; } + public int Count { get; } + public bool Me { get; } + + internal RestReaction(Emoji emoji, int count, bool me) + { + Emoji = emoji; + 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); + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs index de90448c7..ee806dbc1 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs @@ -1,8 +1,8 @@ -using Discord.API.Rest; -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; +using System.Linq; using System.Threading.Tasks; using Model = Discord.API.Message; @@ -17,6 +17,7 @@ namespace Discord.Rest private ImmutableArray _attachments; private ImmutableArray _embeds; private ImmutableArray _tags; + private ImmutableArray _reactions; public override bool IsTTS => _isTTS; public override bool IsPinned => _isPinned; @@ -28,6 +29,7 @@ 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); internal RestUserMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author) : base(discord, id, channel, author) @@ -76,7 +78,7 @@ namespace Discord.Rest { var embeds = ImmutableArray.CreateBuilder(value.Length); for (int i = 0; i < value.Length; i++) - embeds.Add(Embed.Create(value[i])); + embeds.Add(value[i].ToEntity()); _embeds = embeds.ToImmutable(); } else @@ -100,6 +102,22 @@ namespace Discord.Rest } } + if (model.Reactions.IsSpecified) + { + var value = model.Reactions.Value; + if (value.Length > 0) + { + var reactions = ImmutableArray.CreateBuilder(value.Length); + for (int i = 0; i < value.Length; i++) + reactions.Add(RestReaction.Create(value[i])); + _reactions = reactions.ToImmutable(); + } + else + _reactions = ImmutableArray.Create(); + } + else + _reactions = ImmutableArray.Create(); + if (model.Content.IsSpecified) { var text = model.Content.Value; @@ -110,15 +128,32 @@ namespace Discord.Rest } } - public async Task ModifyAsync(Action func, RequestOptions options) + public async Task ModifyAsync(Action func, RequestOptions options = null) { var model = await MessageHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); Update(model); } - public Task PinAsync(RequestOptions 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 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); + + + public Task PinAsync(RequestOptions options = null) => MessageHelper.PinAsync(this, Discord, options); - public Task UnpinAsync(RequestOptions options) + public Task UnpinAsync(RequestOptions options = null) => MessageHelper.UnpinAsync(this, Discord, options); public string Resolve(int startIndex, TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, diff --git a/src/Discord.Net.Rest/Entities/RestApplication.cs b/src/Discord.Net.Rest/Entities/RestApplication.cs index 62b434044..f81e4cd7b 100644 --- a/src/Discord.Net.Rest/Entities/RestApplication.cs +++ b/src/Discord.Net.Rest/Entities/RestApplication.cs @@ -18,7 +18,7 @@ namespace Discord.Rest public IUser Owner { get; private set; } public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); - public string IconUrl => API.CDN.GetApplicationIconUrl(Id, _iconId); + public string IconUrl => CDN.GetApplicationIconUrl(Id, _iconId); internal RestApplication(BaseDiscordClient discord, ulong id) : base(discord, id) diff --git a/src/Discord.Net.Rest/Entities/Roles/RestRole.cs b/src/Discord.Net.Rest/Entities/Roles/RestRole.cs index eee1fdf0a..dfdbb150d 100644 --- a/src/Discord.Net.Rest/Entities/Roles/RestRole.cs +++ b/src/Discord.Net.Rest/Entities/Roles/RestRole.cs @@ -1,5 +1,4 @@ -using Discord.API.Rest; -using System; +using System; using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.Role; @@ -44,7 +43,7 @@ namespace Discord.Rest Permissions = new GuildPermissions(model.Permissions); } - public async Task ModifyAsync(Action func, RequestOptions options = null) + public async Task ModifyAsync(Action func, RequestOptions options = null) { var model = await RoleHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); Update(model); @@ -52,7 +51,7 @@ namespace Discord.Rest public Task DeleteAsync(RequestOptions options = null) => RoleHelper.DeleteAsync(this, Discord, options); - public int CompareTo(IRole role) => this.Compare(role); + public int CompareTo(IRole role) => RoleUtils.Compare(this, role); public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id})"; diff --git a/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs b/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs index 0b102098c..0081351f0 100644 --- a/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs +++ b/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs @@ -1,5 +1,4 @@ -using Discord.API.Rest; -using System; +using System; using System.Threading.Tasks; using Model = Discord.API.Role; @@ -14,11 +13,20 @@ namespace Discord.Rest await client.ApiClient.DeleteGuildRoleAsync(role.Guild.Id, role.Id, options).ConfigureAwait(false); } public static async Task ModifyAsync(IRole role, BaseDiscordClient client, - Action func, RequestOptions options) + Action func, RequestOptions options) { - var args = new ModifyGuildRoleParams(); + var args = new RoleProperties(); func(args); - return await client.ApiClient.ModifyGuildRoleAsync(role.Guild.Id, role.Id, args, options).ConfigureAwait(false); + var apiArgs = new API.Rest.ModifyGuildRoleParams + { + Color = args.Color.IsSpecified ? args.Color.Value.RawValue : Optional.Create(), + Hoist = args.Hoist, + Mentionable = args.Mentionable, + Name = args.Name, + Permissions = args.Permissions.IsSpecified ? args.Permissions.Value.RawValue : Optional.Create(), + Position = args.Position + }; + return await client.ApiClient.ModifyGuildRoleAsync(role.Guild.Id, role.Id, apiArgs, options).ConfigureAwait(false); } } } diff --git a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs index 180ad38bc..8de42608d 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs @@ -1,8 +1,8 @@ -using Discord.API.Rest; -using System; +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; @@ -30,7 +30,7 @@ namespace Discord.Rest } } public IReadOnlyCollection RoleIds => _roleIds; - + public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); internal RestGuildUser(BaseDiscordClient discord, IGuild guild, ulong id) @@ -46,7 +46,8 @@ namespace Discord.Rest } internal void Update(Model model) { - _joinedAtTicks = model.JoinedAt.UtcTicks; + if (model.JoinedAt.IsSpecified) + _joinedAtTicks = model.JoinedAt.Value.UtcTicks; if (model.Nick.IsSpecified) Nickname = model.Nick.Value; IsDeafened = model.Deaf; @@ -61,21 +62,25 @@ namespace Discord.Rest roles.Add(roleIds[i]); _roleIds = roles.ToImmutable(); } - + public override async Task UpdateAsync(RequestOptions options = null) { var model = await Discord.ApiClient.GetGuildMemberAsync(GuildId, Id, options).ConfigureAwait(false); Update(model); } - public async Task ModifyAsync(Action func, RequestOptions options = null) - { + public async Task ModifyAsync(Action func, RequestOptions options = null) + { var args = await UserHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); if (args.Deaf.IsSpecified) IsDeafened = args.Deaf.Value; if (args.Mute.IsSpecified) IsMuted = args.Mute.Value; - if (args.RoleIds.IsSpecified) - UpdateRoles(args.RoleIds.Value); + if (args.Nickname.IsSpecified) + Nickname = args.Nickname.Value; + if (args.Roles.IsSpecified) + UpdateRoles(args.Roles.Value.Select(x => x.Id).ToArray()); + else if (args.RoleIds.IsSpecified) + UpdateRoles(args.RoleIds.Value.ToArray()); } public Task KickAsync(RequestOptions options = null) => UserHelper.KickAsync(this, Discord, options); diff --git a/src/Discord.Net.Rest/Entities/Users/RestSelfUser.cs b/src/Discord.Net.Rest/Entities/Users/RestSelfUser.cs index c97443522..ab5ec4a3b 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestSelfUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestSelfUser.cs @@ -1,5 +1,4 @@ -using Discord.API.Rest; -using System; +using System; using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.User; @@ -43,7 +42,7 @@ namespace Discord.Rest Update(model); } - public async Task ModifyAsync(Action func, RequestOptions options = null) + public async Task ModifyAsync(Action func, RequestOptions options = null) { if (Id != Discord.CurrentUser.Id) throw new InvalidOperationException("Unable to modify this object using a different token."); diff --git a/src/Discord.Net.Rest/Entities/Users/RestUser.cs b/src/Discord.Net.Rest/Entities/Users/RestUser.cs index 25419932f..b439fb886 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -13,7 +13,7 @@ namespace Discord.Rest public ushort DiscriminatorValue { get; private set; } public string AvatarId { get; private set; } - public string AvatarUrl => API.CDN.GetUserAvatarUrl(Id, AvatarId); + public string GetAvatarUrl(AvatarFormat format = AvatarFormat.Png, ushort size = 128) => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); @@ -52,7 +52,7 @@ namespace Discord.Rest => UserHelper.CreateDMChannelAsync(this, Discord, options); public override string ToString() => $"{Username}#{Discriminator}"; - internal string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; + private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; //IUser Task IUser.GetDMChannelAsync(CacheMode mode, RequestOptions options) diff --git a/src/Discord.Net.Rest/Entities/Users/UserHelper.cs b/src/Discord.Net.Rest/Entities/Users/UserHelper.cs index 545703f0d..5189851fd 100644 --- a/src/Discord.Net.Rest/Entities/Users/UserHelper.cs +++ b/src/Discord.Net.Rest/Entities/Users/UserHelper.cs @@ -2,24 +2,52 @@ using System; using System.Threading.Tasks; using Model = Discord.API.User; +using ImageModel = Discord.API.Image; +using System.Linq; namespace Discord.Rest { internal static class UserHelper { - public static async Task ModifyAsync(ISelfUser user, BaseDiscordClient client, Action func, + public static async Task ModifyAsync(ISelfUser user, BaseDiscordClient client, Action func, RequestOptions options) { - var args = new ModifyCurrentUserParams(); + var args = new SelfUserProperties(); func(args); - return await client.ApiClient.ModifySelfAsync(args, options).ConfigureAwait(false); + var apiArgs = new API.Rest.ModifyCurrentUserParams + { + Avatar = args.Avatar.IsSpecified ? args.Avatar.Value?.ToModel() : Optional.Create(), + Username = args.Username + }; + + if (!apiArgs.Avatar.IsSpecified && user.AvatarId != null) + apiArgs.Avatar = new ImageModel(user.AvatarId); + + return await client.ApiClient.ModifySelfAsync(apiArgs, options).ConfigureAwait(false); } - public static async Task ModifyAsync(IGuildUser user, BaseDiscordClient client, Action func, + public static async Task ModifyAsync(IGuildUser user, BaseDiscordClient client, Action func, RequestOptions options) { - var args = new ModifyGuildMemberParams(); + var args = new GuildUserProperties(); func(args); - await client.ApiClient.ModifyGuildMemberAsync(user.GuildId, user.Id, args, options).ConfigureAwait(false); + var apiArgs = new API.Rest.ModifyGuildMemberParams + { + Deaf = args.Deaf, + Mute = args.Mute, + Nickname = args.Nickname + }; + + if (args.Channel.IsSpecified) + apiArgs.ChannelId = args.Channel.Value.Id; + else if (args.ChannelId.IsSpecified) + apiArgs.ChannelId = args.ChannelId.Value; + + if (args.Roles.IsSpecified) + apiArgs.RoleIds = args.Roles.Value.Select(x => x.Id).ToArray(); + else if (args.RoleIds.IsSpecified) + apiArgs.RoleIds = args.RoleIds.Value.ToArray(); + + await client.ApiClient.ModifyGuildMemberAsync(user.GuildId, user.Id, apiArgs, options).ConfigureAwait(false); return args; } diff --git a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs new file mode 100644 index 000000000..7a9643674 --- /dev/null +++ b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs @@ -0,0 +1,124 @@ +using System.Collections.Immutable; +using System.Linq; + +namespace Discord.Rest +{ + internal static class EntityExtensions + { + public static GuildEmoji ToEntity(this API.Emoji model) + { + return new GuildEmoji(model.Id.Value, model.Name, model.Managed, model.RequireColons, ImmutableArray.Create(model.Roles)); + } + + public static Embed ToEntity(this API.Embed model) + { + return new Embed(model.Type, model.Title, model.Description, model.Url, model.Timestamp, + model.Color.HasValue ? new Color(model.Color.Value) : (Color?)null, + model.Image.IsSpecified ? model.Image.Value.ToEntity() : (EmbedImage?)null, + model.Video.IsSpecified ? model.Video.Value.ToEntity() : (EmbedVideo?)null, + model.Author.IsSpecified ? model.Author.Value.ToEntity() : (EmbedAuthor?)null, + model.Footer.IsSpecified ? model.Footer.Value.ToEntity() : (EmbedFooter?)null, + model.Provider.IsSpecified ? model.Provider.Value.ToEntity() : (EmbedProvider?)null, + model.Thumbnail.IsSpecified ? model.Thumbnail.Value.ToEntity() : (EmbedThumbnail?)null, + model.Fields.IsSpecified ? model.Fields.Value.Select(x => x.ToEntity()).ToImmutableArray() : ImmutableArray.Create()); + } + public static API.Embed ToModel(this Embed entity) + { + var model = new API.Embed + { + Type = entity.Type, + Title = entity.Title, + Description = entity.Description, + Url = entity.Url, + Timestamp = entity.Timestamp, + Color = entity.Color?.RawValue + }; + if (entity.Author != null) + model.Author = entity.Author.Value.ToModel(); + model.Fields = entity.Fields.Select(x => x.ToModel()).ToArray(); + if (entity.Footer != null) + model.Footer = entity.Footer.Value.ToModel(); + if (entity.Image != null) + model.Image = entity.Image.Value.ToModel(); + if (entity.Provider != null) + model.Provider = entity.Provider.Value.ToModel(); + if (entity.Thumbnail != null) + model.Thumbnail = entity.Thumbnail.Value.ToModel(); + if (entity.Video != null) + model.Video = entity.Video.Value.ToModel(); + return model; + } + public static EmbedAuthor ToEntity(this API.EmbedAuthor model) + { + return new EmbedAuthor(model.Name, model.Url, model.IconUrl, model.ProxyIconUrl); + } + public static API.EmbedAuthor ToModel(this EmbedAuthor entity) + { + return new API.EmbedAuthor { Name = entity.Name, Url = entity.Url, IconUrl = entity.IconUrl }; + } + public static EmbedField ToEntity(this API.EmbedField model) + { + return new EmbedField(model.Name, model.Value, model.Inline); + } + public static API.EmbedField ToModel(this EmbedField entity) + { + return new API.EmbedField { Name = entity.Name, Value = entity.Value, Inline = entity.Inline }; + } + public static EmbedFooter ToEntity(this API.EmbedFooter model) + { + return new EmbedFooter(model.Text, model.IconUrl, model.ProxyIconUrl); + } + public static API.EmbedFooter ToModel(this EmbedFooter entity) + { + return new API.EmbedFooter { Text = entity.Text, IconUrl = entity.IconUrl }; + } + public static EmbedImage ToEntity(this API.EmbedImage model) + { + return new EmbedImage(model.Url, model.ProxyUrl, + model.Height.IsSpecified ? model.Height.Value : (int?)null, + model.Width.IsSpecified ? model.Width.Value : (int?)null); + } + public static API.EmbedImage ToModel(this EmbedImage entity) + { + return new API.EmbedImage { Url = entity.Url }; + } + public static EmbedProvider ToEntity(this API.EmbedProvider model) + { + return new EmbedProvider(model.Name, model.Url); + } + public static API.EmbedProvider ToModel(this EmbedProvider entity) + { + return new API.EmbedProvider { Name = entity.Name, Url = entity.Url }; + } + public static EmbedThumbnail ToEntity(this API.EmbedThumbnail model) + { + return new EmbedThumbnail(model.Url, model.ProxyUrl, + model.Height.IsSpecified ? model.Height.Value : (int?)null, + model.Width.IsSpecified ? model.Width.Value : (int?)null); + } + public static API.EmbedThumbnail ToModel(this EmbedThumbnail entity) + { + return new API.EmbedThumbnail { Url = entity.Url }; + } + public static EmbedVideo ToEntity(this API.EmbedVideo model) + { + return new EmbedVideo(model.Url, + model.Height.IsSpecified ? model.Height.Value : (int?)null, + model.Width.IsSpecified ? model.Width.Value : (int?)null); + } + public static API.EmbedVideo ToModel(this EmbedVideo entity) + { + return new API.EmbedVideo { Url = entity.Url }; + } + + public static API.Image ToModel(this Image entity) + { + return new API.Image(entity.Stream); + } + + public static Overwrite ToEntity(this API.Overwrite model) + { + return new Overwrite(model.TargetId, model.TargetType, new OverwritePermissions(model.Allow, model.Deny)); + } + } +} diff --git a/src/Discord.Net.Core/Net/Converters/ArrayConverter.cs b/src/Discord.Net.Rest/Net/Converters/ArrayConverter.cs similarity index 100% rename from src/Discord.Net.Core/Net/Converters/ArrayConverter.cs rename to src/Discord.Net.Rest/Net/Converters/ArrayConverter.cs diff --git a/src/Discord.Net.Core/Net/Converters/DiscordContractResolver.cs b/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs similarity index 65% rename from src/Discord.Net.Core/Net/Converters/DiscordContractResolver.cs rename to src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs index 3357932d4..104b913da 100644 --- a/src/Discord.Net.Core/Net/Converters/DiscordContractResolver.cs +++ b/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs @@ -22,26 +22,7 @@ namespace Discord.Net.Converters var propInfo = member as PropertyInfo; if (propInfo != null) { - JsonConverter converter; - Type type = propInfo.PropertyType; - Type genericType = type.IsConstructedGenericType ? type.GetGenericTypeDefinition() : null; - - if (genericType == typeof(Optional<>)) - { - var typeInput = propInfo.DeclaringType; - var innerTypeOutput = type.GenericTypeArguments[0]; - - var getter = typeof(Func<,>).MakeGenericType(typeInput, type); - var getterDelegate = propInfo.GetMethod.CreateDelegate(getter); - var shouldSerialize = _shouldSerialize.MakeGenericMethod(typeInput, innerTypeOutput); - var shouldSerializeDelegate = (Func)shouldSerialize.CreateDelegate(typeof(Func)); - property.ShouldSerialize = x => shouldSerializeDelegate(x, getterDelegate); - - converter = MakeGenericConverter(propInfo, typeof(OptionalConverter<>), innerTypeOutput); - } - else - converter = GetConverter(propInfo, type); - + var converter = GetConverter(property, propInfo, propInfo.PropertyType, 0); if (converter != null) { property.Converter = converter; @@ -53,26 +34,38 @@ namespace Discord.Net.Converters return property; } - private static JsonConverter GetConverter(PropertyInfo propInfo, Type type, TypeInfo typeInfo = null, int depth = 0) + private static JsonConverter GetConverter(JsonProperty property, PropertyInfo propInfo, Type type, int depth) { if (type.IsArray) - return MakeGenericConverter(propInfo, typeof(ArrayConverter<>), type.GetElementType()); + return MakeGenericConverter(property, propInfo, typeof(ArrayConverter<>), type.GetElementType(), depth); if (type.IsConstructedGenericType) { Type genericType = type.GetGenericTypeDefinition(); - if (genericType == typeof(EntityOrId<>)) - return MakeGenericConverter(propInfo, typeof(UInt64EntityOrIdConverter<>), type.GenericTypeArguments[0]); - } + if (depth == 0 && genericType == typeof(Optional<>)) + { + var typeInput = propInfo.DeclaringType; + var innerTypeOutput = type.GenericTypeArguments[0]; - bool hasInt53 = propInfo.GetCustomAttribute() != null; + var getter = typeof(Func<,>).MakeGenericType(typeInput, type); + var getterDelegate = propInfo.GetMethod.CreateDelegate(getter); + var shouldSerialize = _shouldSerialize.MakeGenericMethod(typeInput, innerTypeOutput); + var shouldSerializeDelegate = (Func)shouldSerialize.CreateDelegate(typeof(Func)); + property.ShouldSerialize = x => shouldSerializeDelegate(x, getterDelegate); + + return MakeGenericConverter(property, propInfo, typeof(OptionalConverter<>), innerTypeOutput, depth); + } + else if (genericType == typeof(Nullable<>)) + return MakeGenericConverter(property, propInfo, typeof(NullableConverter<>), type.GenericTypeArguments[0], depth); + else if (genericType == typeof(EntityOrId<>)) + return MakeGenericConverter(property, propInfo, typeof(UInt64EntityOrIdConverter<>), type.GenericTypeArguments[0], depth); + } //Primitives + bool hasInt53 = propInfo.GetCustomAttribute() != null; if (!hasInt53) { if (type == typeof(ulong)) return UInt64Converter.Instance; - if (type == typeof(ulong?)) - return NullableUInt64Converter.Instance; } //Enums @@ -82,12 +75,11 @@ namespace Discord.Net.Converters return UserStatusConverter.Instance; //Special - if (type == typeof(Image)) - return ImageConverter.Instance; - - if (typeInfo == null) typeInfo = type.GetTypeInfo(); + if (type == typeof(API.Image)) + return ImageConverter.Instance; //Entities + var typeInfo = type.GetTypeInfo(); if (typeInfo.ImplementedInterfaces.Any(x => x == typeof(IEntity))) return UInt64EntityConverter.Instance; if (typeInfo.ImplementedInterfaces.Any(x => x == typeof(IEntity))) @@ -101,18 +93,11 @@ namespace Discord.Net.Converters return (getter as Func>)((TOwner)owner).IsSpecified; } - private static JsonConverter MakeGenericConverter(PropertyInfo propInfo, Type converterType, Type innerType) + private static JsonConverter MakeGenericConverter(JsonProperty property, PropertyInfo propInfo, Type converterType, Type innerType, int depth) { var genericType = converterType.MakeGenericType(innerType).GetTypeInfo(); - //var instanceField = genericType.GetDeclaredField("Instance"); - //var converter = instanceField.GetValue(null) as JsonConverter; - //if (converter == null) - //{ - var innerConverter = GetConverter(propInfo, innerType); - var converter = genericType.DeclaredConstructors.First().Invoke(new object[] { innerConverter }) as JsonConverter; - //instanceField.SetValue(null, converter); - //} - return converter; + var innerConverter = GetConverter(property, propInfo, innerType, depth + 1); + return genericType.DeclaredConstructors.First().Invoke(new object[] { innerConverter }) as JsonConverter; } } } diff --git a/src/Discord.Net.Core/Net/Converters/ImageConverter.cs b/src/Discord.Net.Rest/Net/Converters/ImageConverter.cs similarity index 91% rename from src/Discord.Net.Core/Net/Converters/ImageConverter.cs rename to src/Discord.Net.Rest/Net/Converters/ImageConverter.cs index 79e8c984d..f4d591d7e 100644 --- a/src/Discord.Net.Core/Net/Converters/ImageConverter.cs +++ b/src/Discord.Net.Rest/Net/Converters/ImageConverter.cs @@ -1,6 +1,6 @@ -using Discord.API; -using Newtonsoft.Json; +using Newtonsoft.Json; using System; +using Model = Discord.API.Image; namespace Discord.Net.Converters { @@ -19,7 +19,7 @@ namespace Discord.Net.Converters public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { - var image = (Image)value; + var image = (Model)value; if (image.Stream != null) { diff --git a/src/Discord.Net.Rest/Net/Converters/NullableConverter.cs b/src/Discord.Net.Rest/Net/Converters/NullableConverter.cs new file mode 100644 index 000000000..0b149e725 --- /dev/null +++ b/src/Discord.Net.Rest/Net/Converters/NullableConverter.cs @@ -0,0 +1,50 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.Net.Converters +{ + internal class NullableConverter : JsonConverter + where T : struct + { + private readonly JsonConverter _innerConverter; + + public override bool CanConvert(Type objectType) => true; + public override bool CanRead => true; + public override bool CanWrite => true; + + public NullableConverter(JsonConverter innerConverter) + { + _innerConverter = innerConverter; + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + object value = reader.Value; + if (value == null) + return null; + else + { + T obj; + if (_innerConverter != null) + obj = (T)_innerConverter.ReadJson(reader, typeof(T), null, serializer); + else + obj = serializer.Deserialize(reader); + return obj; + } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value == null) + writer.WriteNull(); + else + { + var nullable = (T?)value; + if (_innerConverter != null) + _innerConverter.WriteJson(writer, nullable.Value, serializer); + else + serializer.Serialize(writer, nullable.Value, typeof(T)); + } + } + } +} diff --git a/src/Discord.Net.Core/Net/Converters/OptionalConverter.cs b/src/Discord.Net.Rest/Net/Converters/OptionalConverter.cs similarity index 100% rename from src/Discord.Net.Core/Net/Converters/OptionalConverter.cs rename to src/Discord.Net.Rest/Net/Converters/OptionalConverter.cs diff --git a/src/Discord.Net.Core/Net/Converters/PermissionTargetConverter.cs b/src/Discord.Net.Rest/Net/Converters/PermissionTargetConverter.cs similarity index 100% rename from src/Discord.Net.Core/Net/Converters/PermissionTargetConverter.cs rename to src/Discord.Net.Rest/Net/Converters/PermissionTargetConverter.cs diff --git a/src/Discord.Net.Core/Net/Converters/StringEntityConverter.cs b/src/Discord.Net.Rest/Net/Converters/StringEntityConverter.cs similarity index 100% rename from src/Discord.Net.Core/Net/Converters/StringEntityConverter.cs rename to src/Discord.Net.Rest/Net/Converters/StringEntityConverter.cs diff --git a/src/Discord.Net.Core/Net/Converters/UInt64Converter.cs b/src/Discord.Net.Rest/Net/Converters/UInt64Converter.cs similarity index 100% rename from src/Discord.Net.Core/Net/Converters/UInt64Converter.cs rename to src/Discord.Net.Rest/Net/Converters/UInt64Converter.cs diff --git a/src/Discord.Net.Core/Net/Converters/UInt64EntityConverter.cs b/src/Discord.Net.Rest/Net/Converters/UInt64EntityConverter.cs similarity index 100% rename from src/Discord.Net.Core/Net/Converters/UInt64EntityConverter.cs rename to src/Discord.Net.Rest/Net/Converters/UInt64EntityConverter.cs diff --git a/src/Discord.Net.Core/Net/Converters/UInt64EntityOrIdConverter.cs b/src/Discord.Net.Rest/Net/Converters/UInt64EntityOrIdConverter.cs similarity index 100% rename from src/Discord.Net.Core/Net/Converters/UInt64EntityOrIdConverter.cs rename to src/Discord.Net.Rest/Net/Converters/UInt64EntityOrIdConverter.cs diff --git a/src/Discord.Net.Core/Net/Converters/UserStatusConverter.cs b/src/Discord.Net.Rest/Net/Converters/UserStatusConverter.cs similarity index 100% rename from src/Discord.Net.Core/Net/Converters/UserStatusConverter.cs rename to src/Discord.Net.Rest/Net/Converters/UserStatusConverter.cs diff --git a/src/Discord.Net.Core/Net/Rest/DefaultRestClient.cs b/src/Discord.Net.Rest/Net/DefaultRestClient.cs similarity index 98% rename from src/Discord.Net.Core/Net/Rest/DefaultRestClient.cs rename to src/Discord.Net.Rest/Net/DefaultRestClient.cs index 588785230..39b94294f 100644 --- a/src/Discord.Net.Core/Net/Rest/DefaultRestClient.cs +++ b/src/Discord.Net.Rest/Net/DefaultRestClient.cs @@ -12,7 +12,7 @@ using System.Threading.Tasks; namespace Discord.Net.Rest { - public sealed class DefaultRestClient : IRestClient + internal sealed class DefaultRestClient : IRestClient, IDisposable { private const int HR_SECURECHANNELFAILED = -2146233079; diff --git a/src/Discord.Net.Rest/Net/DefaultRestClientProvider.cs b/src/Discord.Net.Rest/Net/DefaultRestClientProvider.cs new file mode 100644 index 000000000..311a53562 --- /dev/null +++ b/src/Discord.Net.Rest/Net/DefaultRestClientProvider.cs @@ -0,0 +1,19 @@ +using System; + +namespace Discord.Net.Rest +{ + public static class DefaultRestClientProvider + { + public static readonly RestClientProvider Instance = url => + { + try + { + return new DefaultRestClient(url); + } + catch (PlatformNotSupportedException ex) + { + throw new PlatformNotSupportedException("The default RestClientProvider is not supported on this platform.", ex); + } + }; + } +} diff --git a/src/Discord.Net.Core/Net/Queue/ClientBucket.cs b/src/Discord.Net.Rest/Net/Queue/ClientBucket.cs similarity index 100% rename from src/Discord.Net.Core/Net/Queue/ClientBucket.cs rename to src/Discord.Net.Rest/Net/Queue/ClientBucket.cs diff --git a/src/Discord.Net.Core/Net/Queue/RequestQueue.cs b/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs similarity index 90% rename from src/Discord.Net.Core/Net/Queue/RequestQueue.cs rename to src/Discord.Net.Rest/Net/Queue/RequestQueue.cs index 1ea586481..fce7e3e1b 100644 --- a/src/Discord.Net.Core/Net/Queue/RequestQueue.cs +++ b/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Concurrent; +#if DEBUG_LIMITS using System.Diagnostics; +#endif using System.IO; using System.Linq; using System.Threading; @@ -8,7 +10,7 @@ using System.Threading.Tasks; namespace Discord.Net.Queue { - public class RequestQueue : IDisposable + internal class RequestQueue : IDisposable { public event Func RateLimitTriggered; @@ -63,7 +65,11 @@ namespace Discord.Net.Queue public async Task SendAsync(RestRequest request) { - request.CancelToken = _requestCancelToken; + if (request.Options.CancelToken.CanBeCanceled) + request.Options.CancelToken = CancellationTokenSource.CreateLinkedTokenSource(_requestCancelToken, request.Options.CancelToken).Token; + else + request.Options.CancelToken = _requestCancelToken; + var bucket = GetOrCreateBucket(request.Options.BucketId, request); return await bucket.SendAsync(request).ConfigureAwait(false); } @@ -85,9 +91,9 @@ namespace Discord.Net.Queue await Task.Delay(millis).ConfigureAwait(false); } } - internal void PauseGlobal(RateLimitInfo info, TimeSpan lag) + internal void PauseGlobal(RateLimitInfo info) { - _waitUntil = DateTimeOffset.UtcNow.AddMilliseconds(info.RetryAfter.Value + lag.TotalMilliseconds); + _waitUntil = DateTimeOffset.UtcNow.AddMilliseconds(info.RetryAfter.Value + (info.Lag?.TotalMilliseconds ?? 0.0)); } private RequestBucket GetOrCreateBucket(string id, RestRequest request) diff --git a/src/Discord.Net.Core/Net/Queue/RequestQueueBucket.cs b/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs similarity index 83% rename from src/Discord.Net.Core/Net/Queue/RequestQueueBucket.cs rename to src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs index 78a8e63cb..2cc4b8a10 100644 --- a/src/Discord.Net.Core/Net/Queue/RequestQueueBucket.cs +++ b/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs @@ -1,5 +1,4 @@ -using Discord.Net.Rest; -using Newtonsoft.Json; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; #if DEBUG_LIMITS @@ -55,12 +54,10 @@ namespace Discord.Net.Queue #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Sending..."); #endif - TimeSpan lag = default(TimeSpan); RateLimitInfo info = default(RateLimitInfo); try { var response = await request.SendAsync().ConfigureAwait(false); - lag = DateTimeOffset.UtcNow - DateTimeOffset.Parse(response.Headers["Date"]); info = new RateLimitInfo(response.Headers); if (response.StatusCode < (HttpStatusCode)200 || response.StatusCode >= (HttpStatusCode)300) @@ -73,14 +70,14 @@ namespace Discord.Net.Queue #if DEBUG_LIMITS Debug.WriteLine($"[{id}] (!) 429 [Global]"); #endif - _queue.PauseGlobal(info, lag); + _queue.PauseGlobal(info); } else { #if DEBUG_LIMITS Debug.WriteLine($"[{id}] (!) 429"); #endif - UpdateRateLimit(id, request, info, lag, true); + UpdateRateLimit(id, request, info, true); } await _queue.RaiseRateLimitTriggered(Id, info).ConfigureAwait(false); continue; //Retry @@ -88,8 +85,12 @@ namespace Discord.Net.Queue #if DEBUG_LIMITS Debug.WriteLine($"[{id}] (!) 502"); #endif - continue; //Continue + if ((request.Options.RetryMode & RetryMode.Retry502) == 0) + throw new HttpException(HttpStatusCode.BadGateway, null); + + continue; //Retry default: + int? code = null; string reason = null; if (response.Stream != null) { @@ -99,12 +100,13 @@ namespace Discord.Net.Queue using (var jsonReader = new JsonTextReader(reader)) { var json = JToken.Load(jsonReader); - reason = json.Value("message"); + try { code = json.Value("code"); } catch { }; + try { reason = json.Value("message"); } catch { }; } } catch { } } - throw new HttpException(response.StatusCode, reason); + throw new HttpException(response.StatusCode, code, reason); } } else @@ -115,16 +117,32 @@ namespace Discord.Net.Queue return response.Stream; } } + //catch (HttpException) { throw; } //Pass through + catch (TimeoutException) + { #if DEBUG_LIMITS - catch + Debug.WriteLine($"[{id}] Timeout"); +#endif + if ((request.Options.RetryMode & RetryMode.RetryTimeouts) == 0) + throw; + + await Task.Delay(500); + continue; //Retry + } + /*catch (Exception) { +#if DEBUG_LIMITS Debug.WriteLine($"[{id}] Error"); - throw; - } #endif + if ((request.Options.RetryMode & RetryMode.RetryErrors) == 0) + throw; + + await Task.Delay(500); + continue; //Retry + }*/ finally { - UpdateRateLimit(id, request, info, lag, false); + UpdateRateLimit(id, request, info, false); #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Stop"); #endif @@ -140,7 +158,7 @@ namespace Discord.Net.Queue while (true) { - if (DateTimeOffset.UtcNow > request.TimeoutAt || request.CancelToken.IsCancellationRequested) + if (DateTimeOffset.UtcNow > request.TimeoutAt || request.Options.CancelToken.IsCancellationRequested) { if (!isRateLimited) throw new TimeoutException(); @@ -162,6 +180,10 @@ namespace Discord.Net.Queue isRateLimited = true; await _queue.RaiseRateLimitTriggered(Id, null).ConfigureAwait(false); } + + if ((request.Options.RetryMode & RetryMode.RetryRatelimit) == 0) + throw new RateLimitedException(); + if (resetAt.HasValue) { if (resetAt > timeoutAt) @@ -171,7 +193,7 @@ namespace Discord.Net.Queue Debug.WriteLine($"[{id}] Sleeping {millis} ms (Pre-emptive)"); #endif if (millis > 0) - await Task.Delay(millis, request.CancelToken).ConfigureAwait(false); + await Task.Delay(millis, request.Options.CancelToken).ConfigureAwait(false); } else { @@ -180,7 +202,7 @@ namespace Discord.Net.Queue #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Sleeping 500* ms (Pre-emptive)"); #endif - await Task.Delay(500, request.CancelToken).ConfigureAwait(false); + await Task.Delay(500, request.Options.CancelToken).ConfigureAwait(false); } continue; } @@ -192,7 +214,7 @@ namespace Discord.Net.Queue } } - private void UpdateRateLimit(int id, RestRequest request, RateLimitInfo info, TimeSpan lag, bool is429) + private void UpdateRateLimit(int id, RestRequest request, RateLimitInfo info, bool is429) { if (WindowCount == 0) return; @@ -209,7 +231,7 @@ namespace Discord.Net.Queue #endif } - var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + var now = DateTimeUtils.ToUnixSeconds(DateTimeOffset.UtcNow); DateTimeOffset? resetTick = null; //Using X-RateLimit-Remaining causes a race condition @@ -228,10 +250,10 @@ namespace Discord.Net.Queue } else if (info.Reset.HasValue) { - resetTick = info.Reset.Value.AddSeconds(/*1.0 +*/ lag.TotalSeconds); + resetTick = info.Reset.Value.AddSeconds(info.Lag?.TotalSeconds ?? 1.0); int diff = (int)(resetTick.Value - DateTimeOffset.UtcNow).TotalMilliseconds; #if DEBUG_LIMITS - Debug.WriteLine($"[{id}] X-RateLimit-Reset: {info.Reset.Value.ToUnixTimeSeconds()} ({diff} ms, {lag.TotalMilliseconds} ms lag)"); + Debug.WriteLine($"[{id}] X-RateLimit-Reset: {info.Reset.Value.ToUnixTimeSeconds()} ({diff} ms, {info.Lag?.TotalMilliseconds} ms lag)"); #endif } else if (request.Options.IsClientBucket && request.Options.BucketId != null) diff --git a/src/Discord.Net.Core/Net/Queue/Requests/JsonRestRequest.cs b/src/Discord.Net.Rest/Net/Queue/Requests/JsonRestRequest.cs similarity index 89% rename from src/Discord.Net.Core/Net/Queue/Requests/JsonRestRequest.cs rename to src/Discord.Net.Rest/Net/Queue/Requests/JsonRestRequest.cs index 75869d52a..83c5e0eb5 100644 --- a/src/Discord.Net.Core/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, CancelToken, Options.HeaderOnly).ConfigureAwait(false); + return await Client.SendAsync(Method, Endpoint, Json, Options.CancelToken, Options.HeaderOnly).ConfigureAwait(false); } } } diff --git a/src/Discord.Net.Core/Net/Queue/Requests/MultipartRestRequest.cs b/src/Discord.Net.Rest/Net/Queue/Requests/MultipartRestRequest.cs similarity index 90% rename from src/Discord.Net.Core/Net/Queue/Requests/MultipartRestRequest.cs rename to src/Discord.Net.Rest/Net/Queue/Requests/MultipartRestRequest.cs index d132ef395..424a5325e 100644 --- a/src/Discord.Net.Core/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, CancelToken, Options.HeaderOnly).ConfigureAwait(false); + return await Client.SendAsync(Method, Endpoint, MultipartParams, Options.CancelToken, Options.HeaderOnly).ConfigureAwait(false); } } } diff --git a/src/Discord.Net.Core/Net/Queue/Requests/RestRequest.cs b/src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs similarity index 79% rename from src/Discord.Net.Core/Net/Queue/Requests/RestRequest.cs rename to src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs index 5d5bc1e59..7f358e786 100644 --- a/src/Discord.Net.Core/Net/Queue/Requests/RestRequest.cs +++ b/src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs @@ -1,7 +1,6 @@ using Discord.Net.Rest; using System; using System.IO; -using System.Threading; using System.Threading.Tasks; namespace Discord.Net.Queue @@ -14,7 +13,6 @@ namespace Discord.Net.Queue public DateTimeOffset? TimeoutAt { get; } public TaskCompletionSource Promise { get; } public RequestOptions Options { get; } - public CancellationToken CancelToken { get; internal set; } public RestRequest(IRestClient client, string method, string endpoint, RequestOptions options) { @@ -24,14 +22,13 @@ namespace Discord.Net.Queue Method = method; Endpoint = endpoint; Options = options; - CancelToken = CancellationToken.None; TimeoutAt = options.Timeout.HasValue ? DateTimeOffset.UtcNow.AddMilliseconds(options.Timeout.Value) : (DateTimeOffset?)null; Promise = new TaskCompletionSource(); } public virtual async Task SendAsync() { - return await Client.SendAsync(Method, Endpoint, CancelToken, Options.HeaderOnly).ConfigureAwait(false); + return await Client.SendAsync(Method, Endpoint, Options.CancelToken, Options.HeaderOnly).ConfigureAwait(false); } } } diff --git a/src/Discord.Net.Core/Net/Queue/Requests/WebSocketRequest.cs b/src/Discord.Net.Rest/Net/Queue/Requests/WebSocketRequest.cs similarity index 100% rename from src/Discord.Net.Core/Net/Queue/Requests/WebSocketRequest.cs rename to src/Discord.Net.Rest/Net/Queue/Requests/WebSocketRequest.cs diff --git a/src/Discord.Net.Core/Net/RateLimitInfo.cs b/src/Discord.Net.Rest/Net/RateLimitInfo.cs similarity index 73% rename from src/Discord.Net.Core/Net/RateLimitInfo.cs rename to src/Discord.Net.Rest/Net/RateLimitInfo.cs index 2c2faccf8..d8d168aec 100644 --- a/src/Discord.Net.Core/Net/RateLimitInfo.cs +++ b/src/Discord.Net.Rest/Net/RateLimitInfo.cs @@ -3,13 +3,14 @@ using System.Collections.Generic; namespace Discord.Net { - public struct RateLimitInfo + internal struct RateLimitInfo { public bool IsGlobal { get; } public int? Limit { get; } public int? Remaining { get; } public int? RetryAfter { get; } public DateTimeOffset? Reset { get; } + public TimeSpan? Lag { get; } internal RateLimitInfo(Dictionary headers) { @@ -17,8 +18,11 @@ namespace Discord.Net 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) ? DateTimeOffset.FromUnixTimeSeconds(int.Parse(temp)) : (DateTimeOffset?)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; } } } diff --git a/src/Discord.Net.Rest/Utils/TypingNotifier.cs b/src/Discord.Net.Rest/Utils/TypingNotifier.cs index 45b715a76..b4bd2f44b 100644 --- a/src/Discord.Net.Rest/Utils/TypingNotifier.cs +++ b/src/Discord.Net.Rest/Utils/TypingNotifier.cs @@ -32,7 +32,7 @@ namespace Discord.Rest await _channel.TriggerTypingAsync(_options).ConfigureAwait(false); } catch { } - await Task.Delay(9750, token).ConfigureAwait(false); + await Task.Delay(9500, token).ConfigureAwait(false); } } catch (OperationCanceledException) { } diff --git a/src/Discord.Net.Rest/project.json b/src/Discord.Net.Rest/project.json deleted file mode 100644 index b1ad15cb8..000000000 --- a/src/Discord.Net.Rest/project.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "version": "1.0.0-*", - "description": "A core Discord.Net library containing the REST client and models.", - "authors": [ "RogueException" ], - - "packOptions": { - "tags": [ "discord", "discordapp" ], - "licenseUrl": "http://opensource.org/licenses/MIT", - "projectUrl": "https://github.com/RogueException/Discord.Net", - "repository": { - "type": "git", - "url": "git://github.com/RogueException/Discord.Net" - } - }, - - "configurations": { - "Release": { - "buildOptions": { - "define": [ "RELEASE" ], - "nowarn": [ "CS1573", "CS1591" ], - "optimize": true, - "warningsAsErrors": true, - "xmlDoc": true - } - } - }, - - "dependencies": { - "Discord.Net.Core": { - "target": "project" - }, - "System.IO.FileSystem": "4.3.0" - }, - - "frameworks": { - "netstandard1.3": { - "imports": [ - "dotnet5.4", - "dnxcore50", - "portable-net45+win8" - ] - } - } -} \ No newline at end of file diff --git a/src/Discord.Net.Rpc/API/Rpc/AuthenticateParams.cs b/src/Discord.Net.Rpc/API/Rpc/AuthenticateParams.cs index 9c35c5ede..f4b69a3b7 100644 --- a/src/Discord.Net.Rpc/API/Rpc/AuthenticateParams.cs +++ b/src/Discord.Net.Rpc/API/Rpc/AuthenticateParams.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Rpc { - public class AuthenticateParams + internal class AuthenticateParams { [JsonProperty("access_token")] public string AccessToken { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/AuthenticateResponse.cs b/src/Discord.Net.Rpc/API/Rpc/AuthenticateResponse.cs index 244577f84..6c6cba957 100644 --- a/src/Discord.Net.Rpc/API/Rpc/AuthenticateResponse.cs +++ b/src/Discord.Net.Rpc/API/Rpc/AuthenticateResponse.cs @@ -4,7 +4,7 @@ using System; namespace Discord.API.Rpc { - public class AuthenticateResponse + internal class AuthenticateResponse { [JsonProperty("application")] public Application Application { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/AuthorizeParams.cs b/src/Discord.Net.Rpc/API/Rpc/AuthorizeParams.cs index 367aafd41..91678d97c 100644 --- a/src/Discord.Net.Rpc/API/Rpc/AuthorizeParams.cs +++ b/src/Discord.Net.Rpc/API/Rpc/AuthorizeParams.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; namespace Discord.API.Rpc { - public class AuthorizeParams + internal class AuthorizeParams { [JsonProperty("client_id")] public string ClientId { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/AuthorizeResponse.cs b/src/Discord.Net.Rpc/API/Rpc/AuthorizeResponse.cs index a4f42b6f5..42a9138fe 100644 --- a/src/Discord.Net.Rpc/API/Rpc/AuthorizeResponse.cs +++ b/src/Discord.Net.Rpc/API/Rpc/AuthorizeResponse.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Rpc { - public class AuthorizeResponse + internal class AuthorizeResponse { [JsonProperty("code")] public string Code { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/Channel.cs b/src/Discord.Net.Rpc/API/Rpc/Channel.cs index 1b8f3775c..0fc7ac0ee 100644 --- a/src/Discord.Net.Rpc/API/Rpc/Channel.cs +++ b/src/Discord.Net.Rpc/API/Rpc/Channel.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Rpc { - public class Channel + internal class Channel { //Shared [JsonProperty("id")] diff --git a/src/Discord.Net.Rpc/API/Rpc/ChannelSubscriptionParams.cs b/src/Discord.Net.Rpc/API/Rpc/ChannelSubscriptionParams.cs index 70d05e285..7443a6f7d 100644 --- a/src/Discord.Net.Rpc/API/Rpc/ChannelSubscriptionParams.cs +++ b/src/Discord.Net.Rpc/API/Rpc/ChannelSubscriptionParams.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Rpc { - public class ChannelSubscriptionParams + internal class ChannelSubscriptionParams { [JsonProperty("channel_id")] public ulong ChannelId { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/ChannelSummary.cs b/src/Discord.Net.Rpc/API/Rpc/ChannelSummary.cs index 34acd049b..43e59c1a1 100644 --- a/src/Discord.Net.Rpc/API/Rpc/ChannelSummary.cs +++ b/src/Discord.Net.Rpc/API/Rpc/ChannelSummary.cs @@ -2,7 +2,7 @@ namespace Discord.API.Rpc { - public class ChannelSummary + internal class ChannelSummary { [JsonProperty("id")] public ulong Id { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/ErrorEvent.cs b/src/Discord.Net.Rpc/API/Rpc/ErrorEvent.cs index 2a3cf6506..c59275154 100644 --- a/src/Discord.Net.Rpc/API/Rpc/ErrorEvent.cs +++ b/src/Discord.Net.Rpc/API/Rpc/ErrorEvent.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Rpc { - public class ErrorEvent + internal class ErrorEvent { [JsonProperty("code")] public int Code { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/ExtendedVoiceState.cs b/src/Discord.Net.Rpc/API/Rpc/ExtendedVoiceState.cs index 032914f0f..6722d3a29 100644 --- a/src/Discord.Net.Rpc/API/Rpc/ExtendedVoiceState.cs +++ b/src/Discord.Net.Rpc/API/Rpc/ExtendedVoiceState.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Rpc { - public class ExtendedVoiceState + internal class ExtendedVoiceState { [JsonProperty("user")] public User User { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/GetChannelParams.cs b/src/Discord.Net.Rpc/API/Rpc/GetChannelParams.cs index a94c8740e..4c0e18600 100644 --- a/src/Discord.Net.Rpc/API/Rpc/GetChannelParams.cs +++ b/src/Discord.Net.Rpc/API/Rpc/GetChannelParams.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Rpc { - public class GetChannelParams + internal class GetChannelParams { [JsonProperty("channel_id")] public ulong ChannelId { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/GetChannelsParams.cs b/src/Discord.Net.Rpc/API/Rpc/GetChannelsParams.cs index d2d4409e6..61e4886ef 100644 --- a/src/Discord.Net.Rpc/API/Rpc/GetChannelsParams.cs +++ b/src/Discord.Net.Rpc/API/Rpc/GetChannelsParams.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Rpc { - public class GetChannelsParams + internal class GetChannelsParams { [JsonProperty("guild_id")] public ulong GuildId { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/GetChannelsResponse.cs b/src/Discord.Net.Rpc/API/Rpc/GetChannelsResponse.cs index e105341a1..004da31b5 100644 --- a/src/Discord.Net.Rpc/API/Rpc/GetChannelsResponse.cs +++ b/src/Discord.Net.Rpc/API/Rpc/GetChannelsResponse.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; namespace Discord.API.Rpc { - public class GetChannelsResponse + internal class GetChannelsResponse { [JsonProperty("channels")] public IReadOnlyCollection Channels { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/GetGuildParams.cs b/src/Discord.Net.Rpc/API/Rpc/GetGuildParams.cs index 2fd8e4152..54d5018d0 100644 --- a/src/Discord.Net.Rpc/API/Rpc/GetGuildParams.cs +++ b/src/Discord.Net.Rpc/API/Rpc/GetGuildParams.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Rpc { - public class GetGuildParams + internal class GetGuildParams { [JsonProperty("guild_id")] public ulong GuildId { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/GetGuildsParams.cs b/src/Discord.Net.Rpc/API/Rpc/GetGuildsParams.cs index a1ff5f210..b4350ea21 100644 --- a/src/Discord.Net.Rpc/API/Rpc/GetGuildsParams.cs +++ b/src/Discord.Net.Rpc/API/Rpc/GetGuildsParams.cs @@ -2,7 +2,7 @@ namespace Discord.API.Rpc { - public class GetGuildsParams + internal class GetGuildsParams { } } diff --git a/src/Discord.Net.Rpc/API/Rpc/GetGuildsResponse.cs b/src/Discord.Net.Rpc/API/Rpc/GetGuildsResponse.cs index e69bedeae..4d57ae491 100644 --- a/src/Discord.Net.Rpc/API/Rpc/GetGuildsResponse.cs +++ b/src/Discord.Net.Rpc/API/Rpc/GetGuildsResponse.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Rpc { - public class GetGuildsResponse + internal class GetGuildsResponse { [JsonProperty("guilds")] public GuildSummary[] Guilds { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/Guild.cs b/src/Discord.Net.Rpc/API/Rpc/Guild.cs index 1d6bf3678..fde5ef2ee 100644 --- a/src/Discord.Net.Rpc/API/Rpc/Guild.cs +++ b/src/Discord.Net.Rpc/API/Rpc/Guild.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; namespace Discord.API.Rpc { - public class Guild + internal class Guild { [JsonProperty("id")] public ulong Id { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/GuildMember.cs b/src/Discord.Net.Rpc/API/Rpc/GuildMember.cs index af74dd919..be8fba9dc 100644 --- a/src/Discord.Net.Rpc/API/Rpc/GuildMember.cs +++ b/src/Discord.Net.Rpc/API/Rpc/GuildMember.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Rpc { - public class GuildMember + internal class GuildMember { [JsonProperty("user")] public User User { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/GuildStatusEvent.cs b/src/Discord.Net.Rpc/API/Rpc/GuildStatusEvent.cs index 03326c374..3cfbf3454 100644 --- a/src/Discord.Net.Rpc/API/Rpc/GuildStatusEvent.cs +++ b/src/Discord.Net.Rpc/API/Rpc/GuildStatusEvent.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Rpc { - public class GuildStatusEvent + internal class GuildStatusEvent { [JsonProperty("guild")] public Guild Guild { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/GuildSubscriptionParams.cs b/src/Discord.Net.Rpc/API/Rpc/GuildSubscriptionParams.cs index 6ad108a5c..a34c71023 100644 --- a/src/Discord.Net.Rpc/API/Rpc/GuildSubscriptionParams.cs +++ b/src/Discord.Net.Rpc/API/Rpc/GuildSubscriptionParams.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Rpc { - public class GuildSubscriptionParams + internal class GuildSubscriptionParams { [JsonProperty("guild_id")] public ulong GuildId { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/GuildSummary.cs b/src/Discord.Net.Rpc/API/Rpc/GuildSummary.cs index c36da5267..09928e16e 100644 --- a/src/Discord.Net.Rpc/API/Rpc/GuildSummary.cs +++ b/src/Discord.Net.Rpc/API/Rpc/GuildSummary.cs @@ -2,7 +2,7 @@ namespace Discord.API.Rpc { - public class GuildSummary + internal class GuildSummary { [JsonProperty("id")] public ulong Id { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/Message.cs b/src/Discord.Net.Rpc/API/Rpc/Message.cs index a72fba123..6cbd364bb 100644 --- a/src/Discord.Net.Rpc/API/Rpc/Message.cs +++ b/src/Discord.Net.Rpc/API/Rpc/Message.cs @@ -2,7 +2,7 @@ namespace Discord.API.Rpc { - public class Message : Discord.API.Message + internal class Message : Discord.API.Message { [JsonProperty("blocked")] public Optional IsBlocked { get; } diff --git a/src/Discord.Net.Rpc/API/Rpc/MessageEvent.cs b/src/Discord.Net.Rpc/API/Rpc/MessageEvent.cs index 41ff13288..4d656d5e3 100644 --- a/src/Discord.Net.Rpc/API/Rpc/MessageEvent.cs +++ b/src/Discord.Net.Rpc/API/Rpc/MessageEvent.cs @@ -2,7 +2,7 @@ using Newtonsoft.Json; namespace Discord.API.Rpc { - public class MessageEvent + internal class MessageEvent { [JsonProperty("channel_id")] public ulong ChannelId { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/Pan.cs b/src/Discord.Net.Rpc/API/Rpc/Pan.cs index e2a97c369..dc9cbef0a 100644 --- a/src/Discord.Net.Rpc/API/Rpc/Pan.cs +++ b/src/Discord.Net.Rpc/API/Rpc/Pan.cs @@ -2,7 +2,7 @@ namespace Discord.API.Rpc { - public class Pan + internal class Pan { [JsonProperty("left")] public float Left { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/ReadyEvent.cs b/src/Discord.Net.Rpc/API/Rpc/ReadyEvent.cs index 4cd8c9677..8de69405f 100644 --- a/src/Discord.Net.Rpc/API/Rpc/ReadyEvent.cs +++ b/src/Discord.Net.Rpc/API/Rpc/ReadyEvent.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Rpc { - public class ReadyEvent + internal class ReadyEvent { [JsonProperty("v")] public int Version { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/RpcConfig.cs b/src/Discord.Net.Rpc/API/Rpc/RpcConfig.cs index c874462b6..4a8928a0d 100644 --- a/src/Discord.Net.Rpc/API/Rpc/RpcConfig.cs +++ b/src/Discord.Net.Rpc/API/Rpc/RpcConfig.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Rpc { - public class RpcConfig + internal class RpcConfig { [JsonProperty("cdn_host")] public string CdnHost { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/SelectChannelParams.cs b/src/Discord.Net.Rpc/API/Rpc/SelectChannelParams.cs index 52c9b00e8..6fc9314a3 100644 --- a/src/Discord.Net.Rpc/API/Rpc/SelectChannelParams.cs +++ b/src/Discord.Net.Rpc/API/Rpc/SelectChannelParams.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Rpc { - public class SelectChannelParams + internal class SelectChannelParams { [JsonProperty("channel_id")] public ulong? ChannelId { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/SetLocalVolumeParams.cs b/src/Discord.Net.Rpc/API/Rpc/SetLocalVolumeParams.cs index 9aff39df9..345ad906d 100644 --- a/src/Discord.Net.Rpc/API/Rpc/SetLocalVolumeParams.cs +++ b/src/Discord.Net.Rpc/API/Rpc/SetLocalVolumeParams.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Rpc { - public class SetLocalVolumeParams + internal class SetLocalVolumeParams { [JsonProperty("volume")] public int Volume { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/SetLocalVolumeResponse.cs b/src/Discord.Net.Rpc/API/Rpc/SetLocalVolumeResponse.cs index b1dfdc7ef..33927b7d9 100644 --- a/src/Discord.Net.Rpc/API/Rpc/SetLocalVolumeResponse.cs +++ b/src/Discord.Net.Rpc/API/Rpc/SetLocalVolumeResponse.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Rpc { - public class SetLocalVolumeResponse + internal class SetLocalVolumeResponse { [JsonProperty("user_id")] public ulong UserId { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/SpeakingEvent.cs b/src/Discord.Net.Rpc/API/Rpc/SpeakingEvent.cs index 4d8804d2f..913d7d768 100644 --- a/src/Discord.Net.Rpc/API/Rpc/SpeakingEvent.cs +++ b/src/Discord.Net.Rpc/API/Rpc/SpeakingEvent.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Rpc { - public class SpeakingEvent + internal class SpeakingEvent { [JsonProperty("user_id")] public ulong UserId { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/SubscriptionResponse.cs b/src/Discord.Net.Rpc/API/Rpc/SubscriptionResponse.cs index 7fe13ed6c..76adc8d1b 100644 --- a/src/Discord.Net.Rpc/API/Rpc/SubscriptionResponse.cs +++ b/src/Discord.Net.Rpc/API/Rpc/SubscriptionResponse.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Rpc { - public class SubscriptionResponse + internal class SubscriptionResponse { [JsonProperty("evt")] public string Event { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/UserVoiceSettings.cs b/src/Discord.Net.Rpc/API/Rpc/UserVoiceSettings.cs index 9c876a66f..ff338a4a4 100644 --- a/src/Discord.Net.Rpc/API/Rpc/UserVoiceSettings.cs +++ b/src/Discord.Net.Rpc/API/Rpc/UserVoiceSettings.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Discord.API.Rpc { - public class UserVoiceSettings + internal class UserVoiceSettings { [JsonProperty("userId")] internal ulong UserId { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/VoiceDevice.cs b/src/Discord.Net.Rpc/API/Rpc/VoiceDevice.cs index 4dc99d4cd..52bdef8a3 100644 --- a/src/Discord.Net.Rpc/API/Rpc/VoiceDevice.cs +++ b/src/Discord.Net.Rpc/API/Rpc/VoiceDevice.cs @@ -2,7 +2,7 @@ namespace Discord.API.Rpc { - public class VoiceDevice + internal class VoiceDevice { [JsonProperty("id")] public string Id { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/VoiceDeviceSettings.cs b/src/Discord.Net.Rpc/API/Rpc/VoiceDeviceSettings.cs index 38473c803..c06eb2c02 100644 --- a/src/Discord.Net.Rpc/API/Rpc/VoiceDeviceSettings.cs +++ b/src/Discord.Net.Rpc/API/Rpc/VoiceDeviceSettings.cs @@ -2,7 +2,7 @@ namespace Discord.API.Rpc { - public class VoiceDeviceSettings + internal class VoiceDeviceSettings { [JsonProperty("device_id")] public Optional DeviceId { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/VoiceMode.cs b/src/Discord.Net.Rpc/API/Rpc/VoiceMode.cs index a502cc960..35e9d453e 100644 --- a/src/Discord.Net.Rpc/API/Rpc/VoiceMode.cs +++ b/src/Discord.Net.Rpc/API/Rpc/VoiceMode.cs @@ -2,7 +2,7 @@ namespace Discord.API.Rpc { - public class VoiceMode + internal class VoiceMode { [JsonProperty("type")] public Optional Type { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/VoiceSettings.cs b/src/Discord.Net.Rpc/API/Rpc/VoiceSettings.cs index c3268a719..11fb3b6a8 100644 --- a/src/Discord.Net.Rpc/API/Rpc/VoiceSettings.cs +++ b/src/Discord.Net.Rpc/API/Rpc/VoiceSettings.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Discord.API.Rpc { - public class VoiceSettings + internal class VoiceSettings { [JsonProperty("input")] public VoiceDeviceSettings Input { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/VoiceShortcut.cs b/src/Discord.Net.Rpc/API/Rpc/VoiceShortcut.cs index 5b0939d79..65e258033 100644 --- a/src/Discord.Net.Rpc/API/Rpc/VoiceShortcut.cs +++ b/src/Discord.Net.Rpc/API/Rpc/VoiceShortcut.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Rpc { - public class VoiceShortcut + internal class VoiceShortcut { [JsonProperty("type")] public Optional Type { get; set; } diff --git a/src/Discord.Net.Rpc/API/RpcFrame.cs b/src/Discord.Net.Rpc/API/RpcFrame.cs index cac150e3b..523378b04 100644 --- a/src/Discord.Net.Rpc/API/RpcFrame.cs +++ b/src/Discord.Net.Rpc/API/RpcFrame.cs @@ -4,7 +4,7 @@ using System; namespace Discord.API.Rpc { - public class RpcFrame + internal class RpcFrame { [JsonProperty("cmd")] public string Cmd { get; set; } diff --git a/src/Discord.Net.Rpc/Commands/RpcCommandContext.cs b/src/Discord.Net.Rpc/Commands/RpcCommandContext.cs new file mode 100644 index 000000000..80dfddbab --- /dev/null +++ b/src/Discord.Net.Rpc/Commands/RpcCommandContext.cs @@ -0,0 +1,29 @@ +using Discord.Rpc; + +namespace Discord.Commands +{ + public class RpcCommandContext : ICommandContext + { + public DiscordRpcClient Client { get; } + public IMessageChannel Channel { get; } + public RpcUser User { get; } + public RpcUserMessage Message { get; } + + public bool IsPrivate => Channel is IPrivateChannel; + + public RpcCommandContext(DiscordRpcClient client, RpcUserMessage msg) + { + Client = client; + Channel = msg.Channel; + User = msg.Author; + Message = msg; + } + + //ICommandContext + IDiscordClient ICommandContext.Client => Client; + IGuild ICommandContext.Guild => null; + IMessageChannel ICommandContext.Channel => Channel; + IUser ICommandContext.User => User; + IUserMessage ICommandContext.Message => Message; + } +} diff --git a/src/Discord.Net.Rpc/Discord.Net.Rpc.csproj b/src/Discord.Net.Rpc/Discord.Net.Rpc.csproj index efd2ea893..85c2bf4e0 100644 --- a/src/Discord.Net.Rpc/Discord.Net.Rpc.csproj +++ b/src/Discord.Net.Rpc/Discord.Net.Rpc.csproj @@ -1,48 +1,41 @@ - - + - A core Discord.Net library containing the RPC client and models. - 1.0.0-beta2 - netstandard1.3 + 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 - $(PackageTargetFallback);dotnet5.4;dnxcore50;portable-net45+win8 + Discord.Rpc + true - - - + + Net\DefaultWebSocketClient.cs + + + ConnectionManager.cs + - - - 1.0.0-alpha-20161104-2 - All - - - 4.3.0 - - - 4.3.0 - + + + + - - - False - - $(DefineConstants);RELEASE $(NoWarn);CS1573;CS1591 true true - \ No newline at end of file diff --git a/src/Discord.Net.Rpc/API/DiscordRpcApiClient.cs b/src/Discord.Net.Rpc/DiscordRpcApiClient.cs similarity index 96% rename from src/Discord.Net.Rpc/API/DiscordRpcApiClient.cs rename to src/Discord.Net.Rpc/DiscordRpcApiClient.cs index 050783f28..8c83d24d6 100644 --- a/src/Discord.Net.Rpc/API/DiscordRpcApiClient.cs +++ b/src/Discord.Net.Rpc/DiscordRpcApiClient.cs @@ -17,7 +17,7 @@ using System.Threading.Tasks; namespace Discord.API { - public class DiscordRpcApiClient : DiscordRestApiClient, IDisposable + internal class DiscordRpcApiClient : DiscordRestApiClient, IDisposable { private abstract class RpcRequest { @@ -59,7 +59,6 @@ namespace Discord.API private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); private readonly ConcurrentDictionary _requests; - private readonly RequestQueue _requestQueue; private readonly IWebSocketClient _webSocketClient; private readonly SemaphoreSlim _connectionLock; private readonly string _clientId; @@ -69,16 +68,13 @@ namespace Discord.API public ConnectionState ConnectionState { get; private set; } public DiscordRpcApiClient(string clientId, string userAgent, string origin, RestClientProvider restClientProvider, WebSocketProvider webSocketProvider, - JsonSerializer serializer = null, RequestQueue requestQueue = null) - : base(restClientProvider, userAgent, serializer, requestQueue) + RetryMode defaultRetryMode = RetryMode.AlwaysRetry, JsonSerializer serializer = null) + : base(restClientProvider, userAgent, defaultRetryMode, serializer) { _connectionLock = new SemaphoreSlim(1, 1); _clientId = clientId; _origin = origin; - - FetchCurrentUser = false; - - _requestQueue = requestQueue ?? new RequestQueue(); + _requests = new ConcurrentDictionary(); _webSocketClient = webSocketProvider(); @@ -218,9 +214,6 @@ namespace Discord.API private async Task SendRpcAsyncInternal(string cmd, object payload, Optional evt, RequestOptions options) where TResponse : class { - if (!options.IgnoreState) - CheckState(); - byte[] bytes = null; var guid = Guid.NewGuid(); payload = new API.Rpc.RpcFrame { Cmd = cmd, Event = evt, Args = payload, Nonce = guid }; @@ -233,7 +226,7 @@ namespace Discord.API var requestTracker = new RpcRequest(options); _requests[guid] = requestTracker; - await _requestQueue.SendAsync(new WebSocketRequest(_webSocketClient, null, bytes, true, options)).ConfigureAwait(false); + await RequestQueue.SendAsync(new WebSocketRequest(_webSocketClient, null, bytes, true, options)).ConfigureAwait(false); await _sentRpcMessageEvent.InvokeAsync(cmd).ConfigureAwait(false); return await requestTracker.Promise.Task.ConfigureAwait(false); } @@ -244,7 +237,7 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); var msg = new AuthenticateParams { - AccessToken = _authToken + AccessToken = AuthToken }; options.IgnoreState = true; return await SendRpcAsync("AUTHENTICATE", msg, options: options).ConfigureAwait(false); diff --git a/src/Discord.Net.Rpc/DiscordRpcClient.cs b/src/Discord.Net.Rpc/DiscordRpcClient.cs index 52fe6172f..01d641204 100644 --- a/src/Discord.Net.Rpc/DiscordRpcClient.cs +++ b/src/Discord.Net.Rpc/DiscordRpcClient.cs @@ -1,7 +1,6 @@ using Discord.API.Rpc; using Discord.Logging; using Discord.Net.Converters; -using Discord.Net.Queue; using Discord.Rest; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -9,31 +8,25 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Threading; using System.Threading.Tasks; +using System.Threading; namespace Discord.Rpc { - public partial class DiscordRpcClient : BaseDiscordClient + public partial class DiscordRpcClient : BaseDiscordClient, IDiscordClient { - private readonly Logger _rpcLogger; private readonly JsonSerializer _serializer; - - private TaskCompletionSource _connectTask; - private CancellationTokenSource _cancelToken, _reconnectCancelToken; - private Task _reconnectTask; - private bool _canReconnect; + private readonly ConnectionManager _connection; + private readonly Logger _rpcLogger; + private readonly SemaphoreSlim _stateLock, _authorizeLock; public ConnectionState ConnectionState { get; private set; } public IReadOnlyCollection Scopes { get; private set; } public DateTimeOffset TokenExpiresAt { get; private set; } - //From DiscordRpcConfig - internal int ConnectionTimeout { get; private set; } - - public new API.DiscordRpcApiClient ApiClient => base.ApiClient as API.DiscordRpcApiClient; + internal new API.DiscordRpcApiClient ApiClient => base.ApiClient as API.DiscordRpcApiClient; public new RestSelfUser CurrentUser { get { return base.CurrentUser as RestSelfUser; } private set { base.CurrentUser = value; } } - public RestApplication CurrentApplication { get; private set; } + public RestApplication ApplicationInfo { get; private set; } /// Creates a new RPC discord client. public DiscordRpcClient(string clientId, string origin) @@ -42,8 +35,13 @@ namespace Discord.Rpc public DiscordRpcClient(string clientId, string origin, DiscordRpcConfig config) : base(config, CreateApiClient(clientId, origin, config)) { - ConnectionTimeout = config.ConnectionTimeout; + _stateLock = new SemaphoreSlim(1, 1); + _authorizeLock = new SemaphoreSlim(1, 1); _rpcLogger = LogManager.CreateLogger("RPC"); + _connection = new ConnectionManager(_stateLock, _rpcLogger, config.ConnectionTimeout, + OnConnectingAsync, OnDisconnectingAsync, x => ApiClient.Disconnected += x); + _connection.Connected += () => _connectedEvent.InvokeAsync(); + _connection.Disconnected += (ex, recon) => _disconnectedEvent.InvokeAsync(ex); _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; _serializer.Error += (s, e) => @@ -54,181 +52,52 @@ namespace Discord.Rpc ApiClient.SentRpcMessage += async opCode => await _rpcLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false); ApiClient.ReceivedRpcEvent += ProcessMessageAsync; - ApiClient.Disconnected += async ex => - { - if (ex != null) - { - await _rpcLogger.WarningAsync($"Connection Closed", ex).ConfigureAwait(false); - await StartReconnectAsync(ex).ConfigureAwait(false); - } - else - await _rpcLogger.WarningAsync($"Connection Closed").ConfigureAwait(false); - }; } private static API.DiscordRpcApiClient CreateApiClient(string clientId, string origin, DiscordRpcConfig config) - => new API.DiscordRpcApiClient(clientId, DiscordRestConfig.UserAgent, origin, config.RestClientProvider, config.WebSocketProvider, requestQueue: new RequestQueue()); - - /// - public Task ConnectAsync() => ConnectAsync(false); - internal async Task ConnectAsync(bool ignoreLoginCheck) + => new API.DiscordRpcApiClient(clientId, DiscordRestConfig.UserAgent, origin, config.RestClientProvider, config.WebSocketProvider); + internal override void Dispose(bool disposing) { - await _connectionLock.WaitAsync().ConfigureAwait(false); - try + if (disposing) { - await ConnectInternalAsync(ignoreLoginCheck, false).ConfigureAwait(false); + StopAsync().GetAwaiter().GetResult(); + ApiClient.Dispose(); } - finally { _connectionLock.Release(); } } - private async Task ConnectInternalAsync(bool ignoreLoginCheck, bool isReconnecting) - { - if (!ignoreLoginCheck && LoginState != LoginState.LoggedIn) - throw new InvalidOperationException("You must log in before connecting."); - - if (!isReconnecting && _reconnectCancelToken != null && !_reconnectCancelToken.IsCancellationRequested) - _reconnectCancelToken.Cancel(); - var state = ConnectionState; - if (state == ConnectionState.Connecting || state == ConnectionState.Connected) - await DisconnectInternalAsync(null, isReconnecting).ConfigureAwait(false); + public Task StartAsync() => _connection.StartAsync(); + public Task StopAsync() => _connection.StopAsync(); - ConnectionState = ConnectionState.Connecting; - await _rpcLogger.InfoAsync("Connecting").ConfigureAwait(false); - try - { - var connectTask = new TaskCompletionSource(); - _connectTask = connectTask; - _cancelToken = new CancellationTokenSource(); - - //Abort connection on timeout - var _ = Task.Run(async () => - { - await Task.Delay(ConnectionTimeout).ConfigureAwait(false); - connectTask.TrySetException(new TimeoutException()); - }); - - await ApiClient.ConnectAsync().ConfigureAwait(false); - await _connectedEvent.InvokeAsync().ConfigureAwait(false); - - await _connectTask.Task.ConfigureAwait(false); - if (!isReconnecting) - _canReconnect = true; - ConnectionState = ConnectionState.Connected; - await _rpcLogger.InfoAsync("Connected").ConfigureAwait(false); - } - catch (Exception) - { - await DisconnectInternalAsync(null, isReconnecting).ConfigureAwait(false); - throw; - } - } - /// - public async Task DisconnectAsync() + private async Task OnConnectingAsync() { - if (_connectTask?.TrySetCanceled() ?? false) return; - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - await DisconnectInternalAsync(null, false).ConfigureAwait(false); - } - finally { _connectionLock.Release(); } + await _rpcLogger.DebugAsync("Connecting ApiClient").ConfigureAwait(false); + await ApiClient.ConnectAsync().ConfigureAwait(false); + + await _connection.WaitAsync().ConfigureAwait(false); } - private async Task DisconnectInternalAsync(Exception ex, bool isReconnecting) + private async Task OnDisconnectingAsync(Exception ex) { - if (!isReconnecting) - { - _canReconnect = false; - - if (_reconnectCancelToken != null && !_reconnectCancelToken.IsCancellationRequested) - _reconnectCancelToken.Cancel(); - } - - if (ConnectionState == ConnectionState.Disconnected) return; - ConnectionState = ConnectionState.Disconnecting; - await _rpcLogger.InfoAsync("Disconnecting").ConfigureAwait(false); - - await _rpcLogger.DebugAsync("Disconnecting - CancelToken").ConfigureAwait(false); - //Signal tasks to complete - try { _cancelToken.Cancel(); } catch { } - - await _rpcLogger.DebugAsync("Disconnecting - ApiClient").ConfigureAwait(false); - //Disconnect from server + await _rpcLogger.DebugAsync("Disconnecting ApiClient").ConfigureAwait(false); await ApiClient.DisconnectAsync().ConfigureAwait(false); - - ConnectionState = ConnectionState.Disconnected; - await _rpcLogger.InfoAsync("Disconnected").ConfigureAwait(false); - - await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); } - private async Task StartReconnectAsync(Exception ex) - { - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - if (!_canReconnect || _reconnectTask != null) return; - _reconnectCancelToken = new CancellationTokenSource(); - _reconnectTask = ReconnectInternalAsync(ex, _reconnectCancelToken.Token); - } - finally { _connectionLock.Release(); } - } - private async Task ReconnectInternalAsync(Exception ex, CancellationToken cancelToken) + public async Task AuthorizeAsync(string[] scopes, string rpcToken = null, RequestOptions options = null) { - if (ex == null) - { - if (_connectTask?.TrySetCanceled() ?? false) return; - } - else - { - if (_connectTask?.TrySetException(ex) ?? false) return; - } - + await _authorizeLock.WaitAsync().ConfigureAwait(false); try { - Random jitter = new Random(); - int nextReconnectDelay = 1000; - while (true) - { - await Task.Delay(nextReconnectDelay, cancelToken).ConfigureAwait(false); - nextReconnectDelay = nextReconnectDelay * 2 + jitter.Next(-250, 250); - if (nextReconnectDelay > 60000) - nextReconnectDelay = 60000; - - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - if (cancelToken.IsCancellationRequested) return; - await ConnectInternalAsync(false, true).ConfigureAwait(false); - _reconnectTask = null; - return; - } - catch (Exception ex2) - { - await _rpcLogger.WarningAsync("Reconnect failed", ex2).ConfigureAwait(false); - } - finally { _connectionLock.Release(); } - } + await _connection.StartAsync().ConfigureAwait(false); + await _connection.WaitAsync().ConfigureAwait(false); + var result = await ApiClient.SendAuthorizeAsync(scopes, rpcToken, options).ConfigureAwait(false); + await _connection.StopAsync().ConfigureAwait(false); + return result.Code; } - catch (OperationCanceledException) + finally { - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - await _rpcLogger.DebugAsync("Reconnect cancelled").ConfigureAwait(false); - _reconnectTask = null; - } - finally { _connectionLock.Release(); } + _authorizeLock.Release(); } } - public async Task AuthorizeAsync(string[] scopes, string rpcToken = null, RequestOptions options = null) - { - await ConnectAsync(true).ConfigureAwait(false); - var result = await ApiClient.SendAuthorizeAsync(scopes, rpcToken, options).ConfigureAwait(false); - await DisconnectAsync().ConfigureAwait(false); - return result.Code; - } - public async Task SubscribeGlobal(RpcGlobalEvent evnt, RequestOptions options = null) { await ApiClient.SendGlobalSubscribeAsync(GetEventName(evnt), options).ConfigureAwait(false); @@ -312,20 +181,67 @@ namespace Discord.Rpc var model = await ApiClient.GetVoiceSettingsAsync(options).ConfigureAwait(false); return VoiceSettings.Create(model); } - public async Task SetVoiceSettingsAsync(Action func, RequestOptions options = null) + public async Task SetVoiceSettingsAsync(Action func, RequestOptions options = null) { - var settings = new API.Rpc.VoiceSettings(); - settings.Input = new VoiceDeviceSettings(); - settings.Output = new VoiceDeviceSettings(); - settings.Mode = new VoiceMode(); + if (func == null) throw new NullReferenceException(nameof(func)); + + var settings = new VoiceProperties(); + settings.Input = new VoiceDeviceProperties(); + settings.Output = new VoiceDeviceProperties(); + settings.Mode = new VoiceModeProperties(); func(settings); - await ApiClient.SetVoiceSettingsAsync(settings, options).ConfigureAwait(false); + + var model = new API.Rpc.VoiceSettings + { + AutomaticGainControl = settings.AutomaticGainControl, + EchoCancellation = settings.EchoCancellation, + NoiseSuppression = settings.NoiseSuppression, + QualityOfService = settings.QualityOfService, + SilenceWarning = settings.SilenceWarning + }; + model.Input = new API.Rpc.VoiceDeviceSettings + { + DeviceId = settings.Input.DeviceId, + Volume = settings.Input.Volume + }; + model.Output = new API.Rpc.VoiceDeviceSettings + { + DeviceId = settings.Output.DeviceId, + Volume = settings.Output.Volume + }; + model.Mode = new API.Rpc.VoiceMode + { + AutoThreshold = settings.Mode.AutoThreshold, + Delay = settings.Mode.Delay, + Threshold = settings.Mode.Threshold, + Type = settings.Mode.Type + }; + + if (settings.Input.AvailableDevices.IsSpecified) + model.Input.AvailableDevices = settings.Input.AvailableDevices.Value.Select(x => x.ToModel()).ToArray(); + if (settings.Output.AvailableDevices.IsSpecified) + model.Output.AvailableDevices = settings.Output.AvailableDevices.Value.Select(x => x.ToModel()).ToArray(); + if (settings.Mode.Shortcut.IsSpecified) + model.Mode.Shortcut = settings.Mode.Shortcut.Value.Select(x => x.ToModel()).ToArray(); + + await ApiClient.SetVoiceSettingsAsync(model, options).ConfigureAwait(false); } - public async Task SetUserVoiceSettingsAsync(ulong userId, Action func, RequestOptions options = null) + public async Task SetUserVoiceSettingsAsync(ulong userId, Action func, RequestOptions options = null) { - var settings = new API.Rpc.UserVoiceSettings(); + if (func == null) throw new NullReferenceException(nameof(func)); + + var settings = new UserVoiceProperties(); func(settings); - await ApiClient.SetUserVoiceSettingsAsync(userId, settings, options).ConfigureAwait(false); + + var model = new API.Rpc.UserVoiceSettings + { + Mute = settings.Mute, + UserId = settings.UserId, + Volume = settings.Volume + }; + if (settings.Pan.IsSpecified) + model.Pan = settings.Pan.Value.ToModel(); + await ApiClient.SetUserVoiceSettingsAsync(userId, model, options).ConfigureAwait(false); } private static string GetEventName(RpcGlobalEvent rpcEvent) @@ -393,11 +309,12 @@ namespace Discord.Rpc { var response = await ApiClient.SendAuthenticateAsync(options).ConfigureAwait(false); CurrentUser = RestSelfUser.Create(this, response.User); - CurrentApplication = RestApplication.Create(this, response.Application); + ApiClient.CurrentUserId = CurrentUser.Id; + ApplicationInfo = RestApplication.Create(this, response.Application); Scopes = response.Scopes; TokenExpiresAt = response.Expires; - - var __ = _connectTask.TrySetResultAsync(true); //Signal the .Connect() call to complete + + var __ = _connection.CompleteAsync(); await _rpcLogger.InfoAsync("Ready").ConfigureAwait(false); } catch (Exception ex) @@ -409,7 +326,7 @@ namespace Discord.Rpc } else { - var _ = _connectTask.TrySetResultAsync(true); //Signal the .Connect() call to complete + var _ = _connection.CompleteAsync(); await _rpcLogger.InfoAsync("Ready").ConfigureAwait(false); } } @@ -547,5 +464,15 @@ namespace Discord.Rpc return; } } + + //IDiscordClient + ConnectionState IDiscordClient.ConnectionState => _connection.State; + + Task IDiscordClient.GetApplicationInfoAsync() => Task.FromResult(ApplicationInfo); + + async Task IDiscordClient.StartAsync() + => await StartAsync().ConfigureAwait(false); + async Task IDiscordClient.StopAsync() + => await StopAsync().ConfigureAwait(false); } } diff --git a/src/Discord.Net.Rpc/DiscordRpcConfig.cs b/src/Discord.Net.Rpc/DiscordRpcConfig.cs index d1e69376c..1866e838b 100644 --- a/src/Discord.Net.Rpc/DiscordRpcConfig.cs +++ b/src/Discord.Net.Rpc/DiscordRpcConfig.cs @@ -1,5 +1,6 @@ using Discord.Net.WebSockets; using Discord.Rest; +using System; namespace Discord.Rpc { @@ -14,6 +15,19 @@ namespace Discord.Rpc public int ConnectionTimeout { get; set; } = 30000; /// Gets or sets the provider used to generate new websocket connections. - public WebSocketProvider WebSocketProvider { get; set; } = () => new DefaultWebSocketClient(); + public WebSocketProvider WebSocketProvider { get; set; } + + public DiscordRpcConfig() + { +#if NETSTANDARD1_3 + WebSocketProvider = () => new DefaultWebSocketClient(); +#else + WebSocketProvider = () => + { + throw new InvalidOperationException("The default websocket provider is not supported on this platform.\n" + + "You must specify a WebSocketProvider or target a runtime supporting .NET Standard 1.3, such as .NET Framework 4.6+."); + }; +#endif + } } } diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs index af0102574..1fb6d5867 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs @@ -44,10 +44,12 @@ namespace Discord.Rpc public Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - public Task SendMessageAsync(string text, bool isTTS = false, EmbedBuilder embed = null, RequestOptions options = null) + 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 public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); +#endif public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); @@ -100,11 +102,13 @@ namespace Discord.Rpc async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); +#if NETSTANDARD1_3 async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); +#endif async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, EmbedBuilder embed, RequestOptions options) + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs index c88621d8f..504bf8670 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs @@ -46,10 +46,12 @@ namespace Discord.Rpc public Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - public Task SendMessageAsync(string text, bool isTTS = false, EmbedBuilder embed = null, RequestOptions options = null) + 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 public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); +#endif public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); @@ -99,11 +101,13 @@ namespace Discord.Rpc async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); +#if NETSTANDARD1_3 async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); +#endif async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, EmbedBuilder embed, RequestOptions options) + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs index 8a0ac2a9e..48eb8ec3e 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Discord.API.Rest; using Model = Discord.API.Rpc.Channel; using Discord.Rest; @@ -36,7 +35,7 @@ namespace Discord.Rpc Position = model.Position.Value; } - public Task ModifyAsync(Action func, RequestOptions options = null) + public Task ModifyAsync(Action func, RequestOptions options = null) => ChannelHelper.ModifyAsync(this, Discord, func, options); public Task DeleteAsync(RequestOptions options = null) => ChannelHelper.DeleteAsync(this, Discord, options); @@ -52,8 +51,8 @@ namespace Discord.Rpc public async Task> GetInvitesAsync(RequestOptions options = null) => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); - public async Task CreateInviteAsync(int? maxAge = 3600, int? maxUses = null, bool isTemporary = true, RequestOptions options = null) - => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, options).ConfigureAwait(false); + public async Task CreateInviteAsync(int? maxAge = 3600, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); public override string ToString() => Name; @@ -69,8 +68,8 @@ namespace Discord.Rpc async Task> IGuildChannel.GetInvitesAsync(RequestOptions options) => await GetInvitesAsync(options).ConfigureAwait(false); - async Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, RequestOptions options) - => await CreateInviteAsync(maxAge, maxUses, isTemporary, options).ConfigureAwait(false); + async Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) + => await CreateInviteAsync(maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); IReadOnlyCollection IGuildChannel.PermissionOverwrites { get { throw new NotSupportedException(); } } OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IUser user) diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs index a5779bdbb..9a88072b9 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs @@ -1,5 +1,4 @@ -using Discord.API.Rest; -using Discord.Rest; +using Discord.Rest; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -34,7 +33,7 @@ namespace Discord.Rpc CachedMessages = model.Messages.Select(x => RpcMessage.Create(Discord, Id, x)).ToImmutableArray(); } - public Task ModifyAsync(Action func, RequestOptions options = null) + public Task ModifyAsync(Action func, RequestOptions options = null) => ChannelHelper.ModifyAsync(this, Discord, func, options); //TODO: Use RPC cache @@ -49,10 +48,12 @@ namespace Discord.Rpc public Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - public Task SendMessageAsync(string text, bool isTTS = false, EmbedBuilder embed = null, RequestOptions options = null) + 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 public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); +#endif public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); @@ -101,11 +102,13 @@ namespace Discord.Rpc async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); +#if NETSTANDARD1_3 async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); +#endif async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, EmbedBuilder embed, RequestOptions options) + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs index 1e6510a38..3d5acfda9 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs @@ -1,5 +1,4 @@ -using Discord.API.Rest; -using Discord.Audio; +using Discord.Audio; using Discord.Rest; using System; using System.Collections.Generic; @@ -14,8 +13,8 @@ namespace Discord.Rpc [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RpcVoiceChannel : RpcGuildChannel, IRpcAudioChannel, IVoiceChannel { - public int UserLimit { get; private set; } public int Bitrate { get; private set; } + public int? UserLimit { get; private set; } public IReadOnlyCollection VoiceStates { get; private set; } internal RpcVoiceChannel(DiscordRpcClient discord, ulong id, ulong guildId) @@ -32,13 +31,13 @@ namespace Discord.Rpc { base.Update(model); if (model.UserLimit.IsSpecified) - UserLimit = model.UserLimit.Value; + UserLimit = model.UserLimit.Value != 0 ? model.UserLimit.Value : (int?)null; if (model.Bitrate.IsSpecified) Bitrate = model.Bitrate.Value; VoiceStates = model.VoiceStates.Select(x => RpcVoiceState.Create(Discord, x)).ToImmutableArray(); } - public Task ModifyAsync(Action func, RequestOptions options = null) + public Task ModifyAsync(Action func, RequestOptions options = null) => ChannelHelper.ModifyAsync(this, Discord, func, options); private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; diff --git a/src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs b/src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs index d0464487b..b85071f2a 100644 --- a/src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs +++ b/src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs @@ -33,7 +33,7 @@ namespace Discord.Rpc public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); - internal RpcMessage(DiscordRpcClient discord, ulong id, IMessageChannel channel, RpcUser author) + internal RpcMessage(DiscordRpcClient discord, ulong id, RestVirtualMessageChannel channel, RpcUser author) : base(discord, id) { Channel = channel; @@ -62,7 +62,9 @@ namespace Discord.Rpc => MessageHelper.DeleteAsync(this, Discord, options); public override string ToString() => Content; - + + //IMessage + IMessageChannel IMessage.Channel => Channel; MessageType IMessage.Type => MessageType.Default; IUser IMessage.Author => Author; IReadOnlyCollection IMessage.Attachments => Attachments; diff --git a/src/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs b/src/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs index 734ef38bc..e8c918bc6 100644 --- a/src/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs +++ b/src/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs @@ -9,7 +9,7 @@ namespace Discord.Rpc { public MessageType Type { get; private set; } - internal RpcSystemMessage(DiscordRpcClient discord, ulong id, IMessageChannel channel, RpcUser author) + internal RpcSystemMessage(DiscordRpcClient discord, ulong id, RestVirtualMessageChannel channel, RpcUser author) : base(discord, id, channel, author) { } diff --git a/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs b/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs index 3fc489ff9..240290fab 100644 --- a/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs +++ b/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs @@ -1,5 +1,4 @@ -using Discord.API.Rest; -using Discord.Rest; +using Discord.Rest; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -30,8 +29,9 @@ 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(); - internal RpcUserMessage(DiscordRpcClient discord, ulong id, IMessageChannel channel, RpcUser author) + internal RpcUserMessage(DiscordRpcClient discord, ulong id, RestVirtualMessageChannel channel, RpcUser author) : base(discord, id, channel, author) { } @@ -83,7 +83,7 @@ namespace Discord.Rpc { var embeds = ImmutableArray.CreateBuilder(value.Length); for (int i = 0; i < value.Length; i++) - embeds.Add(Embed.Create(value[i])); + embeds.Add(value[i].ToEntity()); _embeds = embeds.ToImmutable(); } else @@ -98,9 +98,25 @@ namespace Discord.Rpc } } - public Task ModifyAsync(Action func, RequestOptions options) + 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 RemoveAllReactionsAsync(RequestOptions options = null) + => MessageHelper.RemoveAllReactionsAsync(this, Discord, options); + + public Task> GetReactionUsersAsync(string emoji, int limit, ulong? afterUserId, RequestOptions options = null) + => MessageHelper.GetReactionUsersAsync(this, emoji, x => { x.Limit = limit; x.AfterUserId = afterUserId.HasValue ? afterUserId.Value : Optional.Create(); }, Discord, options); + public Task PinAsync(RequestOptions options) => MessageHelper.PinAsync(this, Discord, options); public Task UnpinAsync(RequestOptions options) diff --git a/src/Discord.Net.Rpc/Entities/UserVoiceProperties.cs b/src/Discord.Net.Rpc/Entities/UserVoiceProperties.cs new file mode 100644 index 000000000..830ba16a3 --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/UserVoiceProperties.cs @@ -0,0 +1,18 @@ +#pragma warning disable CS1591 + +using Newtonsoft.Json; + +namespace Discord.Rpc +{ + public class UserVoiceProperties + { + [JsonProperty("userId")] + internal ulong UserId { get; set; } + [JsonProperty("pan")] + public Optional Pan { get; set; } + [JsonProperty("volume")] + public Optional Volume { get; set; } + [JsonProperty("mute")] + public Optional Mute { get; set; } + } +} diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs b/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs index 360039beb..e78aee008 100644 --- a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs +++ b/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs @@ -14,7 +14,7 @@ namespace Discord.Rpc public ushort DiscriminatorValue { get; private set; } public string AvatarId { get; private set; } - public string AvatarUrl => API.CDN.GetUserAvatarUrl(Id, AvatarId); + public string GetAvatarUrl(AvatarFormat format = AvatarFormat.Png, ushort size = 128) => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); @@ -47,7 +47,7 @@ namespace Discord.Rpc => UserHelper.CreateDMChannelAsync(this, Discord, options); public override string ToString() => $"{Username}#{Discriminator}"; - internal string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; + private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; //IUser Task IUser.GetDMChannelAsync(CacheMode mode, RequestOptions options) diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcVoiceState.cs b/src/Discord.Net.Rpc/Entities/Users/RpcVoiceState.cs index f18a51434..66b4186d6 100644 --- a/src/Discord.Net.Rpc/Entities/Users/RpcVoiceState.cs +++ b/src/Discord.Net.Rpc/Entities/Users/RpcVoiceState.cs @@ -71,7 +71,7 @@ namespace Discord.Rpc } public override string ToString() => User.ToString(); - internal string DebuggerDisplay => $"{User} ({_voiceStates})"; + private string DebuggerDisplay => $"{User} ({_voiceStates})"; string IVoiceState.VoiceSessionId { get { throw new NotSupportedException(); } } IVoiceChannel IVoiceState.VoiceChannel { get { throw new NotSupportedException(); } } diff --git a/src/Discord.Net.Rpc/Entities/VoiceDevice.cs b/src/Discord.Net.Rpc/Entities/VoiceDevice.cs index 34a718adc..18f929f93 100644 --- a/src/Discord.Net.Rpc/Entities/VoiceDevice.cs +++ b/src/Discord.Net.Rpc/Entities/VoiceDevice.cs @@ -20,6 +20,6 @@ namespace Discord.Rpc } public override string ToString() => $"{Name}"; - internal string DebuggerDisplay => $"{Name} ({Id})"; + private string DebuggerDisplay => $"{Name} ({Id})"; } } diff --git a/src/Discord.Net.Rpc/Entities/VoiceDeviceProperties.cs b/src/Discord.Net.Rpc/Entities/VoiceDeviceProperties.cs new file mode 100644 index 000000000..bdf87b235 --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/VoiceDeviceProperties.cs @@ -0,0 +1,9 @@ +namespace Discord.Rpc +{ + public class VoiceDeviceProperties + { + public Optional DeviceId { get; set; } + public Optional Volume { get; set; } + public Optional AvailableDevices { get; set; } + } +} diff --git a/src/Discord.Net.Rpc/Entities/VoiceModeProperties.cs b/src/Discord.Net.Rpc/Entities/VoiceModeProperties.cs new file mode 100644 index 000000000..da791e7a7 --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/VoiceModeProperties.cs @@ -0,0 +1,11 @@ +namespace Discord.Rpc +{ + public class VoiceModeProperties + { + public Optional Type { get; set; } + public Optional AutoThreshold { get; set; } + public Optional Threshold { get; set; } + public Optional Shortcut { get; set; } + public Optional Delay { get; set; } + } +} diff --git a/src/Discord.Net.Rpc/Entities/VoiceProperties.cs b/src/Discord.Net.Rpc/Entities/VoiceProperties.cs new file mode 100644 index 000000000..5939f83c8 --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/VoiceProperties.cs @@ -0,0 +1,14 @@ +namespace Discord.Rpc +{ + public class VoiceProperties + { + public VoiceDeviceProperties Input { get; set; } + public VoiceDeviceProperties Output { get; set; } + public VoiceModeProperties Mode { get; set; } + public Optional AutomaticGainControl { get; set; } + public Optional EchoCancellation { get; set; } + public Optional NoiseSuppression { get; set; } + public Optional QualityOfService { get; set; } + public Optional SilenceWarning { get; set; } + } +} diff --git a/src/Discord.Net.Rpc/Entities/VoiceShortcut.cs b/src/Discord.Net.Rpc/Entities/VoiceShortcut.cs index 93ef21804..ea9be9977 100644 --- a/src/Discord.Net.Rpc/Entities/VoiceShortcut.cs +++ b/src/Discord.Net.Rpc/Entities/VoiceShortcut.cs @@ -22,6 +22,6 @@ namespace Discord.Rpc } public override string ToString() => $"{Name}"; - internal string DebuggerDisplay => $"{Name} ({Code}, {Type})"; + private string DebuggerDisplay => $"{Name} ({Code}, {Type})"; } } diff --git a/src/Discord.Net.Rpc/Extensions/EntityExtensions.cs b/src/Discord.Net.Rpc/Extensions/EntityExtensions.cs new file mode 100644 index 000000000..00ccf5c68 --- /dev/null +++ b/src/Discord.Net.Rpc/Extensions/EntityExtensions.cs @@ -0,0 +1,31 @@ +namespace Discord.Rpc +{ + internal static class EntityExtensions + { + public static API.Rpc.Pan ToModel(this Pan entity) + { + return new API.Rpc.Pan + { + Left = entity.Left, + Right = entity.Right + }; + } + public static API.Rpc.VoiceDevice ToModel(this VoiceDevice entity) + { + return new API.Rpc.VoiceDevice + { + Id = entity.Id, + Name = entity.Name + }; + } + public static API.Rpc.VoiceShortcut ToModel(this VoiceShortcut entity) + { + return new API.Rpc.VoiceShortcut + { + Code = entity.Code, + Name = entity.Name, + Type = entity.Type + }; + } + } +} diff --git a/src/Discord.Net.Rpc/project.json b/src/Discord.Net.Rpc/project.json deleted file mode 100644 index 8b2db7421..000000000 --- a/src/Discord.Net.Rpc/project.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "version": "1.0.0-*", - "description": "A core Discord.Net library containing the RPC client and models.", - "authors": [ "RogueException" ], - - "packOptions": { - "tags": [ "discord", "discordapp" ], - "licenseUrl": "http://opensource.org/licenses/MIT", - "projectUrl": "https://github.com/RogueException/Discord.Net", - "repository": { - "type": "git", - "url": "git://github.com/RogueException/Discord.Net" - } - }, - - "configurations": { - "Release": { - "buildOptions": { - "define": [ "RELEASE" ], - "nowarn": [ "CS1573", "CS1591" ], - "optimize": true, - "warningsAsErrors": true, - "xmlDoc": true - } - } - }, - - "dependencies": { - "Discord.Net.Core": { - "target": "project" - }, - "Discord.Net.Rest": { - "target": "project" - }, - "System.IO.Compression": "4.3.0", - "System.Net.WebSockets.Client": "4.3.0" - }, - - "frameworks": { - "netstandard1.3": { - "imports": [ - "dotnet5.4", - "dnxcore50", - "portable-net45+win8" - ] - } - } -} \ No newline at end of file diff --git a/src/Discord.Net.WebSocket/API/Gateway/ExtendedGuild.cs b/src/Discord.Net.WebSocket/API/Gateway/ExtendedGuild.cs index 5d5fb8001..910f6d909 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/ExtendedGuild.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/ExtendedGuild.cs @@ -4,7 +4,7 @@ using System; namespace Discord.API.Gateway { - public class ExtendedGuild : Guild + internal class ExtendedGuild : Guild { [JsonProperty("unavailable")] public bool? Unavailable { get; set; } diff --git a/src/Discord.Net.WebSocket/API/Gateway/GatewayOpCode.cs b/src/Discord.Net.WebSocket/API/Gateway/GatewayOpCode.cs index b6712922a..13a2bb462 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/GatewayOpCode.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/GatewayOpCode.cs @@ -1,7 +1,7 @@ #pragma warning disable CS1591 namespace Discord.API.Gateway { - public enum GatewayOpCode : byte + internal enum GatewayOpCode : byte { /// C←S - Used to send most events. Dispatch = 0, diff --git a/src/Discord.Net.WebSocket/API/Gateway/GuildBanEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildBanEvent.cs index c3c4a02ce..59a3304dd 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/GuildBanEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/GuildBanEvent.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Gateway { - public class GuildBanEvent + internal class GuildBanEvent { [JsonProperty("guild_id")] public ulong GuildId { get; set; } diff --git a/src/Discord.Net.WebSocket/API/Gateway/GuildEmojiUpdateEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildEmojiUpdateEvent.cs index 5d895cbf8..715341dc5 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/GuildEmojiUpdateEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/GuildEmojiUpdateEvent.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Gateway { - public class GuildEmojiUpdateEvent + internal class GuildEmojiUpdateEvent { [JsonProperty("guild_id")] public ulong GuildId { get; set; } diff --git a/src/Discord.Net.WebSocket/API/Gateway/GuildMemberAddEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildMemberAddEvent.cs index 856fb01f9..350652faf 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/GuildMemberAddEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/GuildMemberAddEvent.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Gateway { - public class GuildMemberAddEvent : GuildMember + internal class GuildMemberAddEvent : GuildMember { [JsonProperty("guild_id")] public ulong GuildId { get; set; } diff --git a/src/Discord.Net.WebSocket/API/Gateway/GuildMemberRemoveEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildMemberRemoveEvent.cs index 4a9b0fc07..501408a7f 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/GuildMemberRemoveEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/GuildMemberRemoveEvent.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Gateway { - public class GuildMemberRemoveEvent + internal class GuildMemberRemoveEvent { [JsonProperty("guild_id")] public ulong GuildId { get; set; } diff --git a/src/Discord.Net.WebSocket/API/Gateway/GuildMemberUpdateEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildMemberUpdateEvent.cs index 74a67bce2..a234d6da5 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/GuildMemberUpdateEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/GuildMemberUpdateEvent.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Gateway { - public class GuildMemberUpdateEvent : GuildMember + internal class GuildMemberUpdateEvent : GuildMember { [JsonProperty("guild_id")] public ulong GuildId { get; set; } diff --git a/src/Discord.Net.WebSocket/API/Gateway/GuildMembersChunkEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildMembersChunkEvent.cs index b5f2e82f2..e401d7fa1 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/GuildMembersChunkEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/GuildMembersChunkEvent.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Gateway { - public class GuildMembersChunkEvent + internal class GuildMembersChunkEvent { [JsonProperty("guild_id")] public ulong GuildId { get; set; } diff --git a/src/Discord.Net.WebSocket/API/Gateway/GuildRoleCreateEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildRoleCreateEvent.cs index 50f067eb1..3409b1c91 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/GuildRoleCreateEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/GuildRoleCreateEvent.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Gateway { - public class GuildRoleCreateEvent + internal class GuildRoleCreateEvent { [JsonProperty("guild_id")] public ulong GuildId { get; set; } diff --git a/src/Discord.Net.WebSocket/API/Gateway/GuildRoleDeleteEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildRoleDeleteEvent.cs index 05203df7f..dbdaeff67 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/GuildRoleDeleteEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/GuildRoleDeleteEvent.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Gateway { - public class GuildRoleDeleteEvent + internal class GuildRoleDeleteEvent { [JsonProperty("guild_id")] public ulong GuildId { get; set; } diff --git a/src/Discord.Net.WebSocket/API/Gateway/GuildRoleUpdateEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildRoleUpdateEvent.cs index fde9c5907..b04ecb182 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/GuildRoleUpdateEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/GuildRoleUpdateEvent.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Gateway { - public class GuildRoleUpdateEvent + internal class GuildRoleUpdateEvent { [JsonProperty("guild_id")] public ulong GuildId { get; set; } diff --git a/src/Discord.Net.WebSocket/API/Gateway/GuildSyncEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildSyncEvent.cs index 9a34fb8b8..6b2e6c02f 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/GuildSyncEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/GuildSyncEvent.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Gateway { - public class GuildSyncEvent + internal class GuildSyncEvent { [JsonProperty("id")] public ulong Id { get; set; } diff --git a/src/Discord.Net.WebSocket/API/Gateway/HelloEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/HelloEvent.cs index 1f03bc037..e1ed9463c 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/HelloEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/HelloEvent.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Gateway { - public class HelloEvent + internal class HelloEvent { [JsonProperty("heartbeat_interval")] public int HeartbeatInterval { get; set; } diff --git a/src/Discord.Net.WebSocket/API/Gateway/IdentifyParams.cs b/src/Discord.Net.WebSocket/API/Gateway/IdentifyParams.cs index 22aee7daa..e87c58221 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/IdentifyParams.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/IdentifyParams.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; namespace Discord.API.Gateway { [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class IdentifyParams + internal class IdentifyParams { [JsonProperty("token")] public string Token { get; set; } diff --git a/src/Discord.Net.WebSocket/API/Gateway/MessageDeleteBulkEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/MessageDeleteBulkEvent.cs index 7a3df47b2..aba4a2f1c 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/MessageDeleteBulkEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/MessageDeleteBulkEvent.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; namespace Discord.API.Gateway { - public class MessageDeleteBulkEvent + internal class MessageDeleteBulkEvent { [JsonProperty("channel_id")] public ulong ChannelId { get; set; } diff --git a/src/Discord.Net.WebSocket/API/Gateway/Reaction.cs b/src/Discord.Net.WebSocket/API/Gateway/Reaction.cs new file mode 100644 index 000000000..62de456e2 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Gateway/Reaction.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class Reaction + { + [JsonProperty("user_id")] + public ulong UserId { get; set; } + [JsonProperty("message_id")] + public ulong MessageId { get; set; } + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + [JsonProperty("emoji")] + public Emoji Emoji { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/API/Gateway/ReadyEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/ReadyEvent.cs index a677ac281..ab92d8c36 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/ReadyEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/ReadyEvent.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Gateway { - public class ReadyEvent + internal class ReadyEvent { public class ReadState { diff --git a/src/Discord.Net.WebSocket/API/Gateway/RecipientEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/RecipientEvent.cs index 308b4958f..336ffd029 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/RecipientEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/RecipientEvent.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Gateway { - public class RecipientEvent + internal class RecipientEvent { [JsonProperty("user")] public User User { get; set; } diff --git a/src/Discord.Net.WebSocket/API/Gateway/RemoveAllReactionsEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/RemoveAllReactionsEvent.cs new file mode 100644 index 000000000..4833c5123 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Gateway/RemoveAllReactionsEvent.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class RemoveAllReactionsEvent + { + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + [JsonProperty("message_id")] + public ulong MessageId { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/API/Gateway/RequestMembersParams.cs b/src/Discord.Net.WebSocket/API/Gateway/RequestMembersParams.cs index 05ec87f56..6a8d283ed 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/RequestMembersParams.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/RequestMembersParams.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; namespace Discord.API.Gateway { [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class RequestMembersParams + internal class RequestMembersParams { [JsonProperty("query")] public string Query { get; set; } diff --git a/src/Discord.Net.WebSocket/API/Gateway/ResumeParams.cs b/src/Discord.Net.WebSocket/API/Gateway/ResumeParams.cs index 219adcf16..ffb46327b 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/ResumeParams.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/ResumeParams.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Discord.API.Gateway { [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class ResumeParams + internal class ResumeParams { [JsonProperty("token")] public string Token { get; set; } diff --git a/src/Discord.Net.WebSocket/API/Gateway/ResumedEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/ResumedEvent.cs index 398a716fd..d1347beae 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/ResumedEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/ResumedEvent.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Gateway { - public class ResumedEvent + internal class ResumedEvent { [JsonProperty("heartbeat_interval")] public int HeartbeatInterval { get; set; } diff --git a/src/Discord.Net.WebSocket/API/Gateway/StatusUpdateParams.cs b/src/Discord.Net.WebSocket/API/Gateway/StatusUpdateParams.cs index ae1f79283..fc0964c17 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/StatusUpdateParams.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/StatusUpdateParams.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Discord.API.Gateway { [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class StatusUpdateParams + internal class StatusUpdateParams { [JsonProperty("status")] public UserStatus Status { get; set; } diff --git a/src/Discord.Net.WebSocket/API/Gateway/TypingStartEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/TypingStartEvent.cs index 3a6cd3d5c..3cce512bd 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/TypingStartEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/TypingStartEvent.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Gateway { - public class TypingStartEvent + internal class TypingStartEvent { [JsonProperty("user_id")] public ulong UserId { get; set; } diff --git a/src/Discord.Net.WebSocket/API/Gateway/VoiceServerUpdateEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/VoiceServerUpdateEvent.cs index a300f0d2c..29167c1cc 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/VoiceServerUpdateEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/VoiceServerUpdateEvent.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Gateway { - public class VoiceServerUpdateEvent + internal class VoiceServerUpdateEvent { [JsonProperty("guild_id")] public ulong GuildId { get; set; } diff --git a/src/Discord.Net.WebSocket/API/Gateway/VoiceStateUpdateParams.cs b/src/Discord.Net.WebSocket/API/Gateway/VoiceStateUpdateParams.cs index e75e4412c..521160126 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/VoiceStateUpdateParams.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/VoiceStateUpdateParams.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Discord.API.Gateway { [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class VoiceStateUpdateParams + internal class VoiceStateUpdateParams { [JsonProperty("self_mute")] public bool SelfMute { get; set; } @@ -13,10 +13,7 @@ namespace Discord.API.Gateway [JsonProperty("guild_id")] public ulong? GuildId { get; set; } - public IGuild Guild { set { GuildId = value?.Id; } } - [JsonProperty("channel_id")] public ulong? ChannelId { get; set; } - public IChannel Channel { set { ChannelId = value?.Id; } } } } diff --git a/src/Discord.Net.WebSocket/API/SocketFrame.cs b/src/Discord.Net.WebSocket/API/SocketFrame.cs index fd9367ca4..fae741432 100644 --- a/src/Discord.Net.WebSocket/API/SocketFrame.cs +++ b/src/Discord.Net.WebSocket/API/SocketFrame.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API { - public class SocketFrame + internal class SocketFrame { [JsonProperty("op")] public int Operation { get; set; } diff --git a/src/Discord.Net.WebSocket/API/Voice/IdentifyParams.cs b/src/Discord.Net.WebSocket/API/Voice/IdentifyParams.cs index e6a1fd288..d446867e1 100644 --- a/src/Discord.Net.WebSocket/API/Voice/IdentifyParams.cs +++ b/src/Discord.Net.WebSocket/API/Voice/IdentifyParams.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Voice { - public class IdentifyParams + internal class IdentifyParams { [JsonProperty("server_id")] public ulong GuildId { get; set; } diff --git a/src/Discord.Net.WebSocket/API/Voice/ReadyEvent.cs b/src/Discord.Net.WebSocket/API/Voice/ReadyEvent.cs index 0fe52a587..e4446f814 100644 --- a/src/Discord.Net.WebSocket/API/Voice/ReadyEvent.cs +++ b/src/Discord.Net.WebSocket/API/Voice/ReadyEvent.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Voice { - public class ReadyEvent + internal class ReadyEvent { [JsonProperty("ssrc")] public uint SSRC { get; set; } diff --git a/src/Discord.Net.WebSocket/API/Voice/SelectProtocolParams.cs b/src/Discord.Net.WebSocket/API/Voice/SelectProtocolParams.cs index 4c4e3ca32..8c577e5b5 100644 --- a/src/Discord.Net.WebSocket/API/Voice/SelectProtocolParams.cs +++ b/src/Discord.Net.WebSocket/API/Voice/SelectProtocolParams.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Voice { - public class SelectProtocolParams + internal class SelectProtocolParams { [JsonProperty("protocol")] public string Protocol { get; set; } diff --git a/src/Discord.Net.WebSocket/API/Voice/SessionDescriptionEvent.cs b/src/Discord.Net.WebSocket/API/Voice/SessionDescriptionEvent.cs index 5a8ec8a41..45befadcf 100644 --- a/src/Discord.Net.WebSocket/API/Voice/SessionDescriptionEvent.cs +++ b/src/Discord.Net.WebSocket/API/Voice/SessionDescriptionEvent.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Voice { - public class SessionDescriptionEvent + internal class SessionDescriptionEvent { [JsonProperty("secret_key")] public byte[] SecretKey { get; set; } diff --git a/src/Discord.Net.WebSocket/API/Voice/SpeakingParams.cs b/src/Discord.Net.WebSocket/API/Voice/SpeakingParams.cs index ee0e737dc..abdf90667 100644 --- a/src/Discord.Net.WebSocket/API/Voice/SpeakingParams.cs +++ b/src/Discord.Net.WebSocket/API/Voice/SpeakingParams.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Voice { - public class SpeakingParams + internal class SpeakingParams { [JsonProperty("speaking")] public bool IsSpeaking { get; set; } diff --git a/src/Discord.Net.WebSocket/API/Voice/UdpProtocolInfo.cs b/src/Discord.Net.WebSocket/API/Voice/UdpProtocolInfo.cs index eb57471a4..6f4719e7e 100644 --- a/src/Discord.Net.WebSocket/API/Voice/UdpProtocolInfo.cs +++ b/src/Discord.Net.WebSocket/API/Voice/UdpProtocolInfo.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Voice { - public class UdpProtocolInfo + internal class UdpProtocolInfo { [JsonProperty("address")] public string Address { get; set; } diff --git a/src/Discord.Net.WebSocket/API/Voice/VoiceOpCode.cs b/src/Discord.Net.WebSocket/API/Voice/VoiceOpCode.cs index e7cda249d..ae11a4c8f 100644 --- a/src/Discord.Net.WebSocket/API/Voice/VoiceOpCode.cs +++ b/src/Discord.Net.WebSocket/API/Voice/VoiceOpCode.cs @@ -1,7 +1,7 @@ #pragma warning disable CS1591 namespace Discord.API.Voice { - public enum VoiceOpCode : byte + internal enum VoiceOpCode : byte { /// C→S - Used to associate a connection with a token. Identify = 0, diff --git a/src/Discord.Net.WebSocket/AssemblyInfo.cs b/src/Discord.Net.WebSocket/AssemblyInfo.cs index c6b5997b4..ca3e05e2f 100644 --- a/src/Discord.Net.WebSocket/AssemblyInfo.cs +++ b/src/Discord.Net.WebSocket/AssemblyInfo.cs @@ -1,3 +1,4 @@ using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("Discord.Net.Relay")] [assembly: InternalsVisibleTo("Discord.Net.Tests")] \ No newline at end of file diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index 04eec4541..e2586d0f3 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -1,20 +1,21 @@ using Discord.API.Voice; +using Discord.Audio.Streams; using Discord.Logging; using Discord.Net.Converters; using Discord.WebSocket; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; -using System.IO; +using System.Collections.Concurrent; using System.Linq; -using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Discord.Audio { - public class AudioClient : IAudioClient, IDisposable + //TODO: Add audio reconnecting + internal class AudioClient : IAudioClient, IDisposable { public event Func Connected { @@ -36,158 +37,139 @@ namespace Discord.Audio private readonly AsyncEvent> _latencyUpdatedEvent = new AsyncEvent>(); private readonly Logger _audioLogger; - internal readonly SemaphoreSlim _connectionLock; private readonly JsonSerializer _serializer; + private readonly ConnectionManager _connection; + private readonly SemaphoreSlim _stateLock; + private readonly ConcurrentQueue _heartbeatTimes; - private TaskCompletionSource _connectTask; - private CancellationTokenSource _cancelToken; private Task _heartbeatTask; - private long _heartbeatTime; - private string _url; - private bool _isDisposed; + private long _lastMessageTime; + private string _url, _sessionId, _token; + private ulong _userId; private uint _ssrc; private byte[] _secretKey; public SocketGuild Guild { get; } public DiscordVoiceAPIClient ApiClient { get; private set; } - public ConnectionState ConnectionState { get; private set; } public int Latency { 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) { Guild = guild; - _audioLogger = Discord.LogManager.CreateLogger($"Audio #{id}"); - _connectionLock = new SemaphoreSlim(1, 1); + ApiClient = new DiscordVoiceAPIClient(guild.Id, Discord.WebSocketProvider, Discord.UdpSocketProvider); + ApiClient.SentGatewayMessage += async opCode => await _audioLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false); + ApiClient.SentDiscovery += async () => await _audioLogger.DebugAsync($"Sent Discovery").ConfigureAwait(false); + //ApiClient.SentData += async bytes => await _audioLogger.DebugAsync($"Sent {bytes} Bytes").ConfigureAwait(false); + ApiClient.ReceivedEvent += ProcessMessageAsync; + ApiClient.ReceivedPacket += ProcessPacketAsync; + _stateLock = new SemaphoreSlim(1, 1); + _connection = new ConnectionManager(_stateLock, _audioLogger, 30000, + OnConnectingAsync, OnDisconnectingAsync, x => ApiClient.Disconnected += x); + _connection.Connected += () => _connectedEvent.InvokeAsync(); + _connection.Disconnected += (ex, recon) => _disconnectedEvent.InvokeAsync(ex); + _heartbeatTimes = new ConcurrentQueue(); + _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; _serializer.Error += (s, e) => { _audioLogger.WarningAsync(e.ErrorContext.Error).GetAwaiter().GetResult(); e.ErrorContext.Handled = true; }; - - ApiClient = new DiscordVoiceAPIClient(guild.Id, Discord.WebSocketProvider); - - ApiClient.SentGatewayMessage += async opCode => await _audioLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false); - ApiClient.SentDiscovery += async () => await _audioLogger.DebugAsync($"Sent Discovery").ConfigureAwait(false); - ApiClient.SentData += async bytes => await _audioLogger.DebugAsync($"Sent {bytes} Bytes").ConfigureAwait(false); - ApiClient.ReceivedEvent += ProcessMessageAsync; - ApiClient.ReceivedPacket += ProcessPacketAsync; - ApiClient.Disconnected += async ex => - { - if (ex != null) - await _audioLogger.WarningAsync($"Connection Closed", ex).ConfigureAwait(false); - else - await _audioLogger.WarningAsync($"Connection Closed").ConfigureAwait(false); - }; LatencyUpdated += async (old, val) => await _audioLogger.VerboseAsync($"Latency = {val} ms").ConfigureAwait(false); } - /// - internal async Task ConnectAsync(string url, ulong userId, string sessionId, string token) + internal async Task StartAsync(string url, ulong userId, string sessionId, string token) { - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - await ConnectInternalAsync(url, userId, sessionId, token).ConfigureAwait(false); - } - finally { _connectionLock.Release(); } + _url = url; + _userId = userId; + _sessionId = sessionId; + _token = token; + await _connection.StartAsync().ConfigureAwait(false); } - private async Task ConnectInternalAsync(string url, ulong userId, string sessionId, string token) - { - var state = ConnectionState; - if (state == ConnectionState.Connecting || state == ConnectionState.Connected) - await DisconnectInternalAsync(null).ConfigureAwait(false); - - ConnectionState = ConnectionState.Connecting; - await _audioLogger.InfoAsync("Connecting").ConfigureAwait(false); - try - { - _url = url; - _connectTask = new TaskCompletionSource(); - _cancelToken = new CancellationTokenSource(); + public async Task StopAsync() + => await _connection.StopAsync().ConfigureAwait(false); - await ApiClient.ConnectAsync("wss://" + url).ConfigureAwait(false); - await ApiClient.SendIdentityAsync(userId, sessionId, token).ConfigureAwait(false); - await _connectTask.Task.ConfigureAwait(false); - - await _connectedEvent.InvokeAsync().ConfigureAwait(false); - ConnectionState = ConnectionState.Connected; - await _audioLogger.InfoAsync("Connected").ConfigureAwait(false); - } - catch (Exception) - { - await DisconnectInternalAsync(null).ConfigureAwait(false); - throw; - } - } - /// - public async Task DisconnectAsync() - { - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - await DisconnectInternalAsync(null).ConfigureAwait(false); - } - finally { _connectionLock.Release(); } - } - private async Task DisconnectAsync(Exception ex) + private async Task OnConnectingAsync() { - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - await DisconnectInternalAsync(ex).ConfigureAwait(false); - } - finally { _connectionLock.Release(); } + await _audioLogger.DebugAsync("Connecting ApiClient").ConfigureAwait(false); + await ApiClient.ConnectAsync("wss://" + _url).ConfigureAwait(false); + await _audioLogger.DebugAsync("Sending Identity").ConfigureAwait(false); + await ApiClient.SendIdentityAsync(_userId, _sessionId, _token).ConfigureAwait(false); + + //Wait for READY + await _connection.WaitAsync().ConfigureAwait(false); } - private async Task DisconnectInternalAsync(Exception ex) + private async Task OnDisconnectingAsync(Exception ex) { - if (ConnectionState == ConnectionState.Disconnected) return; - ConnectionState = ConnectionState.Disconnecting; - await _audioLogger.InfoAsync("Disconnecting").ConfigureAwait(false); - - //Signal tasks to complete - try { _cancelToken.Cancel(); } catch { } - - //Disconnect from server + await _audioLogger.DebugAsync("Disconnecting ApiClient").ConfigureAwait(false); await ApiClient.DisconnectAsync().ConfigureAwait(false); //Wait for tasks to complete + await _audioLogger.DebugAsync("Waiting for heartbeater").ConfigureAwait(false); var heartbeatTask = _heartbeatTask; if (heartbeatTask != null) await heartbeatTask.ConfigureAwait(false); _heartbeatTask = null; - ConnectionState = ConnectionState.Disconnected; - await _audioLogger.InfoAsync("Disconnected").ConfigureAwait(false); + long time; + while (_heartbeatTimes.TryDequeue(out time)) { } + _lastMessageTime = 0; - await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); + await _audioLogger.DebugAsync("Sending Voice State").ConfigureAwait(false); + await Discord.ApiClient.SendVoiceStateUpdateAsync(Guild.Id, null, false, false).ConfigureAwait(false); } - public void Send(byte[] data, int count) + public AudioOutStream CreateOpusStream(int samplesPerFrame, int bufferMillis) { - //TODO: Queue these? - ApiClient.SendAsync(data, count).ConfigureAwait(false); + 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); } - - public Stream CreateOpusStream(int samplesPerFrame, int bufferSize = 4000) + public AudioOutStream CreateDirectOpusStream(int samplesPerFrame) + { + CheckSamplesPerFrame(samplesPerFrame); + var outputStream = new OutputStream(ApiClient); + var sodiumEncrypter = new SodiumEncryptStream(outputStream, _secretKey); + return new RTPWriteStream(sodiumEncrypter, samplesPerFrame, _ssrc); + } + public AudioOutStream CreatePCMStream(AudioApplication application, int samplesPerFrame, int channels, int? bitrate, int bufferMillis) { - return new RTPWriteStream(this, _secretKey, samplesPerFrame, _ssrc, bufferSize = 4000); + 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); } - public Stream CreatePCMStream(int samplesPerFrame, int? bitrate = null, - OpusApplication application = OpusApplication.MusicOrMixed, int bufferSize = 4000) + public AudioOutStream CreateDirectPCMStream(AudioApplication application, int samplesPerFrame, int channels, int? bitrate) { - return new OpusEncodeStream(this, _secretKey, samplesPerFrame, _ssrc, bitrate, application, bufferSize); + 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); + } + private void CheckSamplesPerFrame(int samplesPerFrame) + { + 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)); } private async Task ProcessMessageAsync(VoiceOpCode opCode, object payload) { + _lastMessageTime = Environment.TickCount; + try { switch (opCode) @@ -202,12 +184,9 @@ namespace Discord.Audio if (!data.Modes.Contains(DiscordVoiceAPIClient.Mode)) throw new InvalidOperationException($"Discord does not support {DiscordVoiceAPIClient.Mode}"); - _heartbeatTime = 0; - _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _cancelToken.Token); - - var entry = await Dns.GetHostEntryAsync(_url).ConfigureAwait(false); - - ApiClient.SetUdpEndpoint(new IPEndPoint(entry.AddressList[0], data.Port)); + _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _connection.CancelToken); + + ApiClient.SetUdpEndpoint(_url, data.Port); await ApiClient.SendDiscoveryAsync(_ssrc).ConfigureAwait(false); } break; @@ -222,19 +201,17 @@ namespace Discord.Audio _secretKey = data.SecretKey; await ApiClient.SendSetSpeaking(true).ConfigureAwait(false); - var _ = _connectTask.TrySetResultAsync(true); + var _ = _connection.CompleteAsync(); } break; case VoiceOpCode.HeartbeatAck: { await _audioLogger.DebugAsync("Received HeartbeatAck").ConfigureAwait(false); - var heartbeatTime = _heartbeatTime; - if (heartbeatTime != 0) + long time; + if (_heartbeatTimes.TryDequeue(out time)) { - int latency = (int)(Environment.TickCount - _heartbeatTime); - _heartbeatTime = 0; - + int latency = (int)(Environment.TickCount - time); int before = Latency; Latency = latency; @@ -255,7 +232,7 @@ namespace Discord.Audio } private async Task ProcessPacketAsync(byte[] packet) { - if (!_connectTask.Task.IsCompleted) + if (!_connection.IsCompleted) { if (packet.Length == 70) { @@ -276,35 +253,53 @@ namespace Discord.Audio private async Task RunHeartbeatAsync(int intervalMillis, CancellationToken cancelToken) { - //Clean this up when Discord's session patch is live + //TODO: Clean this up when Discord's session patch is live try { + await _audioLogger.DebugAsync("Heartbeat Started").ConfigureAwait(false); while (!cancelToken.IsCancellationRequested) { - await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false); + var now = Environment.TickCount; - if (_heartbeatTime != 0) //Server never responded to our last heartbeat + //Did server respond to our last heartbeat? + if (_heartbeatTimes.Count != 0 && (now - _lastMessageTime) > intervalMillis && + ConnectionState == ConnectionState.Connected) { - if (ConnectionState == ConnectionState.Connected) - { - await _audioLogger.WarningAsync("Server missed last heartbeat").ConfigureAwait(false); - await DisconnectInternalAsync(new Exception("Server missed last heartbeat")).ConfigureAwait(false); - return; - } + _connection.Error(new Exception("Server missed last heartbeat")); + return; + } + + _heartbeatTimes.Enqueue(now); + try + { + await ApiClient.SendHeartbeatAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + await _audioLogger.WarningAsync("Heartbeat Errored", ex).ConfigureAwait(false); } - else - _heartbeatTime = Environment.TickCount; - await ApiClient.SendHeartbeatAsync().ConfigureAwait(false); + + await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false); } + await _audioLogger.DebugAsync("Heartbeat Stopped").ConfigureAwait(false); + } + catch (OperationCanceledException) + { + await _audioLogger.DebugAsync("Heartbeat Stopped").ConfigureAwait(false); + } + catch (Exception ex) + { + await _audioLogger.ErrorAsync("Heartbeat Errored", ex).ConfigureAwait(false); } - catch (OperationCanceledException) { } } internal void Dispose(bool disposing) { - if (!_isDisposed) - _isDisposed = true; - ApiClient.Dispose(); + if (disposing) + { + StopAsync().GetAwaiter().GetResult(); + ApiClient.Dispose(); + } } /// public void Dispose() => Dispose(true); diff --git a/src/Discord.Net.Core/Audio/Opus/OpusApplication.cs b/src/Discord.Net.WebSocket/Audio/Opus/OpusApplication.cs similarity index 75% rename from src/Discord.Net.Core/Audio/Opus/OpusApplication.cs rename to src/Discord.Net.WebSocket/Audio/Opus/OpusApplication.cs index d6a3ce0cf..e288bb626 100644 --- a/src/Discord.Net.Core/Audio/Opus/OpusApplication.cs +++ b/src/Discord.Net.WebSocket/Audio/Opus/OpusApplication.cs @@ -1,6 +1,6 @@ namespace Discord.Audio { - public enum OpusApplication : int + internal enum OpusApplication : int { Voice = 2048, MusicOrMixed = 2049, diff --git a/src/Discord.Net.WebSocket/Audio/Opus/OpusCtl.cs b/src/Discord.Net.WebSocket/Audio/Opus/OpusCtl.cs index e71213ae6..0b6a4e37f 100644 --- a/src/Discord.Net.WebSocket/Audio/Opus/OpusCtl.cs +++ b/src/Discord.Net.WebSocket/Audio/Opus/OpusCtl.cs @@ -1,10 +1,12 @@ namespace Discord.Audio { + //https://github.com/gcp/opus/blob/master/include/opus_defines.h internal enum OpusCtl : int { - SetBitrateRequest = 4002, - GetBitrateRequest = 4003, - SetInbandFECRequest = 4012, - GetInbandFECRequest = 4013 + SetBitrate = 4002, + SetBandwidth = 4008, + SetInbandFEC = 4012, + SetPacketLossPercent = 4014, + SetSignal = 4024 } } diff --git a/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs b/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs index ea9376f82..b2ecf5987 100644 --- a/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs +++ b/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs @@ -11,6 +11,8 @@ namespace Discord.Audio private static extern void DestroyDecoder(IntPtr decoder); [DllImport("opus", EntryPoint = "opus_decode", CallingConvention = CallingConvention.Cdecl)] private static extern int Decode(IntPtr st, byte* data, int len, byte* pcm, int max_frame_size, int decode_fec); + [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) diff --git a/src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs b/src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs index c1eb3843d..1f0b35d77 100644 --- a/src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs +++ b/src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs @@ -15,17 +15,65 @@ namespace Discord.Audio private static extern int EncoderCtl(IntPtr st, OpusCtl request, int value); /// Gets the coding mode of the encoder. - public OpusApplication Application { get; } + public AudioApplication Application { get; } + public int BitRate { get;} - public OpusEncoder(int samplingRate, int channels, OpusApplication application = OpusApplication.MusicOrMixed) + public OpusEncoder(int samplingRate, int channels, int bitrate, AudioApplication application) : base(samplingRate, channels) { + if (bitrate < 1 || bitrate > DiscordVoiceAPIClient.MaxBitrate) + throw new ArgumentOutOfRangeException(nameof(bitrate)); + Application = application; + BitRate = bitrate; + + OpusApplication opusApplication; + OpusSignal opusSignal; + switch (application) + { + case AudioApplication.Mixed: + opusApplication = OpusApplication.MusicOrMixed; + opusSignal = OpusSignal.Auto; + break; + case AudioApplication.Music: + opusApplication = OpusApplication.MusicOrMixed; + opusSignal = OpusSignal.Music; + break; + case AudioApplication.Voice: + opusApplication = OpusApplication.Voice; + opusSignal = OpusSignal.Voice; + break; + default: + throw new ArgumentOutOfRangeException(nameof(application)); + } OpusError error; - _ptr = CreateEncoder(samplingRate, channels, (int)application, out 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}"); + }*/ } /// Produces Opus encoded audio from PCM samples. @@ -44,25 +92,6 @@ namespace Discord.Audio return result; } - /// Gets or sets whether Forward Error Correction is enabled. - public void SetForwardErrorCorrection(bool value) - { - var result = EncoderCtl(_ptr, OpusCtl.SetInbandFECRequest, value ? 1 : 0); - if (result < 0) - throw new Exception($"Opus Error: {(OpusError)result}"); - } - - /// Gets or sets whether Forward Error Correction is enabled. - public void SetBitrate(int value) - { - if (value < 1 || value > DiscordVoiceAPIClient.MaxBitrate) - throw new ArgumentOutOfRangeException(nameof(value)); - - var result = EncoderCtl(_ptr, OpusCtl.SetBitrateRequest, value * 1000); - if (result < 0) - throw new Exception($"Opus Error: {(OpusError)result}"); - } - protected override void Dispose(bool disposing) { if (_ptr != IntPtr.Zero) diff --git a/src/Discord.Net.WebSocket/Audio/Opus/OpusSignal.cs b/src/Discord.Net.WebSocket/Audio/Opus/OpusSignal.cs new file mode 100644 index 000000000..3f95183f4 --- /dev/null +++ b/src/Discord.Net.WebSocket/Audio/Opus/OpusSignal.cs @@ -0,0 +1,9 @@ +namespace Discord.Audio +{ + internal enum OpusSignal : int + { + Auto = -1000, + Voice = 3001, + Music = 3002, + } +} diff --git a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs new file mode 100644 index 000000000..dcd053cc1 --- /dev/null +++ b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs @@ -0,0 +1,162 @@ +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. + public class BufferedWriteStream : AudioOutStream + { + private struct Frame + { + public Frame(byte[] buffer, int bytes) + { + Buffer = buffer; + Bytes = bytes; + } + + public readonly byte[] Buffer; + public readonly int Bytes; + } + + private static readonly byte[] _silenceFrame = new byte[0]; + + private readonly AudioOutStream _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; + + 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) + { + //maxFrameSize = 1275 was too limiting at 128kbps,2ch,60ms + _next = next; + _ticksPerFrame = samplesPerFrame / 48; + _logger = logger; + _queueLength = (bufferMillis + (_ticksPerFrame - 1)) / _ticksPerFrame; //Round up + + _cancelTokenSource = new CancellationTokenSource(); + _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, cancelToken).Token; + _queuedFrames = new ConcurrentQueue(); + _bufferPool = new ConcurrentQueue(); + for (int i = 0; i < _queueLength; i++) + _bufferPool.Enqueue(new byte[maxFrameSize]); + _queueLock = new SemaphoreSlim(_queueLength, _queueLength); + + _task = Run(); + } + + 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; + while (!_cancelToken.IsCancellationRequested) + { + long tick = Environment.TickCount; + long dist = nextTick - tick; + if (dist <= 0) + { + Frame frame; + if (_queuedFrames.TryDequeue(out frame)) + { + await _next.WriteAsync(frame.Buffer, 0, frame.Bytes).ConfigureAwait(false); + _bufferPool.Enqueue(frame.Buffer); + _queueLock.Release(); + nextTick += _ticksPerFrame; +#if DEBUG + var _ = _logger.DebugAsync($"{num++}: Sent {frame.Bytes} bytes ({_queuedFrames.Count} frames buffered)"); +#endif + } + else + { + while ((nextTick - tick) <= 0) + { + await _next.WriteAsync(_silenceFrame, 0, _silenceFrame.Length).ConfigureAwait(false); + nextTick += _ticksPerFrame; + } +#if DEBUG + var _ = _logger.DebugAsync($"{num++}: Buffer underrun"); +#endif + } + } + else + await Task.Delay((int)(dist)/*, _cancelToken*/).ConfigureAwait(false); + } + } + catch (OperationCanceledException) { } + }); + } + + 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; + + await _queueLock.WaitAsync(-1, cancelToken).ConfigureAwait(false); + byte[] buffer; + if (!_bufferPool.TryDequeue(out buffer)) + { +#if DEBUG + 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"); +#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) + { + Frame ignored; + do + cancelToken.ThrowIfCancellationRequested(); + while (_queuedFrames.TryDequeue(out 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 new file mode 100644 index 000000000..d46db128b --- /dev/null +++ b/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Audio.Streams +{ + /// Reads the payload from an RTP frame + public class InputStream : AudioInStream + { + private ConcurrentQueue _frames; + private ushort _nextSeq; + private uint _nextTimestamp; + private bool _hasHeader; + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => true; + + public InputStream(byte[] secretKey) + { + _frames = new ConcurrentQueue(); + } + + 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) + { + cancelToken.ThrowIfCancellationRequested(); + + if (_frames.TryDequeue(out var frame)) + { + 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); + } + return Task.FromResult(0); + } + + public void WriteHeader(ushort seq, uint timestamp) + { + if (_hasHeader) + throw new InvalidOperationException("Header received with no payload"); + _hasHeader = true; + _nextSeq = seq; + _nextTimestamp = timestamp; + } + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) + { + cancelToken.ThrowIfCancellationRequested(); + + if (_frames.Count > 1000) + return Task.Delay(0); //Buffer overloaded + if (_hasHeader) + throw new InvalidOperationException("Received payload with an RTP header"); + byte[] payload = new byte[count]; + Buffer.BlockCopy(buffer, offset, payload, 0, count); + + _frames.Enqueue(new RTPFrame( + sequence: _nextSeq, + timestamp: _nextTimestamp, + payload: payload + )); + _hasHeader = false; + return Task.Delay(0); + } + } +} diff --git a/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs index 3a650eeaf..9df553bfe 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs @@ -1,22 +1,35 @@ -namespace Discord.Audio +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Audio.Streams { - internal class OpusDecodeStream : RTPReadStream + /// Converts Opus to PCM + public class OpusDecodeStream : AudioOutStream { + private readonly AudioOutStream _next; private readonly byte[] _buffer; private readonly OpusDecoder _decoder; - internal OpusDecodeStream(AudioClient audioClient, byte[] secretKey, int samplingRate, - int channels = OpusConverter.MaxChannels, int bufferSize = 4000) - : base(audioClient, secretKey) + public OpusDecodeStream(AudioOutStream next, int samplingRate, int channels = OpusConverter.MaxChannels, int bufferSize = 4000) { + _next = next; _buffer = new byte[bufferSize]; _decoder = new OpusDecoder(samplingRate, channels); } - public override int Read(byte[] buffer, int offset, int count) + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { count = _decoder.DecodeFrame(buffer, offset, count, _buffer, 0); - return base.Read(_buffer, 0, count); + await _next.WriteAsync(_buffer, 0, count, cancellationToken).ConfigureAwait(false); + } + + public override async Task FlushAsync(CancellationToken cancelToken) + { + await _next.FlushAsync(cancelToken).ConfigureAwait(false); + } + public override async Task ClearAsync(CancellationToken cancelToken) + { + await _next.ClearAsync(cancelToken).ConfigureAwait(false); } protected override void Dispose(bool disposing) diff --git a/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs index 69d8b3d81..ada8311fe 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs @@ -1,27 +1,76 @@ -namespace Discord.Audio +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Audio.Streams { - internal class OpusEncodeStream : RTPWriteStream + /// Converts PCM to Opus + public class OpusEncodeStream : AudioOutStream { - public int SampleRate = 48000; - public int Channels = 2; + public const int SampleRate = 48000; + private readonly AudioOutStream _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) + { + _next = next; + _encoder = new OpusEncoder(SampleRate, channels, bitrate, application); + _frameSize = samplesPerFrame * channels * 2; + _buffer = new byte[bufferSize]; + _partialFrameBuffer = new byte[_frameSize]; + } - internal OpusEncodeStream(AudioClient audioClient, byte[] secretKey, int samplesPerFrame, uint ssrc, int? bitrate = null, - OpusApplication application = OpusApplication.MusicOrMixed, int bufferSize = 4000) - : base(audioClient, secretKey, samplesPerFrame, ssrc, bufferSize) + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { - _encoder = new OpusEncoder(SampleRate, Channels); + //Assume threadsafe + while (count > 0) + { + if (_partialFramePos + count >= _frameSize) + { + int partialSize = _frameSize - _partialFramePos; + Buffer.BlockCopy(buffer, offset, _partialFrameBuffer, _partialFramePos, partialSize); + offset += partialSize; + count -= partialSize; + _partialFramePos = 0; - _encoder.SetForwardErrorCorrection(true); - if (bitrate != null) - _encoder.SetBitrate(bitrate.Value); + int encFrameSize = _encoder.EncodeFrame(_partialFrameBuffer, 0, _frameSize, _buffer, 0); + await _next.WriteAsync(_buffer, 0, encFrameSize, cancellationToken).ConfigureAwait(false); + } + else + { + Buffer.BlockCopy(buffer, offset, _partialFrameBuffer, _partialFramePos, count); + _partialFramePos += count; + break; + } + } } - public override void Write(byte[] buffer, int offset, int count) + /* + public override async Task FlushAsync(CancellationToken cancellationToken) + { + try + { + int encFrameSize = _encoder.EncodeFrame(_partialFrameBuffer, 0, _partialFramePos, _buffer, 0); + base.Write(_buffer, 0, encFrameSize); + } + catch (Exception) { } //Incomplete frame + _partialFramePos = 0; + await base.FlushAsync(cancellationToken).ConfigureAwait(false); + }*/ + + public override async Task FlushAsync(CancellationToken cancelToken) + { + await _next.FlushAsync(cancelToken).ConfigureAwait(false); + } + public override async Task ClearAsync(CancellationToken cancelToken) { - count = _encoder.EncodeFrame(buffer, offset, count, _buffer, 0); - base.Write(_buffer, 0, count); + await _next.ClearAsync(cancelToken).ConfigureAwait(false); } protected override void Dispose(bool disposing) diff --git a/src/Discord.Net.WebSocket/Audio/Streams/OutputStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/OutputStream.cs new file mode 100644 index 000000000..6238e93b4 --- /dev/null +++ b/src/Discord.Net.WebSocket/Audio/Streams/OutputStream.cs @@ -0,0 +1,23 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Audio.Streams +{ + /// Wraps an IAudioClient, sending voice data on write. + public class OutputStream : AudioOutStream + { + private readonly DiscordVoiceAPIClient _client; + public OutputStream(IAudioClient client) + : this((client as AudioClient).ApiClient) { } + internal OutputStream(DiscordVoiceAPIClient client) + { + _client = client; + } + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) + { + cancelToken.ThrowIfCancellationRequested(); + await _client.SendAsync(buffer, offset, count).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs index cfc804abe..9a57612bf 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs @@ -1,53 +1,49 @@ using System; -using System.Collections.Concurrent; using System.IO; +using System.Threading; +using System.Threading.Tasks; -namespace Discord.Audio +namespace Discord.Audio.Streams { - internal class RTPReadStream : Stream + /// Reads the payload from an RTP frame + public class RTPReadStream : AudioOutStream { - private readonly BlockingCollection _queuedData; //TODO: Replace with max-length ring buffer - private readonly AudioClient _audioClient; + private readonly InputStream _queue; + private readonly AudioOutStream _next; private readonly byte[] _buffer, _nonce, _secretKey; public override bool CanRead => true; public override bool CanSeek => false; public override bool CanWrite => true; - internal RTPReadStream(AudioClient audioClient, byte[] secretKey, int bufferSize = 4000) + 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) { - _audioClient = audioClient; + _queue = queue; + _next = next; _secretKey = secretKey; _buffer = new byte[bufferSize]; - _queuedData = new BlockingCollection(100); _nonce = new byte[24]; } - public override int Read(byte[] buffer, int offset, int count) + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) { - var queuedData = _queuedData.Take(); - Buffer.BlockCopy(queuedData, 0, buffer, offset, Math.Min(queuedData.Length, count)); - return queuedData.Length; - } - public override void Write(byte[] buffer, int offset, int count) - { - Buffer.BlockCopy(buffer, 0, _nonce, 0, 12); - count = SecretBox.Decrypt(buffer, offset, count, _buffer, 0, _nonce, _secretKey); - var newBuffer = new byte[count]; - Buffer.BlockCopy(_buffer, 0, newBuffer, 0, count); - _queuedData.Add(newBuffer); - } + cancelToken.ThrowIfCancellationRequested(); - public override void Flush() { throw new NotSupportedException(); } + var payload = new byte[count - 12]; + Buffer.BlockCopy(buffer, offset + 12, payload, 0, count - 12); - public override long Length { get { throw new NotSupportedException(); } } - public override long Position - { - get { throw new NotSupportedException(); } - set { throw new NotSupportedException(); } - } + ushort seq = (ushort)((buffer[offset + 3] << 8) | + (buffer[offset + 2] << 0)); + + uint timestamp = (uint)((buffer[offset + 4] << 24) | + (buffer[offset + 5] << 16) | + (buffer[offset + 6] << 16) | + (buffer[offset + 7] << 0)); - public override void SetLength(long value) { throw new NotSupportedException(); } - public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); } + _queue.WriteHeader(seq, timestamp); + await (_next ?? _queue as Stream).WriteAsync(buffer, offset, count, cancelToken).ConfigureAwait(false); + } } } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs index db755c877..836cb4852 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs @@ -1,67 +1,62 @@ using System; -using System.IO; +using System.Threading; +using System.Threading.Tasks; -namespace Discord.Audio +namespace Discord.Audio.Streams { - internal class RTPWriteStream : Stream + /// Wraps data in an RTP frame + public class RTPWriteStream : AudioOutStream { - private readonly AudioClient _audioClient; - private readonly byte[] _nonce, _secretKey; + private readonly AudioOutStream _next; + private readonly byte[] _header; private int _samplesPerFrame; private uint _ssrc, _timestamp = 0; protected readonly byte[] _buffer; - public override bool CanRead => false; - public override bool CanSeek => false; - public override bool CanWrite => true; - - internal RTPWriteStream(AudioClient audioClient, byte[] secretKey, int samplesPerFrame, uint ssrc, int bufferSize = 4000) + public RTPWriteStream(AudioOutStream next, int samplesPerFrame, uint ssrc, int bufferSize = 4000) { - _audioClient = audioClient; - _secretKey = secretKey; + _next = next; _samplesPerFrame = samplesPerFrame; _ssrc = ssrc; _buffer = new byte[bufferSize]; - _nonce = new byte[24]; - _nonce[0] = 0x80; - _nonce[1] = 0x78; - _nonce[8] = (byte)(_ssrc >> 24); - _nonce[9] = (byte)(_ssrc >> 16); - _nonce[10] = (byte)(_ssrc >> 8); - _nonce[11] = (byte)(_ssrc >> 0); + _header = new byte[24]; + _header[0] = 0x80; + _header[1] = 0x78; + _header[8] = (byte)(_ssrc >> 24); + _header[9] = (byte)(_ssrc >> 16); + _header[10] = (byte)(_ssrc >> 8); + _header[11] = (byte)(_ssrc >> 0); } - public override void Write(byte[] buffer, int offset, int count) + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); + unchecked { - if (_nonce[3]++ == byte.MaxValue) - _nonce[2]++; + if (_header[3]++ == byte.MaxValue) + _header[2]++; _timestamp += (uint)_samplesPerFrame; - _nonce[4] = (byte)(_timestamp >> 24); - _nonce[5] = (byte)(_timestamp >> 16); - _nonce[6] = (byte)(_timestamp >> 8); - _nonce[7] = (byte)(_timestamp >> 0); + _header[4] = (byte)(_timestamp >> 24); + _header[5] = (byte)(_timestamp >> 16); + _header[6] = (byte)(_timestamp >> 8); + _header[7] = (byte)(_timestamp >> 0); } + Buffer.BlockCopy(_header, 0, _buffer, 0, 12); //Copy RTP header from to the buffer + Buffer.BlockCopy(buffer, offset, _buffer, 12, count); - count = SecretBox.Encrypt(buffer, offset, count, _buffer, 12, _nonce, _secretKey); - Buffer.BlockCopy(_nonce, 0, _buffer, 0, 12); //Copy the RTP header from nonce to buffer - _audioClient.Send(_buffer, count + 12); + await _next.WriteAsync(_buffer, 0, count + 12).ConfigureAwait(false); } - public override void Flush() { } - - public override long Length { get { throw new NotSupportedException(); } } - public override long Position + public override async Task FlushAsync(CancellationToken cancelToken) { - get { throw new NotSupportedException(); } - set { throw new NotSupportedException(); } + await _next.FlushAsync(cancelToken).ConfigureAwait(false); + } + public override async Task ClearAsync(CancellationToken cancelToken) + { + await _next.ClearAsync(cancelToken).ConfigureAwait(false); } - - 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.WebSocket/Audio/Streams/SodiumDecryptStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/SodiumDecryptStream.cs new file mode 100644 index 000000000..f1421d28b --- /dev/null +++ b/src/Discord.Net.WebSocket/Audio/Streams/SodiumDecryptStream.cs @@ -0,0 +1,46 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Audio.Streams +{ + /// Decrypts an RTP frame using libsodium + public class SodiumDecryptStream : AudioOutStream + { + private readonly AudioOutStream _next; + private readonly byte[] _buffer, _nonce, _secretKey; + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => true; + + public SodiumDecryptStream(AudioOutStream next, byte[] secretKey, int bufferSize = 4000) + { + _next = next; + _secretKey = secretKey; + _buffer = new byte[bufferSize]; + _nonce = new byte[24]; + } + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) + { + cancelToken.ThrowIfCancellationRequested(); + + Buffer.BlockCopy(buffer, 0, _nonce, 0, 12); //Copy RTP header to nonce + count = SecretBox.Decrypt(buffer, offset, count, _buffer, 0, _nonce, _secretKey); + + var newBuffer = new byte[count]; + Buffer.BlockCopy(_buffer, 0, newBuffer, 0, count); + await _next.WriteAsync(buffer, 0, count + 12, cancelToken).ConfigureAwait(false); + } + + public override async Task FlushAsync(CancellationToken cancelToken) + { + await _next.FlushAsync(cancelToken).ConfigureAwait(false); + } + public override async Task ClearAsync(CancellationToken cancelToken) + { + await _next.ClearAsync(cancelToken).ConfigureAwait(false); + } + } +} diff --git a/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs new file mode 100644 index 000000000..90bc35e9d --- /dev/null +++ b/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs @@ -0,0 +1,41 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Audio.Streams +{ + /// Encrypts an RTP frame using libsodium + public class SodiumEncryptStream : AudioOutStream + { + private readonly AudioOutStream _next; + private readonly byte[] _nonce, _secretKey; + + //protected readonly byte[] _buffer; + + public SodiumEncryptStream(AudioOutStream next, byte[] secretKey/*, int bufferSize = 4000*/) + { + _next = next; + _secretKey = secretKey; + //_buffer = new byte[bufferSize]; //TODO: Can Sodium do an in-place encrypt? + _nonce = new byte[24]; + } + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) + { + cancelToken.ThrowIfCancellationRequested(); + + Buffer.BlockCopy(buffer, offset, _nonce, 0, 12); //Copy nonce from RTP header + count = SecretBox.Encrypt(buffer, offset + 12, count - 12, buffer, 12, _nonce, _secretKey); + await _next.WriteAsync(buffer, 0, count + 12, cancelToken).ConfigureAwait(false); + } + + public override async Task FlushAsync(CancellationToken cancelToken) + { + await _next.FlushAsync(cancelToken).ConfigureAwait(false); + } + public override async Task ClearAsync(CancellationToken cancelToken) + { + await _next.ClearAsync(cancelToken).ConfigureAwait(false); + } + } +} diff --git a/src/Discord.Net.WebSocket/Commands/ShardedCommandContext.cs b/src/Discord.Net.WebSocket/Commands/ShardedCommandContext.cs new file mode 100644 index 000000000..627b9b390 --- /dev/null +++ b/src/Discord.Net.WebSocket/Commands/ShardedCommandContext.cs @@ -0,0 +1,31 @@ +using Discord.WebSocket; + +namespace Discord.Commands +{ + public class ShardedCommandContext : 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 ShardedCommandContext(DiscordShardedClient client, SocketUserMessage msg) + { + Client = client; + Guild = (msg.Channel as SocketGuildChannel)?.Guild; + Channel = msg.Channel; + User = msg.Author; + Message = msg; + } + + //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/Commands/SocketCommandContext.cs b/src/Discord.Net.WebSocket/Commands/SocketCommandContext.cs new file mode 100644 index 000000000..c8b0747e7 --- /dev/null +++ b/src/Discord.Net.WebSocket/Commands/SocketCommandContext.cs @@ -0,0 +1,31 @@ +using Discord.WebSocket; + +namespace Discord.Commands +{ + public class SocketCommandContext : ICommandContext + { + public DiscordSocketClient 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 SocketCommandContext(DiscordSocketClient client, SocketUserMessage msg) + { + Client = client; + Guild = (msg.Channel as SocketGuildChannel)?.Guild; + Channel = msg.Channel; + User = msg.Author; + Message = msg; + } + + //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 new file mode 100644 index 000000000..72926e2e3 --- /dev/null +++ b/src/Discord.Net.WebSocket/ConnectionManager.cs @@ -0,0 +1,206 @@ +using Discord.Logging; +using System; +using System.Threading; +using System.Threading.Tasks; +using Discord.Net; + +namespace Discord +{ + internal class ConnectionManager + { + 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>(); + + private readonly SemaphoreSlim _stateLock; + private readonly Logger _logger; + private readonly int _connectionTimeout; + private readonly Func _onConnecting; + private readonly Func _onDisconnecting; + + private TaskCompletionSource _connectionPromise, _readyPromise; + private CancellationTokenSource _combinedCancelToken, _reconnectCancelToken, _connectionCancelToken; + private Task _task; + + 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) + { + _stateLock = stateLock; + _logger = logger; + _connectionTimeout = connectionTimeout; + _onConnecting = onConnecting; + _onDisconnecting = onDisconnecting; + + clientDisconnectHandler(ex => + { + if (ex != null) + { + var ex2 = ex as WebSocketClosedException; + if (ex2?.CloseCode == 4006) + CriticalError(new Exception("WebSocket session expired", ex)); + else + Error(new Exception("WebSocket connection was closed", ex)); + } + else + Error(new Exception("WebSocket connection was closed")); + return Task.Delay(0); + }); + } + + public virtual async Task StartAsync() + { + await AcquireConnectionLock().ConfigureAwait(false); + var reconnectCancelToken = new CancellationTokenSource(); + _reconnectCancelToken = reconnectCancelToken; + _task = Task.Run(async () => + { + try + { + Random jitter = new Random(); + int nextReconnectDelay = 1000; + while (!reconnectCancelToken.IsCancellationRequested) + { + try + { + await ConnectAsync(reconnectCancelToken).ConfigureAwait(false); + nextReconnectDelay = 1000; //Reset delay + await _connectionPromise.Task.ConfigureAwait(false); + } + catch (OperationCanceledException ex) + { + Cancel(); //In case this exception didn't come from another Error call + await DisconnectAsync(ex, !reconnectCancelToken.IsCancellationRequested).ConfigureAwait(false); + } + catch (Exception ex) + { + Error(ex); //In case this exception didn't come from another Error call + if (!reconnectCancelToken.IsCancellationRequested) + { + await _logger.WarningAsync(ex).ConfigureAwait(false); + await DisconnectAsync(ex, true).ConfigureAwait(false); + } + else + { + await _logger.ErrorAsync(ex).ConfigureAwait(false); + await DisconnectAsync(ex, false).ConfigureAwait(false); + } + } + + if (!reconnectCancelToken.IsCancellationRequested) + { + //Wait before reconnecting + await Task.Delay(nextReconnectDelay, reconnectCancelToken.Token).ConfigureAwait(false); + nextReconnectDelay = (nextReconnectDelay * 2) + jitter.Next(-250, 250); + if (nextReconnectDelay > 60000) + nextReconnectDelay = 60000; + } + } + } + finally { _stateLock.Release(); } + }); + } + public virtual async Task StopAsync() + { + Cancel(); + var task = _task; + if (task != null) + await task.ConfigureAwait(false); + } + + private async Task ConnectAsync(CancellationTokenSource reconnectCancelToken) + { + _connectionCancelToken = new CancellationTokenSource(); + _combinedCancelToken = CancellationTokenSource.CreateLinkedTokenSource(_connectionCancelToken.Token, reconnectCancelToken.Token); + CancelToken = _combinedCancelToken.Token; + + _connectionPromise = new TaskCompletionSource(); + State = ConnectionState.Connecting; + await _logger.InfoAsync("Connecting").ConfigureAwait(false); + + try + { + var readyPromise = new TaskCompletionSource(); + _readyPromise = readyPromise; + + //Abort connection on timeout + var cancelToken = CancelToken; + var _ = Task.Run(async () => + { + try + { + await Task.Delay(_connectionTimeout, cancelToken).ConfigureAwait(false); + readyPromise.TrySetException(new TimeoutException()); + } + catch (OperationCanceledException) { } + }); + + await _onConnecting().ConfigureAwait(false); + + await _logger.InfoAsync("Connected").ConfigureAwait(false); + State = ConnectionState.Connected; + await _logger.DebugAsync("Raising Event").ConfigureAwait(false); + await _connectedEvent.InvokeAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + Error(ex); + throw; + } + } + private async Task DisconnectAsync(Exception ex, bool isReconnecting) + { + if (State == ConnectionState.Disconnected) return; + State = ConnectionState.Disconnecting; + await _logger.InfoAsync("Disconnecting").ConfigureAwait(false); + + await _onDisconnecting(ex).ConfigureAwait(false); + + await _logger.InfoAsync("Disconnected").ConfigureAwait(false); + State = ConnectionState.Disconnected; + await _disconnectedEvent.InvokeAsync(ex, isReconnecting).ConfigureAwait(false); + } + + public async Task CompleteAsync() + { + await _readyPromise.TrySetResultAsync(true).ConfigureAwait(false); + } + public async Task WaitAsync() + { + await _readyPromise.Task.ConfigureAwait(false); + } + + public void Cancel() + { + _readyPromise?.TrySetCanceled(); + _connectionPromise?.TrySetCanceled(); + _reconnectCancelToken?.Cancel(); + _connectionCancelToken?.Cancel(); + } + public void Error(Exception ex) + { + _readyPromise.TrySetException(ex); + _connectionPromise.TrySetException(ex); + _connectionCancelToken?.Cancel(); + } + public void CriticalError(Exception ex) + { + _reconnectCancelToken?.Cancel(); + Error(ex); + } + private async Task AcquireConnectionLock() + { + while (true) + { + await StopAsync().ConfigureAwait(false); + if (await _stateLock.WaitAsync(0).ConfigureAwait(false)) + break; + } + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj index dd6541412..b5dab98e5 100644 --- a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj +++ b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj @@ -1,58 +1,37 @@ - - + - A core Discord.Net library containing the WebSocket client and models. - 1.0.0-beta2 - netstandard1.3 - true + 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 - $(PackageTargetFallback);dotnet5.4;dnxcore50;portable-net45+win8 + Discord.WebSocket + true + true - - - - - - - - 1.0.0-alpha-20161104-2 - All - - - 4.3.0 - - - 4.3.0 - - - 4.3.0 - - - 4.3.0 - - - 4.3.0 - + + + + + + + - - - False - - $(DefineConstants);RELEASE $(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 new file mode 100644 index 000000000..874062c56 --- /dev/null +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.Events.cs @@ -0,0 +1,200 @@ +using System; +using System.Threading.Tasks; +using Discord.Net; + +namespace Discord.WebSocket +{ + //TODO: Add event docstrings + public partial class DiscordShardedClient + { + //Channels + public event Func ChannelCreated + { + add { _channelCreatedEvent.Add(value); } + remove { _channelCreatedEvent.Remove(value); } + } + private readonly AsyncEvent> _channelCreatedEvent = new AsyncEvent>(); + public event Func ChannelDestroyed + { + add { _channelDestroyedEvent.Add(value); } + remove { _channelDestroyedEvent.Remove(value); } + } + private readonly AsyncEvent> _channelDestroyedEvent = new AsyncEvent>(); + public event Func ChannelUpdated + { + add { _channelUpdatedEvent.Add(value); } + remove { _channelUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _channelUpdatedEvent = new AsyncEvent>(); + + //Messages + public event Func MessageReceived + { + add { _messageReceivedEvent.Add(value); } + remove { _messageReceivedEvent.Remove(value); } + } + private readonly AsyncEvent> _messageReceivedEvent = new AsyncEvent>(); + public event Func, ISocketMessageChannel, Task> MessageDeleted + { + add { _messageDeletedEvent.Add(value); } + remove { _messageDeletedEvent.Remove(value); } + } + private readonly AsyncEvent, ISocketMessageChannel, Task>> _messageDeletedEvent = new AsyncEvent, ISocketMessageChannel, Task>>(); + public event Func, SocketMessage, ISocketMessageChannel, Task> MessageUpdated + { + add { _messageUpdatedEvent.Add(value); } + remove { _messageUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent, SocketMessage, ISocketMessageChannel, Task>> _messageUpdatedEvent = new AsyncEvent, SocketMessage, ISocketMessageChannel, Task>>(); + public event Func, ISocketMessageChannel, SocketReaction, Task> ReactionAdded + { + add { _reactionAddedEvent.Add(value); } + remove { _reactionAddedEvent.Remove(value); } + } + private readonly AsyncEvent, ISocketMessageChannel, SocketReaction, Task>> _reactionAddedEvent = new AsyncEvent, ISocketMessageChannel, SocketReaction, Task>>(); + public event Func, ISocketMessageChannel, SocketReaction, Task> ReactionRemoved + { + add { _reactionRemovedEvent.Add(value); } + remove { _reactionRemovedEvent.Remove(value); } + } + private readonly AsyncEvent, ISocketMessageChannel, SocketReaction, Task>> _reactionRemovedEvent = new AsyncEvent, ISocketMessageChannel, SocketReaction, Task>>(); + public event Func, ISocketMessageChannel, Task> ReactionsCleared + { + add { _reactionsClearedEvent.Add(value); } + remove { _reactionsClearedEvent.Remove(value); } + } + private readonly AsyncEvent, ISocketMessageChannel, Task>> _reactionsClearedEvent = new AsyncEvent, ISocketMessageChannel, Task>>(); + + //Roles + public event Func RoleCreated + { + add { _roleCreatedEvent.Add(value); } + remove { _roleCreatedEvent.Remove(value); } + } + private readonly AsyncEvent> _roleCreatedEvent = new AsyncEvent>(); + public event Func RoleDeleted + { + add { _roleDeletedEvent.Add(value); } + remove { _roleDeletedEvent.Remove(value); } + } + private readonly AsyncEvent> _roleDeletedEvent = new AsyncEvent>(); + public event Func RoleUpdated + { + add { _roleUpdatedEvent.Add(value); } + remove { _roleUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _roleUpdatedEvent = new AsyncEvent>(); + + //Guilds + public event Func JoinedGuild + { + add { _joinedGuildEvent.Add(value); } + remove { _joinedGuildEvent.Remove(value); } + } + private AsyncEvent> _joinedGuildEvent = new AsyncEvent>(); + public event Func LeftGuild + { + add { _leftGuildEvent.Add(value); } + remove { _leftGuildEvent.Remove(value); } + } + private AsyncEvent> _leftGuildEvent = new AsyncEvent>(); + public event Func GuildAvailable + { + add { _guildAvailableEvent.Add(value); } + remove { _guildAvailableEvent.Remove(value); } + } + private AsyncEvent> _guildAvailableEvent = new AsyncEvent>(); + public event Func GuildUnavailable + { + add { _guildUnavailableEvent.Add(value); } + remove { _guildUnavailableEvent.Remove(value); } + } + private AsyncEvent> _guildUnavailableEvent = new AsyncEvent>(); + public event Func GuildMembersDownloaded + { + add { _guildMembersDownloadedEvent.Add(value); } + remove { _guildMembersDownloadedEvent.Remove(value); } + } + private AsyncEvent> _guildMembersDownloadedEvent = new AsyncEvent>(); + public event Func GuildUpdated + { + add { _guildUpdatedEvent.Add(value); } + remove { _guildUpdatedEvent.Remove(value); } + } + private AsyncEvent> _guildUpdatedEvent = new AsyncEvent>(); + + //Users + public event Func UserJoined + { + add { _userJoinedEvent.Add(value); } + remove { _userJoinedEvent.Remove(value); } + } + private readonly AsyncEvent> _userJoinedEvent = new AsyncEvent>(); + public event Func UserLeft + { + add { _userLeftEvent.Add(value); } + remove { _userLeftEvent.Remove(value); } + } + private readonly AsyncEvent> _userLeftEvent = new AsyncEvent>(); + public event Func UserBanned + { + add { _userBannedEvent.Add(value); } + remove { _userBannedEvent.Remove(value); } + } + private readonly AsyncEvent> _userBannedEvent = new AsyncEvent>(); + public event Func UserUnbanned + { + add { _userUnbannedEvent.Add(value); } + remove { _userUnbannedEvent.Remove(value); } + } + private readonly AsyncEvent> _userUnbannedEvent = new AsyncEvent>(); + public event Func UserUpdated + { + add { _userUpdatedEvent.Add(value); } + remove { _userUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _userUpdatedEvent = new AsyncEvent>(); + public event Func GuildMemberUpdated + { + add { _guildMemberUpdatedEvent.Add(value); } + 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); } + remove { _userVoiceStateUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _userVoiceStateUpdatedEvent = new AsyncEvent>(); + public event Func CurrentUserUpdated + { + add { _selfUpdatedEvent.Add(value); } + remove { _selfUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _selfUpdatedEvent = new AsyncEvent>(); + public event Func UserIsTyping + { + add { _userIsTypingEvent.Add(value); } + remove { _userIsTypingEvent.Remove(value); } + } + private readonly AsyncEvent> _userIsTypingEvent = new AsyncEvent>(); + public event Func RecipientAdded + { + add { _recipientAddedEvent.Add(value); } + remove { _recipientAddedEvent.Remove(value); } + } + private readonly AsyncEvent> _recipientAddedEvent = new AsyncEvent>(); + public event Func RecipientRemoved + { + add { _recipientRemovedEvent.Add(value); } + remove { _recipientRemovedEvent.Remove(value); } + } + private readonly AsyncEvent> _recipientRemovedEvent = new AsyncEvent>(); + } +} diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs new file mode 100644 index 000000000..06f83c8dc --- /dev/null +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -0,0 +1,340 @@ +using Discord.API; +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Threading; + +namespace Discord.WebSocket +{ + public partial class DiscordShardedClient : BaseDiscordClient, IDiscordClient + { + private readonly DiscordSocketConfig _baseConfig; + private readonly SemaphoreSlim _connectionGroupLock; + private int[] _shardIds; + private Dictionary _shardIdsToIndex; + private DiscordSocketClient[] _shards; + private int _totalShards; + private bool _automaticShards; + + /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. + public int Latency => GetLatency(); + public UserStatus Status => _shards[0].Status; + public Game? Game => _shards[0].Game; + + internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; + public new SocketSelfUser CurrentUser { get { return base.CurrentUser as SocketSelfUser; } private set { base.CurrentUser = value; } } + public IReadOnlyCollection Guilds => GetGuilds().ToReadOnlyCollection(() => GetGuildCount()); + public IReadOnlyCollection PrivateChannels => GetPrivateChannels().ToReadOnlyCollection(() => GetPrivateChannelCount()); + public IReadOnlyCollection Shards => _shards; + public IReadOnlyCollection VoiceRegions => _shards[0].VoiceRegions; + + /// Creates a new REST/WebSocket discord client. + public DiscordShardedClient() : this(null, new DiscordSocketConfig()) { } + /// Creates a new REST/WebSocket discord client. + public DiscordShardedClient(DiscordSocketConfig config) : this(null, config, CreateApiClient(config)) { } + /// Creates a new REST/WebSocket discord client. + public DiscordShardedClient(int[] ids) : this(ids, new DiscordSocketConfig()) { } + /// Creates a new REST/WebSocket discord client. + public DiscordShardedClient(int[] ids, DiscordSocketConfig config) : this(ids, config, CreateApiClient(config)) { } + private DiscordShardedClient(int[] ids, DiscordSocketConfig config, API.DiscordSocketApiClient client) + : base(config, client) + { + if (config.ShardId != null) + throw new ArgumentException($"{nameof(config.ShardId)} must not be set."); + if (ids != null && config.TotalShards == null) + throw new ArgumentException($"Custom ids are not supported when {nameof(config.TotalShards)} is not specified."); + + _shardIdsToIndex = new Dictionary(); + config.DisplayInitialLog = false; + _baseConfig = config; + _connectionGroupLock = new SemaphoreSlim(1, 1); + + if (config.TotalShards == null) + _automaticShards = true; + else + { + _totalShards = config.TotalShards.Value; + _shardIds = ids ?? Enumerable.Range(0, _totalShards).ToArray(); + _shards = new DiscordSocketClient[_shardIds.Length]; + for (int i = 0; i < _shardIds.Length; i++) + { + _shardIdsToIndex.Add(_shardIds[i], i); + var newConfig = config.Clone(); + newConfig.ShardId = _shardIds[i]; + _shards[i] = new DiscordSocketClient(newConfig, _connectionGroupLock, i != 0 ? _shards[0] : null); + RegisterEvents(_shards[i]); + } + } + } + private static API.DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config) + => new API.DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent); + + internal override async Task OnLoginAsync(TokenType tokenType, string token) + { + if (_automaticShards) + { + var response = await ApiClient.GetBotGatewayAsync().ConfigureAwait(false); + _shardIds = Enumerable.Range(0, response.Shards).ToArray(); + _totalShards = _shardIds.Length; + _shards = new DiscordSocketClient[_shardIds.Length]; + for (int i = 0; i < _shardIds.Length; i++) + { + _shardIdsToIndex.Add(_shardIds[i], i); + var newConfig = _baseConfig.Clone(); + newConfig.ShardId = _shardIds[i]; + newConfig.TotalShards = _totalShards; + _shards[i] = new DiscordSocketClient(newConfig, _connectionGroupLock, i != 0 ? _shards[0] : null); + RegisterEvents(_shards[i]); + } + } + + //Assume threadsafe: already in a connection lock + for (int i = 0; i < _shards.Length; i++) + await _shards[i].LoginAsync(tokenType, token, false); + } + internal override async Task OnLogoutAsync() + { + //Assume threadsafe: already in a connection lock + for (int i = 0; i < _shards.Length; i++) + await _shards[i].LogoutAsync(); + + CurrentUser = null; + if (_automaticShards) + { + _shardIds = new int[0]; + _shardIdsToIndex.Clear(); + _totalShards = 0; + _shards = null; + } + } + + /// + public async Task StartAsync() + { + await Task.WhenAll(_shards.Select(x => x.StartAsync())).ConfigureAwait(false); + } + /// + public async Task StopAsync() + { + await Task.WhenAll(_shards.Select(x => x.StopAsync())).ConfigureAwait(false); + } + + public DiscordSocketClient GetShard(int id) + { + if (_shardIdsToIndex.TryGetValue(id, out id)) + return _shards[id]; + return null; + } + private int GetShardIdFor(ulong guildId) + => (int)((guildId >> 22) % (uint)_totalShards); + public int GetShardIdFor(IGuild guild) + => GetShardIdFor(guild.Id); + private DiscordSocketClient GetShardFor(ulong guildId) + => GetShard(GetShardIdFor(guildId)); + public DiscordSocketClient GetShardFor(IGuild guild) + => GetShardFor(guild.Id); + + /// + public async Task GetApplicationInfoAsync() + => await _shards[0].GetApplicationInfoAsync().ConfigureAwait(false); + + /// + 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); + + /// + public SocketChannel GetChannel(ulong id) + { + for (int i = 0; i < _shards.Length; i++) + { + var channel = _shards[i].GetChannel(id); + if (channel != null) + return channel; + } + return null; + } + private IEnumerable GetPrivateChannels() + { + for (int i = 0; i < _shards.Length; i++) + { + foreach (var channel in _shards[i].PrivateChannels) + yield return channel; + } + } + private int GetPrivateChannelCount() + { + int result = 0; + for (int i = 0; i < _shards.Length; i++) + result += _shards[i].PrivateChannels.Count; + return result; + } + + /// + public Task> GetConnectionsAsync() + => ClientHelper.GetConnectionsAsync(this); + + private IEnumerable GetGuilds() + { + for (int i = 0; i < _shards.Length; i++) + { + foreach (var guild in _shards[i].Guilds) + yield return guild; + } + } + private int GetGuildCount() + { + int result = 0; + for (int i = 0; i < _shards.Length; i++) + result += _shards[i].Guilds.Count; + return result; + } + + /// + public Task GetInviteAsync(string inviteId) + => ClientHelper.GetInviteAsync(this, inviteId); + + /// + public SocketUser GetUser(ulong id) + { + for (int i = 0; i < _shards.Length; i++) + { + var user = _shards[i].GetUser(id); + if (user != null) + return user; + } + return null; + } + /// + public SocketUser GetUser(string username, string discriminator) + { + for (int i = 0; i < _shards.Length; i++) + { + var user = _shards[i].GetUser(username, discriminator); + if (user != null) + return user; + } + return null; + } + + /// + public RestVoiceRegion GetVoiceRegion(string id) + => _shards[0].GetVoiceRegion(id); + + /// Downloads the users list for the provided guilds, if they don't have a complete list. + public async Task DownloadUsersAsync(IEnumerable guilds) + { + for (int i = 0; i < _shards.Length; i++) + { + int id = _shardIds[i]; + var arr = guilds.Where(x => GetShardIdFor(x) == id).ToArray(); + if (arr.Length > 0) + await _shards[i].DownloadUsersAsync(arr); + } + } + + private int GetLatency() + { + int total = 0; + for (int i = 0; i < _shards.Length; i++) + total += _shards[i].Latency; + return (int)Math.Round(total / (double)_shards.Length); + } + + public async Task SetStatusAsync(UserStatus status) + { + for (int i = 0; i < _shards.Length; i++) + await _shards[i].SetStatusAsync(status).ConfigureAwait(false); + } + public async Task SetGameAsync(string name, string streamUrl = null, StreamType streamType = StreamType.NotStreaming) + { + for (int i = 0; i < _shards.Length; i++) + await _shards[i].SetGameAsync(name, streamUrl, streamType).ConfigureAwait(false); + } + + private void RegisterEvents(DiscordSocketClient client) + { + client.Log += (msg) => _logEvent.InvokeAsync(msg); + client.LoggedOut += () => + { + var state = LoginState; + if (state == LoginState.LoggedIn || state == LoginState.LoggingIn) + { + //Should only happen if token is changed + var _ = LogoutAsync(); //Signal the logout, fire and forget + } + return Task.Delay(0); + }; + + client.ChannelCreated += (channel) => _channelCreatedEvent.InvokeAsync(channel); + client.ChannelDestroyed += (channel) => _channelDestroyedEvent.InvokeAsync(channel); + client.ChannelUpdated += (oldChannel, newChannel) => _channelUpdatedEvent.InvokeAsync(oldChannel, newChannel); + + client.MessageReceived += (msg) => _messageReceivedEvent.InvokeAsync(msg); + client.MessageDeleted += (cache, channel) => _messageDeletedEvent.InvokeAsync(cache, channel); + client.MessageUpdated += (oldMsg, newMsg, channel) => _messageUpdatedEvent.InvokeAsync(oldMsg, newMsg, channel); + client.ReactionAdded += (cache, channel, reaction) => _reactionAddedEvent.InvokeAsync(cache, channel, reaction); + client.ReactionRemoved += (cache, channel, reaction) => _reactionRemovedEvent.InvokeAsync(cache, channel, reaction); + client.ReactionsCleared += (cache, channel) => _reactionsClearedEvent.InvokeAsync(cache, channel); + + client.RoleCreated += (role) => _roleCreatedEvent.InvokeAsync(role); + client.RoleDeleted += (role) => _roleDeletedEvent.InvokeAsync(role); + client.RoleUpdated += (oldRole, newRole) => _roleUpdatedEvent.InvokeAsync(oldRole, newRole); + + client.JoinedGuild += (guild) => _joinedGuildEvent.InvokeAsync(guild); + client.LeftGuild += (guild) => _leftGuildEvent.InvokeAsync(guild); + client.GuildAvailable += (guild) => _guildAvailableEvent.InvokeAsync(guild); + client.GuildUnavailable += (guild) => _guildUnavailableEvent.InvokeAsync(guild); + client.GuildMembersDownloaded += (guild) => _guildMembersDownloadedEvent.InvokeAsync(guild); + client.GuildUpdated += (oldGuild, newGuild) => _guildUpdatedEvent.InvokeAsync(oldGuild, newGuild); + + client.UserJoined += (user) => _userJoinedEvent.InvokeAsync(user); + client.UserLeft += (user) => _userLeftEvent.InvokeAsync(user); + 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.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); + } + + //IDiscordClient + async Task IDiscordClient.GetApplicationInfoAsync() + => await GetApplicationInfoAsync().ConfigureAwait(false); + + Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode) + => Task.FromResult(GetChannel(id)); + Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode) + => Task.FromResult>(PrivateChannels); + + async Task> IDiscordClient.GetConnectionsAsync() + => await GetConnectionsAsync().ConfigureAwait(false); + + async Task IDiscordClient.GetInviteAsync(string inviteId) + => await GetInviteAsync(inviteId).ConfigureAwait(false); + + Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode) + => Task.FromResult(GetGuild(id)); + Task> IDiscordClient.GetGuildsAsync(CacheMode mode) + => Task.FromResult>(Guilds); + async Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon) + => await CreateGuildAsync(name, region, jpegIcon).ConfigureAwait(false); + + Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode) + => Task.FromResult(GetUser(id)); + Task IDiscordClient.GetUserAsync(string username, string discriminator) + => Task.FromResult(GetUser(username, discriminator)); + + Task> IDiscordClient.GetVoiceRegionsAsync() + => Task.FromResult>(VoiceRegions); + Task IDiscordClient.GetVoiceRegionAsync(string id) + => Task.FromResult(GetVoiceRegion(id)); + } +} diff --git a/src/Discord.Net.WebSocket/API/DiscordSocketApiClient.cs b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs similarity index 81% rename from src/Discord.Net.WebSocket/API/DiscordSocketApiClient.cs rename to src/Discord.Net.WebSocket/DiscordSocketApiClient.cs index f0dd5f852..7d680eaf2 100644 --- a/src/Discord.Net.WebSocket/API/DiscordSocketApiClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs @@ -16,7 +16,7 @@ using System.Threading.Tasks; namespace Discord.API { - public class DiscordSocketApiClient : DiscordRestApiClient + internal class DiscordSocketApiClient : DiscordRestApiClient { public event Func SentGatewayMessage { add { _sentGatewayMessageEvent.Add(value); } remove { _sentGatewayMessageEvent.Remove(value); } } private readonly AsyncEvent> _sentGatewayMessageEvent = new AsyncEvent>(); @@ -26,18 +26,24 @@ namespace Discord.API public event Func Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); - private readonly IWebSocketClient _gatewayClient; private CancellationTokenSource _connectCancelToken; private string _gatewayUrl; + private bool _isExplicitUrl; + + internal IWebSocketClient WebSocketClient { get; } public ConnectionState ConnectionState { get; private set; } - public DiscordSocketApiClient(RestClientProvider restClientProvider, string userAgent, WebSocketProvider webSocketProvider, JsonSerializer serializer = null, RequestQueue requestQueue = null) - : base(restClientProvider, userAgent, serializer, requestQueue) - { - _gatewayClient = webSocketProvider(); - //_gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .Net 4.6+) - _gatewayClient.BinaryMessage += async (data, index, count) => + public DiscordSocketApiClient(RestClientProvider restClientProvider, WebSocketProvider webSocketProvider, string userAgent, + string url = null, RetryMode defaultRetryMode = RetryMode.AlwaysRetry, JsonSerializer serializer = null) + : base(restClientProvider, userAgent, defaultRetryMode, serializer) + { + _gatewayUrl = url; + if (url != null) + _isExplicitUrl = true; + WebSocketClient = webSocketProvider(); + //WebSocketClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .NET Framework 4.6+) + WebSocketClient.BinaryMessage += async (data, index, count) => { using (var compressed = new MemoryStream(data, index + 2, count - 2)) using (var decompressed = new MemoryStream()) @@ -49,20 +55,22 @@ namespace Discord.API using (var jsonReader = new JsonTextReader(reader)) { var msg = _serializer.Deserialize(jsonReader); - await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); + if (msg != null) + await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); } } }; - _gatewayClient.TextMessage += async text => + WebSocketClient.TextMessage += async text => { using (var reader = new StringReader(text)) using (var jsonReader = new JsonTextReader(reader)) { var msg = _serializer.Deserialize(jsonReader); - await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); + if (msg != null) + await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); } }; - _gatewayClient.Closed += async ex => + WebSocketClient.Closed += async ex => { await DisconnectAsync().ConfigureAwait(false); await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); @@ -75,7 +83,7 @@ namespace Discord.API if (disposing) { _connectCancelToken?.Dispose(); - (_gatewayClient as IDisposable)?.Dispose(); + (WebSocketClient as IDisposable)?.Dispose(); } _isDisposed = true; } @@ -94,28 +102,29 @@ namespace Discord.API { if (LoginState != LoginState.LoggedIn) throw new InvalidOperationException("You must log in before connecting."); - if (_gatewayClient == null) + if (WebSocketClient == null) throw new NotSupportedException("This client is not configured with websocket support."); ConnectionState = ConnectionState.Connecting; try { _connectCancelToken = new CancellationTokenSource(); - if (_gatewayClient != null) - _gatewayClient.SetCancelToken(_connectCancelToken.Token); + if (WebSocketClient != null) + WebSocketClient.SetCancelToken(_connectCancelToken.Token); - if (_gatewayUrl == null) + if (!_isExplicitUrl) { var gatewayResponse = await GetGatewayAsync().ConfigureAwait(false); _gatewayUrl = $"{gatewayResponse.Url}?v={DiscordConfig.APIVersion}&encoding={DiscordSocketConfig.GatewayEncoding}"; } - await _gatewayClient.ConnectAsync(_gatewayUrl).ConfigureAwait(false); + await WebSocketClient.ConnectAsync(_gatewayUrl).ConfigureAwait(false); ConnectionState = ConnectionState.Connected; } - catch (Exception) + catch { - _gatewayUrl = null; //Uncache in case the gateway url changed + if (!_isExplicitUrl) + _gatewayUrl = null; //Uncache in case the gateway url changed await DisconnectInternalAsync().ConfigureAwait(false); throw; } @@ -141,7 +150,7 @@ namespace Discord.API } internal override async Task DisconnectInternalAsync() { - if (_gatewayClient == null) + if (WebSocketClient == null) throw new NotSupportedException("This client is not configured with websocket support."); if (ConnectionState == ConnectionState.Disconnected) return; @@ -150,7 +159,7 @@ namespace Discord.API try { _connectCancelToken?.Cancel(false); } catch { } - await _gatewayClient.DisconnectAsync().ConfigureAwait(false); + await WebSocketClient.DisconnectAsync().ConfigureAwait(false); ConnectionState = ConnectionState.Disconnected; } @@ -167,7 +176,7 @@ namespace Discord.API payload = new SocketFrame { Operation = (int)opCode, Payload = payload }; if (payload != null) bytes = Encoding.UTF8.GetBytes(SerializeJson(payload)); - await RequestQueue.SendAsync(new WebSocketRequest(_gatewayClient, null, bytes, true, options)).ConfigureAwait(false); + await RequestQueue.SendAsync(new WebSocketRequest(WebSocketClient, null, bytes, true, options)).ConfigureAwait(false); await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false); } @@ -177,6 +186,11 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); return await SendAsync("GET", () => "gateway", new BucketIds(), options: options).ConfigureAwait(false); } + public async Task GetBotGatewayAsync(RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + return await SendAsync("GET", () => "gateway/bot", new BucketIds(), options: options).ConfigureAwait(false); + } public async Task SendIdentifyAsync(int largeThreshold = 100, bool useCompression = true, int shardID = 0, int totalShards = 1, RequestOptions options = null) { options = RequestOptions.CreateOrClone(options); @@ -186,7 +200,7 @@ namespace Discord.API }; var msg = new IdentifyParams() { - Token = _authToken, + Token = AuthToken, Properties = props, LargeThreshold = largeThreshold, UseCompression = useCompression, @@ -201,7 +215,7 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); var msg = new ResumeParams() { - Token = _authToken, + Token = AuthToken, SessionId = sessionId, Sequence = lastSeq }; diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs index 529caaa87..313e661f3 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs @@ -59,18 +59,36 @@ namespace Discord.WebSocket remove { _messageReceivedEvent.Remove(value); } } private readonly AsyncEvent> _messageReceivedEvent = new AsyncEvent>(); - public event Func, Task> MessageDeleted + public event Func, ISocketMessageChannel, Task> MessageDeleted { add { _messageDeletedEvent.Add(value); } remove { _messageDeletedEvent.Remove(value); } } - private readonly AsyncEvent, Task>> _messageDeletedEvent = new AsyncEvent, Task>>(); - public event Func, SocketMessage, Task> MessageUpdated + private readonly AsyncEvent, ISocketMessageChannel, Task>> _messageDeletedEvent = new AsyncEvent, ISocketMessageChannel, Task>>(); + public event Func, SocketMessage, ISocketMessageChannel, Task> MessageUpdated { add { _messageUpdatedEvent.Add(value); } remove { _messageUpdatedEvent.Remove(value); } } - private readonly AsyncEvent, SocketMessage, Task>> _messageUpdatedEvent = new AsyncEvent, SocketMessage, Task>>(); + private readonly AsyncEvent, SocketMessage, ISocketMessageChannel, Task>> _messageUpdatedEvent = new AsyncEvent, SocketMessage, ISocketMessageChannel, Task>>(); + public event Func, ISocketMessageChannel, SocketReaction, Task> ReactionAdded + { + add { _reactionAddedEvent.Add(value); } + remove { _reactionAddedEvent.Remove(value); } + } + private readonly AsyncEvent, ISocketMessageChannel, SocketReaction, Task>> _reactionAddedEvent = new AsyncEvent, ISocketMessageChannel, SocketReaction, Task>>(); + public event Func, ISocketMessageChannel, SocketReaction, Task> ReactionRemoved + { + add { _reactionRemovedEvent.Add(value); } + remove { _reactionRemovedEvent.Remove(value); } + } + private readonly AsyncEvent, ISocketMessageChannel, SocketReaction, Task>> _reactionRemovedEvent = new AsyncEvent, ISocketMessageChannel, SocketReaction, Task>>(); + public event Func, ISocketMessageChannel, Task> ReactionsCleared + { + add { _reactionsClearedEvent.Add(value); } + remove { _reactionsClearedEvent.Remove(value); } + } + private readonly AsyncEvent, ISocketMessageChannel, Task>> _reactionsClearedEvent = new AsyncEvent, ISocketMessageChannel, Task>>(); //Roles public event Func RoleCreated @@ -98,37 +116,37 @@ namespace Discord.WebSocket add { _joinedGuildEvent.Add(value); } remove { _joinedGuildEvent.Remove(value); } } - private AsyncEvent> _joinedGuildEvent = new AsyncEvent>(); + private readonly AsyncEvent> _joinedGuildEvent = new AsyncEvent>(); public event Func LeftGuild { add { _leftGuildEvent.Add(value); } remove { _leftGuildEvent.Remove(value); } } - private AsyncEvent> _leftGuildEvent = new AsyncEvent>(); + private readonly AsyncEvent> _leftGuildEvent = new AsyncEvent>(); public event Func GuildAvailable { add { _guildAvailableEvent.Add(value); } remove { _guildAvailableEvent.Remove(value); } } - private AsyncEvent> _guildAvailableEvent = new AsyncEvent>(); + private readonly AsyncEvent> _guildAvailableEvent = new AsyncEvent>(); public event Func GuildUnavailable { add { _guildUnavailableEvent.Add(value); } remove { _guildUnavailableEvent.Remove(value); } } - private AsyncEvent> _guildUnavailableEvent = new AsyncEvent>(); + private readonly AsyncEvent> _guildUnavailableEvent = new AsyncEvent>(); public event Func GuildMembersDownloaded { add { _guildMembersDownloadedEvent.Add(value); } remove { _guildMembersDownloadedEvent.Remove(value); } } - private AsyncEvent> _guildMembersDownloadedEvent = new AsyncEvent>(); + private readonly AsyncEvent> _guildMembersDownloadedEvent = new AsyncEvent>(); public event Func GuildUpdated { add { _guildUpdatedEvent.Add(value); } remove { _guildUpdatedEvent.Remove(value); } } - private AsyncEvent> _guildUpdatedEvent = new AsyncEvent>(); + private readonly AsyncEvent> _guildUpdatedEvent = new AsyncEvent>(); //Users public event Func UserJoined @@ -203,7 +221,5 @@ namespace Discord.WebSocket remove { _recipientRemovedEvent.Remove(value); } } private readonly AsyncEvent> _recipientRemovedEvent = new AsyncEvent>(); - - //TODO: Add PresenceUpdated? VoiceStateUpdated?, VoiceConnected, VoiceDisconnected; } } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 3eb4158d1..66c25e5f6 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -3,7 +3,7 @@ using Discord.API.Gateway; using Discord.Audio; using Discord.Logging; using Discord.Net.Converters; -using Discord.Net.Queue; +using Discord.Net.Udp; using Discord.Net.WebSockets; using Discord.Rest; using Newtonsoft.Json; @@ -23,21 +23,24 @@ namespace Discord.WebSocket public partial class DiscordSocketClient : BaseDiscordClient, IDiscordClient { private readonly ConcurrentQueue _largeGuilds; - private readonly Logger _gatewayLogger; private readonly JsonSerializer _serializer; + private readonly SemaphoreSlim _connectionGroupLock; + private readonly DiscordSocketClient _parentClient; + private readonly ConcurrentQueue _heartbeatTimes; + private readonly ConnectionManager _connection; + private readonly Logger _gatewayLogger; + private readonly SemaphoreSlim _stateLock; private string _sessionId; private int _lastSeq; private ImmutableDictionary _voiceRegions; - private TaskCompletionSource _connectTask; - private CancellationTokenSource _cancelToken, _reconnectCancelToken; - private Task _heartbeatTask, _guildDownloadTask, _reconnectTask; - private long _heartbeatTime; + private Task _heartbeatTask, _guildDownloadTask; private int _unavailableGuilds; - private long _lastGuildAvailableTime; + private long _lastGuildAvailableTime, _lastMessageTime; private int _nextAudioId; - private bool _canReconnect; private DateTimeOffset? _statusSince; + private RestApplication _applicationInfo; + private ConcurrentHashSet _downloadUsersFor; /// Gets the shard of of this client. public int ShardId { get; } @@ -54,52 +57,60 @@ namespace Discord.WebSocket internal int LargeThreshold { get; private set; } internal AudioMode AudioMode { get; private set; } internal ClientState State { get; private set; } - internal int ConnectionTimeout { get; private set; } + internal UdpSocketProvider UdpSocketProvider { get; private set; } internal WebSocketProvider WebSocketProvider { get; private set; } + internal bool AlwaysDownloadUsers { get; private set; } - public new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; + internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; public new SocketSelfUser CurrentUser { get { return base.CurrentUser as SocketSelfUser; } private set { base.CurrentUser = value; } } public IReadOnlyCollection Guilds => State.Guilds; public IReadOnlyCollection PrivateChannels => State.PrivateChannels; + public IReadOnlyCollection DMChannels + => State.PrivateChannels.Select(x => x as SocketDMChannel).Where(x => x != null).ToImmutableArray(); + public IReadOnlyCollection GroupChannels + => State.PrivateChannels.Select(x => x as SocketGroupChannel).Where(x => x != null).ToImmutableArray(); + public IReadOnlyCollection VoiceRegions => _voiceRegions.ToReadOnlyCollection(); /// Creates a new REST/WebSocket discord client. public DiscordSocketClient() : this(new DiscordSocketConfig()) { } /// Creates a new REST/WebSocket discord client. - public DiscordSocketClient(DiscordSocketConfig config) : this(config, CreateApiClient(config)) { } - private DiscordSocketClient(DiscordSocketConfig config, API.DiscordSocketApiClient client) + public DiscordSocketClient(DiscordSocketConfig config) : this(config, CreateApiClient(config), null, null) { } + internal DiscordSocketClient(DiscordSocketConfig config, SemaphoreSlim groupLock, DiscordSocketClient parentClient) : this(config, CreateApiClient(config), groupLock, parentClient) { } + private DiscordSocketClient(DiscordSocketConfig config, API.DiscordSocketApiClient client, SemaphoreSlim groupLock, DiscordSocketClient parentClient) : base(config, client) { - ShardId = config.ShardId; - TotalShards = config.TotalShards; + ShardId = config.ShardId ?? 0; + TotalShards = config.TotalShards ?? 1; MessageCacheSize = config.MessageCacheSize; LargeThreshold = config.LargeThreshold; AudioMode = config.AudioMode; + UdpSocketProvider = config.UdpSocketProvider; WebSocketProvider = config.WebSocketProvider; - ConnectionTimeout = config.ConnectionTimeout; + AlwaysDownloadUsers = config.AlwaysDownloadUsers; 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); _nextAudioId = 1; - _gatewayLogger = LogManager.CreateLogger("Gateway"); + _connectionGroupLock = groupLock; + _parentClient = parentClient; _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; _serializer.Error += (s, e) => { - _gatewayLogger.WarningAsync(e.ErrorContext.Error).GetAwaiter().GetResult(); + _gatewayLogger.WarningAsync("Serializer Error", e.ErrorContext.Error).GetAwaiter().GetResult(); e.ErrorContext.Handled = true; }; ApiClient.SentGatewayMessage += async opCode => await _gatewayLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false); ApiClient.ReceivedGatewayEvent += ProcessMessageAsync; - ApiClient.Disconnected += async ex => - { - if (ex != null) - { - await _gatewayLogger.WarningAsync($"Connection Closed", ex).ConfigureAwait(false); - await StartReconnectAsync(ex).ConfigureAwait(false); - } - else - await _gatewayLogger.WarningAsync($"Connection Closed").ConfigureAwait(false); - }; LeftGuild += async g => await _gatewayLogger.InfoAsync($"Left {g.Name}").ConfigureAwait(false); JoinedGuild += async g => await _gatewayLogger.InfoAsync($"Joined {g.Name}").ConfigureAwait(false); @@ -107,74 +118,63 @@ namespace Discord.WebSocket GuildUnavailable += async g => await _gatewayLogger.VerboseAsync($"Disconnected from {g.Name}").ConfigureAwait(false); LatencyUpdated += async (old, val) => await _gatewayLogger.VerboseAsync($"Latency = {val} ms").ConfigureAwait(false); + GuildAvailable += g => + { + if (ConnectionState == ConnectionState.Connected && (AlwaysDownloadUsers || _downloadUsersFor.ContainsKey(g.Id))) + { + if (!g.HasAllMembers) + { + var _ = g.DownloadUsersAsync(); + } + } + return Task.Delay(0); + }; + _voiceRegions = ImmutableDictionary.Create(); _largeGuilds = new ConcurrentQueue(); } private static API.DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config) - => new API.DiscordSocketApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent, config.WebSocketProvider, requestQueue: new RequestQueue()); - - protected override async Task OnLoginAsync(TokenType tokenType, string token) + => new API.DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent, config.GatewayHost); + internal override void Dispose(bool disposing) { - var voiceRegions = await ApiClient.GetVoiceRegionsAsync(new RequestOptions { IgnoreState = true}).ConfigureAwait(false); - _voiceRegions = voiceRegions.Select(x => RestVoiceRegion.Create(this, x)).ToImmutableDictionary(x => x.Id); - } - protected override async Task OnLogoutAsync() - { - if (ConnectionState != ConnectionState.Disconnected) - await DisconnectInternalAsync(null, false).ConfigureAwait(false); - - _voiceRegions = ImmutableDictionary.Create(); + if (disposing) + { + StopAsync().GetAwaiter().GetResult(); + ApiClient.Dispose(); + } } - /// - public async Task ConnectAsync(bool waitForGuilds = true) + internal override async Task OnLoginAsync(TokenType tokenType, string token) { - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - await ConnectInternalAsync(false).ConfigureAwait(false); - } - finally { _connectionLock.Release(); } - - if (waitForGuilds) + if (_parentClient == null) { - var downloadTask = _guildDownloadTask; - if (downloadTask != null) - await _guildDownloadTask.ConfigureAwait(false); + var voiceRegions = await ApiClient.GetVoiceRegionsAsync(new RequestOptions { IgnoreState = true, RetryMode = RetryMode.AlwaysRetry }).ConfigureAwait(false); + _voiceRegions = voiceRegions.Select(x => RestVoiceRegion.Create(this, x)).ToImmutableDictionary(x => x.Id); } + else + _voiceRegions = _parentClient._voiceRegions; } - private async Task ConnectInternalAsync(bool isReconnecting) + internal override async Task OnLogoutAsync() { - if (LoginState != LoginState.LoggedIn) - throw new InvalidOperationException("Client is not logged in."); - - if (!isReconnecting && _reconnectCancelToken != null && !_reconnectCancelToken.IsCancellationRequested) - _reconnectCancelToken.Cancel(); - - var state = ConnectionState; - if (state == ConnectionState.Connecting || state == ConnectionState.Connected) - await DisconnectInternalAsync(null, isReconnecting).ConfigureAwait(false); + await StopAsync().ConfigureAwait(false); + _applicationInfo = null; + _voiceRegions = ImmutableDictionary.Create(); + _downloadUsersFor.Clear(); + } - ConnectionState = ConnectionState.Connecting; - await _gatewayLogger.InfoAsync("Connecting").ConfigureAwait(false); - + public async Task StartAsync() + => await _connection.StartAsync().ConfigureAwait(false); + public async Task StopAsync() + => await _connection.StopAsync().ConfigureAwait(false); + + private async Task OnConnectingAsync() + { + if (_connectionGroupLock != null) + await _connectionGroupLock.WaitAsync(_connection.CancelToken).ConfigureAwait(false); try { - var connectTask = new TaskCompletionSource(); - _connectTask = connectTask; - _cancelToken = new CancellationTokenSource(); - - //Abort connection on timeout - var _ = Task.Run(async () => - { - await Task.Delay(ConnectionTimeout).ConfigureAwait(false); - connectTask.TrySetException(new TimeoutException()); - }); - await _gatewayLogger.DebugAsync("Connecting ApiClient").ConfigureAwait(false); await ApiClient.ConnectAsync().ConfigureAwait(false); - await _gatewayLogger.DebugAsync("Raising Event").ConfigureAwait(false); - await _connectedEvent.InvokeAsync().ConfigureAwait(false); if (_sessionId != null) { @@ -187,58 +187,29 @@ namespace Discord.WebSocket await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards).ConfigureAwait(false); } - await _connectTask.Task.ConfigureAwait(false); - + //Wait for READY + await _connection.WaitAsync().ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Sending Status").ConfigureAwait(false); - await SendStatus().ConfigureAwait(false); + await SendStatusAsync().ConfigureAwait(false); - await _gatewayLogger.DebugAsync("Raising Event").ConfigureAwait(false); - if (!isReconnecting) - _canReconnect = true; - ConnectionState = ConnectionState.Connected; - await _gatewayLogger.InfoAsync("Connected").ConfigureAwait(false); + await ProcessUserDownloadsAsync(_downloadUsersFor.Select(x => GetGuild(x)) + .Where(x => x != null).ToImmutableArray()).ConfigureAwait(false); } - catch (Exception) + finally { - await DisconnectInternalAsync(null, isReconnecting).ConfigureAwait(false); - throw; - } - } - /// - public async Task DisconnectAsync() - { - if (_connectTask?.TrySetCanceled() ?? false) return; - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - await DisconnectInternalAsync(null, false).ConfigureAwait(false); + if (_connectionGroupLock != null) + { + await Task.Delay(5000).ConfigureAwait(false); + _connectionGroupLock.Release(); + } } - finally { _connectionLock.Release(); } } - private async Task DisconnectInternalAsync(Exception ex, bool isReconnecting) + private async Task OnDisconnectingAsync(Exception ex) { - if (!isReconnecting) - { - _canReconnect = false; - _sessionId = null; - _lastSeq = 0; - - if (_reconnectCancelToken != null && !_reconnectCancelToken.IsCancellationRequested) - _reconnectCancelToken.Cancel(); - } - ulong guildId; - if (ConnectionState == ConnectionState.Disconnected) return; - ConnectionState = ConnectionState.Disconnecting; - await _gatewayLogger.InfoAsync("Disconnecting").ConfigureAwait(false); - - await _gatewayLogger.DebugAsync("Cancelling current tasks").ConfigureAwait(false); - //Signal tasks to complete - try { _cancelToken.Cancel(); } catch { } - await _gatewayLogger.DebugAsync("Disconnecting ApiClient").ConfigureAwait(false); - //Disconnect from server await ApiClient.DisconnectAsync().ConfigureAwait(false); //Wait for tasks to complete @@ -248,6 +219,10 @@ namespace Discord.WebSocket await heartbeatTask.ConfigureAwait(false); _heartbeatTask = null; + long time; + while (_heartbeatTimes.TryDequeue(out time)) { } + _lastMessageTime = 0; + await _gatewayLogger.DebugAsync("Waiting for guild downloader").ConfigureAwait(false); var guildDownloadTask = _guildDownloadTask; if (guildDownloadTask != null) @@ -265,76 +240,13 @@ namespace Discord.WebSocket if (guild._available) await _guildUnavailableEvent.InvokeAsync(guild).ConfigureAwait(false); } - - ConnectionState = ConnectionState.Disconnected; - await _gatewayLogger.InfoAsync("Disconnected").ConfigureAwait(false); - - await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); - } - - private async Task StartReconnectAsync(Exception ex) - { - if (ex == null) - { - if (_connectTask?.TrySetCanceled() ?? false) return; - } - else - { - if (_connectTask?.TrySetException(ex) ?? false) return; - } - - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - if (!_canReconnect || _reconnectTask != null) return; - _reconnectCancelToken = new CancellationTokenSource(); - _reconnectTask = ReconnectInternalAsync(ex, _reconnectCancelToken.Token); - } - finally { _connectionLock.Release(); } - } - private async Task ReconnectInternalAsync(Exception ex, CancellationToken cancelToken) - { - try - { - Random jitter = new Random(); - int nextReconnectDelay = 1000; - while (true) - { - await Task.Delay(nextReconnectDelay, cancelToken).ConfigureAwait(false); - nextReconnectDelay = nextReconnectDelay * 2 + jitter.Next(-250, 250); - if (nextReconnectDelay > 60000) - nextReconnectDelay = 60000; - - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - if (cancelToken.IsCancellationRequested) return; - await ConnectInternalAsync(true).ConfigureAwait(false); - _reconnectTask = null; - return; - } - catch (Exception ex2) - { - await _gatewayLogger.WarningAsync("Reconnect failed", ex2).ConfigureAwait(false); - } - finally { _connectionLock.Release(); } - } - } - catch (OperationCanceledException) - { - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - await _gatewayLogger.DebugAsync("Reconnect cancelled").ConfigureAwait(false); - _reconnectTask = null; - } - finally { _connectionLock.Release(); } - } } /// - public Task GetApplicationInfoAsync() - => ClientHelper.GetApplicationInfoAsync(this); + public async Task GetApplicationInfoAsync() + { + return _applicationInfo ?? (_applicationInfo = await ClientHelper.GetApplicationInfoAsync(this)); + } /// public SocketGuild GetGuild(ulong id) @@ -402,35 +314,23 @@ namespace Discord.WebSocket return null; } - /// Downloads the users list for all large guilds. - public Task DownloadAllUsersAsync() - => DownloadUsersAsync(State.Guilds.Where(x => !x.HasAllMembers)); /// Downloads the users list for the provided guilds, if they don't have a complete list. - public Task DownloadUsersAsync(IEnumerable guilds) - => DownloadUsersAsync(guilds.Select(x => x as SocketGuild).Where(x => x != null)); - public Task DownloadUsersAsync(params IGuild[] guilds) - => DownloadUsersAsync(guilds.Select(x => x as SocketGuild).Where(x => x != null)); - private async Task DownloadUsersAsync(IEnumerable guilds) + public async Task DownloadUsersAsync(IEnumerable guilds) { - var cachedGuilds = guilds.ToImmutableArray(); - if (cachedGuilds.Length == 0) return; - - //Wait for unsynced guilds to sync first. - var unsyncedGuilds = guilds.Select(x => x.SyncPromise).Where(x => !x.IsCompleted).ToImmutableArray(); - if (unsyncedGuilds.Length > 0) - await Task.WhenAll(unsyncedGuilds).ConfigureAwait(false); + foreach (var guild in guilds) + _downloadUsersFor.TryAdd(guild.Id); - //Download offline members - const short batchSize = 50; - - if (cachedGuilds.Length == 1) + if (ConnectionState == ConnectionState.Connected) { - if (!cachedGuilds[0].HasAllMembers) - await ApiClient.SendRequestMembersAsync(new ulong[] { cachedGuilds[0].Id }).ConfigureAwait(false); - await cachedGuilds[0].DownloaderPromise.ConfigureAwait(false); - return; + //Race condition leads to guilds being requested twice, probably okay + await ProcessUserDownloadsAsync(guilds.Select(x => GetGuild(x.Id)).Where(x => x != null)).ConfigureAwait(false); } + } + private async Task ProcessUserDownloadsAsync(IEnumerable guilds) + { + var cachedGuilds = guilds.ToImmutableArray(); + const short batchSize = 50; ulong[] batchIds = new ulong[Math.Min(batchSize, cachedGuilds.Length)]; Task[] batchTasks = new Task[batchIds.Length]; int batchCount = (cachedGuilds.Length + (batchSize - 1)) / batchSize; @@ -456,25 +356,25 @@ namespace Discord.WebSocket } } - public async Task SetStatus(UserStatus status) + public async Task SetStatusAsync(UserStatus status) { Status = status; if (status == UserStatus.AFK) _statusSince = DateTimeOffset.UtcNow; else _statusSince = null; - await SendStatus().ConfigureAwait(false); + await SendStatusAsync().ConfigureAwait(false); } - public async Task SetGame(string name, string streamUrl = null, StreamType streamType = StreamType.NotStreaming) + public async Task SetGameAsync(string name, string streamUrl = null, StreamType streamType = StreamType.NotStreaming) { if (name != null) Game = new Game(name, streamUrl, streamType); else Game = null; CurrentUser.Presence = new SocketPresence(Status, Game); - await SendStatus().ConfigureAwait(false); + await SendStatusAsync().ConfigureAwait(false); } - private async Task SendStatus() + private async Task SendStatusAsync() { var game = Game; var status = Status; @@ -497,7 +397,7 @@ namespace Discord.WebSocket await ApiClient.SendStatusUpdateAsync( status, status == UserStatus.AFK, - statusSince != null ? _statusSince.Value.ToUnixTimeMilliseconds() : (long?)null, + statusSince != null ? DateTimeUtils.ToUnixMilliseconds(_statusSince.Value) : (long?)null, gameModel).ConfigureAwait(false); } @@ -505,6 +405,8 @@ namespace Discord.WebSocket { if (seq != null) _lastSeq = seq.Value; + _lastMessageTime = Environment.TickCount; + try { switch (opCode) @@ -514,8 +416,7 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Hello").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - _heartbeatTime = 0; - _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _cancelToken.Token, LogManager.ClientLogger); + _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _connection.CancelToken); } break; case GatewayOpCode.Heartbeat: @@ -529,12 +430,10 @@ namespace Discord.WebSocket { await _gatewayLogger.DebugAsync("Received HeartbeatAck").ConfigureAwait(false); - var heartbeatTime = _heartbeatTime; - if (heartbeatTime != 0) + long time; + if (_heartbeatTimes.TryDequeue(out time)) { - int latency = (int)(Environment.TickCount - _heartbeatTime); - _heartbeatTime = 0; - + int latency = (int)(Environment.TickCount - time); int before = Latency; Latency = latency; @@ -555,9 +454,7 @@ namespace Discord.WebSocket case GatewayOpCode.Reconnect: { await _gatewayLogger.DebugAsync("Received Reconnect").ConfigureAwait(false); - await _gatewayLogger.WarningAsync("Server requested a reconnect").ConfigureAwait(false); - - await StartReconnectAsync(new Exception("Server requested a reconnect")).ConfigureAwait(false); + _connection.Error(new Exception("Server requested a reconnect")); } break; case GatewayOpCode.Dispatch: @@ -574,6 +471,7 @@ namespace Discord.WebSocket var state = new ClientState(data.Guilds.Length, data.PrivateChannels.Length); var currentUser = SocketSelfUser.Create(this, state, data.User); + ApiClient.CurrentUserId = currentUser.Id; int unavailableGuilds = 0; for (int i = 0; i < data.Guilds.Length; i++) { @@ -594,7 +492,7 @@ namespace Discord.WebSocket } catch (Exception ex) { - _connectTask.TrySetException(new Exception("Processing READY failed", ex)); + _connection.CriticalError(new Exception("Processing READY failed", ex)); return; } @@ -602,11 +500,11 @@ namespace Discord.WebSocket await SyncGuildsAsync().ConfigureAwait(false); _lastGuildAvailableTime = Environment.TickCount; - _guildDownloadTask = WaitForGuildsAsync(_cancelToken.Token, LogManager.ClientLogger); + _guildDownloadTask = WaitForGuildsAsync(_connection.CancelToken, _gatewayLogger); await _readyEvent.InvokeAsync().ConfigureAwait(false); - var _ = _connectTask.TrySetResultAsync(true); //Signal the .Connect() call to complete + var _ = _connection.CompleteAsync(); await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false); } break; @@ -614,7 +512,7 @@ namespace Discord.WebSocket { await _gatewayLogger.DebugAsync("Received Dispatch (RESUMED)").ConfigureAwait(false); - var _ = _connectTask.TrySetResultAsync(true); //Signal the .Connect() call to complete + var _ = _connection.CompleteAsync(); //Notify the client that these guilds are available again foreach (var guild in State.Guilds) @@ -757,6 +655,7 @@ namespace Discord.WebSocket { await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_DELETE)").ConfigureAwait(false); + _downloadUsersFor.TryRemove(data.Id); var guild = RemoveGuild(data.Id); if (guild != null) { @@ -1202,13 +1101,8 @@ namespace Discord.WebSocket return; } - SocketUser author; - if (guild != null) - author = guild.GetUser(data.Author.Value.Id); - else - author = (channel as SocketChannel).GetUser(data.Author.Value.Id); - if (author == null) - author = SocketSimpleUser.Create(this, State, data.Author.Value); + 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) { @@ -1239,14 +1133,15 @@ namespace Discord.WebSocket { var guild = (channel as SocketGuildChannel)?.Guild; if (guild != null && !guild.IsSynced) - { + { await _gatewayLogger.DebugAsync("Ignored MESSAGE_UPDATE, guild is not synced yet.").ConfigureAwait(false); return; } SocketMessage before = null, after = null; SocketMessage cachedMsg = channel.GetCachedMessage(data.Id); - if (cachedMsg != null) + bool isCached = cachedMsg != null; + if (isCached) { before = cachedMsg.Clone(); cachedMsg.Update(State, data); @@ -1265,10 +1160,9 @@ namespace Discord.WebSocket after = SocketMessage.Create(this, State, author, channel, data); } - if (before != null) - await _messageUpdatedEvent.InvokeAsync(before, after).ConfigureAwait(false); - else - await _messageUpdatedEvent.InvokeAsync(Optional.Create(), after).ConfigureAwait(false); + var cacheableBefore = new Cacheable(before, data.Id, isCached , async () => await channel.GetMessageAsync(data.Id)); + + await _messageUpdatedEvent.InvokeAsync(cacheableBefore, after, channel).ConfigureAwait(false); } else { @@ -1280,22 +1174,22 @@ namespace Discord.WebSocket case "MESSAGE_DELETE": { 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 (!((channel as SocketGuildChannel)?.Guild.IsSynced ?? true)) - { + { await _gatewayLogger.DebugAsync("Ignored MESSAGE_DELETE, guild is not synced yet.").ConfigureAwait(false); return; } var msg = SocketChannelHelper.RemoveMessage(channel, this, data.Id); - if (msg != null) - await _messageDeletedEvent.InvokeAsync(data.Id, msg).ConfigureAwait(false); - else - await _messageDeletedEvent.InvokeAsync(data.Id, Optional.Create()).ConfigureAwait(false); + 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); } else { @@ -1304,6 +1198,79 @@ namespace Discord.WebSocket } } break; + case "MESSAGE_REACTION_ADD": + { + 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) + { + SocketUserMessage 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 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); + } + else + { + await _gatewayLogger.WarningAsync("MESSAGE_REACTION_ADD referenced an unknown channel.").ConfigureAwait(false); + return; + } + 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) + { + SocketUserMessage 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 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); + } + else + { + await _gatewayLogger.WarningAsync("MESSAGE_REACTION_REMOVE referenced an unknown channel.").ConfigureAwait(false); + return; + } + 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) + { + SocketUserMessage 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); + } + else + { + await _gatewayLogger.WarningAsync("MESSAGE_REACTION_REMOVE_ALL referenced an unknown channel.").ConfigureAwait(false); + return; + } + break; + } case "MESSAGE_DELETE_BULK": { await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE_BULK)").ConfigureAwait(false); @@ -1321,10 +1288,9 @@ namespace Discord.WebSocket foreach (var id in data.Ids) { var msg = SocketChannelHelper.RemoveMessage(channel, this, id); - if (msg != null) - await _messageDeletedEvent.InvokeAsync(id, msg).ConfigureAwait(false); - else - await _messageDeletedEvent.InvokeAsync(id, Optional.Create()).ConfigureAwait(false); + bool isCached = msg != null; + var cacheable = new Cacheable(msg, id, isCached, async () => await channel.GetMessageAsync(id)); + await _messageDeletedEvent.InvokeAsync(cacheable, channel).ConfigureAwait(false); } } else @@ -1341,6 +1307,7 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (PRESENCE_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); + if (data.GuildId.IsSpecified) { var guild = State.GetGuild(data.GuildId.Value); @@ -1355,25 +1322,28 @@ namespace Discord.WebSocket return; } - SocketPresence before; + SocketPresence beforePresence; + SocketGlobalUser beforeGlobal; var user = guild.GetUser(data.User.Id); if (user != null) { - before = user.Presence.Clone(); + beforePresence = user.Presence.Clone(); + beforeGlobal = user.GlobalUser.Clone(); user.Update(State, data); } else { - before = new SocketPresence(UserStatus.Offline, null); + beforePresence = new SocketPresence(UserStatus.Offline, null); user = guild.AddOrUpdateUser(data); + beforeGlobal = user.GlobalUser.Clone(); } - await _userPresenceUpdatedEvent.InvokeAsync(guild, user, before, user.Presence).ConfigureAwait(false); - if (data.User.Username.IsSpecified || data.Roles.IsSpecified) + if (data.User.Username.IsSpecified || data.User.Avatar.IsSpecified) { - var before2 = user.Clone(); - await _guildMemberUpdatedEvent.InvokeAsync(before2, user).ConfigureAwait(false); + await _userUpdatedEvent.InvokeAsync(beforeGlobal, user).ConfigureAwait(false); + return; } + await _userPresenceUpdatedEvent.InvokeAsync(guild, user, beforePresence, user.Presence).ConfigureAwait(false); } else { @@ -1381,14 +1351,14 @@ namespace Discord.WebSocket if (channel != null) { var user = channel.GetUser(data.User.Id); - var before = user.Presence.Clone(); + var beforePresence = user.Presence.Clone(); + var before = user.GlobalUser.Clone(); user.Update(State, data); - await _userPresenceUpdatedEvent.InvokeAsync(Optional.Create(), user, before, user.Presence).ConfigureAwait(false); - if (data.User.Username.IsSpecified) + await _userPresenceUpdatedEvent.InvokeAsync(Optional.Create(), user, beforePresence, user.Presence).ConfigureAwait(false); + if (data.User.Username.IsSpecified || data.User.Avatar.IsSpecified) { - var before2 = user.Clone(); - await _userUpdatedEvent.InvokeAsync(before2, user).ConfigureAwait(false); + await _userUpdatedEvent.InvokeAsync(before, user).ConfigureAwait(false); } } } @@ -1461,16 +1431,16 @@ namespace Discord.WebSocket if (data.ChannelId != null) { - before = guild.GetVoiceState(data.UserId)?.Clone() ?? new SocketVoiceState(null, null, false, false, false); + before = guild.GetVoiceState(data.UserId)?.Clone() ?? SocketVoiceState.Default; after = guild.AddOrUpdateVoiceState(State, data); - if (data.UserId == CurrentUser.Id) + /*if (data.UserId == CurrentUser.Id) { var _ = guild.FinishJoinAudioChannel().ConfigureAwait(false); - } + }*/ } else { - before = guild.RemoveVoiceState(data.UserId) ?? new SocketVoiceState(null, null, false, false, false); + before = guild.RemoveVoiceState(data.UserId) ?? SocketVoiceState.Default; after = SocketVoiceState.Create(null, data); } @@ -1483,12 +1453,12 @@ namespace Discord.WebSocket { if (data.ChannelId != null) { - before = groupChannel.GetVoiceState(data.UserId)?.Clone() ?? new SocketVoiceState(null, null, false, false, false); + before = groupChannel.GetVoiceState(data.UserId)?.Clone() ?? SocketVoiceState.Default; after = groupChannel.AddOrUpdateVoiceState(State, data); } else { - before = groupChannel.RemoveVoiceState(data.UserId) ?? new SocketVoiceState(null, null, false, false, false); + before = groupChannel.RemoveVoiceState(data.UserId) ?? SocketVoiceState.Default; after = SocketVoiceState.Create(null, data); } user = groupChannel.GetUser(data.UserId); @@ -1568,46 +1538,54 @@ namespace Discord.WebSocket } } - private async Task RunHeartbeatAsync(int intervalMillis, CancellationToken cancelToken, Logger logger) + private async Task RunHeartbeatAsync(int intervalMillis, CancellationToken cancelToken) { try { - await logger.DebugAsync("Heartbeat Started").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Heartbeat Started").ConfigureAwait(false); while (!cancelToken.IsCancellationRequested) { - if (_heartbeatTime != 0) //Server never responded to our last heartbeat + var 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) { - if (ConnectionState == ConnectionState.Connected && (_guildDownloadTask?.IsCompleted ?? false)) + if (ConnectionState == ConnectionState.Connected && (_guildDownloadTask?.IsCompleted ?? true)) { - await _gatewayLogger.WarningAsync("Server missed last heartbeat").ConfigureAwait(false); - await StartReconnectAsync(new Exception("Server missed last heartbeat")).ConfigureAwait(false); + _connection.Error(new Exception("Server missed last heartbeat")); return; } } - _heartbeatTime = Environment.TickCount; + _heartbeatTimes.Enqueue(now); try { await ApiClient.SendHeartbeatAsync(_lastSeq).ConfigureAwait(false); } catch (Exception ex) { - await logger.WarningAsync("Heartbeat Errored", ex).ConfigureAwait(false); + await _gatewayLogger.WarningAsync("Heartbeat Errored", ex).ConfigureAwait(false); } await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false); } - await logger.DebugAsync("Heartbeat Stopped").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Heartbeat Stopped").ConfigureAwait(false); } catch (OperationCanceledException) { - await logger.DebugAsync("Heartbeat Stopped").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Heartbeat Stopped").ConfigureAwait(false); } catch (Exception ex) { - await logger.ErrorAsync("Heartbeat Errored", ex).ConfigureAwait(false); + await _gatewayLogger.ErrorAsync("Heartbeat Errored", ex).ConfigureAwait(false); } } + 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 @@ -1673,10 +1651,7 @@ namespace Discord.WebSocket } //IDiscordClient - DiscordRestApiClient IDiscordClient.ApiClient => ApiClient; - - Task IDiscordClient.ConnectAsync() - => ConnectAsync(); + ConnectionState IDiscordClient.ConnectionState => _connection.State; async Task IDiscordClient.GetApplicationInfoAsync() => await GetApplicationInfoAsync().ConfigureAwait(false); @@ -1685,6 +1660,10 @@ namespace Discord.WebSocket => Task.FromResult(GetChannel(id)); Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode) => Task.FromResult>(PrivateChannels); + Task> IDiscordClient.GetDMChannelsAsync(CacheMode mode) + => Task.FromResult>(DMChannels); + Task> IDiscordClient.GetGroupChannelsAsync(CacheMode mode) + => Task.FromResult>(GroupChannels); async Task> IDiscordClient.GetConnectionsAsync() => await GetConnectionsAsync().ConfigureAwait(false); @@ -1705,8 +1684,13 @@ namespace Discord.WebSocket => Task.FromResult(GetUser(username, discriminator)); Task> IDiscordClient.GetVoiceRegionsAsync() - => Task.FromResult>(_voiceRegions.ToReadOnlyCollection()); + => Task.FromResult>(VoiceRegions); Task IDiscordClient.GetVoiceRegionAsync(string id) => Task.FromResult(GetVoiceRegion(id)); + + async Task IDiscordClient.StartAsync() + => await StartAsync().ConfigureAwait(false); + async Task IDiscordClient.StopAsync() + => await StopAsync().ConfigureAwait(false); } } diff --git a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs index dc0347a1c..f42744c79 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs @@ -1,4 +1,5 @@ using Discord.Audio; +using Discord.Net.Udp; using Discord.Net.WebSockets; using Discord.Rest; @@ -8,13 +9,16 @@ namespace Discord.WebSocket { public const string GatewayEncoding = "json"; + /// Gets or sets the websocket host to connect to. If null, the client will use the /gateway endpoint. + public string GatewayHost { get; set; } = null; + /// Gets or sets the time, in milliseconds, to wait for a connection to complete before aborting. public int ConnectionTimeout { get; set; } = 30000; /// Gets or sets the id for this shard. Must be less than TotalShards. - public int ShardId { get; set; } = 0; + public int? ShardId { get; set; } = null; /// Gets or sets the total number of shards for this application. - public int TotalShards { get; set; } = 1; + public int? TotalShards { get; set; } = null; /// Gets or sets the number of messages per channel that should be kept in cache. Setting this to zero disables the message cache entirely. public int MessageCacheSize { get; set; } = 0; @@ -27,6 +31,19 @@ namespace Discord.WebSocket public AudioMode AudioMode { get; set; } = AudioMode.Disabled; /// Gets or sets the provider used to generate new websocket connections. - public WebSocketProvider WebSocketProvider { get; set; } = () => new DefaultWebSocketClient(); + public WebSocketProvider WebSocketProvider { get; set; } + /// Gets or sets the provider used to generate new udp sockets. + public UdpSocketProvider UdpSocketProvider { get; set; } + + /// Gets or sets whether or not all users should be downloaded as guilds come available. + public bool AlwaysDownloadUsers { get; set; } = false; + + public DiscordSocketConfig() + { + WebSocketProvider = DefaultWebSocketProvider.Instance; + UdpSocketProvider = DefaultUdpSocketProvider.Instance; + } + + internal DiscordSocketConfig Clone() => MemberwiseClone() as DiscordSocketConfig; } } diff --git a/src/Discord.Net.WebSocket/API/DiscordVoiceApiClient.cs b/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs similarity index 78% rename from src/Discord.Net.WebSocket/API/DiscordVoiceApiClient.cs rename to src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs index 378acd22e..fa619b58c 100644 --- a/src/Discord.Net.WebSocket/API/DiscordVoiceApiClient.cs +++ b/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs @@ -2,6 +2,7 @@ using Discord.API; using Discord.API.Voice; using Discord.Net.Converters; +using Discord.Net.Udp; using Discord.Net.WebSockets; using Newtonsoft.Json; using System; @@ -9,17 +10,15 @@ using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Compression; -using System.Net; -using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Discord.Audio { - public class DiscordVoiceAPIClient + internal class DiscordVoiceAPIClient { - public const int MaxBitrate = 128; + public const int MaxBitrate = 128 * 1024; public const string Mode = "xsalsa20_poly1305"; public event Func SentRequest { add { _sentRequestEvent.Add(value); } remove { _sentRequestEvent.Remove(value); } } @@ -39,26 +38,34 @@ namespace Discord.Audio private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); private readonly JsonSerializer _serializer; - private readonly IWebSocketClient _webSocketClient; private readonly SemaphoreSlim _connectionLock; private CancellationTokenSource _connectCancelToken; - private UdpClient _udp; - private IPEndPoint _udpEndpoint; - private Task _udpRecieveTask; + private IUdpSocket _udp; private bool _isDisposed; public ulong GuildId { get; } + internal IWebSocketClient WebSocketClient { get; } public ConnectionState ConnectionState { get; private set; } - internal DiscordVoiceAPIClient(ulong guildId, WebSocketProvider webSocketProvider, JsonSerializer serializer = null) + internal DiscordVoiceAPIClient(ulong guildId, WebSocketProvider webSocketProvider, UdpSocketProvider udpSocketProvider, JsonSerializer serializer = null) { GuildId = guildId; _connectionLock = new SemaphoreSlim(1, 1); - _udp = new UdpClient(new IPEndPoint(IPAddress.Any, 0)); + _udp = udpSocketProvider(); + _udp.ReceivedDatagram += async (data, index, count) => + { + if (index != 0) + { + var newData = new byte[count]; + Buffer.BlockCopy(data, index, newData, 0, count); + data = newData; + } + await _receivedPacketEvent.InvokeAsync(data).ConfigureAwait(false); + }; - _webSocketClient = webSocketProvider(); - //_gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .Net 4.6+) - _webSocketClient.BinaryMessage += async (data, index, count) => + WebSocketClient = webSocketProvider(); + //_gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); //(Causes issues in .Net 4.6+) + WebSocketClient.BinaryMessage += async (data, index, count) => { using (var compressed = new MemoryStream(data, index + 2, count - 2)) using (var decompressed = new MemoryStream()) @@ -73,12 +80,12 @@ namespace Discord.Audio } } }; - _webSocketClient.TextMessage += async text => + WebSocketClient.TextMessage += async text => { var msg = JsonConvert.DeserializeObject(text); await _receivedEvent.InvokeAsync((VoiceOpCode)msg.Operation, msg.Payload).ConfigureAwait(false); }; - _webSocketClient.Closed += async ex => + WebSocketClient.Closed += async ex => { await DisconnectAsync().ConfigureAwait(false); await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); @@ -93,7 +100,8 @@ namespace Discord.Audio if (disposing) { _connectCancelToken?.Dispose(); - (_webSocketClient as IDisposable)?.Dispose(); + (_udp as IDisposable)?.Dispose(); + (WebSocketClient as IDisposable)?.Dispose(); } _isDisposed = true; } @@ -106,23 +114,19 @@ namespace Discord.Audio payload = new SocketFrame { Operation = (int)opCode, Payload = payload }; if (payload != null) bytes = Encoding.UTF8.GetBytes(SerializeJson(payload)); - await _webSocketClient.SendAsync(bytes, 0, bytes.Length, true).ConfigureAwait(false); + await WebSocketClient.SendAsync(bytes, 0, bytes.Length, true).ConfigureAwait(false); await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false); } - public async Task SendAsync(byte[] data, int bytes) + public async Task SendAsync(byte[] data, int offset, int bytes) { - if (_udpEndpoint != null) - { - await _udp.SendAsync(data, bytes, _udpEndpoint).ConfigureAwait(false); - await _sentDataEvent.InvokeAsync(bytes).ConfigureAwait(false); - } - + await _udp.SendAsync(data, offset, bytes).ConfigureAwait(false); + await _sentDataEvent.InvokeAsync(bytes).ConfigureAwait(false); } //WebSocket public async Task SendHeartbeatAsync(RequestOptions options = null) { - await SendAsync(VoiceOpCode.Heartbeat, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), options: options).ConfigureAwait(false); + await SendAsync(VoiceOpCode.Heartbeat, DateTimeUtils.ToUnixMilliseconds(DateTimeOffset.UtcNow), options: options).ConfigureAwait(false); } public async Task SendIdentityAsync(ulong userId, string sessionId, string token) { @@ -171,9 +175,13 @@ namespace Discord.Audio try { _connectCancelToken = new CancellationTokenSource(); - _webSocketClient.SetCancelToken(_connectCancelToken.Token); - await _webSocketClient.ConnectAsync(url).ConfigureAwait(false); - _udpRecieveTask = ReceiveAsync(_connectCancelToken.Token); + var cancelToken = _connectCancelToken.Token; + + WebSocketClient.SetCancelToken(cancelToken); + await WebSocketClient.ConnectAsync(url).ConfigureAwait(false); + + _udp.SetCancelToken(cancelToken); + await _udp.StartAsync().ConfigureAwait(false); ConnectionState = ConnectionState.Connected; } @@ -202,9 +210,8 @@ namespace Discord.Audio catch { } //Wait for tasks to complete - await _udpRecieveTask.ConfigureAwait(false); - - await _webSocketClient.DisconnectAsync().ConfigureAwait(false); + await _udp.StopAsync().ConfigureAwait(false); + await WebSocketClient.DisconnectAsync().ConfigureAwait(false); ConnectionState = ConnectionState.Disconnected; } @@ -217,26 +224,13 @@ namespace Discord.Audio packet[1] = (byte)(ssrc >> 16); packet[2] = (byte)(ssrc >> 8); packet[3] = (byte)(ssrc >> 0); - await SendAsync(packet, 70).ConfigureAwait(false); + await SendAsync(packet, 0, 70).ConfigureAwait(false); await _sentDiscoveryEvent.InvokeAsync().ConfigureAwait(false); } - public void SetUdpEndpoint(IPEndPoint endpoint) + public void SetUdpEndpoint(string host, int port) { - _udpEndpoint = endpoint; - } - private async Task ReceiveAsync(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; - - await _receivedPacketEvent.InvokeAsync(receiveTask.Result.Buffer).ConfigureAwait(false); - } + _udp.SetDestination(host, port); } //Helpers diff --git a/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs index 7056a4df5..7b9bf07f0 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs @@ -1,6 +1,10 @@ -namespace Discord.WebSocket +using Discord.Audio; +using System.Threading.Tasks; + +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 f1d4221a2..43246f5ca 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs @@ -11,9 +11,11 @@ namespace Discord.WebSocket IReadOnlyCollection CachedMessages { get; } /// Sends a message to this message channel. - new Task SendMessageAsync(string text, bool isTTS = false, EmbedBuilder embed = null, RequestOptions options = null); + new Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null); +#if NETSTANDARD1_3 /// 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 /// Sends a file to this text channel, with an optional caption. new Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, RequestOptions options = null); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs index 48f87764b..c976b64f8 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs @@ -66,10 +66,12 @@ namespace Discord.WebSocket public Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - public Task SendMessageAsync(string text, bool isTTS = false, EmbedBuilder embed = null, RequestOptions options = null) + 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 public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); +#endif public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); @@ -130,11 +132,13 @@ 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 async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); +#endif async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, EmbedBuilder embed, RequestOptions options) + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index 93407e22e..ceba50a6e 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -1,4 +1,5 @@ -using Discord.Rest; +using Discord.Audio; +using Discord.Rest; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -64,6 +65,11 @@ namespace Discord.WebSocket public Task LeaveAsync(RequestOptions options = null) => ChannelHelper.DeleteAsync(this, Discord, options); + public Task ConnectAsync() + { + throw new NotSupportedException("Voice is not yet supported for group channels."); + } + //Messages public SocketMessage GetCachedMessage(ulong id) => _messages?.Get(id); @@ -89,10 +95,12 @@ namespace Discord.WebSocket public Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - public Task SendMessageAsync(string text, bool isTTS = false, EmbedBuilder embed = null, RequestOptions options = null) + 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 public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); +#endif public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); @@ -193,11 +201,13 @@ 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 async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); +#endif async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, EmbedBuilder embed, RequestOptions options) + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs index 45d58b42c..0e7cfde82 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -1,5 +1,4 @@ -using Discord.API.Rest; -using Discord.Rest; +using Discord.Rest; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -47,11 +46,11 @@ namespace Discord.WebSocket var overwrites = model.PermissionOverwrites.Value; var newOverwrites = ImmutableArray.CreateBuilder(overwrites.Length); for (int i = 0; i < overwrites.Length; i++) - newOverwrites.Add(new Overwrite(overwrites[i])); + newOverwrites.Add(overwrites[i].ToEntity()); _overwrites = newOverwrites.ToImmutable(); } - public Task ModifyAsync(Action func, RequestOptions options = null) + public Task ModifyAsync(Action func, RequestOptions options = null) => ChannelHelper.ModifyAsync(this, Discord, func, options); public Task DeleteAsync(RequestOptions options = null) => ChannelHelper.DeleteAsync(this, Discord, options); @@ -77,12 +76,12 @@ namespace Discord.WebSocket public async Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions perms, RequestOptions options = null) { await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, user, perms, options).ConfigureAwait(false); - _overwrites = _overwrites.Add(new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = user.Id, TargetType = PermissionTarget.User })); + _overwrites = _overwrites.Add(new Overwrite(user.Id, PermissionTarget.User, new OverwritePermissions(perms.AllowValue, perms.DenyValue))); } public async Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions perms, RequestOptions options = null) { await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, role, perms, options).ConfigureAwait(false); - _overwrites.Add(new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = role.Id, TargetType = PermissionTarget.Role })); + _overwrites = _overwrites.Add(new Overwrite(role.Id, PermissionTarget.Role, new OverwritePermissions(perms.AllowValue, perms.DenyValue))); } public async Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null) { @@ -113,8 +112,8 @@ namespace Discord.WebSocket public async Task> GetInvitesAsync(RequestOptions options = null) => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); - public async Task CreateInviteAsync(int? maxAge = 3600, int? maxUses = null, bool isTemporary = true, RequestOptions options = null) - => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, options).ConfigureAwait(false); + public async Task CreateInviteAsync(int? maxAge = 3600, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); public new abstract SocketGuildUser GetUser(ulong id); @@ -131,8 +130,8 @@ namespace Discord.WebSocket async Task> IGuildChannel.GetInvitesAsync(RequestOptions options) => await GetInvitesAsync(options).ConfigureAwait(false); - async Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, RequestOptions options) - => await CreateInviteAsync(maxAge, maxUses, isTemporary, options).ConfigureAwait(false); + async Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) + => await CreateInviteAsync(maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IRole role) => GetPermissionOverwrite(role); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index 9b687c9fb..98aefcf9b 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -1,5 +1,4 @@ -using Discord.API.Rest; -using Discord.Rest; +using Discord.Rest; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -44,7 +43,7 @@ namespace Discord.WebSocket Topic = model.Topic.Value; } - public Task ModifyAsync(Action func, RequestOptions options = null) + public Task ModifyAsync(Action func, RequestOptions options = null) => ChannelHelper.ModifyAsync(this, Discord, func, options); //Messages @@ -72,10 +71,12 @@ namespace Discord.WebSocket public Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - public Task SendMessageAsync(string text, bool isTTS = false, EmbedBuilder embed = null, RequestOptions options = null) + 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 public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); +#endif public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); @@ -131,11 +132,13 @@ 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 async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); +#endif async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, EmbedBuilder embed, RequestOptions options) + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs index b311f3c01..71017a7c8 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs @@ -1,5 +1,4 @@ -using Discord.API.Rest; -using Discord.Audio; +using Discord.Audio; using Discord.Rest; using System; using System.Collections.Generic; @@ -15,7 +14,7 @@ namespace Discord.WebSocket public class SocketVoiceChannel : SocketGuildChannel, IVoiceChannel, ISocketAudioChannel { public int Bitrate { get; private set; } - public int UserLimit { get; private set; } + public int? UserLimit { get; private set; } public override IReadOnlyCollection Users => Guild.Users.Where(x => x.VoiceChannel?.Id == Id).ToImmutableArray(); @@ -35,12 +34,23 @@ namespace Discord.WebSocket base.Update(state, model); Bitrate = model.Bitrate.Value; - UserLimit = model.UserLimit.Value; + UserLimit = model.UserLimit.Value != 0 ? model.UserLimit.Value : (int?)null; } - public Task ModifyAsync(Action func, RequestOptions options = null) + public Task ModifyAsync(Action func, RequestOptions options = null) => ChannelHelper.ModifyAsync(this, Discord, func, options); + public async Task ConnectAsync() + { + 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); + } + public override SocketGuildUser GetUser(ulong id) { var user = Guild.GetUser(id); @@ -52,9 +62,6 @@ namespace Discord.WebSocket private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; internal new SocketVoiceChannel Clone() => MemberwiseClone() as SocketVoiceChannel; - //IVoiceChannel - Task IVoiceChannel.ConnectAsync() { throw new NotSupportedException(); } - //IGuildChannel Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index d5d93f991..007f52124 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -1,5 +1,4 @@ -using Discord.API.Rest; -using Discord.Audio; +using Discord.Audio; using Discord.Rest; using System; using System.Collections.Concurrent; @@ -32,6 +31,7 @@ namespace Discord.WebSocket private ConcurrentDictionary _voiceStates; private ImmutableArray _emojis; private ImmutableArray _features; + private AudioClient _audioClient; internal bool _available; public string Name { get; private set; } @@ -42,23 +42,44 @@ namespace Discord.WebSocket public DefaultMessageNotifications DefaultMessageNotifications { get; private set; } public int MemberCount { get; set; } public int DownloadedMemberCount { get; private set; } - public AudioClient AudioClient { get; private set; } - public ulong? AFKChannelId { get; private set; } - public ulong? EmbedChannelId { get; private set; } + internal ulong? AFKChannelId { get; private set; } + internal ulong? EmbedChannelId { get; private set; } public ulong OwnerId { get; private set; } + public SocketGuildUser Owner => GetUser(OwnerId); public string VoiceRegionId { get; private set; } public string IconId { get; private set; } public string SplashId { get; private set; } public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); - public ulong DefaultChannelId => Id; - public string IconUrl => API.CDN.GetGuildIconUrl(Id, IconId); - public string SplashUrl => API.CDN.GetGuildSplashUrl(Id, SplashId); - public bool HasAllMembers => _downloaderPromise.Task.IsCompleted; + public SocketTextChannel DefaultChannel => GetTextChannel(Id); + public string IconUrl => CDN.GetGuildIconUrl(Id, IconId); + public string SplashUrl => CDN.GetGuildSplashUrl(Id, SplashId); + public bool HasAllMembers => MemberCount == DownloadedMemberCount;// _downloaderPromise.Task.IsCompleted; public bool IsSynced => _syncPromise.Task.IsCompleted; public Task SyncPromise => _syncPromise.Task; public Task DownloaderPromise => _downloaderPromise.Task; + public IAudioClient AudioClient => _audioClient; + public SocketVoiceChannel AFKChannel + { + get + { + var id = AFKChannelId; + return id.HasValue ? GetVoiceChannel(id.Value) : null; + } + } + public SocketVoiceChannel EmbedChannel + { + get + { + var id = EmbedChannelId; + return id.HasValue ? GetVoiceChannel(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 @@ -69,7 +90,6 @@ namespace Discord.WebSocket return null; } } - public SocketRole EveryoneRole => GetRole(Id); public IReadOnlyCollection Channels { @@ -84,7 +104,6 @@ namespace Discord.WebSocket public IReadOnlyCollection Features => _features; public IReadOnlyCollection Users => _members.ToReadOnlyCollection(); public IReadOnlyCollection Roles => _roles.ToReadOnlyCollection(); - public IReadOnlyCollection VoiceStates => _voiceStates.ToReadOnlyCollection(); internal SocketGuild(DiscordSocketClient client, ulong id) : base(client, id) @@ -192,7 +211,7 @@ namespace Discord.WebSocket { var emojis = ImmutableArray.CreateBuilder(model.Emojis.Length); for (int i = 0; i < model.Emojis.Length; i++) - emojis.Add(GuildEmoji.Create(model.Emojis[i])); + emojis.Add(model.Emojis[i].ToEntity()); _emojis = emojis.ToImmutable(); } else @@ -245,7 +264,7 @@ namespace Discord.WebSocket { var emojis = ImmutableArray.CreateBuilder(model.Emojis.Length); for (int i = 0; i < model.Emojis.Length; i++) - emojis.Add(GuildEmoji.Create(model.Emojis[i])); + emojis.Add(model.Emojis[i].ToEntity()); _emojis = emojis.ToImmutable(); } @@ -253,13 +272,13 @@ namespace Discord.WebSocket public Task DeleteAsync(RequestOptions options = null) => GuildHelper.DeleteAsync(this, Discord, options); - public Task ModifyAsync(Action func, RequestOptions options = null) + public Task ModifyAsync(Action func, RequestOptions options = null) => GuildHelper.ModifyAsync(this, Discord, func, options); - public Task ModifyEmbedAsync(Action func, RequestOptions options = null) + public Task ModifyEmbedAsync(Action func, RequestOptions options = null) => GuildHelper.ModifyEmbedAsync(this, Discord, func, options); - public Task ModifyChannelsAsync(IEnumerable args, RequestOptions options = null) + public Task ModifyChannelsAsync(IEnumerable args, RequestOptions options = null) => GuildHelper.ModifyChannelsAsync(this, Discord, args, options); - public Task ModifyRolesAsync(IEnumerable args, RequestOptions options = null) + public Task ModifyRolesAsync(IEnumerable args, RequestOptions options = null) => GuildHelper.ModifyRolesAsync(this, Discord, args, options); public Task LeaveAsync(RequestOptions options = null) @@ -287,6 +306,10 @@ namespace Discord.WebSocket return channel; return null; } + public SocketTextChannel GetTextChannel(ulong id) + => GetChannel(id) as SocketTextChannel; + public SocketVoiceChannel GetVoiceChannel(ulong id) + => GetChannel(id) as SocketVoiceChannel; public Task CreateTextChannelAsync(string name, RequestOptions options = null) => GuildHelper.CreateTextChannelAsync(this, Discord, name, options); public Task CreateVoiceChannelAsync(string name, RequestOptions options = null) @@ -422,102 +445,107 @@ namespace Discord.WebSocket } //Audio - public async Task DisconnectAudioAsync(AudioClient client = null) + internal async Task ConnectAudioAsync(ulong channelId, bool selfDeaf, bool selfMute) { + selfDeaf = false; + selfMute = false; + + TaskCompletionSource promise; + await _audioLock.WaitAsync().ConfigureAwait(false); try { - await DisconnectAudioInternalAsync(client).ConfigureAwait(false); + await DisconnectAudioInternalAsync().ConfigureAwait(false); + promise = new TaskCompletionSource(); + _audioConnectPromise = promise; + await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, channelId, selfDeaf, selfMute).ConfigureAwait(false); + } + catch (Exception) + { + await DisconnectAudioInternalAsync().ConfigureAwait(false); + throw; } finally { _audioLock.Release(); } + + try + { + var timeoutTask = Task.Delay(15000); + if (await Task.WhenAny(promise.Task, timeoutTask) == timeoutTask) + throw new TimeoutException(); + return await promise.Task.ConfigureAwait(false); + } + catch (Exception) + { + await DisconnectAudioAsync().ConfigureAwait(false); + throw; + } } - private async Task DisconnectAudioInternalAsync(AudioClient client = null) + + internal async Task DisconnectAudioAsync() { - var oldClient = AudioClient; - if (oldClient != null) + await _audioLock.WaitAsync().ConfigureAwait(false); + try { - if (client == null || oldClient == client) - { - _audioConnectPromise?.TrySetCanceledAsync(); //Cancel any previous audio connection - _audioConnectPromise = null; - } - if (oldClient == client) - { - AudioClient = null; - await oldClient.DisconnectAsync().ConfigureAwait(false); - } + await DisconnectAudioInternalAsync().ConfigureAwait(false); + } + finally + { + _audioLock.Release(); } } + private async Task DisconnectAudioInternalAsync() + { + _audioConnectPromise?.TrySetCanceledAsync(); //Cancel any previous audio connection + _audioConnectPromise = null; + if (_audioClient != null) + await _audioClient.StopAsync().ConfigureAwait(false); + _audioClient = null; + } internal async Task FinishConnectAudio(int id, string url, string token) { + //TODO: Mem Leak: Disconnected/Connected handlers arent cleaned up var voiceState = GetVoiceState(Discord.CurrentUser.Id).Value; await _audioLock.WaitAsync().ConfigureAwait(false); try { - if (AudioClient == null) + var promise = _audioConnectPromise; + if (_audioClient == null) { var audioClient = new AudioClient(this, id); audioClient.Disconnected += async ex => { - await _audioLock.WaitAsync().ConfigureAwait(false); - try - { - if (AudioClient == audioClient) //Only reconnect if we're still assigned as this guild's audio client - { - if (ex != null) - { - //Reconnect if we still have channel info. - //TODO: Is this threadsafe? Could channel data be deleted before we access it? - var voiceState2 = GetVoiceState(Discord.CurrentUser.Id); - if (voiceState2.HasValue) - { - var voiceChannelId = voiceState2.Value.VoiceChannel?.Id; - if (voiceChannelId != null) - await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, voiceChannelId, voiceState2.Value.IsSelfDeafened, voiceState2.Value.IsSelfMuted); - } - } - else - { - try { AudioClient.Dispose(); } catch { } - AudioClient = null; - } - } - } - finally + if (!promise.Task.IsCompleted) { - _audioLock.Release(); + try { audioClient.Dispose(); } catch { } + _audioClient = null; + if (ex != null) + await promise.TrySetExceptionAsync(ex); + else + await promise.TrySetCanceledAsync(); + return; } }; - AudioClient = audioClient; + _audioClient = audioClient; } - await AudioClient.ConnectAsync(url, Discord.CurrentUser.Id, voiceState.VoiceSessionId, token).ConfigureAwait(false); - await _audioConnectPromise.TrySetResultAsync(AudioClient).ConfigureAwait(false); + _audioClient.Connected += () => + { + var _ = promise.TrySetResultAsync(_audioClient); + return Task.Delay(0); + }; + await _audioClient.StartAsync(url, Discord.CurrentUser.Id, voiceState.VoiceSessionId, token).ConfigureAwait(false); } catch (OperationCanceledException) { - await DisconnectAudioAsync().ConfigureAwait(false); + await DisconnectAudioInternalAsync().ConfigureAwait(false); } catch (Exception e) { await _audioConnectPromise.SetExceptionAsync(e).ConfigureAwait(false); - await DisconnectAudioAsync().ConfigureAwait(false); - } - finally - { - _audioLock.Release(); - } - } - internal async Task FinishJoinAudioChannel() - { - await _audioLock.WaitAsync().ConfigureAwait(false); - try - { - if (AudioClient != null) - await _audioConnectPromise.TrySetResultAsync(AudioClient).ConfigureAwait(false); + await DisconnectAudioInternalAsync().ConfigureAwait(false); } finally { @@ -530,8 +558,11 @@ namespace Discord.WebSocket internal SocketGuild Clone() => MemberwiseClone() as SocketGuild; //IGuild - bool IGuild.Available => true; + ulong? IGuild.AFKChannelId => AFKChannelId; IAudioClient IGuild.AudioClient => null; + bool IGuild.Available => true; + ulong IGuild.DefaultChannelId => Id; + ulong? IGuild.EmbedChannelId => EmbedChannelId; IRole IGuild.EveryoneRole => EveryoneRole; IReadOnlyCollection IGuild.Roles => Roles; @@ -542,6 +573,20 @@ namespace Discord.WebSocket => Task.FromResult>(Channels); Task IGuild.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetChannel(id)); + Task> IGuild.GetTextChannelsAsync(CacheMode mode, RequestOptions options) + => Task.FromResult>(TextChannels); + Task IGuild.GetTextChannelAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(GetTextChannel(id)); + Task> IGuild.GetVoiceChannelsAsync(CacheMode mode, RequestOptions options) + => Task.FromResult>(VoiceChannels); + Task IGuild.GetVoiceChannelAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(GetVoiceChannel(id)); + Task IGuild.GetAFKChannelAsync(CacheMode mode, RequestOptions options) + => Task.FromResult(AFKChannel); + Task IGuild.GetDefaultChannelAsync(CacheMode mode, RequestOptions options) + => Task.FromResult(DefaultChannel); + 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) @@ -566,6 +611,8 @@ namespace Discord.WebSocket => Task.FromResult(GetUser(id)); Task IGuild.GetCurrentUserAsync(CacheMode mode, RequestOptions options) => Task.FromResult(CurrentUser); + Task IGuild.GetOwnerAsync(CacheMode mode, RequestOptions options) + => Task.FromResult(Owner); Task IGuild.DownloadUsersAsync() { throw new NotSupportedException(); } } } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs new file mode 100644 index 000000000..c12d0fdea --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs @@ -0,0 +1,28 @@ +using Model = Discord.API.Gateway.Reaction; + +namespace Discord.WebSocket +{ + public class SocketReaction : IReaction + { + public ulong UserId { get; } + public Optional User { get; } + public ulong MessageId { get; } + public Optional Message { get; } + public ISocketMessageChannel Channel { get; } + public Emoji Emoji { get; } + + internal SocketReaction(ISocketMessageChannel channel, ulong messageId, Optional message, ulong userId, Optional user, Emoji emoji) + { + Channel = channel; + MessageId = messageId; + Message = message; + UserId = userId; + User = user; + Emoji = 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)); + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs index 7678bb412..50cdb964b 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs @@ -4,7 +4,7 @@ using Model = Discord.API.Message; namespace Discord.WebSocket { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - internal class SocketSystemMessage : SocketMessage, ISystemMessage + public class SocketSystemMessage : SocketMessage, ISystemMessage { public MessageType Type { get; private set; } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index 2261076f7..e1a6853e2 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -1,9 +1,9 @@ -using Discord.API.Rest; -using Discord.Rest; +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.Message; @@ -18,6 +18,7 @@ namespace Discord.WebSocket private ImmutableArray _attachments; private ImmutableArray _embeds; private ImmutableArray _tags; + private List _reactions = new List(); public override bool IsTTS => _isTTS; public override bool IsPinned => _isPinned; @@ -29,6 +30,7 @@ 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()); internal SocketUserMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author) : base(discord, id, channel, author) @@ -77,7 +79,7 @@ namespace Discord.WebSocket { var embeds = ImmutableArray.CreateBuilder(value.Length); for (int i = 0; i < value.Length; i++) - embeds.Add(Embed.Create(value[i])); + embeds.Add(value[i].ToEntity()); _embeds = embeds.ToImmutable(); } else @@ -109,10 +111,39 @@ namespace Discord.WebSocket model.Content = text; } } + internal void AddReaction(SocketReaction reaction) + { + _reactions.Add(reaction); + } + internal void RemoveReaction(SocketReaction reaction) + { + if (_reactions.Contains(reaction)) + _reactions.Remove(reaction); + } + internal void ClearReactions() + { + _reactions.Clear(); + } - public Task ModifyAsync(Action func, RequestOptions options = null) + 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 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); + public Task PinAsync(RequestOptions options = null) => MessageHelper.PinAsync(this, Discord, options); public Task UnpinAsync(RequestOptions options = null) diff --git a/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs b/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs index 5fa585138..61fd4310f 100644 --- a/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs +++ b/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs @@ -1,5 +1,4 @@ -using Discord.API.Rest; -using Discord.Rest; +using Discord.Rest; using System; using System.Diagnostics; using System.Threading.Tasks; @@ -46,7 +45,7 @@ namespace Discord.WebSocket Permissions = new GuildPermissions(model.Permissions); } - public Task ModifyAsync(Action func, RequestOptions options = null) + public Task ModifyAsync(Action func, RequestOptions options = null) => RoleHelper.ModifyAsync(this, Discord, func, options); public Task DeleteAsync(RequestOptions options = null) => RoleHelper.DeleteAsync(this, Discord, options); @@ -55,8 +54,9 @@ namespace Discord.WebSocket private string DebuggerDisplay => $"{Name} ({Id})"; internal SocketRole Clone() => MemberwiseClone() as SocketRole; + public int CompareTo(IRole role) => RoleUtils.Compare(this, role); + //IRole IGuild IRole.Guild => Guild; - public int CompareTo(IRole role) => this.CompareTo(role); } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index 3ef45d230..5162839d7 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -1,9 +1,9 @@ -using Discord.API.Rest; -using Discord.Rest; +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 PresenceModel = Discord.API.Presence; @@ -33,7 +33,7 @@ namespace Discord.WebSocket public bool IsDeafened => VoiceState?.IsDeafened ?? false; public bool IsMuted => VoiceState?.IsMuted ?? false; public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); - public IReadOnlyCollection RoleIds => _roleIds; + public IEnumerable Roles => _roleIds.Select(id => Guild.GetRole(id)).ToReadOnlyCollection(() => _roleIds.Count()); public SocketVoiceChannel VoiceChannel => VoiceState?.VoiceChannel; public string VoiceSessionId => VoiceState?.VoiceSessionId ?? ""; public SocketVoiceState? VoiceState => Guild.GetVoiceState(Id); @@ -80,7 +80,8 @@ namespace Discord.WebSocket internal void Update(ClientState state, Model model) { base.Update(state, model.User); - _joinedAtTicks = model.JoinedAt.UtcTicks; + if (model.JoinedAt.IsSpecified) + _joinedAtTicks = model.JoinedAt.Value.UtcTicks; if (model.Nick.IsSpecified) Nickname = model.Nick.Value; UpdateRoles(model.Roles); @@ -102,7 +103,7 @@ namespace Discord.WebSocket _roleIds = roles.ToImmutable(); } - public Task ModifyAsync(Action func, RequestOptions options = null) + 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); @@ -115,7 +116,7 @@ namespace Discord.WebSocket //IGuildUser IGuild IGuildUser.Guild => Guild; ulong IGuildUser.GuildId => Guild.Id; - IReadOnlyCollection IGuildUser.RoleIds => RoleIds; + IReadOnlyCollection IGuildUser.RoleIds => _roleIds; //IUser Task IUser.GetDMChannelAsync(CacheMode mode, RequestOptions options) diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs index 629aa2093..00d4b4bbc 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs @@ -17,11 +17,11 @@ namespace Discord.WebSocket } internal static SocketPresence Create(Model model) { - return new SocketPresence(model.Status, model.Game != null ? Discord.Game.Create(model.Game) : (Game?)null); + return new SocketPresence(model.Status, model.Game != null ? model.Game.ToEntity() : (Game?)null); } public override string ToString() => Status.ToString(); - internal string DebuggerDisplay => $"{Status}{(Game != null ? $", {Game.Value.Name} ({Game.Value.StreamType})" : "")}"; + private string DebuggerDisplay => $"{Status}{(Game != null ? $", {Game.Value.Name} ({Game.Value.StreamType})" : "")}"; internal SocketPresence Clone() => this; } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs index 9b85e15e0..0f6d4e4f1 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs @@ -1,5 +1,4 @@ -using Discord.API.Rest; -using Discord.Rest; +using Discord.Rest; using System; using System.Diagnostics; using System.Threading.Tasks; @@ -44,7 +43,7 @@ namespace Discord.WebSocket IsMfaEnabled = model.MfaEnabled.Value; } - public Task ModifyAsync(Action func, RequestOptions options = null) + public Task ModifyAsync(Action func, RequestOptions options = null) => UserHelper.ModifyAsync(this, Discord, func, options); internal new SocketSelfUser Clone() => MemberwiseClone() as SocketSelfUser; diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index 674239be7..574e79b6e 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -15,7 +15,7 @@ namespace Discord.WebSocket internal abstract SocketGlobalUser GlobalUser { get; } internal abstract SocketPresence Presence { get; set; } - public string AvatarUrl => API.CDN.GetUserAvatarUrl(Id, AvatarId); + public string GetAvatarUrl(AvatarFormat format = AvatarFormat.Png, ushort size = 128) => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); @@ -40,13 +40,14 @@ namespace Discord.WebSocket internal virtual void Update(ClientState state, PresenceModel model) { Presence = SocketPresence.Create(model); + Update(state, model.User); } public Task CreateDMChannelAsync(RequestOptions options = null) => UserHelper.CreateDMChannelAsync(this, Discord, options); public override string ToString() => $"{Username}#{Discriminator}"; - internal string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; + private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; internal SocketUser Clone() => MemberwiseClone() as SocketUser; //IUser diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs index ed4036362..480103326 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs @@ -8,6 +8,8 @@ namespace Discord.WebSocket [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public struct SocketVoiceState : IVoiceState { + public static readonly SocketVoiceState Default = new SocketVoiceState(null, null, false, false, false, false, false); + [Flags] private enum Flags : byte { @@ -30,7 +32,7 @@ namespace Discord.WebSocket public bool IsSelfMuted => (_voiceStates & Flags.SelfMuted) != 0; public bool IsSelfDeafened => (_voiceStates & Flags.SelfDeafened) != 0; - internal SocketVoiceState(SocketVoiceChannel voiceChannel, string sessionId, bool isSelfMuted, bool isSelfDeafened, bool isSuppressed) + internal SocketVoiceState(SocketVoiceChannel voiceChannel, string sessionId, bool isSelfMuted, bool isSelfDeafened, bool isMuted, bool isDeafened, bool isSuppressed) { VoiceChannel = voiceChannel; VoiceSessionId = sessionId; @@ -40,17 +42,21 @@ namespace Discord.WebSocket voiceStates |= Flags.SelfMuted; if (isSelfDeafened) voiceStates |= Flags.SelfDeafened; + if (isMuted) + voiceStates |= Flags.Muted; + if (isDeafened) + voiceStates |= Flags.Deafened; if (isSuppressed) voiceStates |= Flags.Suppressed; _voiceStates = voiceStates; } internal static SocketVoiceState Create(SocketVoiceChannel voiceChannel, Model model) { - return new SocketVoiceState(voiceChannel, model.SessionId, model.SelfMute, model.SelfDeaf, model.Suppress); + return new SocketVoiceState(voiceChannel, model.SessionId, model.SelfMute, model.SelfDeaf, model.Mute, model.Deaf, model.Suppress); } public override string ToString() => VoiceChannel?.Name ?? "Unknown"; - internal string DebuggerDisplay => $"{VoiceChannel?.Name ?? "Unknown"} ({_voiceStates})"; + private string DebuggerDisplay => $"{VoiceChannel?.Name ?? "Unknown"} ({_voiceStates})"; internal SocketVoiceState Clone() => this; IVoiceChannel IVoiceState.VoiceChannel => VoiceChannel; diff --git a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs new file mode 100644 index 000000000..636ef68f4 --- /dev/null +++ b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs @@ -0,0 +1,12 @@ +namespace Discord.WebSocket +{ + internal static class EntityExtensions + { + public static Game ToEntity(this API.Game model) + { + return new Game(model.Name, + model.StreamUrl.GetValueOrDefault(null), + model.StreamType.GetValueOrDefault(null) ?? StreamType.NotStreaming); + } + } +} diff --git a/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs b/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs new file mode 100644 index 000000000..eb184e345 --- /dev/null +++ b/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs @@ -0,0 +1,130 @@ +#if NETSTANDARD1_3 +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.Udp +{ + internal class DefaultUdpSocket : IUdpSocket, IDisposable + { + public event Func ReceivedDatagram; + + private readonly SemaphoreSlim _lock; + private UdpClient _udp; + private IPEndPoint _destination; + private CancellationTokenSource _cancelTokenSource; + private CancellationToken _cancelToken, _parentToken; + private Task _task; + private bool _isDisposed; + + public DefaultUdpSocket() + { + _lock = new SemaphoreSlim(1, 1); + _cancelTokenSource = new CancellationTokenSource(); + } + 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 UdpClient(0); + + _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.Dispose(); } + 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); + } + } + } +} +#endif \ No newline at end of file diff --git a/src/Discord.Net.WebSocket/Net/DefaultUdpSocketProvider.cs b/src/Discord.Net.WebSocket/Net/DefaultUdpSocketProvider.cs new file mode 100644 index 000000000..cba4fecb0 --- /dev/null +++ b/src/Discord.Net.WebSocket/Net/DefaultUdpSocketProvider.cs @@ -0,0 +1,27 @@ +using System; + +namespace Discord.Net.Udp +{ + public static class DefaultUdpSocketProvider + { +#if NETSTANDARD1_3 + public static readonly UdpSocketProvider Instance = () => + { + try + { + return new DefaultUdpSocket(); + } + catch (PlatformNotSupportedException ex) + { + throw new PlatformNotSupportedException("The default UdpSocketProvider is not supported on this platform.", ex); + } + }; +#else + public static readonly UdpSocketProvider Instance = () => + { + throw new PlatformNotSupportedException("The default UdpSocketProvider is not supported on this platform.\n" + + "You must specify a UdpSocketProvider or target a runtime supporting .NET Standard 1.3, such as .NET Framework 4.6+."); + }; +#endif + } +} \ No newline at end of file diff --git a/src/Discord.Net.Core/Net/WebSockets/DefaultWebSocketClient.cs b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs similarity index 78% rename from src/Discord.Net.Core/Net/WebSockets/DefaultWebSocketClient.cs rename to src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs index 707f7663b..6f667ae41 100644 --- a/src/Discord.Net.Core/Net/WebSockets/DefaultWebSocketClient.cs +++ b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs @@ -1,4 +1,5 @@ -using System; +#if NETSTANDARD1_3 +using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; @@ -9,7 +10,7 @@ using System.Threading.Tasks; namespace Discord.Net.WebSockets { - public class DefaultWebSocketClient : IWebSocketClient + internal class DefaultWebSocketClient : IWebSocketClient, IDisposable { public const int ReceiveChunkSize = 16 * 1024; //16KB public const int SendChunkSize = 4 * 1024; //4KB @@ -19,17 +20,17 @@ namespace Discord.Net.WebSockets public event Func TextMessage; public event Func Closed; - private readonly SemaphoreSlim _sendLock; + private readonly SemaphoreSlim _lock; private readonly Dictionary _headers; private ClientWebSocket _client; private Task _task; private CancellationTokenSource _cancelTokenSource; private CancellationToken _cancelToken, _parentToken; - private bool _isDisposed; + private bool _isDisposed, _isDisconnecting; public DefaultWebSocketClient() { - _sendLock = new SemaphoreSlim(1, 1); + _lock = new SemaphoreSlim(1, 1); _cancelTokenSource = new CancellationTokenSource(); _cancelToken = CancellationToken.None; _parentToken = CancellationToken.None; @@ -40,7 +41,7 @@ namespace Discord.Net.WebSockets if (!_isDisposed) { if (disposing) - _client.Dispose(); + DisconnectInternalAsync(true).GetAwaiter().GetResult(); _isDisposed = true; } } @@ -51,14 +52,14 @@ namespace Discord.Net.WebSockets public async Task ConnectAsync(string host) { - await _sendLock.WaitAsync().ConfigureAwait(false); + await _lock.WaitAsync().ConfigureAwait(false); try { await ConnectInternalAsync(host).ConfigureAwait(false); } finally { - _sendLock.Release(); + _lock.Release(); } } private async Task ConnectInternalAsync(string host) @@ -83,28 +84,57 @@ namespace Discord.Net.WebSockets public async Task DisconnectAsync() { - await _sendLock.WaitAsync().ConfigureAwait(false); + await _lock.WaitAsync().ConfigureAwait(false); try { await DisconnectInternalAsync().ConfigureAwait(false); } finally { - _sendLock.Release(); + _lock.Release(); } } - private async Task DisconnectInternalAsync() + private async Task DisconnectInternalAsync(bool isDisposing = false) { try { _cancelTokenSource.Cancel(false); } catch { } - await (_task ?? Task.CompletedTask).ConfigureAwait(false); + _isDisconnecting = true; + try + { + await (_task ?? Task.Delay(0)).ConfigureAwait(false); + _task = null; + } + finally { _isDisconnecting = false; } - if (_client != null && _client.State == WebSocketState.Open) + if (_client != null) { - _client.Dispose(); + if (!isDisposing) + { + try { await _client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", new CancellationToken()); } + catch { } + } + try { _client.Dispose(); } + catch { } + _client = null; } } + private async Task OnClosed(Exception ex) + { + if (_isDisconnecting) + return; //Ignore, this disconnect was requested. + + await _lock.WaitAsync().ConfigureAwait(false); + try + { + await DisconnectInternalAsync(false); + } + finally + { + _lock.Release(); + } + await Closed(ex); + } public void SetHeader(string key, string value) { @@ -118,7 +148,7 @@ namespace Discord.Net.WebSockets public async Task SendAsync(byte[] data, int index, int count, bool isText) { - await _sendLock.WaitAsync().ConfigureAwait(false); + await _lock.WaitAsync().ConfigureAwait(false); try { if (_client == null) return; @@ -141,7 +171,7 @@ namespace Discord.Net.WebSockets } finally { - _sendLock.Release(); + _lock.Release(); } } @@ -158,10 +188,7 @@ namespace Discord.Net.WebSockets int resultCount; if (socketResult.MessageType == WebSocketMessageType.Close) - { - var _ = Closed(new WebSocketClosedException((int)socketResult.CloseStatus, socketResult.CloseStatusDescription)); - return; - } + throw new WebSocketClosedException((int)socketResult.CloseStatus, socketResult.CloseStatusDescription); if (!socketResult.EndOfMessage) { @@ -204,14 +231,15 @@ namespace Discord.Net.WebSockets } catch (Win32Exception ex) when (ex.HResult == HR_TIMEOUT) { - var _ = Closed(new Exception("Connection timed out.", ex)); + var _ = OnClosed(new Exception("Connection timed out.", ex)); } catch (OperationCanceledException) { } catch (Exception ex) { //This cannot be awaited otherwise we'll deadlock when DiscordApiClient waits for this task to complete. - var _ = Closed(ex); + var _ = OnClosed(ex); } } } } +#endif \ No newline at end of file diff --git a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs new file mode 100644 index 000000000..d93ded57d --- /dev/null +++ b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs @@ -0,0 +1,27 @@ +using System; + +namespace Discord.Net.WebSockets +{ + public static class DefaultWebSocketProvider + { +#if NETSTANDARD1_3 + public static readonly WebSocketProvider Instance = () => + { + try + { + return new DefaultWebSocketClient(); + } + catch (PlatformNotSupportedException ex) + { + throw new PlatformNotSupportedException("The default WebSocketProvider is not supported on this platform.", ex); + } + }; +#else + public static readonly WebSocketProvider Instance = () => + { + throw new PlatformNotSupportedException("The default WebSocketProvider is not supported on this platform.\n" + + "You must specify a WebSocketProvider or target a runtime supporting .NET Standard 1.3, such as .NET Framework 4.6+."); + }; +#endif + } +} \ No newline at end of file diff --git a/src/Discord.Net.WebSocket/project.json b/src/Discord.Net.WebSocket/project.json deleted file mode 100644 index 46eb0eccd..000000000 --- a/src/Discord.Net.WebSocket/project.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "version": "1.0.0-*", - "description": "A core Discord.Net library containing the WebSocket client and models.", - "authors": [ "RogueException" ], - - "packOptions": { - "tags": [ "discord", "discordapp" ], - "licenseUrl": "http://opensource.org/licenses/MIT", - "projectUrl": "https://github.com/RogueException/Discord.Net", - "repository": { - "type": "git", - "url": "git://github.com/RogueException/Discord.Net" - } - }, - - "buildOptions": { - "allowUnsafe": true - }, - - "configurations": { - "Release": { - "buildOptions": { - "define": [ "RELEASE" ], - "nowarn": [ "CS1573", "CS1591" ], - "optimize": true, - "warningsAsErrors": true, - "xmlDoc": true - } - } - }, - - "dependencies": { - "Discord.Net.Core": { - "target": "project" - }, - "Discord.Net.Rest": { - "target": "project" - }, - "System.IO.Compression": "4.3.0", - "System.Net.NameResolution": "4.3.0", - "System.Net.Sockets": "4.3.0", - "System.Net.WebSockets.Client": "4.3.0", - "System.Runtime.InteropServices": "4.3.0" - }, - - "frameworks": { - "netstandard1.3": { - "imports": [ - "dotnet5.4", - "dnxcore50", - "portable-net45+win8" - ] - } - } -} \ No newline at end of file diff --git a/src/Discord.Net/Discord.Net.csproj b/src/Discord.Net/Discord.Net.csproj deleted file mode 100644 index ae9eb3e2a..000000000 --- a/src/Discord.Net/Discord.Net.csproj +++ /dev/null @@ -1,42 +0,0 @@ - - - - An aynchronous API wrapper for Discord. This metapackage includes all of the optional Discord.Net components. - 1.0.0-beta2 - netstandard1.3 - Discord.Net - discord;discordapp - https://github.com/RogueException/Discord.Net - http://opensource.org/licenses/MIT - git - git://github.com/RogueException/Discord.Net - $(PackageTargetFallback);dotnet5.4;dnxcore50;portable-net45+win8 - - - - - - - - - - - - - - - - - 1.0.0-alpha-20161104-2 - All - - - - - False - - - $(DefineConstants);RELEASE - - - \ No newline at end of file diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec new file mode 100644 index 000000000..9966d9d23 --- /dev/null +++ b/src/Discord.Net/Discord.Net.nuspec @@ -0,0 +1,31 @@ + + + + Discord.Net + 1.0.0-rc-$build$ + Discord.Net + RogueException + RogueException + An aynchronous 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 + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Discord.Net/project.json b/src/Discord.Net/project.json deleted file mode 100644 index 98b3b6211..000000000 --- a/src/Discord.Net/project.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "version": "1.0.0-*", - "description": "An aynchronous API wrapper for Discord. This metapackage includes all of the optional Discord.Net components.", - "authors": [ "RogueException" ], - - "packOptions": { - "tags": [ "discord", "discordapp" ], - "licenseUrl": "http://opensource.org/licenses/MIT", - "projectUrl": "https://github.com/RogueException/Discord.Net", - "repository": { - "type": "git", - "url": "git://github.com/RogueException/Discord.Net" - } - }, - - "dependencies": { - "Discord.Net.Core": { - "target": "project" - }, - "Discord.Net.Rest": { - "target": "project" - }, - "Discord.Net.WebSocket": { - "target": "project" - }, - "Discord.Net.Rpc": { - "target": "project" - }, - "Discord.Net.Commands": { - "target": "project" - } - }, - - "frameworks": { - "netstandard1.3": { - "imports": [ - "dotnet5.4", - "dnxcore50", - "portable-net45+win8" - ] - } - } -} \ No newline at end of file diff --git a/test.ps1 b/test.ps1 new file mode 100644 index 000000000..b8a817743 --- /dev/null +++ b/test.ps1 @@ -0,0 +1,2 @@ +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 2a50610cc..986605eeb 100644 --- a/test/Discord.Net.Tests/Discord.Net.Tests.csproj +++ b/test/Discord.Net.Tests/Discord.Net.Tests.csproj @@ -1,97 +1,26 @@ - - + - Debug - AnyCPU - {855D6B1D-847B-42DA-BE6A-23683EA89511} - Library - Properties - Discord.Tests - Discord.Net.Tests - v4.6.1 - 512 - {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - 10.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages - False - UnitTest - + Exe + netcoreapp1.1 + $(PackageTargetFallback);portable-net45+win8+wp8+wpa81 + Discord - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - ..\..\packages\Newtonsoft.Json.8.0.2\lib\net45\Newtonsoft.Json.dll - True - - - - - - - - - - - - - - - - - + + PreserveNewest + - + + + - - {c6a50d24-cbd3-4e76-852c-4dca60bbd608} - Discord.Net.Net45 - + + + + + + - - - - - False - - - False - - - False - - - False - - - - - - - - \ No newline at end of file + diff --git a/test/Discord.Net.Tests/Net/CacheInfo.cs b/test/Discord.Net.Tests/Net/CacheInfo.cs new file mode 100644 index 000000000..ed2820b8e --- /dev/null +++ b/test/Discord.Net.Tests/Net/CacheInfo.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.Net +{ + internal class CacheInfo + { + [JsonProperty("guild_id")] + public ulong? GuildId { get; set; } + [JsonProperty("version")] + public uint Version { get; set; } + } +} \ No newline at end of file diff --git a/test/Discord.Net.Tests/Net/CachedRestClient.cs b/test/Discord.Net.Tests/Net/CachedRestClient.cs new file mode 100644 index 000000000..324510688 --- /dev/null +++ b/test/Discord.Net.Tests/Net/CachedRestClient.cs @@ -0,0 +1,120 @@ +using Akavache; +using Akavache.Sqlite3; +using Discord.Net.Rest; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Reactive.Linq; +using System.Threading; +using System.Threading.Tasks; +using Splat; +using System.Reactive.Concurrency; + +namespace Discord.Net +{ + internal class CachedRestClient : IRestClient + { + private readonly Dictionary _headers; + private IBlobCache _blobCache; + private string _baseUrl; + private CancellationTokenSource _cancelTokenSource; + private CancellationToken _cancelToken, _parentToken; + private bool _isDisposed; + + public CacheInfo Info { get; private set; } + + public CachedRestClient() + { + _headers = new Dictionary(); + + _cancelTokenSource = new CancellationTokenSource(); + _cancelToken = CancellationToken.None; + _parentToken = CancellationToken.None; + + Locator.CurrentMutable.Register(() => Scheduler.Default, typeof(IScheduler), "Taskpool"); + Locator.CurrentMutable.Register(() => new FilesystemProvider(), typeof(IFilesystemProvider), null); + Locator.CurrentMutable.Register(() => new HttpMixin(), typeof(IAkavacheHttpMixin), null); + //new Akavache.Sqlite3.Registrations().Register(Locator.CurrentMutable); + _blobCache = new SQLitePersistentBlobCache("cache.db"); + } + private void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + _blobCache.Dispose(); + _isDisposed = true; + } + } + public void Dispose() + { + Dispose(true); + } + + public void SetUrl(string url) + { + _baseUrl = url; + } + public void SetHeader(string key, string value) + { + _headers[key] = value; + } + public void SetCancelToken(CancellationToken cancelToken) + { + _parentToken = cancelToken; + _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; + } + + public async Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly) + { + if (method != "GET") + throw new InvalidOperationException("This RestClient only supports GET requests."); + + string uri = Path.Combine(_baseUrl, endpoint); + 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) + { + throw new InvalidOperationException("This RestClient does not support payloads."); + } + public Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly) + { + throw new InvalidOperationException("This RestClient does not support multipart requests."); + } + + public async Task ClearAsync() + { + await _blobCache.InvalidateAll(); + } + + public async Task LoadInfoAsync(ulong guildId) + { + if (Info != null) + return; + + bool needsReset = false; + try + { + Info = await _blobCache.GetObject("info"); + if (Info.GuildId != guildId) + needsReset = true; + } + catch (KeyNotFoundException) + { + needsReset = true; + } + if (needsReset) + { + Info = new CacheInfo() { GuildId = guildId, Version = 0 }; + await SaveInfoAsync().ConfigureAwait(false); + } + } + public async Task SaveInfoAsync() + { + await ClearAsync().ConfigureAwait(false); //Version changed, invalidate cache + await _blobCache.InsertObject("info", Info); + } + } +} \ No newline at end of file diff --git a/test/Discord.Net.Tests/Net/FilesystemProvider.cs b/test/Discord.Net.Tests/Net/FilesystemProvider.cs new file mode 100644 index 000000000..ae1b9a301 --- /dev/null +++ b/test/Discord.Net.Tests/Net/FilesystemProvider.cs @@ -0,0 +1,128 @@ +//From https://github.com/akavache/Akavache +//Copyright (c) 2012 GitHub +//TODO: Remove once netstandard support is added + +using Akavache; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reactive; +using System.Reactive.Concurrency; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Reflection; + +namespace Discord +{ + public class FilesystemProvider : IFilesystemProvider + { + public IObservable OpenFileForReadAsync(string path, IScheduler scheduler) + { + return SafeOpenFileAsync(path, FileMode.Open, FileAccess.Read, FileShare.Read, scheduler); + } + + public IObservable OpenFileForWriteAsync(string path, IScheduler scheduler) + { + return SafeOpenFileAsync(path, FileMode.Create, FileAccess.Write, FileShare.None, scheduler); + } + + public IObservable CreateRecursive(string path) + { + CreateRecursive(new DirectoryInfo(path)); + return Observable.Return(Unit.Default); + } + + public IObservable Delete(string path) + { + return Observable.Start(() => File.Delete(path), Scheduler.Default); + } + + public string GetDefaultRoamingCacheDirectory() + { + throw new NotSupportedException(); + } + + public string GetDefaultSecretCacheDirectory() + { + throw new NotSupportedException(); + } + + public string GetDefaultLocalMachineCacheDirectory() + { + throw new NotSupportedException(); + } + + protected static string GetAssemblyDirectoryName() + { + var assemblyDirectoryName = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); + Debug.Assert(assemblyDirectoryName != null, "The directory name of the assembly location is null"); + return assemblyDirectoryName; + } + + private static IObservable SafeOpenFileAsync(string path, FileMode mode, FileAccess access, FileShare share, IScheduler scheduler = null) + { + scheduler = scheduler ?? Scheduler.Default; + var ret = new AsyncSubject(); + + Observable.Start(() => + { + try + { + var createModes = new[] + { + FileMode.Create, + FileMode.CreateNew, + FileMode.OpenOrCreate, + }; + + + // NB: We do this (even though it's incorrect!) because + // throwing lots of 1st chance exceptions makes debugging + // obnoxious, as well as a bug in VS where it detects + // exceptions caught by Observable.Start as Unhandled. + if (!createModes.Contains(mode) && !File.Exists(path)) + { + ret.OnError(new FileNotFoundException()); + return; + } + + Observable.Start(() => new FileStream(path, mode, access, share, 4096, false), scheduler).Cast().Subscribe(ret); + } + catch (Exception ex) + { + ret.OnError(ex); + } + }, scheduler); + + return ret; + } + private static void CreateRecursive(DirectoryInfo info) + { + SplitFullPath(info).Aggregate((parent, dir) => + { + var path = Path.Combine(parent, dir); + if (!Directory.Exists(path)) + Directory.CreateDirectory(path); + return path; + }); + } + + private static IEnumerable SplitFullPath(DirectoryInfo info) + { + var root = Path.GetPathRoot(info.FullName); + var components = new List(); + for (var path = info.FullName; path != root && path != null; path = Path.GetDirectoryName(path)) + { + var filename = Path.GetFileName(path); + if (String.IsNullOrEmpty(filename)) + continue; + components.Add(filename); + } + components.Add(root); + components.Reverse(); + return components; + } + } +} \ No newline at end of file diff --git a/test/Discord.Net.Tests/Net/HttpMixin.cs b/test/Discord.Net.Tests/Net/HttpMixin.cs new file mode 100644 index 000000000..c4a78ce0b --- /dev/null +++ b/test/Discord.Net.Tests/Net/HttpMixin.cs @@ -0,0 +1,145 @@ +//From https://github.com/akavache/Akavache +//Copyright (c) 2012 GitHub +//TODO: Remove once netstandard support is added + +#pragma warning disable CS0618 + +using Akavache; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Text; +using System.Reactive; +using System.Reactive.Threading.Tasks; + +namespace Discord.Net +{ + public class HttpMixin : IAkavacheHttpMixin + { + /// + /// Download data from an HTTP URL and insert the result into the + /// cache. If the data is already in the cache, this returns + /// a cached value. The URL itself is used as the key. + /// + /// The URL to download. + /// An optional Dictionary containing the HTTP + /// request headers. + /// Force a web request to always be issued, skipping the cache. + /// An optional expiration date. + /// The data downloaded from the URL. + public IObservable DownloadUrl(IBlobCache This, string url, IDictionary headers = null, bool fetchAlways = false, DateTimeOffset? absoluteExpiration = null) + { + return This.DownloadUrl(url, url, headers, fetchAlways, absoluteExpiration); + } + + /// + /// Download data from an HTTP URL and insert the result into the + /// cache. If the data is already in the cache, this returns + /// a cached value. An explicit key is provided rather than the URL itself. + /// + /// The key to store with. + /// The URL to download. + /// An optional Dictionary containing the HTTP + /// request headers. + /// Force a web request to always be issued, skipping the cache. + /// An optional expiration date. + /// The data downloaded from the URL. + public IObservable DownloadUrl(IBlobCache This, string key, string url, IDictionary headers = null, bool fetchAlways = false, DateTimeOffset? absoluteExpiration = null) + { + var doFetch = MakeWebRequest(new Uri(url), headers).SelectMany(x => ProcessWebResponse(x, url, absoluteExpiration)); + var fetchAndCache = doFetch.SelectMany(x => This.Insert(key, x, absoluteExpiration).Select(_ => x)); + + var ret = default(IObservable); + if (!fetchAlways) + { + ret = This.Get(key).Catch(fetchAndCache); + } + else + { + ret = fetchAndCache; + } + + var conn = ret.PublishLast(); + conn.Connect(); + return conn; + } + + IObservable ProcessWebResponse(WebResponse wr, string url, DateTimeOffset? absoluteExpiration) + { + var hwr = (HttpWebResponse)wr; + Debug.Assert(hwr != null, "The Web Response is somehow null but shouldn't be."); + if ((int)hwr.StatusCode >= 400) + { + return Observable.Throw(new WebException(hwr.StatusDescription)); + } + + var ms = new MemoryStream(); + using (var responseStream = hwr.GetResponseStream()) + { + Debug.Assert(responseStream != null, "The response stream is somehow null"); + responseStream.CopyTo(ms); + } + + var ret = ms.ToArray(); + return Observable.Return(ret); + } + + static IObservable MakeWebRequest( + Uri uri, + IDictionary headers = null, + string content = null, + int retries = 3, + TimeSpan? timeout = null) + { + IObservable request; + + request = Observable.Defer(() => + { + var hwr = CreateWebRequest(uri, headers); + + if (content == null) + return Observable.FromAsyncPattern(hwr.BeginGetResponse, hwr.EndGetResponse)(); + + var buf = Encoding.UTF8.GetBytes(content); + + // NB: You'd think that BeginGetResponse would never block, + // seeing as how it's asynchronous. You'd be wrong :-/ + var ret = new AsyncSubject(); + Observable.Start(() => + { + Observable.FromAsyncPattern(hwr.BeginGetRequestStream, hwr.EndGetRequestStream)() + .SelectMany(x => WriteAsyncRx(x, buf, 0, buf.Length)) + .SelectMany(_ => Observable.FromAsyncPattern(hwr.BeginGetResponse, hwr.EndGetResponse)()) + .Multicast(ret).Connect(); + }, BlobCache.TaskpoolScheduler); + + return ret; + }); + + return request.Timeout(timeout ?? TimeSpan.FromSeconds(15), BlobCache.TaskpoolScheduler).Retry(retries); + } + + private static WebRequest CreateWebRequest(Uri uri, IDictionary headers) + { + var hwr = WebRequest.Create(uri); + if (headers != null) + { + foreach (var x in headers) + { + hwr.Headers[x.Key] = x.Value; + } + } + return hwr; + } + + private static IObservable WriteAsyncRx(Stream stream, byte[] data, int start, int length) + { + return stream.WriteAsync(data, start, length).ToObservable(); + } + } +} \ No newline at end of file diff --git a/test/Discord.Net.Tests/Properties/AssemblyInfo.cs b/test/Discord.Net.Tests/Properties/AssemblyInfo.cs deleted file mode 100644 index 5b1c7b125..000000000 --- a/test/Discord.Net.Tests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Discord.Net.Tests")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Discord.Net.Tests")] -[assembly: AssemblyCopyright("Copyright © 2015")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("855d6b1d-847b-42da-be6a-23683ea89511")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/test/Discord.Net.Tests/TestConfig.cs b/test/Discord.Net.Tests/TestConfig.cs new file mode 100644 index 000000000..bdab13ea7 --- /dev/null +++ b/test/Discord.Net.Tests/TestConfig.cs @@ -0,0 +1,33 @@ +using Newtonsoft.Json; +using System.IO; +using System; + +namespace Discord +{ + internal class TestConfig + { + [JsonProperty("token")] + public string Token { get; private set; } + [JsonProperty("guild_id")] + public ulong GuildId { get; private set; } + + public static TestConfig LoadFile(string path) + { + if (File.Exists(path)) + { + using (var stream = new FileStream(path, FileMode.Open)) + using (var reader = new StreamReader(stream)) + using (var jsonReader = new JsonTextReader(reader)) + return new JsonSerializer().Deserialize(jsonReader); + } + else + { + return new TestConfig() + { + Token = Environment.GetEnvironmentVariable("DNET_TEST_TOKEN"), + GuildId = ulong.Parse(Environment.GetEnvironmentVariable("DNET_TEST_GUILDID")) + }; + } + } + } +} \ No newline at end of file diff --git a/test/Discord.Net.Tests/Tests.Channels.cs b/test/Discord.Net.Tests/Tests.Channels.cs new file mode 100644 index 000000000..d81d28f3e --- /dev/null +++ b/test/Discord.Net.Tests/Tests.Channels.cs @@ -0,0 +1,144 @@ +using Discord.Rest; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace Discord +{ + public partial class Tests + { + internal static async Task Migration_CreateTextChannels(DiscordRestClient client, RestGuild guild) + { + var text1 = await guild.GetDefaultChannelAsync(); + var text2 = await guild.CreateTextChannelAsync("text2"); + var text3 = await guild.CreateTextChannelAsync("text3"); + var text4 = await guild.CreateTextChannelAsync("text4"); + var text5 = await guild.CreateTextChannelAsync("text5"); + + //Modify #general + await text1.ModifyAsync(x => + { + x.Name = "text1"; + x.Position = 1; + x.Topic = "Topic1"; + }); + + await text2.ModifyAsync(x => + { + x.Position = 2; + }); + await text3.ModifyAsync(x => + { + x.Topic = "Topic2"; + }); + await text4.ModifyAsync(x => + { + x.Position = 3; + x.Topic = "Topic2"; + }); + await text5.ModifyAsync(x => + { + }); + + CheckTextChannels(guild, text1, text2, text3, text4, text5); + } + [Fact] + public async Task TestTextChannels() + { + CheckTextChannels(_guild, (await _guild.GetTextChannelsAsync()).ToArray()); + } + private static void CheckTextChannels(RestGuild guild, params RestTextChannel[] textChannels) + { + Assert.Equal(textChannels.Length, 5); + Assert.All(textChannels, x => + { + Assert.NotNull(x); + Assert.NotEqual(x.Id, 0UL); + Assert.True(x.Position >= 0); + }); + + var text1 = textChannels.Where(x => x.Name == "text1").FirstOrDefault(); + var text2 = textChannels.Where(x => x.Name == "text2").FirstOrDefault(); + var text3 = textChannels.Where(x => x.Name == "text3").FirstOrDefault(); + var text4 = textChannels.Where(x => x.Name == "text4").FirstOrDefault(); + var text5 = textChannels.Where(x => x.Name == "text5").FirstOrDefault(); + + Assert.NotNull(text1); + Assert.True(text1.Id == guild.DefaultChannelId); + Assert.Equal(text1.Position, 1); + Assert.Equal(text1.Topic, "Topic1"); + + Assert.NotNull(text2); + Assert.Equal(text2.Position, 2); + Assert.Null(text2.Topic); + + Assert.NotNull(text3); + Assert.Equal(text3.Topic, "Topic2"); + + Assert.NotNull(text4); + Assert.Equal(text4.Position, 3); + Assert.Equal(text4.Topic, "Topic2"); + + Assert.NotNull(text5); + Assert.Null(text5.Topic); + } + + internal static async Task Migration_CreateVoiceChannels(DiscordRestClient client, RestGuild guild) + { + var voice1 = await guild.CreateVoiceChannelAsync("voice1"); + var voice2 = await guild.CreateVoiceChannelAsync("voice2"); + var voice3 = await guild.CreateVoiceChannelAsync("voice3"); + + await voice1.ModifyAsync(x => + { + x.Bitrate = 96000; + x.Position = 1; + }); + await voice2.ModifyAsync(x => + { + x.UserLimit = null; + }); + await voice3.ModifyAsync(x => + { + x.Bitrate = 8000; + x.Position = 1; + x.UserLimit = 16; + }); + + CheckVoiceChannels(voice1, voice2, voice3); + } + [Fact] + public async Task TestVoiceChannels() + { + CheckVoiceChannels((await _guild.GetVoiceChannelsAsync()).ToArray()); + } + private static void CheckVoiceChannels(params RestVoiceChannel[] voiceChannels) + { + Assert.Equal(voiceChannels.Length, 3); + Assert.All(voiceChannels, x => + { + Assert.NotNull(x); + Assert.NotEqual(x.Id, 0UL); + Assert.NotEqual(x.UserLimit, 0); + Assert.True(x.Bitrate > 0); + Assert.True(x.Position >= 0); + }); + + var voice1 = voiceChannels.Where(x => x.Name == "voice1").FirstOrDefault(); + var voice2 = voiceChannels.Where(x => x.Name == "voice2").FirstOrDefault(); + var voice3 = voiceChannels.Where(x => x.Name == "voice3").FirstOrDefault(); + + Assert.NotNull(voice1); + Assert.Equal(voice1.Bitrate, 96000); + Assert.Equal(voice1.Position, 1); + + Assert.NotNull(voice2); + Assert.Equal(voice2.UserLimit, null); + + Assert.NotNull(voice3); + Assert.Equal(voice3.Bitrate, 8000); + Assert.Equal(voice3.Position, 1); + Assert.Equal(voice3.UserLimit, 16); + } + } +} \ No newline at end of file diff --git a/test/Discord.Net.Tests/Tests.Migrations.cs b/test/Discord.Net.Tests/Tests.Migrations.cs new file mode 100644 index 000000000..e786329cd --- /dev/null +++ b/test/Discord.Net.Tests/Tests.Migrations.cs @@ -0,0 +1,72 @@ +using System; +using System.Threading.Tasks; +using Discord.Rest; + +namespace Discord +{ + public partial class TestsFixture + { + public const uint MigrationCount = 3; + + public async Task MigrateAsync() + { + DiscordRestClient client = null; + RestGuild guild = null; + + await _cache.LoadInfoAsync(_config.GuildId).ConfigureAwait(false); + while (_cache.Info.Version != MigrationCount) + { + if (client == null) + { + client = new DiscordRestClient(); + await client.LoginAsync(TokenType.Bot, _config.Token, false).ConfigureAwait(false); + guild = await client.GetGuildAsync(_config.GuildId); + } + + uint nextVer = _cache.Info.Version + 1; + try + { + await DoMigrateAsync(client, guild, nextVer).ConfigureAwait(false); + _cache.Info.Version = nextVer; + await _cache.SaveInfoAsync().ConfigureAwait(false); + } + catch + { + await _cache.ClearAsync().ConfigureAwait(false); + throw; + } + } + } + + private static Task DoMigrateAsync(DiscordRestClient client, RestGuild guild, uint toVersion) + { + switch (toVersion) + { + case 1: return Migration_WipeGuild(client, guild); + case 2: return Tests.Migration_CreateTextChannels(client, guild); + case 3: return Tests.Migration_CreateVoiceChannels(client, guild); + default: throw new InvalidOperationException("Unknown migration: " + toVersion); + } + } + + private static async Task Migration_WipeGuild(DiscordRestClient client, RestGuild guild) + { + var textChannels = await guild.GetTextChannelsAsync(); + var voiceChannels = await guild.GetVoiceChannelsAsync(); + var roles = guild.Roles; + + foreach (var channel in textChannels) + { + if (channel.Id != guild.DefaultChannelId) + await channel.DeleteAsync(); + } + foreach (var channel in voiceChannels) + await channel.DeleteAsync(); + foreach (var role in roles) + { + if (role.Id != guild.EveryoneRole.Id) + await role.DeleteAsync(); + } + } + } +} \ No newline at end of file diff --git a/test/Discord.Net.Tests/Tests.cs b/test/Discord.Net.Tests/Tests.cs index d8d09cd3d..df156d254 100644 --- a/test/Discord.Net.Tests/Tests.cs +++ b/test/Discord.Net.Tests/Tests.cs @@ -1,494 +1,53 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; +using Discord.Net; +using Discord.Rest; +using Xunit; -namespace Discord.Tests +namespace Discord { - //TODO: Tests are massively incomplete and out of date, needing a full rewrite - - [TestClass] - public class Tests + public partial class TestsFixture : IDisposable { - private const int EventTimeout = 10000; //Max time in milliseconds to wait for an event response from our test actions - - private static DiscordSocketClient _hostBot, _targetBot, _observerBot; - private static Guild _testGuild; - private static TextChannel _testGuildChannel; - private static Random _random; - private static PublicInvite _testGuildInvite; - - private static TestContext _context; - - private static string _hostToken; - private static string _observerToken; - private static string _targetToken; - - private static string GetRandomText() - { - lock (_random) - return $"test_{_random.Next()}"; - } - - #region Initialization - - [ClassInitialize] - public static void Initialize(TestContext testContext) - { - _context = testContext; - - _hostToken = Environment.GetEnvironmentVariable("discord-unit-host_token"); - _observerToken = Environment.GetEnvironmentVariable("discord-unit-observer_token"); - _targetToken = Environment.GetEnvironmentVariable("discord-unit-target_token"); - } - - [TestMethod] - [Priority(1)] - public async Task TestInitialize() - { - _context.WriteLine("Initializing."); - - _random = new Random(); - - _hostBot = new DiscordSocketClient(_hostToken); - _targetBot = new DiscordSocketClient(_targetToken); - _observerBot = new DiscordSocketClient(_observerToken); - - await _hostBot.Login(); - - await Task.Delay(3000); - - //Cleanup existing Guilds - (await _hostBot.GetGuilds()).Select(x => x.Owner.Id == _hostBot.CurrentUser.Id ? x.Delete() : x.Leave()); - - //Create new Guild and invite the other bots to it - - _testGuild = await _hostBot.CreateGuild("Discord.Net Testing", _hostBot.GetOptimalVoiceRegion()); - - await Task.Delay(1000); - - PublicInvite invite = await _testGuild.CreateInvite(60, 3, false, false); - _testGuildInvite = invite; - - _context.WriteLine($"Host: {_hostBot.CurrentUser.Username} in {(await _hostBot.GetGuilds()).Count()}"); - } - - [TestMethod] - [Priority(2)] - public async Task TestTokenLogin_Ready() - { - AssertEvent( - "READY never received", - async () => await _observerBot.Login(), - x => _observerBot.Connected += x, - x => _observerBot.Connected -= x, - null, - true); - (await _observerBot.GetGuilds()).Select(x => x.Owner.Id == _observerBot.CurrentUser.Id ? x.Delete() : x.Leave()); - await _observerBot.RestClient.Send(new API.Rest.AcceptInviteRequest(_testGuildInvite.Code)); - } - - [TestMethod] - [Priority(2)] - public async Task TestReady() - { - AssertEvent( - "READY never received", - async () => await _targetBot.Login(), - x => _targetBot.Connected += x, - x => _targetBot.Connected -= x, - null, - true); - - (await _targetBot.GetGuilds()).Select(x => x.Owner.Id == _targetBot.CurrentUser.Id ? x.Delete() : x.Leave()); - _testGuildChannel = _testGuild.DefaultChannel; - } - - #endregion + private readonly TestConfig _config; + private readonly CachedRestClient _cache; + internal readonly DiscordRestClient _client; + internal readonly RestGuild _guild; - // Guilds - - #region Guild Tests - - [TestMethod] - [Priority(3)] - public void TestJoinedGuild() + public TestsFixture() { - AssertEvent( - "Never Got JoinedGuild", - async () => await _targetBot.RestClient.Send(new API.Rest.AcceptInviteRequest(_testGuildInvite.Code)), - x => _targetBot.JoinedGuild += x, - x => _targetBot.JoinedGuild -= x); - } - - #endregion - - #region Channel Tests + _cache = new CachedRestClient(); - //Channels - [TestMethod] - public void TestCreateTextChannel() - { - GuildChannel channel = null; - string name = GetRandomText(); - AssertEvent( - "ChannelCreated event never received", - async () => channel = await _testGuild.CreateTextChannel(name), - x => _targetBot.ChannelCreated += x, - x => _targetBot.ChannelCreated -= x, - (s, e) => e.Channel.Id == channel.Id); - - AssertEvent( - "ChannelDestroyed event never received", - async () => await channel.Delete(), - x => _targetBot.ChannelDestroyed += x, - x => _targetBot.ChannelDestroyed -= x, - (s, e) => e.Channel.Id == channel.Id); - } - [TestMethod] - public void TestCreateVoiceChannel() - { - GuildChannel channel = null; - string name = GetRandomText(); - AssertEvent( - "ChannelCreated event never received", - async () => channel = await _testGuild.CreateVoiceChannel(name), - x => _targetBot.ChannelCreated += x, - x => _targetBot.ChannelCreated -= x, - (s, e) => e.Channel.Id == channel.Id); - - AssertEvent( - "ChannelDestroyed event never received", - async () => await channel.Delete(), - x => _targetBot.ChannelDestroyed += x, - x => _targetBot.ChannelDestroyed -= x, - (s, e) => e.Channel.Id == channel.Id); - } - - [TestMethod] - [ExpectedException(typeof(Net.HttpException))] - public async Task TestCreateChannel_NoName() - { - await _testGuild.CreateTextChannel($""); - } - [TestMethod] - public async Task Test_CreateGetChannel() - { - var name = GetRandomText(); - var channel = await _testGuild.CreateTextChannel(name); - var get_channel = _testGuild.GetChannel(channel.Id); - Assert.AreEqual(channel.Id, get_channel.Id, "ID of Channel and GetChannel were not equal."); - } - [TestMethod] - public void TestSendTyping() - { - var channel = _testGuildChannel; - AssertEvent( - "UserUpdated event never fired.", - async () => await channel.TriggerTyping(), - x => _targetBot.UserIsTyping += x, - x => _targetBot.UserIsTyping -= x); - } - [TestMethod] - public void TestEditChannel() - { - var channel = _testGuildChannel; - AssertEvent( - "ChannelUpdated Never Received", - async () => await channel.Modify(x => - { - x.Name = GetRandomText(); - x.Topic = $"topic - {GetRandomText()}"; - x.Position = 26; - }), - x => _targetBot.ChannelUpdated += x, - x => _targetBot.ChannelUpdated -= x); - } - [TestMethod] - public void TestChannelMention() - { - var channel = _testGuildChannel; - Assert.AreEqual($"<#{channel.Id}>", channel.Mention, "Generated channel mention was not the expected channel mention."); - } - [TestMethod] - public void TestChannelUserCount() - { - Assert.AreEqual(3, _testGuildChannel.Users.Count(), "Read an incorrect number of users in a channel"); - } - - #endregion - - #region Message Tests - - //Messages - [TestMethod] - public async Task TestMessageEvents() - { - string name = GetRandomText(); - var channel = await _testGuild.CreateTextChannel(name); - _context.WriteLine($"Channel Name: {channel.Name} / {channel.Guild.Name}"); - string text = GetRandomText(); - Message message = null; - AssertEvent( - "MessageCreated event never received", - async () => message = await channel.SendMessage(text), - x => _targetBot.MessageReceived += x, - x => _targetBot.MessageReceived -= x, - (s, e) => e.Message.Text == text); - - AssertEvent( - "MessageUpdated event never received", - async () => await message.Modify(x => - { - x.Content = text + " updated"; - }), - x => _targetBot.MessageUpdated += x, - x => _targetBot.MessageUpdated -= x, - (s, e) => e.Before.Text == text && e.After.Text == text + " updated"); - - AssertEvent( - "MessageDeleted event never received", - async () => await message.Delete(), - x => _targetBot.MessageDeleted += x, - x => _targetBot.MessageDeleted -= x, - (s, e) => e.Message.Id == message.Id); - } - [TestMethod] - public async Task TestDownloadMessages() - { - string name = GetRandomText(); - var channel = await _testGuild.CreateTextChannel(name); - for (var i = 0; i < 10; i++) await channel.SendMessage(GetRandomText()); - while (channel.Discord.MessageQueue.Count > 0) await Task.Delay(100); - var messages = await channel.GetMessages(10); - Assert.AreEqual(10, messages.Count(), "Expected 10 messages in downloaded array, did not see 10."); - } - [TestMethod] - public async Task TestSendTTSMessage() - { - var channel = await _testGuild.CreateTextChannel(GetRandomText()); - AssertEvent( - "MessageCreated event never fired", - async () => await channel.SendMessage(GetRandomText(), true), - x => _targetBot.MessageReceived += x, - x => _targetBot.MessageReceived -= x, - (s, e) => e.Message.IsTTS); - } - - #endregion - - #region User Tests - - [TestMethod] - public async Task TestUserMentions() - { - var user = (await _targetBot.GetGuild(_testGuild.Id)).CurrentUser; - Assert.AreEqual($"<@{user.Id}>", user.Mention); - } - [TestMethod] - public void TestUserEdit() - { - var user = _testGuild.GetUser(_targetBot.CurrentUser.Id); - AssertEvent( - "UserUpdated never fired", - async () => await user.Modify(x => - { - x.Deaf = true; - x.Mute = true; - }), - x => _targetBot.UserUpdated += x, - x => _targetBot.UserUpdated -= x); - } - [TestMethod] - public void TestEditSelf() - { - throw new NotImplementedException(); - /*var name = RandomText - AssertEvent( - "UserUpdated never fired", - async () => await _targetBot.CurrentUser.Modify(TargetPassword, name), - x => _obGuildBot.UserUpdated += x, - x => _obGuildBot.UserUpdated -= x, - (s, e) => e.After.Username == name);*/ - } - [TestMethod] - public void TestSetStatus() - { - AssertEvent( - "UserUpdated never fired", - async () => await SetStatus(_targetBot, UserStatus.Idle), - x => _observerBot.UserUpdated += x, - x => _observerBot.UserUpdated -= x, - (s, e) => e.After.Status == UserStatus.Idle); - } - private Task SetStatus(DiscordClient _client, UserStatus status) - { - throw new NotImplementedException(); - /*_client.SetStatus(status); - await Task.Delay(50);*/ - } - [TestMethod] - public void TestSetGame() - { - AssertEvent( - "UserUpdated never fired", - async () => await SetGame(_targetBot, "test game"), - x => _observerBot.UserUpdated += x, - x => _observerBot.UserUpdated -= x, - (s, e) => _targetBot.CurrentUser.CurrentGame == "test game"); - - } - private Task SetGame(DiscordClient _client, string game) - { - throw new NotImplementedException(); - //_client.SetGame(game); - //await Task.Delay(5); - } - - #endregion - - #region Permission Tests - - // Permissions - [TestMethod] - public async Task Test_AddGet_PermissionsRule() - { - var channel = await _testGuild.CreateTextChannel(GetRandomText()); - var user = _testGuild.GetUser(_targetBot.CurrentUser.Id); - var perms = new OverwritePermissions(sendMessages: PermValue.Deny); - await channel.UpdatePermissionOverwrite(user, perms); - var resultPerms = channel.GetPermissionOverwrite(user); - Assert.IsNotNull(resultPerms, "Perms retrieved from Guild were null."); - } - [TestMethod] - public async Task Test_AddRemove_PermissionsRule() - { - var channel = await _testGuild.CreateTextChannel(GetRandomText()); - var user = _testGuild.GetUser(_targetBot.CurrentUser.Id); - var perms = new OverwritePermissions(sendMessages: PermValue.Deny); - await channel.UpdatePermissionOverwrite(user, perms); - await channel.RemovePermissionOverwrite(user); - await Task.Delay(200); - Assert.AreEqual(PermValue.Inherit, channel.GetPermissionOverwrite(user)?.SendMessages); - } - [TestMethod] - public async Task Test_Permissions_Event() - { - var channel = await _testGuild.CreateTextChannel(GetRandomText()); - var user = _testGuild.GetUser(_targetBot.CurrentUser.Id); - var perms = new OverwritePermissions(sendMessages: PermValue.Deny); - AssertEvent - ("ChannelUpdatedEvent never fired.", - async () => await channel.UpdatePermissionOverwrite(user, perms), - x => _targetBot.ChannelUpdated += x, - x => _targetBot.ChannelUpdated -= x, - (s, e) => e.Channel == channel && (e.After as GuildChannel).PermissionOverwrites.Count() != (e.Before as GuildChannel).PermissionOverwrites.Count()); - } - [TestMethod] - [ExpectedException(typeof(Net.HttpException))] - public async Task Test_Affect_Permissions_Invalid_Channel() - { - var channel = await _testGuild.CreateTextChannel(GetRandomText()); - var user = _testGuild.GetUser(_targetBot.CurrentUser.Id); - var perms = new OverwritePermissions(sendMessages: PermValue.Deny); - await channel.Delete(); - await channel.UpdatePermissionOverwrite(user, perms); - } - - #endregion - - - [ClassCleanup] - public static async Task Cleanup() - { - WaitMany( - (await _hostBot.GetGuilds()).Select(x => x.Owner.Id == _hostBot.CurrentUser.Id ? x.Delete() : x.Leave()), - (await _targetBot.GetGuilds()).Select(x => x.Owner.Id == _targetBot.CurrentUser.Id ? x.Delete() : x.Leave()), - (await _observerBot.GetGuilds()).Select(x => x.Owner.Id == _observerBot.CurrentUser.Id ? x.Delete() : x.Leave())); - - WaitAll( - _hostBot.Disconnect(), - _targetBot.Disconnect(), - _observerBot.Disconnect()); - } - - #region Helpers - - // Task Helpers - - private static void AssertEvent(string msg, Func action, Action> addEvent, Action> removeEvent, Func test = null) - { - AssertEvent(msg, action, addEvent, removeEvent, test, true); - } - private static void AssertNoEvent(string msg, Func action, Action> addEvent, Action> removeEvent, Func test = null) - { - AssertEvent(msg, action, addEvent, removeEvent, test, false); - } - private static void AssertEvent(string msg, Func action, Action> addEvent, Action> removeEvent, Func test, bool assertTrue) - { - ManualResetEventSlim trigger = new ManualResetEventSlim(false); - bool result = false; - - EventHandler handler = (s, e) => + _config = TestConfig.LoadFile("./config.json"); + var config = new DiscordRestConfig { - if (test != null) + RestClientProvider = url => { - result |= test(s, e); - trigger.Set(); + _cache.SetUrl(url); + return _cache; } - else - result = true; }; + _client = new DiscordRestClient(config); + _client.LoginAsync(TokenType.Bot, _config.Token).Wait(); - addEvent(handler); - var task = action(); - trigger.Wait(EventTimeout); - task.Wait(); - removeEvent(handler); - - Assert.AreEqual(assertTrue, result, msg); + MigrateAsync().Wait(); + _guild = _client.GetGuildAsync(_config.GuildId).Result; } - private static void AssertEvent(string msg, Func action, Action addEvent, Action removeEvent, Func test, bool assertTrue) + public void Dispose() { - ManualResetEventSlim trigger = new ManualResetEventSlim(false); - bool result = false; - - EventHandler handler = (s, e) => - { - if (test != null) - { - result |= test(s); - trigger.Set(); - } - else - result = true; - }; - - addEvent(handler); - var task = action(); - trigger.Wait(EventTimeout); - task.Wait(); - removeEvent(handler); - - Assert.AreEqual(assertTrue, result, msg); + _client.Dispose(); + _cache.Dispose(); } + } - private static void WaitAll(params Task[] tasks) - { - Task.WaitAll(tasks); - } - private static void WaitAll(IEnumerable tasks) - { - Task.WaitAll(tasks.ToArray()); - } - private static void WaitMany(params IEnumerable[] tasks) + public partial class Tests : IClassFixture + { + private DiscordRestClient _client; + private RestGuild _guild; + + public Tests(TestsFixture fixture) { - Task.WaitAll(tasks.Where(x => x != null).SelectMany(x => x).ToArray()); + _client = fixture._client; + _guild = fixture._guild; } - - #endregion } -} +} \ No newline at end of file diff --git a/test/Discord.Net.Tests/config.json.example b/test/Discord.Net.Tests/config.json.example index 638d65b4d..403afa3bd 100644 --- a/test/Discord.Net.Tests/config.json.example +++ b/test/Discord.Net.Tests/config.json.example @@ -1,14 +1,4 @@ { - "user1": { - "email": "user1@example.com", - "password": "password123" - }, - "user2": { - "email": "user2@example.com", - "password": "password456" - }, - "user3": { - "email": "user3@example.com", - "password": "password789" - } + "token": "AAA.BBB.CCC", + "guild_id": 1234567890 } \ No newline at end of file diff --git a/test/Discord.Net.Tests/packages.config b/test/Discord.Net.Tests/packages.config deleted file mode 100644 index 2abc396bb..000000000 --- a/test/Discord.Net.Tests/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/test/Discord.Net.Tests/xunit.runner.json b/test/Discord.Net.Tests/xunit.runner.json new file mode 100644 index 000000000..ac3e63046 --- /dev/null +++ b/test/Discord.Net.Tests/xunit.runner.json @@ -0,0 +1,4 @@ +{ + "diagnosticMessages": true, + "methodDisplay": "classAndMethod" +} \ No newline at end of file