diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..8248291e8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,44 @@ +# Contributing + +Discord.Net is an open-source project, and we appreciate any and all +contributions made by our community. However, please conform to the +following guidelines when possible: + +## Development Cycle + +We prefer all changes to the library to be discussed beforehand, +either in a GitHub issue, or in a discussion in our Discord channel +with library regulars or other contributors. + +Issues that are tagged as "up for grabs" are free to be picked up by +any member of the community. + +### Pull Requests + +We prefer pull-requests that are descriptive of the changes being made +and highlight any potential benefits/drawbacks of the change, but these +types of write-ups are not required. See this [merge request](https://github.com/RogueException/Discord.Net/pull/793) +for an example of a well-written description. + +## Semantic Versioning + +This project follows [Semantic Versioning](http://semver.org/). When +writing changes to this project, it is recommended to write changes +that are SemVer compliant with the latest version of the library in +development. + +The working release should be the latest build off of the `dev` branch, +but can also be found on the [development board](https://github.com/RogueException/Discord.Net/projects/1). + +We follow the .NET Foundation's [Breaking Change Rules](https://github.com/dotnet/corefx/blob/master/Documentation/coding-guidelines/breaking-change-rules.md) +when determining the SemVer compliance of a change. + +Obsoleting a method is considered a **minor** increment. + +## Coding Style + +We attempt to conform to the .NET Foundation's [Coding Style](https://github.com/dotnet/corefx/blob/master/Documentation/coding-guidelines/coding-style.md) +where possible. + +As a general rule, follow the coding style already set in the file you +are editing, or look at a similar file if you are adding a new one. \ No newline at end of file diff --git a/Discord.Net.sln b/Discord.Net.sln index 58bfcad86..cac6c9064 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26730.12 +VisualStudioVersion = 15.0.27004.2009 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Core", "src\Discord.Net.Core\Discord.Net.Core.csproj", "{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}" EndProject @@ -8,8 +8,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Impls", "Impls", "{288C363D EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Rest", "src\Discord.Net.Rest\Discord.Net.Rest.csproj", "{BFC6DC28-0351-4573-926A-D4124244C04F}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Rpc", "src\Discord.Net.Rpc\Discord.Net.Rpc.csproj", "{5688A353-121E-40A1-8BFA-B17B91FB48FB}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Commands", "src\Discord.Net.Commands\Discord.Net.Commands.csproj", "{078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.WebSocket", "src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj", "{688FD1D8-7F01-4539-B2E9-F473C5D699C7}" @@ -58,18 +56,6 @@ Global {BFC6DC28-0351-4573-926A-D4124244C04F}.Release|x64.Build.0 = Debug|Any CPU {BFC6DC28-0351-4573-926A-D4124244C04F}.Release|x86.ActiveCfg = Debug|Any CPU {BFC6DC28-0351-4573-926A-D4124244C04F}.Release|x86.Build.0 = Debug|Any CPU - {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Debug|x64.ActiveCfg = Debug|Any CPU - {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 = 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 - {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|x86.Build.0 = Debug|Any CPU {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Debug|Any CPU.Build.0 = Debug|Any CPU {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -136,7 +122,6 @@ Global EndGlobalSection 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} diff --git a/Discord.Net.targets b/Discord.Net.targets index 95eccd790..3f623c619 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,7 +1,7 @@ - 2.0.0-alpha - + 2.0.0 + beta RogueException discord;discordapp https://github.com/RogueException/Discord.Net diff --git a/README.md b/README.md index 2b58d4579..bd0ef20c7 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ [![NuGet](https://img.shields.io/nuget/vpre/Discord.Net.svg?maxAge=2592000?style=plastic)](https://www.nuget.org/packages/Discord.Net) [![MyGet](https://img.shields.io/myget/discord-net/vpre/Discord.Net.svg)](https://www.myget.org/feed/Packages/discord-net) [![Build status](https://ci.appveyor.com/api/projects/status/5sb7n8a09w9clute/branch/dev?svg=true)](https://ci.appveyor.com/project/RogueException/discord-net/branch/dev) -[![Discord](https://discordapp.com/api/guilds/81384788765712384/widget.png)](https://discord.gg/0SBTUU1wZTVjAMPx) +[![Discord](https://discordapp.com/api/guilds/81384788765712384/widget.png)](https://discord.gg/jkrBmQR) 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). +Check out the [documentation](https://discord.foxbot.me/docs/) or join the [Discord API Chat](https://discord.gg/jkrBmQR). ## Installation ### Stable (NuGet) diff --git a/appveyor.yml b/appveyor.yml index d94e2ad68..393485fee 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -26,7 +26,6 @@ after_build: - ps: dotnet pack "src\Discord.Net.Core\Discord.Net.Core.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" - ps: dotnet pack "src\Discord.Net.Rest\Discord.Net.Rest.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" - ps: dotnet pack "src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" -- ps: dotnet pack "src\Discord.Net.Rpc\Discord.Net.Rpc.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" - ps: dotnet pack "src\Discord.Net.Commands\Discord.Net.Commands.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" - ps: dotnet pack "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" - ps: dotnet pack "src\Discord.Net.Providers.WS4Net\Discord.Net.Providers.WS4Net.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" @@ -34,7 +33,7 @@ after_build: if ($Env:APPVEYOR_REPO_TAG -eq "true") { nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="" } else { - nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="-build-$Env:BUILD" + nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="-$Env:BUILD" } - ps: Get-ChildItem artifacts\*.nupkg | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } diff --git a/docs/guides/commands/commands.md b/docs/guides/commands/commands.md index 6781764c9..2b012af0e 100644 --- a/docs/guides/commands/commands.md +++ b/docs/guides/commands/commands.md @@ -93,9 +93,9 @@ 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 +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). +Module Group - see below) [RemainderAttribute]: xref:Discord.Commands.RemainderAttribute [CommandAttribute]: xref:Discord.Commands.CommandAttribute @@ -340,4 +340,4 @@ and must be explicitly added. To install a TypeReader, invoke [CommandService.AddTypeReader]. -[CommandService.AddTypeReader]: xref:Discord.Commands.CommandService#Discord_Commands_CommandService_AddTypeReader__1_Discord_Commands_TypeReader_ \ No newline at end of file +[CommandService.AddTypeReader]: xref:Discord.Commands.CommandService#Discord_Commands_CommandService_AddTypeReader__1_Discord_Commands_TypeReader_ diff --git a/src/Discord.Net.Rpc/API/Rpc/AuthenticateParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/AuthenticateParams.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/AuthenticateParams.cs rename to experiment/Discord.Net.Rpc/API/Rpc/AuthenticateParams.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/AuthenticateResponse.cs b/experiment/Discord.Net.Rpc/API/Rpc/AuthenticateResponse.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/AuthenticateResponse.cs rename to experiment/Discord.Net.Rpc/API/Rpc/AuthenticateResponse.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/AuthorizeParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/AuthorizeParams.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/AuthorizeParams.cs rename to experiment/Discord.Net.Rpc/API/Rpc/AuthorizeParams.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/AuthorizeResponse.cs b/experiment/Discord.Net.Rpc/API/Rpc/AuthorizeResponse.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/AuthorizeResponse.cs rename to experiment/Discord.Net.Rpc/API/Rpc/AuthorizeResponse.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/Channel.cs b/experiment/Discord.Net.Rpc/API/Rpc/Channel.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/Channel.cs rename to experiment/Discord.Net.Rpc/API/Rpc/Channel.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/ChannelSubscriptionParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/ChannelSubscriptionParams.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/ChannelSubscriptionParams.cs rename to experiment/Discord.Net.Rpc/API/Rpc/ChannelSubscriptionParams.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/ChannelSummary.cs b/experiment/Discord.Net.Rpc/API/Rpc/ChannelSummary.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/ChannelSummary.cs rename to experiment/Discord.Net.Rpc/API/Rpc/ChannelSummary.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/ErrorEvent.cs b/experiment/Discord.Net.Rpc/API/Rpc/ErrorEvent.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/ErrorEvent.cs rename to experiment/Discord.Net.Rpc/API/Rpc/ErrorEvent.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/ExtendedVoiceState.cs b/experiment/Discord.Net.Rpc/API/Rpc/ExtendedVoiceState.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/ExtendedVoiceState.cs rename to experiment/Discord.Net.Rpc/API/Rpc/ExtendedVoiceState.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/GetChannelParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/GetChannelParams.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/GetChannelParams.cs rename to experiment/Discord.Net.Rpc/API/Rpc/GetChannelParams.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/GetChannelsParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/GetChannelsParams.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/GetChannelsParams.cs rename to experiment/Discord.Net.Rpc/API/Rpc/GetChannelsParams.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/GetChannelsResponse.cs b/experiment/Discord.Net.Rpc/API/Rpc/GetChannelsResponse.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/GetChannelsResponse.cs rename to experiment/Discord.Net.Rpc/API/Rpc/GetChannelsResponse.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/GetGuildParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/GetGuildParams.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/GetGuildParams.cs rename to experiment/Discord.Net.Rpc/API/Rpc/GetGuildParams.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/GetGuildsParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/GetGuildsParams.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/GetGuildsParams.cs rename to experiment/Discord.Net.Rpc/API/Rpc/GetGuildsParams.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/GetGuildsResponse.cs b/experiment/Discord.Net.Rpc/API/Rpc/GetGuildsResponse.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/GetGuildsResponse.cs rename to experiment/Discord.Net.Rpc/API/Rpc/GetGuildsResponse.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/Guild.cs b/experiment/Discord.Net.Rpc/API/Rpc/Guild.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/Guild.cs rename to experiment/Discord.Net.Rpc/API/Rpc/Guild.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/GuildMember.cs b/experiment/Discord.Net.Rpc/API/Rpc/GuildMember.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/GuildMember.cs rename to experiment/Discord.Net.Rpc/API/Rpc/GuildMember.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/GuildStatusEvent.cs b/experiment/Discord.Net.Rpc/API/Rpc/GuildStatusEvent.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/GuildStatusEvent.cs rename to experiment/Discord.Net.Rpc/API/Rpc/GuildStatusEvent.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/GuildSubscriptionParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/GuildSubscriptionParams.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/GuildSubscriptionParams.cs rename to experiment/Discord.Net.Rpc/API/Rpc/GuildSubscriptionParams.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/GuildSummary.cs b/experiment/Discord.Net.Rpc/API/Rpc/GuildSummary.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/GuildSummary.cs rename to experiment/Discord.Net.Rpc/API/Rpc/GuildSummary.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/Message.cs b/experiment/Discord.Net.Rpc/API/Rpc/Message.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/Message.cs rename to experiment/Discord.Net.Rpc/API/Rpc/Message.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/MessageEvent.cs b/experiment/Discord.Net.Rpc/API/Rpc/MessageEvent.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/MessageEvent.cs rename to experiment/Discord.Net.Rpc/API/Rpc/MessageEvent.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/Pan.cs b/experiment/Discord.Net.Rpc/API/Rpc/Pan.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/Pan.cs rename to experiment/Discord.Net.Rpc/API/Rpc/Pan.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/ReadyEvent.cs b/experiment/Discord.Net.Rpc/API/Rpc/ReadyEvent.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/ReadyEvent.cs rename to experiment/Discord.Net.Rpc/API/Rpc/ReadyEvent.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/RpcConfig.cs b/experiment/Discord.Net.Rpc/API/Rpc/RpcConfig.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/RpcConfig.cs rename to experiment/Discord.Net.Rpc/API/Rpc/RpcConfig.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/SelectChannelParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/SelectChannelParams.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/SelectChannelParams.cs rename to experiment/Discord.Net.Rpc/API/Rpc/SelectChannelParams.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/SetLocalVolumeParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/SetLocalVolumeParams.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/SetLocalVolumeParams.cs rename to experiment/Discord.Net.Rpc/API/Rpc/SetLocalVolumeParams.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/SetLocalVolumeResponse.cs b/experiment/Discord.Net.Rpc/API/Rpc/SetLocalVolumeResponse.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/SetLocalVolumeResponse.cs rename to experiment/Discord.Net.Rpc/API/Rpc/SetLocalVolumeResponse.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/SpeakingEvent.cs b/experiment/Discord.Net.Rpc/API/Rpc/SpeakingEvent.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/SpeakingEvent.cs rename to experiment/Discord.Net.Rpc/API/Rpc/SpeakingEvent.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/SubscriptionResponse.cs b/experiment/Discord.Net.Rpc/API/Rpc/SubscriptionResponse.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/SubscriptionResponse.cs rename to experiment/Discord.Net.Rpc/API/Rpc/SubscriptionResponse.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/UserVoiceSettings.cs b/experiment/Discord.Net.Rpc/API/Rpc/UserVoiceSettings.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/UserVoiceSettings.cs rename to experiment/Discord.Net.Rpc/API/Rpc/UserVoiceSettings.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/VoiceDevice.cs b/experiment/Discord.Net.Rpc/API/Rpc/VoiceDevice.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/VoiceDevice.cs rename to experiment/Discord.Net.Rpc/API/Rpc/VoiceDevice.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/VoiceDeviceSettings.cs b/experiment/Discord.Net.Rpc/API/Rpc/VoiceDeviceSettings.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/VoiceDeviceSettings.cs rename to experiment/Discord.Net.Rpc/API/Rpc/VoiceDeviceSettings.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/VoiceMode.cs b/experiment/Discord.Net.Rpc/API/Rpc/VoiceMode.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/VoiceMode.cs rename to experiment/Discord.Net.Rpc/API/Rpc/VoiceMode.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/VoiceSettings.cs b/experiment/Discord.Net.Rpc/API/Rpc/VoiceSettings.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/VoiceSettings.cs rename to experiment/Discord.Net.Rpc/API/Rpc/VoiceSettings.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/VoiceShortcut.cs b/experiment/Discord.Net.Rpc/API/Rpc/VoiceShortcut.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/VoiceShortcut.cs rename to experiment/Discord.Net.Rpc/API/Rpc/VoiceShortcut.cs diff --git a/src/Discord.Net.Rpc/API/RpcFrame.cs b/experiment/Discord.Net.Rpc/API/RpcFrame.cs similarity index 100% rename from src/Discord.Net.Rpc/API/RpcFrame.cs rename to experiment/Discord.Net.Rpc/API/RpcFrame.cs diff --git a/src/Discord.Net.Rpc/AssemblyInfo.cs b/experiment/Discord.Net.Rpc/AssemblyInfo.cs similarity index 100% rename from src/Discord.Net.Rpc/AssemblyInfo.cs rename to experiment/Discord.Net.Rpc/AssemblyInfo.cs diff --git a/src/Discord.Net.Rpc/Commands/RpcCommandContext.cs b/experiment/Discord.Net.Rpc/Commands/RpcCommandContext.cs similarity index 100% rename from src/Discord.Net.Rpc/Commands/RpcCommandContext.cs rename to experiment/Discord.Net.Rpc/Commands/RpcCommandContext.cs diff --git a/src/Discord.Net.Rpc/Discord.Net.Rpc.csproj b/experiment/Discord.Net.Rpc/Discord.Net.Rpc.csproj similarity index 100% rename from src/Discord.Net.Rpc/Discord.Net.Rpc.csproj rename to experiment/Discord.Net.Rpc/Discord.Net.Rpc.csproj diff --git a/src/Discord.Net.Rpc/DiscordRpcApiClient.cs b/experiment/Discord.Net.Rpc/DiscordRpcApiClient.cs similarity index 100% rename from src/Discord.Net.Rpc/DiscordRpcApiClient.cs rename to experiment/Discord.Net.Rpc/DiscordRpcApiClient.cs diff --git a/src/Discord.Net.Rpc/DiscordRpcClient.Events.cs b/experiment/Discord.Net.Rpc/DiscordRpcClient.Events.cs similarity index 100% rename from src/Discord.Net.Rpc/DiscordRpcClient.Events.cs rename to experiment/Discord.Net.Rpc/DiscordRpcClient.Events.cs diff --git a/src/Discord.Net.Rpc/DiscordRpcClient.cs b/experiment/Discord.Net.Rpc/DiscordRpcClient.cs similarity index 100% rename from src/Discord.Net.Rpc/DiscordRpcClient.cs rename to experiment/Discord.Net.Rpc/DiscordRpcClient.cs diff --git a/src/Discord.Net.Rpc/DiscordRpcConfig.cs b/experiment/Discord.Net.Rpc/DiscordRpcConfig.cs similarity index 100% rename from src/Discord.Net.Rpc/DiscordRpcConfig.cs rename to experiment/Discord.Net.Rpc/DiscordRpcConfig.cs diff --git a/src/Discord.Net.Rpc/Entities/Channels/IRpcAudioChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/IRpcAudioChannel.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Channels/IRpcAudioChannel.cs rename to experiment/Discord.Net.Rpc/Entities/Channels/IRpcAudioChannel.cs diff --git a/src/Discord.Net.Rpc/Entities/Channels/IRpcMessageChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/IRpcMessageChannel.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Channels/IRpcMessageChannel.cs rename to experiment/Discord.Net.Rpc/Entities/Channels/IRpcMessageChannel.cs diff --git a/src/Discord.Net.Rpc/Entities/Channels/IRpcPrivateChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/IRpcPrivateChannel.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Channels/IRpcPrivateChannel.cs rename to experiment/Discord.Net.Rpc/Entities/Channels/IRpcPrivateChannel.cs diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs rename to experiment/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcChannelSummary.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcChannelSummary.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Channels/RpcChannelSummary.cs rename to experiment/Discord.Net.Rpc/Entities/Channels/RpcChannelSummary.cs diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs rename to experiment/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs rename to experiment/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs similarity index 91% rename from src/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs rename to experiment/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs index 48eb8ec3e..576a0489c 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs +++ b/experiment/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs @@ -10,6 +10,7 @@ namespace Discord.Rpc { public ulong GuildId { get; } public int Position { get; private set; } + public ulong? CategoryId { get; private set; } internal RpcGuildChannel(DiscordRpcClient discord, ulong id, ulong guildId) : base(discord, id) @@ -51,12 +52,18 @@ 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 = false, bool isUnique = false, RequestOptions options = null) + public async Task CreateInviteAsync(int? maxAge = 86400, 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; //IGuildChannel + public Task GetCategoryAsync() + { + //Always fails + throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); + } + IGuild IGuildChannel.Guild { get diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs similarity index 86% rename from src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs rename to experiment/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs index 9de2968db..8c49f0671 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs +++ b/experiment/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs @@ -68,11 +68,25 @@ namespace Discord.Rpc => ChannelHelper.TriggerTypingAsync(this, Discord, options); public IDisposable EnterTypingState(RequestOptions options = null) => ChannelHelper.EnterTypingState(this, Discord, options); - + + //Webhooks + public Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) + => ChannelHelper.CreateWebhookAsync(this, Discord, name, avatar, options); + public Task GetWebhookAsync(ulong id, RequestOptions options = null) + => ChannelHelper.GetWebhookAsync(this, Discord, id, options); + public Task> GetWebhooksAsync(RequestOptions options = null) + => ChannelHelper.GetWebhooksAsync(this, Discord, options); + private string DebuggerDisplay => $"{Name} ({Id}, Text)"; //ITextChannel string ITextChannel.Topic { get { throw new NotSupportedException(); } } + async Task ITextChannel.CreateWebhookAsync(string name, Stream avatar, RequestOptions options) + => await CreateWebhookAsync(name, avatar, options); + async Task ITextChannel.GetWebhookAsync(ulong id, RequestOptions options) + => await GetWebhookAsync(id, options); + async Task> ITextChannel.GetWebhooksAsync(RequestOptions options) + => await GetWebhooksAsync(options); //IMessageChannel async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs rename to experiment/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs diff --git a/src/Discord.Net.Rpc/Entities/Guilds/RpcGuild.cs b/experiment/Discord.Net.Rpc/Entities/Guilds/RpcGuild.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Guilds/RpcGuild.cs rename to experiment/Discord.Net.Rpc/Entities/Guilds/RpcGuild.cs diff --git a/src/Discord.Net.Rpc/Entities/Guilds/RpcGuildStatus.cs b/experiment/Discord.Net.Rpc/Entities/Guilds/RpcGuildStatus.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Guilds/RpcGuildStatus.cs rename to experiment/Discord.Net.Rpc/Entities/Guilds/RpcGuildStatus.cs diff --git a/src/Discord.Net.Rpc/Entities/Guilds/RpcGuildSummary.cs b/experiment/Discord.Net.Rpc/Entities/Guilds/RpcGuildSummary.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Guilds/RpcGuildSummary.cs rename to experiment/Discord.Net.Rpc/Entities/Guilds/RpcGuildSummary.cs diff --git a/src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs b/experiment/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs rename to experiment/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs diff --git a/src/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs b/experiment/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs rename to experiment/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs diff --git a/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs b/experiment/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs rename to experiment/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs diff --git a/src/Discord.Net.Rpc/Entities/RpcEntity.cs b/experiment/Discord.Net.Rpc/Entities/RpcEntity.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/RpcEntity.cs rename to experiment/Discord.Net.Rpc/Entities/RpcEntity.cs diff --git a/src/Discord.Net.Rpc/Entities/UserVoiceProperties.cs b/experiment/Discord.Net.Rpc/Entities/UserVoiceProperties.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/UserVoiceProperties.cs rename to experiment/Discord.Net.Rpc/Entities/UserVoiceProperties.cs diff --git a/src/Discord.Net.Rpc/Entities/Users/Pan.cs b/experiment/Discord.Net.Rpc/Entities/Users/Pan.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Users/Pan.cs rename to experiment/Discord.Net.Rpc/Entities/Users/Pan.cs diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcGuildUser.cs b/experiment/Discord.Net.Rpc/Entities/Users/RpcGuildUser.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Users/RpcGuildUser.cs rename to experiment/Discord.Net.Rpc/Entities/Users/RpcGuildUser.cs diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs b/experiment/Discord.Net.Rpc/Entities/Users/RpcUser.cs similarity index 98% rename from src/Discord.Net.Rpc/Entities/Users/RpcUser.cs rename to experiment/Discord.Net.Rpc/Entities/Users/RpcUser.cs index c6b0b2fd8..f55c83b75 100644 --- a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs +++ b/experiment/Discord.Net.Rpc/Entities/Users/RpcUser.cs @@ -18,7 +18,7 @@ namespace Discord.Rpc public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); public virtual bool IsWebhook => false; - public virtual Game? Game => null; + public virtual IActivity Activity => null; public virtual UserStatus Status => UserStatus.Offline; internal RpcUser(DiscordRpcClient discord, ulong id) diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcVoiceState.cs b/experiment/Discord.Net.Rpc/Entities/Users/RpcVoiceState.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Users/RpcVoiceState.cs rename to experiment/Discord.Net.Rpc/Entities/Users/RpcVoiceState.cs diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcWebhookUser.cs b/experiment/Discord.Net.Rpc/Entities/Users/RpcWebhookUser.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Users/RpcWebhookUser.cs rename to experiment/Discord.Net.Rpc/Entities/Users/RpcWebhookUser.cs diff --git a/src/Discord.Net.Rpc/Entities/VoiceDevice.cs b/experiment/Discord.Net.Rpc/Entities/VoiceDevice.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/VoiceDevice.cs rename to experiment/Discord.Net.Rpc/Entities/VoiceDevice.cs diff --git a/src/Discord.Net.Rpc/Entities/VoiceDeviceProperties.cs b/experiment/Discord.Net.Rpc/Entities/VoiceDeviceProperties.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/VoiceDeviceProperties.cs rename to experiment/Discord.Net.Rpc/Entities/VoiceDeviceProperties.cs diff --git a/src/Discord.Net.Rpc/Entities/VoiceModeProperties.cs b/experiment/Discord.Net.Rpc/Entities/VoiceModeProperties.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/VoiceModeProperties.cs rename to experiment/Discord.Net.Rpc/Entities/VoiceModeProperties.cs diff --git a/src/Discord.Net.Rpc/Entities/VoiceProperties.cs b/experiment/Discord.Net.Rpc/Entities/VoiceProperties.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/VoiceProperties.cs rename to experiment/Discord.Net.Rpc/Entities/VoiceProperties.cs diff --git a/src/Discord.Net.Rpc/Entities/VoiceSettings.cs b/experiment/Discord.Net.Rpc/Entities/VoiceSettings.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/VoiceSettings.cs rename to experiment/Discord.Net.Rpc/Entities/VoiceSettings.cs diff --git a/src/Discord.Net.Rpc/Entities/VoiceShortcut.cs b/experiment/Discord.Net.Rpc/Entities/VoiceShortcut.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/VoiceShortcut.cs rename to experiment/Discord.Net.Rpc/Entities/VoiceShortcut.cs diff --git a/src/Discord.Net.Rpc/Entities/VoiceShortcutType.cs b/experiment/Discord.Net.Rpc/Entities/VoiceShortcutType.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/VoiceShortcutType.cs rename to experiment/Discord.Net.Rpc/Entities/VoiceShortcutType.cs diff --git a/src/Discord.Net.Rpc/Extensions/EntityExtensions.cs b/experiment/Discord.Net.Rpc/Extensions/EntityExtensions.cs similarity index 100% rename from src/Discord.Net.Rpc/Extensions/EntityExtensions.cs rename to experiment/Discord.Net.Rpc/Extensions/EntityExtensions.cs diff --git a/src/Discord.Net.Rpc/RpcChannelEvent.cs b/experiment/Discord.Net.Rpc/RpcChannelEvent.cs similarity index 100% rename from src/Discord.Net.Rpc/RpcChannelEvent.cs rename to experiment/Discord.Net.Rpc/RpcChannelEvent.cs diff --git a/src/Discord.Net.Rpc/RpcGlobalEvent.cs b/experiment/Discord.Net.Rpc/RpcGlobalEvent.cs similarity index 100% rename from src/Discord.Net.Rpc/RpcGlobalEvent.cs rename to experiment/Discord.Net.Rpc/RpcGlobalEvent.cs diff --git a/src/Discord.Net.Rpc/RpcGuildEvent.cs b/experiment/Discord.Net.Rpc/RpcGuildEvent.cs similarity index 100% rename from src/Discord.Net.Rpc/RpcGuildEvent.cs rename to experiment/Discord.Net.Rpc/RpcGuildEvent.cs diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs index 6be142a45..104252799 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs @@ -57,13 +57,11 @@ namespace Discord.Commands if (ChannelPermission.HasValue) { - var guildChannel = context.Channel as IGuildChannel; - ChannelPermissions perms; - if (guildChannel != null) + if (context.Channel is IGuildChannel guildChannel) perms = guildUser.GetPermissions(guildChannel); else - perms = ChannelPermissions.All(guildChannel); + perms = ChannelPermissions.All(context.Channel); if (!perms.Has(ChannelPermission.Value)) return PreconditionResult.FromError($"Bot requires channel permission {ChannelPermission.Value}"); diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs index 0179aa0ac..14121f35b 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs @@ -56,13 +56,11 @@ namespace Discord.Commands if (ChannelPermission.HasValue) { - var guildChannel = context.Channel as IGuildChannel; - ChannelPermissions perms; - if (guildChannel != null) + if (context.Channel is IGuildChannel guildChannel) perms = guildUser.GetPermissions(guildChannel); else - perms = ChannelPermissions.All(guildChannel); + perms = ChannelPermissions.All(context.Channel); if (!perms.Has(ChannelPermission.Value)) return Task.FromResult(PreconditionResult.FromError($"User requires channel permission {ChannelPermission.Value}")); diff --git a/src/Discord.Net.Commands/CommandParser.cs b/src/Discord.Net.Commands/CommandParser.cs index 28e36d54d..d65d99349 100644 --- a/src/Discord.Net.Commands/CommandParser.cs +++ b/src/Discord.Net.Commands/CommandParser.cs @@ -14,7 +14,7 @@ namespace Discord.Commands QuotedParameter } - public static async Task ParseArgsAsync(CommandInfo command, ICommandContext context, IServiceProvider services, string input, int startPos) + public static async Task ParseArgsAsync(CommandInfo command, ICommandContext context, bool ignoreExtraArgs, IServiceProvider services, string input, int startPos) { ParameterInfo curParam = null; StringBuilder argBuilder = new StringBuilder(input.Length); @@ -109,7 +109,12 @@ namespace Discord.Commands if (argString != null) { if (curParam == null) - return ParseResult.FromError(CommandError.BadArgCount, "The input text has too many parameters."); + { + if (ignoreExtraArgs) + break; + else + return ParseResult.FromError(CommandError.BadArgCount, "The input text has too many parameters."); + } var typeReaderResult = await curParam.ParseAsync(context, argString, services).ConfigureAwait(false); if (!typeReaderResult.IsSuccess && typeReaderResult.Error != CommandError.MultipleMatches) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index cf2b93277..8e7dab898 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -27,7 +27,7 @@ namespace Discord.Commands private readonly HashSet _moduleDefs; private readonly CommandMap _map; - internal readonly bool _caseSensitive, _throwOnError; + internal readonly bool _caseSensitive, _throwOnError, _ignoreExtraArgs; internal readonly char _separatorChar; internal readonly RunMode _defaultRunMode; internal readonly Logger _cmdLogger; @@ -42,6 +42,7 @@ namespace Discord.Commands { _caseSensitive = config.CaseSensitiveCommands; _throwOnError = config.ThrowOnError; + _ignoreExtraArgs = config.IgnoreExtraArgs; _separatorChar = config.SeparatorChar; _defaultRunMode = config.DefaultRunMode; if (_defaultRunMode == RunMode.Default) diff --git a/src/Discord.Net.Commands/CommandServiceConfig.cs b/src/Discord.Net.Commands/CommandServiceConfig.cs index b53b0248c..7fdbe368b 100644 --- a/src/Discord.Net.Commands/CommandServiceConfig.cs +++ b/src/Discord.Net.Commands/CommandServiceConfig.cs @@ -15,5 +15,8 @@ /// Determines whether RunMode.Sync commands should push exceptions up to the caller. public bool ThrowOnError { get; set; } = true; + + /// Determines whether extra parameters should be ignored. + public bool IgnoreExtraArgs { get; set; } = false; } } diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index 9ca7ffff3..f0d406e8d 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -18,6 +18,7 @@ namespace Discord.Commands private static readonly System.Reflection.MethodInfo _convertParamsMethod = typeof(CommandInfo).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList)); private static readonly ConcurrentDictionary, object>> _arrayConverters = new ConcurrentDictionary, object>>(); + private readonly CommandService _commandService; private readonly Func _action; public ModuleInfo Module { get; } @@ -64,6 +65,7 @@ namespace Discord.Commands HasVarArgs = builder.Parameters.Count > 0 ? builder.Parameters[builder.Parameters.Count - 1].IsMultiple : false; _action = builder.Callback; + _commandService = service; } public async Task CheckPreconditionsAsync(ICommandContext context, IServiceProvider services = null) @@ -117,7 +119,7 @@ namespace Discord.Commands return ParseResult.FromError(preconditionResult); string input = searchResult.Text.Substring(startIndex); - return await CommandParser.ParseArgsAsync(this, context, services, input, 0).ConfigureAwait(false); + return await CommandParser.ParseArgsAsync(this, context, _commandService._ignoreExtraArgs, services, input, 0).ConfigureAwait(false); } public Task ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) @@ -199,10 +201,13 @@ namespace Discord.Commands return result; } else + { await task.ConfigureAwait(false); + var result = ExecuteResult.FromSuccess(); + await Module.Service._commandExecutedEvent.InvokeAsync(this, context, result).ConfigureAwait(false); + } var executeResult = ExecuteResult.FromSuccess(); - await Module.Service._commandExecutedEvent.InvokeAsync(this, context, executeResult).ConfigureAwait(false); return executeResult; } catch (Exception ex) diff --git a/src/Discord.Net.Commands/Readers/UserTypeReader.cs b/src/Discord.Net.Commands/Readers/UserTypeReader.cs index ca337aaf6..8fc330d4c 100644 --- a/src/Discord.Net.Commands/Readers/UserTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/UserTypeReader.cs @@ -13,7 +13,7 @@ namespace Discord.Commands public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { var results = new Dictionary(); - IReadOnlyCollection channelUsers = (await context.Channel.GetUsersAsync(CacheMode.CacheOnly).Flatten().ConfigureAwait(false)).ToArray(); //TODO: must be a better way? + IAsyncEnumerable channelUsers = context.Channel.GetUsersAsync(CacheMode.CacheOnly).Flatten(); // it's better IReadOnlyCollection guildUsers = ImmutableArray.Create(); ulong id; @@ -45,7 +45,7 @@ namespace Discord.Commands string username = input.Substring(0, index); if (ushort.TryParse(input.Substring(index + 1), out ushort discriminator)) { - var channelUser = channelUsers.FirstOrDefault(x => x.DiscriminatorValue == discriminator && + var channelUser = await 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); @@ -57,8 +57,9 @@ namespace Discord.Commands //By Username (0.5-0.6) { - foreach (var channelUser in channelUsers.Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase))) - AddResult(results, channelUser as T, channelUser.Username == input ? 0.65f : 0.55f); + await channelUsers + .Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase)) + .ForEachAsync(channelUser => AddResult(results, channelUser as T, channelUser.Username == input ? 0.65f : 0.55f)); foreach (var guildUser in guildUsers.Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase))) AddResult(results, guildUser as T, guildUser.Username == input ? 0.60f : 0.50f); @@ -66,8 +67,9 @@ namespace Discord.Commands //By Nickname (0.5-0.6) { - foreach (var channelUser in channelUsers.Where(x => string.Equals(input, (x as IGuildUser)?.Nickname, StringComparison.OrdinalIgnoreCase))) - AddResult(results, channelUser as T, (channelUser as IGuildUser).Nickname == input ? 0.65f : 0.55f); + await channelUsers + .Where(x => string.Equals(input, (x as IGuildUser)?.Nickname, StringComparison.OrdinalIgnoreCase)) + .ForEachAsync(channelUser => AddResult(results, channelUser as T, (channelUser as IGuildUser).Nickname == input ? 0.65f : 0.55f)); foreach (var guildUser in guildUsers.Where(x => string.Equals(input, (x as IGuildUser).Nickname, StringComparison.OrdinalIgnoreCase))) AddResult(results, guildUser as T, (guildUser as IGuildUser).Nickname == input ? 0.60f : 0.50f); diff --git a/src/Discord.Net.Core/CDN.cs b/src/Discord.Net.Core/CDN.cs index d3ade3722..070b965ee 100644 --- a/src/Discord.Net.Core/CDN.cs +++ b/src/Discord.Net.Core/CDN.cs @@ -19,8 +19,14 @@ namespace Discord => splashId != null ? $"{DiscordConfig.CDNUrl}splashes/{guildId}/{splashId}.jpg" : null; public static string GetChannelIconUrl(ulong channelId, string iconId) => iconId != null ? $"{DiscordConfig.CDNUrl}channel-icons/{channelId}/{iconId}.jpg" : null; - public static string GetEmojiUrl(ulong emojiId) - => $"{DiscordConfig.CDNUrl}emojis/{emojiId}.png"; + public static string GetEmojiUrl(ulong emojiId, bool animated) + => $"{DiscordConfig.CDNUrl}emojis/{emojiId}.{(animated ? "gif" : "png")}"; + + public static string GetRichAssetUrl(ulong appId, string assetId, ushort size, ImageFormat format) + { + string extension = FormatToExtension(format, ""); + return $"{DiscordConfig.CDNUrl}app-assets/{appId}/{assetId}.{extension}?size={size}"; + } private static string FormatToExtension(ImageFormat format, string imageId) { diff --git a/src/Discord.Net.Core/Entities/Activities/Game.cs b/src/Discord.Net.Core/Entities/Activities/Game.cs new file mode 100644 index 000000000..f2b7e8eb6 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/Game.cs @@ -0,0 +1,19 @@ +using System.Diagnostics; + +namespace Discord +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class Game : IActivity + { + public string Name { get; internal set; } + + internal Game() { } + public Game(string name) + { + Name = name; + } + + public override string ToString() => Name; + private string DebuggerDisplay => Name; + } +} diff --git a/src/Discord.Net.Core/Entities/Activities/GameAsset.cs b/src/Discord.Net.Core/Entities/Activities/GameAsset.cs new file mode 100644 index 000000000..385f37214 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/GameAsset.cs @@ -0,0 +1,15 @@ +namespace Discord +{ + public class GameAsset + { + internal GameAsset() { } + + internal ulong ApplicationId { get; set; } + + public string Text { get; internal set; } + public string ImageId { get; internal set; } + + public string GetImageUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => CDN.GetRichAssetUrl(ApplicationId, ImageId, size, format); + } +} \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Activities/GameParty.cs b/src/Discord.Net.Core/Entities/Activities/GameParty.cs new file mode 100644 index 000000000..dbfe5b6ce --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/GameParty.cs @@ -0,0 +1,11 @@ +namespace Discord +{ + public class GameParty + { + internal GameParty() { } + + public string Id { get; internal set; } + public int Members { get; internal set; } + public int Capacity { get; internal set; } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Activities/GameSecrets.cs b/src/Discord.Net.Core/Entities/Activities/GameSecrets.cs new file mode 100644 index 000000000..e9d988ba9 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/GameSecrets.cs @@ -0,0 +1,16 @@ +namespace Discord +{ + public class GameSecrets + { + public string Match { get; } + public string Join { get; } + public string Spectate { get; } + + internal GameSecrets(string match, string join, string spectate) + { + Match = match; + Join = join; + Spectate = spectate; + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Activities/GameTimestamps.cs b/src/Discord.Net.Core/Entities/Activities/GameTimestamps.cs new file mode 100644 index 000000000..8c8c992fa --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/GameTimestamps.cs @@ -0,0 +1,16 @@ +using System; + +namespace Discord +{ + public class GameTimestamps + { + public DateTimeOffset? Start { get; } + public DateTimeOffset? End { get; } + + internal GameTimestamps(DateTimeOffset? start, DateTimeOffset? end) + { + Start = start; + End = end; + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Activities/IActivity.cs b/src/Discord.Net.Core/Entities/Activities/IActivity.cs new file mode 100644 index 000000000..0dcf34273 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/IActivity.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IActivity + { + string Name { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Activities/RichGame.cs b/src/Discord.Net.Core/Entities/Activities/RichGame.cs new file mode 100644 index 000000000..e66eac1d2 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/RichGame.cs @@ -0,0 +1,22 @@ +using System.Diagnostics; + +namespace Discord +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RichGame : Game + { + internal RichGame() { } + + public string Details { get; internal set;} + public string State { get; internal set;} + public ulong ApplicationId { get; internal set; } + public GameAsset SmallAsset { get; internal set; } + public GameAsset LargeAsset { get; internal set; } + public GameParty Party { get; internal set; } + public GameSecrets Secrets { get; internal set; } + public GameTimestamps Timestamps { get; internal set; } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} (Rich)"; + } +} \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Activities/StreamingGame.cs b/src/Discord.Net.Core/Entities/Activities/StreamingGame.cs new file mode 100644 index 000000000..140024272 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/StreamingGame.cs @@ -0,0 +1,21 @@ +using System.Diagnostics; + +namespace Discord +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class StreamingGame : Game + { + public string Url { get; internal set; } + public StreamType StreamType { get; internal set; } + + public StreamingGame(string name, string url, StreamType streamType) + { + Name = name; + Url = url; + StreamType = streamType; + } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Url})"; + } +} \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs index 0ea196a4a..2ac6c8d52 100644 --- a/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs +++ b/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs @@ -26,5 +26,9 @@ /// Move the channel to the following position. This is 0-based! /// public Optional Position { get; set; } + /// + /// Sets the category for this channel + /// + public Optional CategoryId { get; set; } } } diff --git a/src/Discord.Net.Core/Entities/Channels/ICategoryChannel.cs b/src/Discord.Net.Core/Entities/Channels/ICategoryChannel.cs new file mode 100644 index 000000000..0f7f5aa62 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/ICategoryChannel.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public interface ICategoryChannel : IGuildChannel + { + } +} diff --git a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs index 3d08a8c51..c9841cb15 100644 --- a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs @@ -9,6 +9,10 @@ namespace Discord /// Gets the position of this channel in the guild's channel list, relative to others of the same type. int Position { get; } + /// Gets the parentid (category) of this channel in the guild's channel list. + ulong? CategoryId { get; } + /// Gets the parent channel (category) of this channel. + Task GetCategoryAsync(); /// Gets the guild this channel is a member of. IGuild Guild { get; } /// Gets the id of the guild this channel is a member of. @@ -20,10 +24,10 @@ 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, bool isUnique = false, RequestOptions options = null); + Task CreateInviteAsync(int? maxAge = 86400, 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); diff --git a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs index be4dd0260..7c6ec3908 100644 --- a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; namespace Discord @@ -19,5 +20,12 @@ namespace Discord /// Modifies this text channel. Task ModifyAsync(Action func, RequestOptions options = null); + + /// Creates a webhook in this text channel. + Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null); + /// Gets the webhook in this text channel with the provided id, or null if not found. + Task GetWebhookAsync(ulong id, RequestOptions options = null); + /// Gets the webhooks for this text channel. + Task> GetWebhooksAsync(RequestOptions options = null); } } \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Emotes/Emote.cs b/src/Discord.Net.Core/Entities/Emotes/Emote.cs index f498c818e..e3a228c83 100644 --- a/src/Discord.Net.Core/Entities/Emotes/Emote.cs +++ b/src/Discord.Net.Core/Entities/Emotes/Emote.cs @@ -16,13 +16,18 @@ namespace Discord /// The ID of this emote /// public ulong Id { get; } + /// + /// Is this emote animated? + /// + public bool Animated { get; } public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); - public string Url => CDN.GetEmojiUrl(Id); + public string Url => CDN.GetEmojiUrl(Id, Animated); - internal Emote(ulong id, string name) + internal Emote(ulong id, string name, bool animated) { Id = id; Name = name; + Animated = animated; } public override bool Equals(object other) @@ -59,17 +64,20 @@ namespace Discord public static bool TryParse(string text, out Emote result) { result = null; - if (text.Length >= 4 && text[0] == '<' && text[1] == ':' && text[text.Length - 1] == '>') + if (text.Length >= 4 && text[0] == '<' && (text[1] == ':' || (text[1] == 'a' && text[2] == ':')) && text[text.Length - 1] == '>') { - int splitIndex = text.IndexOf(':', 2); + bool animated = text[1] == 'a'; + int startIndex = animated ? 3 : 2; + + int splitIndex = text.IndexOf(':', startIndex); if (splitIndex == -1) return false; if (!ulong.TryParse(text.Substring(splitIndex + 1, text.Length - splitIndex - 2), NumberStyles.None, CultureInfo.InvariantCulture, out ulong id)) return false; - string name = text.Substring(2, splitIndex - 2); - result = new Emote(id, name); + string name = text.Substring(startIndex, splitIndex - startIndex); + result = new Emote(id, name, animated); return true; } return false; @@ -77,6 +85,6 @@ namespace Discord } private string DebuggerDisplay => $"{Name} ({Id})"; - public override string ToString() => $"<:{Name}:{Id}>"; + public override string ToString() => $"<{(Animated ? "a" : "")}:{Name}:{Id}>"; } } diff --git a/src/Discord.Net.Core/Entities/Emotes/EmoteProperties.cs b/src/Discord.Net.Core/Entities/Emotes/EmoteProperties.cs new file mode 100644 index 000000000..be24d306c --- /dev/null +++ b/src/Discord.Net.Core/Entities/Emotes/EmoteProperties.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Discord +{ + public class EmoteProperties + { + public Optional Name { get; set; } + public Optional> Roles { get; set; } + } +} diff --git a/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs b/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs index 8d776a4cd..95b062bd2 100644 --- a/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs +++ b/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs @@ -13,7 +13,7 @@ namespace Discord public bool RequireColons { get; } public IReadOnlyList RoleIds { get; } - internal GuildEmote(ulong id, string name, bool isManaged, bool requireColons, IReadOnlyList roleIds) : base(id, name) + internal GuildEmote(ulong id, string name, bool animated, bool isManaged, bool requireColons, IReadOnlyList roleIds) : base(id, name, animated) { IsManaged = isManaged; RequireColons = requireColons; @@ -21,6 +21,6 @@ namespace Discord } private string DebuggerDisplay => $"{Name} ({Id})"; - public override string ToString() => $"<:{Name}:{Id}>"; + public override string ToString() => $"<{(Animated ? "a" : "")}:{Name}:{Id}>"; } } diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 3ded9e038..2f0599d76 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -84,6 +84,7 @@ namespace Discord 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> GetCategoriesAsync(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 GetSystemChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); @@ -93,6 +94,8 @@ namespace Discord Task CreateTextChannelAsync(string name, RequestOptions options = null); /// Creates a new voice channel. Task CreateVoiceChannelAsync(string name, RequestOptions options = null); + /// Creates a new channel category. + Task CreateCategoryAsync(string name, RequestOptions options = null); Task> GetIntegrationsAsync(RequestOptions options = null); Task CreateIntegrationAsync(ulong id, string type, RequestOptions options = null); @@ -117,5 +120,19 @@ namespace Discord 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. Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null); + + /// Gets the webhook in this guild with the provided id, or null if not found. + Task GetWebhookAsync(ulong id, RequestOptions options = null); + /// Gets a collection of all webhooks for this guild. + Task> GetWebhooksAsync(RequestOptions options = null); + + /// Gets a specific emote from this guild. + Task GetEmoteAsync(ulong id, RequestOptions options = null); + /// Creates a new emote in this guild. + Task CreateEmoteAsync(string name, Image image, Optional> roles = default(Optional>), RequestOptions options = null); + /// Modifies an existing emote in this guild. + Task ModifyEmoteAsync(GuildEmote emote, Action func, RequestOptions options = null); + /// Deletes an existing emote from this guild. + Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null); } } \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs index 3e438f43f..740b6c30b 100644 --- a/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs @@ -11,7 +11,9 @@ namespace Discord // Text AddReactions = 0x00_00_00_40, - ReadMessages = 0x00_00_04_00, + [Obsolete("Use ViewChannel instead.")] + ReadMessages = ViewChannel, + ViewChannel = 0x00_00_04_00, SendMessages = 0x00_00_08_00, SendTTSMessages = 0x00_00_10_00, ManageMessages = 0x00_00_20_00, diff --git a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs index 4c11d0db0..1a8aad53c 100644 --- a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs @@ -41,7 +41,11 @@ namespace Discord /// 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); + [Obsolete("Use ViewChannel instead.")] + public bool ReadMessages => ViewChannel; + /// If True, a user may view channels. + public bool ViewChannel => Permissions.GetValue(RawValue, ChannelPermission.ViewChannel); + /// If True, a user may send messages. public bool SendMessages => Permissions.GetValue(RawValue, ChannelPermission.SendMessages); /// If True, a user may send text-to-speech messages. @@ -82,7 +86,7 @@ namespace Discord 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? viewChannel = 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? manageRoles = null, bool? manageWebhooks = null) @@ -92,7 +96,7 @@ namespace Discord Permissions.SetValue(ref value, createInstantInvite, ChannelPermission.CreateInstantInvite); Permissions.SetValue(ref value, manageChannel, ChannelPermission.ManageChannels); Permissions.SetValue(ref value, addReactions, ChannelPermission.AddReactions); - Permissions.SetValue(ref value, readMessages, ChannelPermission.ReadMessages); + Permissions.SetValue(ref value, viewChannel, ChannelPermission.ViewChannel); Permissions.SetValue(ref value, sendMessages, ChannelPermission.SendMessages); Permissions.SetValue(ref value, sendTTSMessages, ChannelPermission.SendTTSMessages); Permissions.SetValue(ref value, manageMessages, ChannelPermission.ManageMessages); @@ -116,11 +120,11 @@ namespace Discord /// Creates a new ChannelPermissions with the provided permissions. public ChannelPermissions(bool createInstantInvite = false, bool manageChannel = false, bool addReactions = false, - bool readMessages = false, bool sendMessages = false, bool sendTTSMessages = false, bool manageMessages = false, + bool viewChannel = 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 manageRoles = false, bool manageWebhooks = false) - : this(0, createInstantInvite, manageChannel, addReactions, readMessages, sendMessages, sendTTSMessages, manageMessages, + : this(0, createInstantInvite, manageChannel, addReactions, viewChannel, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, manageRoles, manageWebhooks) { } @@ -128,11 +132,11 @@ namespace Discord /// Creates a new ChannelPermissions from this one, changing the provided non-null permissions. 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? viewChannel = 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? manageRoles = null, bool? manageWebhooks = null) - => new ChannelPermissions(RawValue, createInstantInvite, manageChannel, addReactions, readMessages, sendMessages, sendTTSMessages, manageMessages, + => new ChannelPermissions(RawValue, createInstantInvite, manageChannel, addReactions, viewChannel, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, manageRoles, manageWebhooks); diff --git a/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs index 4ee3b0fc6..a880e62ca 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 /// Gets a GuildPermissions that grants all guild permissions for webhook users. public static readonly GuildPermissions Webhook = new GuildPermissions(0b00000_0000000_0001101100000_000000); /// Gets a GuildPermissions that grants all guild permissions. - public static readonly GuildPermissions All = new GuildPermissions(0b11111_1111110_11111111110011_111111); + public static readonly GuildPermissions All = new GuildPermissions(0b11111_1111110_1111111110011_111111); /// Gets a packed value representing all the permissions in this GuildPermissions. public ulong RawValue { get; } diff --git a/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs b/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs index c3e296e2c..108b67273 100644 --- a/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Diagnostics; namespace Discord @@ -27,7 +28,10 @@ namespace Discord /// 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); + [Obsolete("Use ViewChannel instead.")] + public PermValue ReadMessages => ViewChannel; + /// If Allowed, a user may join channels. + public PermValue ViewChannel => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ViewChannel); /// If Allowed, a user may send messages. public PermValue SendMessages => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.SendMessages); /// If Allowed, a user may send text-to-speech messages. @@ -72,7 +76,7 @@ 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? viewChannel = 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? manageRoles = null, @@ -81,7 +85,7 @@ namespace Discord Permissions.SetValue(ref allowValue, ref denyValue, createInstantInvite, ChannelPermission.CreateInstantInvite); Permissions.SetValue(ref allowValue, ref denyValue, manageChannel, ChannelPermission.ManageChannels); Permissions.SetValue(ref allowValue, ref denyValue, addReactions, ChannelPermission.AddReactions); - Permissions.SetValue(ref allowValue, ref denyValue, readMessages, ChannelPermission.ReadMessages); + Permissions.SetValue(ref allowValue, ref denyValue, viewChannel, ChannelPermission.ViewChannel); Permissions.SetValue(ref allowValue, ref denyValue, sendMessages, ChannelPermission.SendMessages); Permissions.SetValue(ref allowValue, ref denyValue, sendTTSMessages, ChannelPermission.SendTTSMessages); Permissions.SetValue(ref allowValue, ref denyValue, manageMessages, ChannelPermission.ManageMessages); diff --git a/src/Discord.Net.Core/Entities/Users/Game.cs b/src/Discord.Net.Core/Entities/Users/Game.cs deleted file mode 100644 index 3405b0dd4..000000000 --- a/src/Discord.Net.Core/Entities/Users/Game.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Diagnostics; - -namespace Discord -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public struct Game - { - public string Name { get; } - public string StreamUrl { get; } - public StreamType StreamType { get; } - - public Game(string name, string streamUrl, StreamType type) - { - Name = name; - StreamUrl = streamUrl; - StreamType = type; - } - private Game(string name) - : this(name, 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 index 33b311604..1c5e5482c 100644 --- a/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs +++ b/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs @@ -34,7 +34,7 @@ namespace Discord /// Should the user have a nickname set? /// /// - /// To clear the user's nickname, this value can be set to null. + /// To clear the user's nickname, this value can be set to or . /// public Optional Nickname { get; set; } /// diff --git a/src/Discord.Net.Core/Entities/Users/IPresence.cs b/src/Discord.Net.Core/Entities/Users/IPresence.cs index 7f182241b..25adcc9c4 100644 --- a/src/Discord.Net.Core/Entities/Users/IPresence.cs +++ b/src/Discord.Net.Core/Entities/Users/IPresence.cs @@ -2,8 +2,8 @@ { public interface IPresence { - /// Gets the game this user is currently playing, if any. - Game? Game { get; } + /// Gets the activity this user is currently doing. + IActivity Activity { get; } /// Gets the current status of this user. UserStatus Status { get; } } diff --git a/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs b/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs index 8f4d42187..be769b944 100644 --- a/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs @@ -1,6 +1,5 @@ namespace Discord { - //TODO: Add webhook endpoints public interface IWebhookUser : IGuildUser { ulong WebhookId { get; } diff --git a/src/Discord.Net.Core/Entities/Webhooks/IWebhook.cs b/src/Discord.Net.Core/Entities/Webhooks/IWebhook.cs new file mode 100644 index 000000000..ef56f72b9 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Webhooks/IWebhook.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IWebhook : IDeletable, ISnowflakeEntity + { + /// Gets the token of this webhook. + string Token { get; } + + /// Gets the default name of this webhook. + string Name { get; } + /// Gets the id of this webhook's default avatar. + string AvatarId { get; } + /// Gets the url to this webhook's default avatar. + string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128); + + /// Gets the channel for this webhook. + ITextChannel Channel { get; } + /// Gets the id of the channel for this webhook. + ulong ChannelId { get; } + + /// Gets the guild owning this webhook. + IGuild Guild { get; } + /// Gets the id of the guild owning this webhook. + ulong? GuildId { get; } + + /// Gets the user that created this webhook. + IUser Creator { get; } + + /// Modifies this webhook. + Task ModifyAsync(Action func, RequestOptions options = null); + } +} diff --git a/src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs b/src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs new file mode 100644 index 000000000..8759a1729 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs @@ -0,0 +1,41 @@ +namespace Discord +{ + /// + /// Modify an with the specified parameters. + /// + /// + /// + /// await webhook.ModifyAsync(x => + /// { + /// x.Name = "Bob"; + /// x.Avatar = new Image("avatar.jpg"); + /// }); + /// + /// + /// + public class WebhookProperties + { + /// + /// The default name of the webhook. + /// + public Optional Name { get; set; } + /// + /// The default avatar of the webhook. + /// + public Optional Image { get; set; } + /// + /// The channel for this webhook. + /// + /// + /// This field is not used when authenticated with . + /// + public Optional Channel { get; set; } + /// + /// The channel id for this webhook. + /// + /// + /// This field is not used when authenticated with . + /// + public Optional ChannelId { get; set; } + } +} diff --git a/src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs b/src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs index f52edd719..345154f1d 100644 --- a/src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs +++ b/src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs @@ -1,14 +1,64 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace Discord { public static class AsyncEnumerableExtensions { - public static async Task> Flatten(this IAsyncEnumerable> source) + /// + /// Flattens the specified pages into one asynchronously + /// + /// + /// + /// + public static async Task> FlattenAsync(this IAsyncEnumerable> source) { - return (await source.ToArray().ConfigureAwait(false)).SelectMany(x => x); + return await source.Flatten().ToArray().ConfigureAwait(false); + } + + public static IAsyncEnumerable Flatten(this IAsyncEnumerable> source) + { + return new PagedCollectionEnumerator(source); + } + + internal class PagedCollectionEnumerator : IAsyncEnumerator, IAsyncEnumerable + { + readonly IAsyncEnumerator> _source; + IEnumerator _enumerator; + + public IAsyncEnumerator GetEnumerator() => this; + + internal PagedCollectionEnumerator(IAsyncEnumerable> source) + { + _source = source.GetEnumerator(); + } + + public T Current => _enumerator.Current; + + public void Dispose() + { + _enumerator?.Dispose(); + _source.Dispose(); + } + + public async Task MoveNext(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + if(!_enumerator?.MoveNext() ?? true) + { + if (!await _source.MoveNext(cancellationToken).ConfigureAwait(false)) + return false; + + _enumerator?.Dispose(); + _enumerator = _source.Current.GetEnumerator(); + return _enumerator.MoveNext(); + } + + return true; + } } } } diff --git a/src/Discord.Net.Core/IDiscordClient.cs b/src/Discord.Net.Core/IDiscordClient.cs index 23e8e9c5b..9abb959b5 100644 --- a/src/Discord.Net.Core/IDiscordClient.cs +++ b/src/Discord.Net.Core/IDiscordClient.cs @@ -34,5 +34,7 @@ namespace Discord Task> GetVoiceRegionsAsync(RequestOptions options = null); Task GetVoiceRegionAsync(string id, RequestOptions options = null); + + Task GetWebhookAsync(ulong id, RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Utils/Permissions.cs b/src/Discord.Net.Core/Utils/Permissions.cs index a7de90623..367926dd1 100644 --- a/src/Discord.Net.Core/Utils/Permissions.cs +++ b/src/Discord.Net.Core/Utils/Permissions.cs @@ -152,7 +152,7 @@ namespace Discord if (channel is ITextChannel textChannel) { - if (!GetValue(resolvedPermissions, ChannelPermission.ReadMessages)) + if (!GetValue(resolvedPermissions, ChannelPermission.ViewChannel)) { //No read permission on a text channel removes all other permissions resolvedPermissions = 0; diff --git a/src/Discord.Net.Rest/API/Common/Channel.cs b/src/Discord.Net.Rest/API/Common/Channel.cs index 608ddcf66..97c35a57b 100644 --- a/src/Discord.Net.Rest/API/Common/Channel.cs +++ b/src/Discord.Net.Rest/API/Common/Channel.cs @@ -23,6 +23,8 @@ namespace Discord.API public Optional Position { get; set; } [JsonProperty("permission_overwrites")] public Optional PermissionOverwrites { get; set; } + [JsonProperty("parent_id")] + public ulong? CategoryId { get; set; } //TextChannel [JsonProperty("topic")] diff --git a/src/Discord.Net.Rest/API/Common/Emoji.cs b/src/Discord.Net.Rest/API/Common/Emoji.cs index bd9c4d466..2bdfdcc36 100644 --- a/src/Discord.Net.Rest/API/Common/Emoji.cs +++ b/src/Discord.Net.Rest/API/Common/Emoji.cs @@ -9,6 +9,8 @@ namespace Discord.API public ulong? Id { get; set; } [JsonProperty("name")] public string Name { get; set; } + [JsonProperty("animated")] + public bool? Animated { get; set; } [JsonProperty("roles")] public ulong[] Roles { get; set; } [JsonProperty("require_colons")] diff --git a/src/Discord.Net.Rest/API/Common/Game.cs b/src/Discord.Net.Rest/API/Common/Game.cs index a499d83b0..bfb861692 100644 --- a/src/Discord.Net.Rest/API/Common/Game.cs +++ b/src/Discord.Net.Rest/API/Common/Game.cs @@ -13,6 +13,22 @@ namespace Discord.API public Optional StreamUrl { get; set; } [JsonProperty("type")] public Optional StreamType { get; set; } + [JsonProperty("details")] + public Optional Details { get; set; } + [JsonProperty("state")] + public Optional State { get; set; } + [JsonProperty("application_id")] + public Optional ApplicationId { get; set; } + [JsonProperty("assets")] + public Optional Assets { get; set; } + [JsonProperty("party")] + public Optional Party { get; set; } + [JsonProperty("secrets")] + public Optional Secrets { get; set; } + [JsonProperty("timestamps")] + public Optional Timestamps { get; set; } + [JsonProperty("instance")] + public Optional Instance { get; set; } [OnError] internal void OnError(StreamingContext context, ErrorContext errorContext) diff --git a/src/Discord.Net.Rest/API/Common/GameAssets.cs b/src/Discord.Net.Rest/API/Common/GameAssets.cs new file mode 100644 index 000000000..b5928a8ab --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/GameAssets.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class GameAssets + { + [JsonProperty("small_text")] + public Optional SmallText { get; set; } + [JsonProperty("small_image")] + public Optional SmallImage { get; set; } + [JsonProperty("large_image")] + public Optional LargeText { get; set; } + [JsonProperty("large_text")] + public Optional LargeImage { get; set; } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/API/Common/GameParty.cs b/src/Discord.Net.Rest/API/Common/GameParty.cs new file mode 100644 index 000000000..e0da4a098 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/GameParty.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class GameParty + { + [JsonProperty("id")] + public string Id { get; set; } + [JsonProperty("size")] + public int[] Size { get; set; } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/API/Common/GameSecrets.cs b/src/Discord.Net.Rest/API/Common/GameSecrets.cs new file mode 100644 index 000000000..e70b48ff0 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/GameSecrets.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class GameSecrets + { + [JsonProperty("match")] + public string Match { get; set; } + [JsonProperty("join")] + public string Join { get; set; } + [JsonProperty("spectate")] + public string Spectate { get; set; } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/API/Common/GameTimestamps.cs b/src/Discord.Net.Rest/API/Common/GameTimestamps.cs new file mode 100644 index 000000000..5c6f10b86 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/GameTimestamps.cs @@ -0,0 +1,15 @@ +using System; +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class GameTimestamps + { + [JsonProperty("start")] + [UnixTimestamp] + public Optional Start { get; set; } + [JsonProperty("end")] + [UnixTimestamp] + public Optional End { get; set; } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/API/Common/Webhook.cs b/src/Discord.Net.Rest/API/Common/Webhook.cs new file mode 100644 index 000000000..cbd5fdad5 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/Webhook.cs @@ -0,0 +1,25 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class Webhook + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + [JsonProperty("token")] + public string Token { get; set; } + + [JsonProperty("name")] + public Optional Name { get; set; } + [JsonProperty("avatar")] + public Optional Avatar { get; set; } + [JsonProperty("guild_id")] + public Optional GuildId { get; set; } + + [JsonProperty("user")] + public Optional Creator { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/CreateGuildEmoteParams.cs b/src/Discord.Net.Rest/API/Rest/CreateGuildEmoteParams.cs new file mode 100644 index 000000000..308199820 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/CreateGuildEmoteParams.cs @@ -0,0 +1,16 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class CreateGuildEmoteParams + { + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("image")] + public Image Image { get; set; } + [JsonProperty("roles")] + public Optional RoleIds { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/CreateWebhookParams.cs b/src/Discord.Net.Rest/API/Rest/CreateWebhookParams.cs new file mode 100644 index 000000000..0d1059fab --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/CreateWebhookParams.cs @@ -0,0 +1,14 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class CreateWebhookParams + { + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("avatar")] + public Optional Avatar { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs index b4add2ac9..120eeb3a8 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs @@ -10,5 +10,7 @@ namespace Discord.API.Rest public Optional Name { get; set; } [JsonProperty("position")] public Optional Position { get; set; } + [JsonProperty("parent_id")] + public Optional CategoryId { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildEmoteParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildEmoteParams.cs new file mode 100644 index 000000000..a2295dd5d --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildEmoteParams.cs @@ -0,0 +1,14 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class ModifyGuildEmoteParams + { + [JsonProperty("name")] + public Optional Name { get; set; } + [JsonProperty("roles")] + public Optional RoleIds { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/ModifyWebhookParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyWebhookParams.cs new file mode 100644 index 000000000..0f2d6e33b --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ModifyWebhookParams.cs @@ -0,0 +1,16 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class ModifyWebhookParams + { + [JsonProperty("name")] + public Optional Name { get; set; } + [JsonProperty("avatar")] + public Optional Avatar { get; set; } + [JsonProperty("channel_id")] + public Optional ChannelId { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs index f2c34c015..6d6eb29b2 100644 --- a/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs +++ b/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs @@ -1,7 +1,7 @@ #pragma warning disable CS1591 -using Discord.Net.Rest; using System.Collections.Generic; using System.IO; +using Discord.Net.Rest; namespace Discord.API.Rest { @@ -15,6 +15,7 @@ namespace Discord.API.Rest public Optional IsTTS { get; set; } public Optional Username { get; set; } public Optional AvatarUrl { get; set; } + public Optional Embeds { get; set; } public UploadWebhookFileParams(Stream file) { @@ -25,6 +26,7 @@ namespace Discord.API.Rest { var d = new Dictionary(); d["file"] = new MultipartFile(File, Filename.GetValueOrDefault("unknown.dat")); + if (Content.IsSpecified) d["content"] = Content.Value; if (IsTTS.IsSpecified) @@ -35,6 +37,8 @@ namespace Discord.API.Rest d["username"] = Username.Value; if (AvatarUrl.IsSpecified) d["avatar_url"] = AvatarUrl.Value; + if (Embeds.IsSpecified) + d["embeds"] = Embeds.Value; return d; } } diff --git a/src/Discord.Net.Rest/API/UnixTimestampAttribute.cs b/src/Discord.Net.Rest/API/UnixTimestampAttribute.cs new file mode 100644 index 000000000..3890ffc46 --- /dev/null +++ b/src/Discord.Net.Rest/API/UnixTimestampAttribute.cs @@ -0,0 +1,7 @@ +using System; + +namespace Discord.API +{ + [AttributeUsage(AttributeTargets.Property)] + internal class UnixTimestampAttribute : Attribute { } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/BaseDiscordClient.cs b/src/Discord.Net.Rest/BaseDiscordClient.cs index 47a946f20..269dedd71 100644 --- a/src/Discord.Net.Rest/BaseDiscordClient.cs +++ b/src/Discord.Net.Rest/BaseDiscordClient.cs @@ -164,6 +164,9 @@ namespace Discord.Rest Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) => Task.FromResult(null); + Task IDiscordClient.GetWebhookAsync(ulong id, RequestOptions options) + => Task.FromResult(null); + Task IDiscordClient.StartAsync() => Task.Delay(0); Task IDiscordClient.StopAsync() diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs index 2f05d5d36..5c9e26433 100644 --- a/src/Discord.Net.Rest/ClientHelper.cs +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -79,7 +79,7 @@ namespace Discord.Rest ulong? fromGuildId, int? limit, RequestOptions options) { return new PagedAsyncEnumerable( - DiscordConfig.MaxUsersPerBatch, + DiscordConfig.MaxGuildsPerBatch, async (info, ct) => { var args = new GetGuildSummariesParams @@ -106,7 +106,7 @@ namespace Discord.Rest } public static async Task> GetGuildsAsync(BaseDiscordClient client, RequestOptions options) { - var summaryModels = await GetGuildSummariesAsync(client, null, null, options).Flatten(); + var summaryModels = await GetGuildSummariesAsync(client, null, null, options).FlattenAsync().ConfigureAwait(false); var guilds = ImmutableArray.CreateBuilder(); foreach (var summaryModel in summaryModels) { @@ -144,6 +144,14 @@ namespace Discord.Rest return null; } + public static async Task GetWebhookAsync(BaseDiscordClient client, ulong id, RequestOptions options) + { + var model = await client.ApiClient.GetWebhookAsync(id); + if (model != null) + return RestWebhook.Create(client, (IGuild)null, model); + return null; + } + public static async Task> GetVoiceRegionsAsync(BaseDiscordClient client, RequestOptions options) { var models = await client.ApiClient.GetVoiceRegionsAsync(options).ConfigureAwait(false); diff --git a/src/Discord.Net.Rest/Discord.Net.Rest.csproj b/src/Discord.Net.Rest/Discord.Net.Rest.csproj index 439b7bbb1..29f79e410 100644 --- a/src/Discord.Net.Rest/Discord.Net.Rest.csproj +++ b/src/Discord.Net.Rest/Discord.Net.Rest.csproj @@ -10,7 +10,8 @@ - + + diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 6d551aa95..689cba9c3 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CS1591 #pragma warning disable CS0618 using Discord.API.Rest; using Discord.Net; @@ -52,7 +52,7 @@ namespace Discord.API _restClientProvider = restClientProvider; UserAgent = userAgent; DefaultRetryMode = defaultRetryMode; - _serializer = serializer ?? new JsonSerializer { DateFormatString = "yyyy-MM-ddTHH:mm:ssZ", ContractResolver = new DiscordContractResolver() }; + _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; RequestQueue = new RequestQueue(); _stateLock = new SemaphoreSlim(1, 1); @@ -473,7 +473,7 @@ namespace Discord.API var ids = new BucketIds(channelId: channelId); return await SendJsonAsync("POST", () => $"channels/{channelId}/messages", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } - public async Task CreateWebhookMessageAsync(ulong webhookId, CreateWebhookMessageParams args, RequestOptions options = null) + public async Task CreateWebhookMessageAsync(ulong webhookId, CreateWebhookMessageParams args, RequestOptions options = null) { if (AuthTokenType != TokenType.Webhook) throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); @@ -486,8 +486,8 @@ namespace Discord.API if (args.Content.Length > DiscordConfig.MaxMessageSize) throw new ArgumentException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); options = RequestOptions.CreateOrClone(options); - - await SendJsonAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}", args, new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + + return await SendJsonAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args, new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } public async Task UploadFileAsync(ulong channelId, UploadFileParams args, RequestOptions options = null) { @@ -503,7 +503,7 @@ namespace Discord.API var ids = new BucketIds(channelId: channelId); return await SendMultipartAsync("POST", () => $"channels/{channelId}/messages", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } - public async Task UploadWebhookFileAsync(ulong webhookId, UploadWebhookFileParams args, RequestOptions options = null) + public async Task UploadWebhookFileAsync(ulong webhookId, UploadWebhookFileParams args, RequestOptions options = null) { if (AuthTokenType != TokenType.Webhook) throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); @@ -522,7 +522,7 @@ namespace Discord.API throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); } - await SendMultipartAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}", args.ToDictionary(), new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + return await SendMultipartAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args.ToDictionary(), new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } public async Task DeleteMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) { @@ -1066,6 +1066,50 @@ namespace Discord.API return await SendJsonAsync>("PATCH", () => $"guilds/{guildId}/roles", args, ids, options: options).ConfigureAwait(false); } + //Guild emoji + public async Task GetGuildEmoteAsync(ulong guildId, ulong emoteId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(emoteId, 0, nameof(emoteId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendAsync("GET", () => $"guilds/{guildId}/emojis/{emoteId}", ids, options: options); + } + + public async Task CreateGuildEmoteAsync(ulong guildId, Rest.CreateGuildEmoteParams args, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotNullOrWhitespace(args.Name, nameof(args.Name)); + Preconditions.NotNull(args.Image.Stream, nameof(args.Image)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendJsonAsync("POST", () => $"guilds/{guildId}/emojis", args, ids, options: options); + } + + public async Task ModifyGuildEmoteAsync(ulong guildId, ulong emoteId, ModifyGuildEmoteParams args, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(emoteId, 0, nameof(emoteId)); + Preconditions.NotNull(args, nameof(args)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendJsonAsync("PATCH", () => $"guilds/{guildId}/emojis/{emoteId}", args, ids, options: options); + } + + public async Task DeleteGuildEmoteAsync(ulong guildId, ulong emoteId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(emoteId, 0, nameof(emoteId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + await SendAsync("DELETE", () => $"guilds/{guildId}/emojis/{emoteId}", ids, options: options); + } + //Users public async Task GetUserAsync(ulong userId, RequestOptions options = null) { @@ -1154,6 +1198,70 @@ namespace Discord.API return await SendAsync>("GET", () => $"guilds/{guildId}/regions", ids, options: options).ConfigureAwait(false); } + //Webhooks + public async Task CreateWebhookAsync(ulong channelId, CreateWebhookParams args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotNull(args.Name, nameof(args.Name)); + options = RequestOptions.CreateOrClone(options); + var ids = new BucketIds(channelId: channelId); + + return await SendJsonAsync("POST", () => $"channels/{channelId}/webhooks", args, ids, options: options); + } + public async Task GetWebhookAsync(ulong webhookId, RequestOptions options = null) + { + Preconditions.NotEqual(webhookId, 0, nameof(webhookId)); + options = RequestOptions.CreateOrClone(options); + + try + { + if (AuthTokenType == TokenType.Webhook) + return await SendAsync("GET", () => $"webhooks/{webhookId}/{AuthToken}", new BucketIds(), options: options).ConfigureAwait(false); + else + return await SendAsync("GET", () => $"webhooks/{webhookId}", new BucketIds(), options: options).ConfigureAwait(false); + } + catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; } + } + public async Task ModifyWebhookAsync(ulong webhookId, ModifyWebhookParams args, RequestOptions options = null) + { + Preconditions.NotEqual(webhookId, 0, nameof(webhookId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name)); + options = RequestOptions.CreateOrClone(options); + + if (AuthTokenType == TokenType.Webhook) + return await SendJsonAsync("PATCH", () => $"webhooks/{webhookId}/{AuthToken}", args, new BucketIds(), options: options).ConfigureAwait(false); + else + return await SendJsonAsync("PATCH", () => $"webhooks/{webhookId}", args, new BucketIds(), options: options).ConfigureAwait(false); + } + public async Task DeleteWebhookAsync(ulong webhookId, RequestOptions options = null) + { + Preconditions.NotEqual(webhookId, 0, nameof(webhookId)); + options = RequestOptions.CreateOrClone(options); + + if (AuthTokenType == TokenType.Webhook) + await SendAsync("DELETE", () => $"webhooks/{webhookId}/{AuthToken}", new BucketIds(), options: options).ConfigureAwait(false); + else + await SendAsync("DELETE", () => $"webhooks/{webhookId}", new BucketIds(), options: options).ConfigureAwait(false); + } + public async Task> GetGuildWebhooksAsync(ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendAsync>("GET", () => $"guilds/{guildId}/webhooks", ids, options: options).ConfigureAwait(false); + } + public async Task> GetChannelWebhooksAsync(ulong channelId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + return await SendAsync>("GET", () => $"channels/{channelId}/webhooks", ids, options: options).ConfigureAwait(false); + } + //Helpers protected void CheckState() { diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index aa9937008..3d90b6c00 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -91,6 +91,9 @@ namespace Discord.Rest /// public Task GetVoiceRegionAsync(string id, RequestOptions options = null) => ClientHelper.GetVoiceRegionAsync(this, id, options); + /// + public Task GetWebhookAsync(ulong id, RequestOptions options = null) + => ClientHelper.GetWebhookAsync(this, id, options); //IDiscordClient async Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) @@ -160,5 +163,8 @@ namespace Discord.Rest => await GetVoiceRegionsAsync(options).ConfigureAwait(false); async Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) => await GetVoiceRegionAsync(id, options).ConfigureAwait(false); + + async Task IDiscordClient.GetWebhookAsync(ulong id, RequestOptions options) + => await GetWebhookAsync(id, options); } } diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index 8dcb8c284..f4b6c7f23 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -7,19 +7,20 @@ using System.Linq; using System.Threading.Tasks; using Model = Discord.API.Channel; using UserModel = Discord.API.User; +using WebhookModel = Discord.API.Webhook; namespace Discord.Rest { internal static class ChannelHelper { //General - public static async Task DeleteAsync(IChannel channel, BaseDiscordClient client, + public static async Task DeleteAsync(IChannel channel, BaseDiscordClient client, RequestOptions options) - { + { await client.ApiClient.DeleteChannelAsync(channel.Id, options).ConfigureAwait(false); } - public static async Task ModifyAsync(IGuildChannel channel, BaseDiscordClient client, - Action func, + public static async Task ModifyAsync(IGuildChannel channel, BaseDiscordClient client, + Action func, RequestOptions options) { var args = new GuildChannelProperties(); @@ -27,12 +28,13 @@ namespace Discord.Rest var apiArgs = new API.Rest.ModifyGuildChannelParams { Name = args.Name, - Position = args.Position + Position = args.Position, + CategoryId = args.CategoryId }; return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); } - public static async Task ModifyAsync(ITextChannel channel, BaseDiscordClient client, - Action func, + public static async Task ModifyAsync(ITextChannel channel, BaseDiscordClient client, + Action func, RequestOptions options) { var args = new TextChannelProperties(); @@ -41,13 +43,14 @@ namespace Discord.Rest { Name = args.Name, Position = args.Position, + CategoryId = args.CategoryId, Topic = args.Topic, IsNsfw = args.IsNsfw }; return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); } - public static async Task ModifyAsync(IVoiceChannel channel, BaseDiscordClient client, - Action func, + public static async Task ModifyAsync(IVoiceChannel channel, BaseDiscordClient client, + Action func, RequestOptions options) { var args = new VoiceChannelProperties(); @@ -57,6 +60,7 @@ namespace Discord.Rest Bitrate = args.Bitrate, Name = args.Name, Position = args.Position, + CategoryId = args.CategoryId, UserLimit = args.UserLimit.IsSpecified ? (args.UserLimit.Value ?? 0) : Optional.Create() }; return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); @@ -86,7 +90,7 @@ namespace Discord.Rest } //Messages - public static async Task GetMessageAsync(IMessageChannel channel, BaseDiscordClient client, + public static async Task GetMessageAsync(IMessageChannel channel, BaseDiscordClient client, ulong id, RequestOptions options) { var guildId = (channel as IGuildChannel)?.GuildId; @@ -97,7 +101,7 @@ namespace Discord.Rest var author = GetAuthor(client, guild, model.Author.Value, model.WebhookId.ToNullable()); return RestMessage.Create(client, channel, author, model); } - public static IAsyncEnumerable> GetMessagesAsync(IMessageChannel channel, BaseDiscordClient client, + public static IAsyncEnumerable> GetMessagesAsync(IMessageChannel channel, BaseDiscordClient client, ulong? fromMessageId, Direction dir, int limit, RequestOptions options) { if (dir == Direction.Around) @@ -123,7 +127,7 @@ namespace Discord.Rest foreach (var model in models) { var author = GetAuthor(client, guild, model.Author.Value, model.WebhookId.ToNullable()); - builder.Add(RestMessage.Create(client, channel, author, model)); + builder.Add(RestMessage.Create(client, channel, author, model)); } return builder.ToImmutable(); }, @@ -179,26 +183,32 @@ namespace Discord.Rest var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS }; var model = await client.ApiClient.UploadFileAsync(channel.Id, args, options).ConfigureAwait(false); return RestUserMessage.Create(client, channel, client.CurrentUser, model); - } + } public static async Task DeleteMessagesAsync(ITextChannel channel, BaseDiscordClient client, IEnumerable messageIds, RequestOptions options) { + const int BATCH_SIZE = 100; + var msgs = messageIds.ToArray(); - if (msgs.Length < 100) - { - var args = new DeleteMessagesParams(msgs); - await client.ApiClient.DeleteMessagesAsync(channel.Id, args, options).ConfigureAwait(false); - } - else + int batches = msgs.Length / BATCH_SIZE; + for (int i = 0; i <= batches; i++) { - var batch = new ulong[100]; - for (int i = 0; i < (msgs.Length + 99) / 100; i++) + ArraySegment batch; + if (i < batches) + { + batch = new ArraySegment(msgs, i * BATCH_SIZE, BATCH_SIZE); + } + else { - Array.Copy(msgs, i * 100, batch, 0, Math.Min(msgs.Length - (100 * i), 100)); - var args = new DeleteMessagesParams(batch); - await client.ApiClient.DeleteMessagesAsync(channel.Id, args, options).ConfigureAwait(false); + batch = new ArraySegment(msgs, i * BATCH_SIZE, msgs.Length - batches * BATCH_SIZE); + if (batch.Count == 0) + { + break; + } } + var args = new DeleteMessagesParams(batch.ToArray()); + await client.ApiClient.DeleteMessagesAsync(channel.Id, args, options).ConfigureAwait(false); } } @@ -234,7 +244,7 @@ namespace Discord.Rest if (model == null) return null; var user = RestGuildUser.Create(client, guild, model); - if (!user.GetPermissions(channel).ReadMessages) + if (!user.GetPermissions(channel).ViewChannel) return null; return user; @@ -255,7 +265,7 @@ namespace Discord.Rest var models = await client.ApiClient.GetGuildMembersAsync(guild.Id, args, options).ConfigureAwait(false); return models .Select(x => RestGuildUser.Create(client, guild, x)) - .Where(x => x.GetPermissions(channel).ReadMessages) + .Where(x => x.GetPermissions(channel).ViewChannel) .ToImmutableArray(); }, nextPage: (info, lastPage) => @@ -276,10 +286,34 @@ namespace Discord.Rest { await client.ApiClient.TriggerTypingIndicatorAsync(channel.Id, options).ConfigureAwait(false); } - public static IDisposable EnterTypingState(IMessageChannel channel, BaseDiscordClient client, + public static IDisposable EnterTypingState(IMessageChannel channel, BaseDiscordClient client, RequestOptions options) => new TypingNotifier(client, channel, options); + //Webhooks + public static async Task CreateWebhookAsync(ITextChannel channel, BaseDiscordClient client, string name, Stream avatar, RequestOptions options) + { + var args = new CreateWebhookParams { Name = name }; + if (avatar != null) + args.Avatar = new API.Image(avatar); + + var model = await client.ApiClient.CreateWebhookAsync(channel.Id, args, options).ConfigureAwait(false); + return RestWebhook.Create(client, channel, model); + } + public static async Task GetWebhookAsync(ITextChannel channel, BaseDiscordClient client, ulong id, RequestOptions options) + { + var model = await client.ApiClient.GetWebhookAsync(id, options: options).ConfigureAwait(false); + if (model == null) + return null; + return RestWebhook.Create(client, channel, model); + } + public static async Task> GetWebhooksAsync(ITextChannel channel, BaseDiscordClient client, RequestOptions options) + { + var models = await client.ApiClient.GetChannelWebhooksAsync(channel.Id, options).ConfigureAwait(false); + return models.Select(x => RestWebhook.Create(client, channel, x)) + .ToImmutableArray(); + } + //Helpers private static IUser GetAuthor(BaseDiscordClient client, IGuild guild, UserModel model, ulong? webhookId) { diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelType.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelType.cs index f05f1598e..e9f069a50 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelType.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelType.cs @@ -5,6 +5,7 @@ Text = 0, DM = 1, Voice = 2, - Group = 3 + Group = 3, + Category = 4 } } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs new file mode 100644 index 000000000..397e14e76 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord.Rest +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestCategoryChannel : RestGuildChannel, ICategoryChannel + { + internal RestCategoryChannel(BaseDiscordClient discord, IGuild guild, ulong id) + : base(discord, guild, id) + { + } + internal new static RestCategoryChannel Create(BaseDiscordClient discord, IGuild guild, Model model) + { + var entity = new RestCategoryChannel(discord, guild, model.Id); + entity.Update(model); + return entity; + } + + private string DebuggerDisplay => $"{Name} ({Id}, Category)"; + + // IGuildChannel + IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + => throw new NotSupportedException(); + Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => throw new NotSupportedException(); + Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) + => throw new NotSupportedException(); + Task> IGuildChannel.GetInvitesAsync(RequestOptions options) + => throw new NotSupportedException(); + + //IChannel + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + => throw new NotSupportedException(); + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => throw new NotSupportedException(); + } +} diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs index 5e335446f..026d03cc8 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs @@ -16,7 +16,7 @@ namespace Discord.Rest internal IGuild Guild { get; } public string Name { get; private set; } public int Position { get; private set; } - + public ulong? CategoryId { get; private set; } public ulong GuildId => Guild.Id; internal RestGuildChannel(BaseDiscordClient discord, IGuild guild, ulong id) @@ -32,6 +32,8 @@ namespace Discord.Rest return RestTextChannel.Create(discord, guild, model); case ChannelType.Voice: return RestVoiceChannel.Create(discord, guild, model); + case ChannelType.Category: + return RestCategoryChannel.Create(discord, guild, model); default: // TODO: Channel categories return new RestGuildChannel(discord, guild, model.Id); @@ -61,7 +63,14 @@ namespace Discord.Rest } public Task DeleteAsync(RequestOptions options = null) => ChannelHelper.DeleteAsync(this, Discord, options); - + + public async Task GetCategoryAsync() + { + if (CategoryId.HasValue) + return (await Guild.GetChannelAsync(CategoryId.Value).ConfigureAwait(false)) as ICategoryChannel; + return null; + } + public OverwritePermissions? GetPermissionOverwrite(IUser user) { for (int i = 0; i < _overwrites.Length; i++) @@ -119,7 +128,7 @@ 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 = false, bool isUnique = false, RequestOptions options = null) + public async Task CreateInviteAsync(int? maxAge = 86400, 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; @@ -139,20 +148,20 @@ namespace Discord.Rest => await GetInvitesAsync(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) + + OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IRole role) => GetPermissionOverwrite(role); OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IUser user) => GetPermissionOverwrite(user); - async Task IGuildChannel.AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options) + async Task IGuildChannel.AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options) => await AddPermissionOverwriteAsync(role, permissions, options).ConfigureAwait(false); - async Task IGuildChannel.AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options) + async Task IGuildChannel.AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options) => await AddPermissionOverwriteAsync(user, permissions, options).ConfigureAwait(false); - async Task IGuildChannel.RemovePermissionOverwriteAsync(IRole role, RequestOptions options) + async Task IGuildChannel.RemovePermissionOverwriteAsync(IRole role, RequestOptions options) => await RemovePermissionOverwriteAsync(role, options).ConfigureAwait(false); - async Task IGuildChannel.RemovePermissionOverwriteAsync(IUser user, RequestOptions options) + async Task IGuildChannel.RemovePermissionOverwriteAsync(IUser user, RequestOptions options) => await RemovePermissionOverwriteAsync(user, options).ConfigureAwait(false); - + IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => AsyncEnumerable.Empty>(); //Overridden //Overridden in Text/Voice Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index 8a096302b..9c29624c1 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -77,8 +77,23 @@ namespace Discord.Rest public IDisposable EnterTypingState(RequestOptions options = null) => ChannelHelper.EnterTypingState(this, Discord, options); + public Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) + => ChannelHelper.CreateWebhookAsync(this, Discord, name, avatar, options); + public Task GetWebhookAsync(ulong id, RequestOptions options = null) + => ChannelHelper.GetWebhookAsync(this, Discord, id, options); + public Task> GetWebhooksAsync(RequestOptions options = null) + => ChannelHelper.GetWebhooksAsync(this, Discord, options); + private string DebuggerDisplay => $"{Name} ({Id}, Text)"; + //ITextChannel + async Task ITextChannel.CreateWebhookAsync(string name, Stream avatar, RequestOptions options) + => await CreateWebhookAsync(name, avatar, options); + async Task ITextChannel.GetWebhookAsync(ulong id, RequestOptions options) + => await GetWebhookAsync(id, options); + async Task> ITextChannel.GetWebhooksAsync(RequestOptions options) + => await GetWebhooksAsync(options); + //IMessageChannel async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) { diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 2fa29928c..12fdb075d 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -157,6 +157,15 @@ namespace Discord.Rest var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); return RestVoiceChannel.Create(client, guild, model); } + public static async Task CreateCategoryChannelAsync(IGuild guild, BaseDiscordClient client, + string name, RequestOptions options) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + + var args = new CreateGuildChannelParams(name, ChannelType.Category); + var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); + return RestCategoryChannel.Create(client, guild, model); + } //Integrations public static async Task> GetIntegrationsAsync(IGuild guild, BaseDiscordClient client, @@ -253,5 +262,60 @@ namespace Discord.Rest model = await client.ApiClient.BeginGuildPruneAsync(guild.Id, args, options).ConfigureAwait(false); return model.Pruned; } + + //Webhooks + public static async Task GetWebhookAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options) + { + var model = await client.ApiClient.GetWebhookAsync(id, options: options).ConfigureAwait(false); + if (model == null) + return null; + return RestWebhook.Create(client, guild, model); + } + public static async Task> GetWebhooksAsync(IGuild guild, BaseDiscordClient client, RequestOptions options) + { + var models = await client.ApiClient.GetGuildWebhooksAsync(guild.Id, options).ConfigureAwait(false); + return models.Select(x => RestWebhook.Create(client, guild, x)).ToImmutableArray(); + } + + //Emotes + public static async Task GetEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options) + { + var emote = await client.ApiClient.GetGuildEmoteAsync(guild.Id, id, options); + return emote.ToEntity(); + } + public static async Task CreateEmoteAsync(IGuild guild, BaseDiscordClient client, string name, Image image, Optional> roles, + RequestOptions options) + { + var apiargs = new CreateGuildEmoteParams + { + Name = name, + Image = image.ToModel() + }; + if (roles.IsSpecified) + apiargs.RoleIds = roles.Value?.Select(xr => xr.Id)?.ToArray(); + + var emote = await client.ApiClient.CreateGuildEmoteAsync(guild.Id, apiargs, options); + return emote.ToEntity(); + } + public static async Task ModifyEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, Action func, + RequestOptions options) + { + if (func == null) throw new ArgumentNullException(nameof(func)); + + var props = new EmoteProperties(); + func(props); + + var apiargs = new ModifyGuildEmoteParams + { + Name = props.Name + }; + if (props.Roles.IsSpecified) + apiargs.RoleIds = props.Roles.Value?.Select(xr => xr.Id)?.ToArray(); + + var emote = await client.ApiClient.ModifyGuildEmoteAsync(guild.Id, id, apiargs, options); + return emote.ToEntity(); + } + public static Task DeleteEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options) + => client.ApiClient.DeleteGuildEmoteAsync(guild.Id, id, options); } } diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index aee305951..5d12731a6 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -23,7 +23,7 @@ namespace Discord.Rest public VerificationLevel VerificationLevel { get; private set; } public MfaLevel MfaLevel { get; private set; } public DefaultMessageNotifications DefaultMessageNotifications { get; private set; } - + public ulong? AFKChannelId { get; private set; } public ulong? EmbedChannelId { get; private set; } public ulong? SystemChannelId { get; private set; } @@ -114,7 +114,7 @@ namespace Discord.Rest Update(model); } public async Task ModifyEmbedAsync(Action func, RequestOptions options = null) - { + { var model = await GuildHelper.ModifyEmbedAsync(this, Discord, func, options).ConfigureAwait(false); Update(model); } @@ -155,7 +155,7 @@ 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); @@ -176,6 +176,11 @@ namespace Discord.Rest 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> GetCategoryChannelsAsync(RequestOptions options = null) + { + var channels = await GuildHelper.GetChannelsAsync(this, Discord, options).ConfigureAwait(false); + return channels.Select(x => x as RestCategoryChannel).Where(x => x != null).ToImmutableArray(); + } public async Task GetAFKChannelAsync(RequestOptions options = null) { @@ -192,14 +197,14 @@ namespace Discord.Rest var channels = await GetTextChannelsAsync(options).ConfigureAwait(false); var user = await GetCurrentUserAsync(options).ConfigureAwait(false); return channels - .Where(c => user.GetPermissions(c).ReadMessages) + .Where(c => user.GetPermissions(c).ViewChannel) .OrderBy(c => c.Position) .FirstOrDefault(); } public async Task GetEmbedChannelAsync(RequestOptions options = null) { var embedId = EmbedChannelId; - if (embedId.HasValue) + if (embedId.HasValue) return await GuildHelper.GetChannelAsync(this, Discord, embedId.Value, options).ConfigureAwait(false); return null; } @@ -217,6 +222,8 @@ namespace Discord.Rest => GuildHelper.CreateTextChannelAsync(this, Discord, name, options); public Task CreateVoiceChannelAsync(string name, RequestOptions options = null) => GuildHelper.CreateVoiceChannelAsync(this, Discord, name, options); + public Task CreateCategoryChannelAsync(string name, RequestOptions options = null) + => GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options); //Integrations public Task> GetIntegrationsAsync(RequestOptions options = null) @@ -236,7 +243,7 @@ namespace Discord.Rest return null; } - public async Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), + public async Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), bool isHoisted = false, RequestOptions options = null) { var role = await GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, options).ConfigureAwait(false); @@ -257,9 +264,25 @@ namespace Discord.Rest public Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null) => GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options); + //Webhooks + public Task GetWebhookAsync(ulong id, RequestOptions options = null) + => GuildHelper.GetWebhookAsync(this, Discord, id, options); + public Task> GetWebhooksAsync(RequestOptions options = null) + => GuildHelper.GetWebhooksAsync(this, Discord, options); + public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id})"; + //Emotes + public Task GetEmoteAsync(ulong id, RequestOptions options = null) + => GuildHelper.GetEmoteAsync(this, Discord, id, options); + public Task CreateEmoteAsync(string name, Image image, Optional> roles = default(Optional>), RequestOptions options = null) + => GuildHelper.CreateEmoteAsync(this, Discord, name, image, roles, options); + public Task ModifyEmoteAsync(GuildEmote emote, Action func, RequestOptions options = null) + => GuildHelper.ModifyEmoteAsync(this, Discord, emote.Id, func, options); + public Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null) + => GuildHelper.DeleteEmoteAsync(this, Discord, emote.Id, options); + //IGuild bool IGuild.Available => Available; IAudioClient IGuild.AudioClient => null; @@ -304,6 +327,13 @@ namespace Discord.Rest else return ImmutableArray.Create(); } + async Task> IGuild.GetCategoriesAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetCategoryChannelsAsync(options).ConfigureAwait(false); + else + return null; + } async Task IGuild.GetVoiceChannelAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -343,6 +373,8 @@ namespace Discord.Rest => await CreateTextChannelAsync(name, options).ConfigureAwait(false); async Task IGuild.CreateVoiceChannelAsync(string name, RequestOptions options) => await CreateVoiceChannelAsync(name, options).ConfigureAwait(false); + async Task IGuild.CreateCategoryAsync(string name, RequestOptions options) + => await CreateCategoryChannelAsync(name, options).ConfigureAwait(false); async Task> IGuild.GetIntegrationsAsync(RequestOptions options) => await GetIntegrationsAsync(options).ConfigureAwait(false); @@ -352,7 +384,7 @@ namespace Discord.Rest async Task> IGuild.GetInvitesAsync(RequestOptions options) => await GetInvitesAsync(options).ConfigureAwait(false); - IRole IGuild.GetRole(ulong id) + IRole IGuild.GetRole(ulong id) => GetRole(id); async Task IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted, RequestOptions options) => await CreateRoleAsync(name, permissions, color, isHoisted, options).ConfigureAwait(false); @@ -381,10 +413,15 @@ namespace Discord.Rest async Task> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) - return (await GetUsersAsync(options).Flatten().ConfigureAwait(false)).ToImmutableArray(); + return (await GetUsersAsync(options).FlattenAsync().ConfigureAwait(false)).ToImmutableArray(); else return ImmutableArray.Create(); } Task IGuild.DownloadUsersAsync() { throw new NotSupportedException(); } + + async Task IGuild.GetWebhookAsync(ulong id, RequestOptions options) + => await GetWebhookAsync(id, options); + async Task> IGuild.GetWebhooksAsync(RequestOptions options) + => await GetWebhooksAsync(options); } } diff --git a/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs b/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs index 05c817935..6d3f72419 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs @@ -18,7 +18,7 @@ namespace Discord.Rest { IEmote emote; if (model.Emoji.Id.HasValue) - emote = new Emote(model.Emoji.Id.Value, model.Emoji.Name); + emote = new Emote(model.Emoji.Id.Value, model.Emoji.Name, model.Emoji.Animated.GetValueOrDefault()); else emote = new Emoji(model.Emoji.Name); return new RestReaction(emote, model.Count, model.Me); diff --git a/src/Discord.Net.Rest/Entities/Users/RestUser.cs b/src/Discord.Net.Rest/Entities/Users/RestUser.cs index d8ade3a6b..c6cf6103a 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -16,7 +16,7 @@ namespace Discord.Rest public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); - public virtual Game? Game => null; + public virtual IActivity Activity => null; public virtual UserStatus Status => UserStatus.Offline; public virtual bool IsWebhook => false; diff --git a/src/Discord.Net.Rest/Entities/Users/UserHelper.cs b/src/Discord.Net.Rest/Entities/Users/UserHelper.cs index 562cfaae8..dfb81ff2c 100644 --- a/src/Discord.Net.Rest/Entities/Users/UserHelper.cs +++ b/src/Discord.Net.Rest/Entities/Users/UserHelper.cs @@ -48,6 +48,14 @@ namespace Discord.Rest else if (args.RoleIds.IsSpecified) apiArgs.RoleIds = args.RoleIds.Value.ToArray(); + /* + * Ensure that the nick passed in the params of the request is not null. + * string.Empty ("") is the only way to reset the user nick in the API, + * a value of null does not. This is a workaround. + */ + if (apiArgs.Nickname.IsSpecified && apiArgs.Nickname.Value == null) + apiArgs.Nickname = new Optional(string.Empty); + await client.ApiClient.ModifyGuildMemberAsync(user.GuildId, user.Id, apiArgs, options).ConfigureAwait(false); return args; } diff --git a/src/Discord.Net.Rest/Entities/Webhooks/RestWebhook.cs b/src/Discord.Net.Rest/Entities/Webhooks/RestWebhook.cs new file mode 100644 index 000000000..47cc50a9c --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Webhooks/RestWebhook.cs @@ -0,0 +1,91 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.Webhook; + +namespace Discord.Rest +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestWebhook : RestEntity, IWebhook, IUpdateable + { + internal IGuild Guild { get; private set; } + internal ITextChannel Channel { get; private set; } + + public ulong ChannelId { get; } + public string Token { get; } + + public string Name { get; private set; } + public string AvatarId { get; private set; } + public ulong? GuildId { get; private set; } + public IUser Creator { get; private set; } + + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + + internal RestWebhook(BaseDiscordClient discord, IGuild guild, ulong id, string token, ulong channelId) + : base(discord, id) + { + Guild = guild; + Token = token; + ChannelId = channelId; + } + internal RestWebhook(BaseDiscordClient discord, ITextChannel channel, ulong id, string token, ulong channelId) + : this(discord, channel.Guild, id, token, channelId) + { + Channel = channel; + } + + internal static RestWebhook Create(BaseDiscordClient discord, IGuild guild, Model model) + { + var entity = new RestWebhook(discord, guild, model.Id, model.Token, model.ChannelId); + entity.Update(model); + return entity; + } + internal static RestWebhook Create(BaseDiscordClient discord, ITextChannel channel, Model model) + { + var entity = new RestWebhook(discord, channel, model.Id, model.Token, model.ChannelId); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + if (model.Avatar.IsSpecified) + AvatarId = model.Avatar.Value; + if (model.Creator.IsSpecified) + Creator = RestUser.Create(Discord, model.Creator.Value); + if (model.GuildId.IsSpecified) + GuildId = model.GuildId.Value; + if (model.Name.IsSpecified) + Name = model.Name.Value; + } + + public async Task UpdateAsync(RequestOptions options = null) + { + var model = await Discord.ApiClient.GetWebhookAsync(Id, options).ConfigureAwait(false); + Update(model); + } + + public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + + public async Task ModifyAsync(Action func, RequestOptions options = null) + { + var model = await WebhookHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); + Update(model); + } + + public Task DeleteAsync(RequestOptions options = null) + => WebhookHelper.DeleteAsync(this, Discord, options); + + public override string ToString() => $"Webhook: {Name}:{Id}"; + private string DebuggerDisplay => $"Webhook: {Name} ({Id})"; + + //IWebhook + IGuild IWebhook.Guild + => Guild ?? throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); + ITextChannel IWebhook.Channel + => Channel ?? throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); + Task IWebhook.ModifyAsync(Action func, RequestOptions options) + => ModifyAsync(func, options); + } +} diff --git a/src/Discord.Net.Rest/Entities/Webhooks/WebhookHelper.cs b/src/Discord.Net.Rest/Entities/Webhooks/WebhookHelper.cs new file mode 100644 index 000000000..50e9cab78 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Webhooks/WebhookHelper.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading.Tasks; +using Discord.API.Rest; +using ImageModel = Discord.API.Image; +using Model = Discord.API.Webhook; + +namespace Discord.Rest +{ + internal static class WebhookHelper + { + public static async Task ModifyAsync(IWebhook webhook, BaseDiscordClient client, + Action func, RequestOptions options) + { + var args = new WebhookProperties(); + func(args); + var apiArgs = new ModifyWebhookParams + { + Avatar = args.Image.IsSpecified ? args.Image.Value?.ToModel() : Optional.Create(), + Name = args.Name + }; + + if (!apiArgs.Avatar.IsSpecified && webhook.AvatarId != null) + apiArgs.Avatar = new ImageModel(webhook.AvatarId); + + if (args.Channel.IsSpecified) + apiArgs.ChannelId = args.Channel.Value.Id; + else if (args.ChannelId.IsSpecified) + apiArgs.ChannelId = args.ChannelId.Value; + + return await client.ApiClient.ModifyWebhookAsync(webhook.Id, apiArgs, options).ConfigureAwait(false); + } + public static async Task DeleteAsync(IWebhook webhook, BaseDiscordClient client, RequestOptions options) + { + await client.ApiClient.DeleteWebhookAsync(webhook.Id, options).ConfigureAwait(false); + } + + } +} diff --git a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs index b88a5b515..74b05dacd 100644 --- a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs @@ -7,7 +7,7 @@ namespace Discord.Rest { public static GuildEmote ToEntity(this API.Emoji model) { - return new GuildEmote(model.Id.Value, model.Name, model.Managed, model.RequireColons, ImmutableArray.Create(model.Roles)); + return new GuildEmote(model.Id.Value, model.Name, model.Animated.GetValueOrDefault(), model.Managed, model.RequireColons, ImmutableArray.Create(model.Roles)); } public static Embed ToEntity(this API.Embed model) diff --git a/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs b/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs index b465fbed2..9213c5d75 100644 --- a/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs +++ b/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs @@ -66,6 +66,12 @@ namespace Discord.Net.Converters if (type == typeof(ulong)) return UInt64Converter.Instance; } + bool hasUnixStamp = propInfo.GetCustomAttribute() != null; + if (hasUnixStamp) + { + if (type == typeof(DateTimeOffset)) + return UnixTimestampConverter.Instance; + } //Enums if (type == typeof(PermissionTarget)) diff --git a/src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs b/src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs new file mode 100644 index 000000000..d4660dc44 --- /dev/null +++ b/src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs @@ -0,0 +1,28 @@ +using System; +using Newtonsoft.Json; + +namespace Discord.Net.Converters +{ + public class UnixTimestampConverter : JsonConverter + { + public static readonly UnixTimestampConverter Instance = new UnixTimestampConverter(); + + 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) + { + // Discord doesn't validate if timestamps contain decimals or not + if (reader.Value is double d) + return new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, TimeSpan.Zero).AddMilliseconds(d); + long offset = (long)reader.Value; + return new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, TimeSpan.Zero).AddMilliseconds(offset); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/Net/DefaultRestClient.cs b/src/Discord.Net.Rest/Net/DefaultRestClient.cs index a54107829..637099fd6 100644 --- a/src/Discord.Net.Rest/Net/DefaultRestClient.cs +++ b/src/Discord.Net.Rest/Net/DefaultRestClient.cs @@ -22,7 +22,7 @@ namespace Discord.Net.Rest private CancellationToken _cancelToken; private bool _isDisposed; - public DefaultRestClient(string baseUrl) + public DefaultRestClient(string baseUrl, bool useProxy = false) { _baseUrl = baseUrl; @@ -30,7 +30,7 @@ namespace Discord.Net.Rest { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, UseCookies = false, - UseProxy = false + UseProxy = useProxy, }); SetHeader("accept-encoding", "gzip, deflate"); diff --git a/src/Discord.Net.Rest/Net/DefaultRestClientProvider.cs b/src/Discord.Net.Rest/Net/DefaultRestClientProvider.cs index 311a53562..e0e776549 100644 --- a/src/Discord.Net.Rest/Net/DefaultRestClientProvider.cs +++ b/src/Discord.Net.Rest/Net/DefaultRestClientProvider.cs @@ -4,16 +4,21 @@ namespace Discord.Net.Rest { public static class DefaultRestClientProvider { - public static readonly RestClientProvider Instance = url => + public static readonly RestClientProvider Instance = Create(); + + public static RestClientProvider Create(bool useProxy = false) { - try - { - return new DefaultRestClient(url); - } - catch (PlatformNotSupportedException ex) + return url => { - throw new PlatformNotSupportedException("The default RestClientProvider is not supported on this platform.", ex); - } - }; + try + { + return new DefaultRestClient(url, useProxy); + } + catch (PlatformNotSupportedException ex) + { + throw new PlatformNotSupportedException("The default RestClientProvider is not supported on this platform.", ex); + } + }; + } } } diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcCategoryChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcCategoryChannel.cs new file mode 100644 index 000000000..cac766f92 --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcCategoryChannel.cs @@ -0,0 +1,36 @@ +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Rpc.Channel; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RpcCategoryChannel : RpcGuildChannel, ICategoryChannel + { + public IReadOnlyCollection CachedMessages { get; private set; } + + public string Mention => MentionUtils.MentionChannel(Id); + + internal RpcCategoryChannel(DiscordRpcClient discord, ulong id, ulong guildId) + : base(discord, id, guildId) + { + } + internal new static RpcCategoryChannel Create(DiscordRpcClient discord, Model model) + { + var entity = new RpcCategoryChannel(discord, model.Id, model.GuildId.Value); + entity.Update(model); + return entity; + } + internal override void Update(Model model) + { + base.Update(model); + CachedMessages = model.Messages.Select(x => RpcMessage.Create(Discord, Id, x)).ToImmutableArray(); + } + } +} diff --git a/src/Discord.Net.WebSocket/API/Gateway/WebhookUpdateEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/WebhookUpdateEvent.cs new file mode 100644 index 000000000..e5c7afe41 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Gateway/WebhookUpdateEvent.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class WebhookUpdateEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.cs b/src/Discord.Net.WebSocket/BaseSocketClient.cs index d248285cd..2ab244aeb 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.cs @@ -13,7 +13,7 @@ namespace Discord.WebSocket /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. public abstract int Latency { get; protected set; } public abstract UserStatus Status { get; protected set; } - public abstract Game? Game { get; protected set; } + public abstract IActivity Activity { get; protected set; } internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; @@ -45,6 +45,7 @@ namespace Discord.WebSocket public abstract Task StopAsync(); public abstract Task SetStatusAsync(UserStatus status); public abstract Task SetGameAsync(string name, string streamUrl = null, StreamType streamType = StreamType.NotStreaming); + public abstract Task SetActivityAsync(IActivity activity); public abstract Task DownloadUsersAsync(IEnumerable guilds); /// diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index 6c2a0f3b9..4e99ae28d 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -1,4 +1,4 @@ -using Discord.API; +using Discord.API; using Discord.Rest; using System; using System.Collections.Generic; @@ -22,7 +22,7 @@ namespace Discord.WebSocket /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. public override int Latency { get => GetLatency(); protected set { } } public override UserStatus Status { get => _shards[0].Status; protected set { } } - public override Game? Game { get => _shards[0].Game; protected set { } } + public override IActivity Activity { get => _shards[0].Activity; protected set { } } internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; public override IReadOnlyCollection Guilds => GetGuilds().ToReadOnlyCollection(() => GetGuildCount()); @@ -133,7 +133,7 @@ namespace Discord.WebSocket private DiscordSocketClient GetShardFor(ulong guildId) => GetShard(GetShardIdFor(guildId)); public DiscordSocketClient GetShardFor(IGuild guild) - => GetShardFor(guild.Id); + => GetShardFor(guild?.Id ?? 0); /// public override async Task GetApplicationInfoAsync(RequestOptions options = null) @@ -239,9 +239,18 @@ namespace Discord.WebSocket await _shards[i].SetStatusAsync(status).ConfigureAwait(false); } public override async Task SetGameAsync(string name, string streamUrl = null, StreamType streamType = StreamType.NotStreaming) + { + IActivity activity = null; + if (streamUrl != null) + activity = new StreamingGame(name, streamUrl, streamType); + else if (name != null) + activity = new Game(name); + await SetActivityAsync(activity).ConfigureAwait(false); + } + public override async Task SetActivityAsync(IActivity activity) { for (int i = 0; i < _shards.Length; i++) - await _shards[i].SetGameAsync(name, streamUrl, streamType).ConfigureAwait(false); + await _shards[i].SetActivityAsync(activity).ConfigureAwait(false); } private void RegisterEvents(DiscordSocketClient client, bool isPrimary) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index d152bbc03..cb3f23c57 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS0618 +#pragma warning disable CS0618 using Discord.API; using Discord.API.Gateway; using Discord.Logging; @@ -48,7 +48,7 @@ namespace Discord.WebSocket /// public override int Latency { get; protected set; } public override UserStatus Status { get; protected set; } = UserStatus.Online; - public override Game? Game { get; protected set; } + public override IActivity Activity { get; protected set; } //From DiscordSocketConfig internal int TotalShards { get; private set; } @@ -328,33 +328,42 @@ namespace Discord.WebSocket } public override async Task SetGameAsync(string name, string streamUrl = null, StreamType streamType = StreamType.NotStreaming) { - if (name != null) - Game = new Game(name, streamUrl, streamType); + if (!string.IsNullOrEmpty(streamUrl)) + Activity = new StreamingGame(name, streamUrl, streamType); + else if (!string.IsNullOrEmpty(name)) + Activity = new Game(name); else - Game = null; + Activity = null; await SendStatusAsync().ConfigureAwait(false); } + public override async Task SetActivityAsync(IActivity activity) + { + Activity = activity; + await SendStatusAsync().ConfigureAwait(false); + } + private async Task SendStatusAsync() { if (CurrentUser == null) return; - var game = Game; var status = Status; var statusSince = _statusSince; - CurrentUser.Presence = new SocketPresence(status, game); + CurrentUser.Presence = new SocketPresence(status, Activity); - GameModel gameModel; - if (game != null) + var gameModel = new GameModel(); + // Discord only accepts rich presence over RPC, don't even bother building a payload + if (Activity is RichGame game) + throw new NotSupportedException("Outgoing Rich Presences are not supported"); + else if (Activity is StreamingGame stream) { - gameModel = new API.Game - { - Name = game.Value.Name, - StreamType = game.Value.StreamType, - StreamUrl = game.Value.StreamUrl - }; + gameModel.StreamUrl = stream.Url; + gameModel.StreamType = stream.StreamType; + } + else if (Activity != null) + { + gameModel.Name = Activity.Name; + gameModel.StreamType = StreamType.NotStreaming; } - else - gameModel = null; await ApiClient.SendStatusUpdateAsync( status, diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs new file mode 100644 index 000000000..d5a183b1e --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Discord.Audio; +using Discord.Rest; +using Model = Discord.API.Channel; + +namespace Discord.WebSocket +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketCategoryChannel : SocketGuildChannel, ICategoryChannel + { + public override IReadOnlyCollection Users + => Guild.Users.Where(x => x.VoiceChannel?.Id == Id).ToImmutableArray(); + + public IReadOnlyCollection Channels + => Guild.Channels.Where(x => x.CategoryId == CategoryId).ToImmutableArray(); + + internal SocketCategoryChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) + : base(discord, id, guild) + { + } + internal new static SocketCategoryChannel Create(SocketGuild guild, ClientState state, Model model) + { + var entity = new SocketCategoryChannel(guild.Discord, model.Id, guild); + entity.Update(state, model); + return entity; + } + + private string DebuggerDisplay => $"{Name} ({Id}, Category)"; + internal new SocketCategoryChannel Clone() => MemberwiseClone() as SocketCategoryChannel; + + // IGuildChannel + IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + => throw new NotSupportedException(); + Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => throw new NotSupportedException(); + Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) + => throw new NotSupportedException(); + Task> IGuildChannel.GetInvitesAsync(RequestOptions options) + => throw new NotSupportedException(); + + //IChannel + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + => throw new NotSupportedException(); + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => throw new NotSupportedException(); + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs index 16453b9fb..2163daf55 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -17,6 +17,9 @@ namespace Discord.WebSocket public SocketGuild Guild { get; } public string Name { get; private set; } public int Position { get; private set; } + public ulong? CategoryId { get; private set; } + public ICategoryChannel Category + => CategoryId.HasValue ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel : null; public IReadOnlyCollection PermissionOverwrites => _overwrites; public new virtual IReadOnlyCollection Users => ImmutableArray.Create(); @@ -34,6 +37,8 @@ namespace Discord.WebSocket return SocketTextChannel.Create(guild, state, model); case ChannelType.Voice: return SocketVoiceChannel.Create(guild, state, model); + case ChannelType.Category: + return SocketCategoryChannel.Create(guild, state, model); default: // TODO: Proper implementation for channel categories return new SocketGuildChannel(guild.Discord, model.Id, guild); @@ -43,6 +48,7 @@ namespace Discord.WebSocket { Name = model.Name.Value; Position = model.Position.Value; + CategoryId = model.CategoryId; var overwrites = model.PermissionOverwrites.Value; var newOverwrites = ImmutableArray.CreateBuilder(overwrites.Length); @@ -113,7 +119,7 @@ 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 = false, bool isUnique = false, RequestOptions options = null) + public async Task CreateInviteAsync(int? maxAge = 86400, 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 virtual SocketGuildUser GetUser(ulong id) => null; @@ -129,6 +135,9 @@ namespace Discord.WebSocket IGuild IGuildChannel.Guild => Guild; ulong IGuildChannel.GuildId => Guild.Id; + Task IGuildChannel.GetCategoryAsync() + => Task.FromResult(Category); + async Task> IGuildChannel.GetInvitesAsync(RequestOptions options) => await GetInvitesAsync(options).ConfigureAwait(false); async Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index 07ec630d3..7b8f572d2 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -25,7 +25,7 @@ namespace Discord.WebSocket public override IReadOnlyCollection Users => Guild.Users.Where(x => Permissions.GetValue( Permissions.ResolveChannel(Guild, x, this, Permissions.ResolveGuild(Guild, x)), - ChannelPermission.ReadMessages)).ToImmutableArray(); + ChannelPermission.ViewChannel)).ToImmutableArray(); internal SocketTextChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) : base(discord, id, guild) @@ -107,15 +107,31 @@ namespace Discord.WebSocket { var guildPerms = Permissions.ResolveGuild(Guild, user); var channelPerms = Permissions.ResolveChannel(Guild, user, this, guildPerms); - if (Permissions.GetValue(channelPerms, ChannelPermission.ReadMessages)) + if (Permissions.GetValue(channelPerms, ChannelPermission.ViewChannel)) return user; } return null; } - + + //Webhooks + public Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) + => ChannelHelper.CreateWebhookAsync(this, Discord, name, avatar, options); + public Task GetWebhookAsync(ulong id, RequestOptions options = null) + => ChannelHelper.GetWebhookAsync(this, Discord, id, options); + public Task> GetWebhooksAsync(RequestOptions options = null) + => ChannelHelper.GetWebhooksAsync(this, Discord, options); + private string DebuggerDisplay => $"{Name} ({Id}, Text)"; internal new SocketTextChannel Clone() => MemberwiseClone() as SocketTextChannel; + //ITextChannel + async Task ITextChannel.CreateWebhookAsync(string name, Stream avatar, RequestOptions options) + => await CreateWebhookAsync(name, avatar, options); + async Task ITextChannel.GetWebhookAsync(ulong id, RequestOptions options) + => await GetWebhookAsync(id, options); + async Task> ITextChannel.GetWebhooksAsync(RequestOptions options) + => await GetWebhooksAsync(options); + //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 6001e4799..ea68a8f54 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -75,7 +75,7 @@ namespace Discord.WebSocket return id.HasValue ? GetVoiceChannel(id.Value) : null; } } - public SocketGuildChannel EmbedChannel + public SocketGuildChannel EmbedChannel { get { @@ -95,6 +95,8 @@ namespace Discord.WebSocket => 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 IReadOnlyCollection CategoryChannels + => Channels.Select(x => x as SocketCategoryChannel).Where(x => x != null).ToImmutableArray(); public SocketGuildUser CurrentUser => _members.TryGetValue(Discord.CurrentUser.Id, out SocketGuildUser member) ? member : null; public SocketRole EveryoneRole => GetRole(Id); public IReadOnlyCollection Channels @@ -317,6 +319,9 @@ namespace Discord.WebSocket => GuildHelper.CreateTextChannelAsync(this, Discord, name, options); public Task CreateVoiceChannelAsync(string name, RequestOptions options = null) => GuildHelper.CreateVoiceChannelAsync(this, Discord, name, options); + public Task CreateCategoryChannelAsync(string name, RequestOptions options = null) + => GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options); + internal SocketGuildChannel AddChannel(ClientState state, ChannelModel model) { var channel = SocketGuildChannel.Create(this, state, model); @@ -348,7 +353,7 @@ namespace Discord.WebSocket return value; return null; } - public Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), + public Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), bool isHoisted = false, RequestOptions options = null) => GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, options); internal SocketRole AddRole(RoleModel model) @@ -433,6 +438,22 @@ namespace Discord.WebSocket _downloaderPromise.TrySetResultAsync(true); } + //Webhooks + public Task GetWebhookAsync(ulong id, RequestOptions options = null) + => GuildHelper.GetWebhookAsync(this, Discord, id, options); + public Task> GetWebhooksAsync(RequestOptions options = null) + => GuildHelper.GetWebhooksAsync(this, Discord, options); + + //Emotes + public Task GetEmoteAsync(ulong id, RequestOptions options = null) + => GuildHelper.GetEmoteAsync(this, Discord, id, options); + public Task CreateEmoteAsync(string name, Image image, Optional> roles = default(Optional>), RequestOptions options = null) + => GuildHelper.CreateEmoteAsync(this, Discord, name, image, roles, options); + public Task ModifyEmoteAsync(GuildEmote emote, Action func, RequestOptions options = null) + => GuildHelper.ModifyEmoteAsync(this, Discord, emote.Id, func, options); + public Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null) + => GuildHelper.DeleteEmoteAsync(this, Discord, emote.Id, options); + //Voice States internal async Task AddOrUpdateVoiceStateAsync(ClientState state, VoiceStateModel model) { @@ -578,7 +599,7 @@ namespace Discord.WebSocket try { await RepopulateAudioStreamsAsync().ConfigureAwait(false); - await _audioClient.StartAsync(url, Discord.CurrentUser.Id, voiceState.VoiceSessionId, token).ConfigureAwait(false); + await _audioClient.StartAsync(url, Discord.CurrentUser.Id, voiceState.VoiceSessionId, token).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -635,6 +656,8 @@ namespace Discord.WebSocket => Task.FromResult(GetTextChannel(id)); Task> IGuild.GetVoiceChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(VoiceChannels); + Task> IGuild.GetCategoriesAsync(CacheMode mode , RequestOptions options) + => Task.FromResult>(CategoryChannels); Task IGuild.GetVoiceChannelAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetVoiceChannel(id)); Task IGuild.GetAFKChannelAsync(CacheMode mode, RequestOptions options) @@ -649,6 +672,8 @@ namespace Discord.WebSocket => await CreateTextChannelAsync(name, options).ConfigureAwait(false); async Task IGuild.CreateVoiceChannelAsync(string name, RequestOptions options) => await CreateVoiceChannelAsync(name, options).ConfigureAwait(false); + async Task IGuild.CreateCategoryAsync(string name, RequestOptions options) + => await CreateCategoryChannelAsync(name, options).ConfigureAwait(false); async Task> IGuild.GetIntegrationsAsync(RequestOptions options) => await GetIntegrationsAsync(options).ConfigureAwait(false); @@ -672,5 +697,10 @@ namespace Discord.WebSocket Task IGuild.GetOwnerAsync(CacheMode mode, RequestOptions options) => Task.FromResult(Owner); Task IGuild.DownloadUsersAsync() { throw new NotSupportedException(); } + + async Task IGuild.GetWebhookAsync(ulong id, RequestOptions options) + => await GetWebhookAsync(id, options); + async Task> IGuild.GetWebhooksAsync(RequestOptions options) + => await GetWebhooksAsync(options); } } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs index 35bee9e68..e8fa17a35 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs @@ -24,7 +24,7 @@ namespace Discord.WebSocket { IEmote emote; if (model.Emoji.Id.HasValue) - emote = new Emote(model.Emoji.Id.Value, model.Emoji.Name); + emote = new Emote(model.Emoji.Id.Value, model.Emoji.Name, model.Emoji.Animated.GetValueOrDefault()); else emote = new Emoji(model.Emoji.Name); return new SocketReaction(channel, model.MessageId, message, model.UserId, user, emote); diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs index 00d4b4bbc..7d7ba16ce 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs @@ -8,20 +8,20 @@ namespace Discord.WebSocket public struct SocketPresence : IPresence { public UserStatus Status { get; } - public Game? Game { get; } + public IActivity Activity { get; } - internal SocketPresence(UserStatus status, Game? game) + internal SocketPresence(UserStatus status, IActivity activity) { Status = status; - Game = game; + Activity= activity; } internal static SocketPresence Create(Model model) { - return new SocketPresence(model.Status, model.Game != null ? model.Game.ToEntity() : (Game?)null); + return new SocketPresence(model.Status, model.Game?.ToEntity()); } public override string ToString() => Status.ToString(); - private string DebuggerDisplay => $"{Status}{(Game != null ? $", {Game.Value.Name} ({Game.Value.StreamType})" : "")}"; + private string DebuggerDisplay => $"{Status}{(Activity != null ? $", {Activity.Name}": "")}"; internal SocketPresence Clone() => this; } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index a0c78b93f..58d5c62a1 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -18,7 +18,7 @@ namespace Discord.WebSocket public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); - public Game? Game => Presence.Game; + public IActivity Activity => Presence.Activity; public UserStatus Status => Presence.Status; internal SocketUser(DiscordSocketClient discord, ulong id) diff --git a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs index 636ef68f4..c66163610 100644 --- a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs @@ -2,11 +2,83 @@ { internal static class EntityExtensions { - public static Game ToEntity(this API.Game model) + public static IActivity ToEntity(this API.Game model) { - return new Game(model.Name, - model.StreamUrl.GetValueOrDefault(null), - model.StreamType.GetValueOrDefault(null) ?? StreamType.NotStreaming); + // Rich Game + if (model.ApplicationId.IsSpecified) + { + ulong appId = model.ApplicationId.Value; + var assets = model.Assets.GetValueOrDefault()?.ToEntity(appId); + return new RichGame + { + ApplicationId = appId, + Name = model.Name, + Details = model.Details.GetValueOrDefault(), + State = model.State.GetValueOrDefault(), + SmallAsset = assets?[0], + LargeAsset = assets?[1], + Party = model.Party.IsSpecified ? model.Party.Value.ToEntity() : null, + Secrets = model.Secrets.IsSpecified ? model.Secrets.Value.ToEntity() : null, + Timestamps = model.Timestamps.IsSpecified ? model.Timestamps.Value.ToEntity() : null + }; + } + // Stream Game + if (model.StreamUrl.IsSpecified) + { + return new StreamingGame( + model.Name, + model.StreamUrl.Value, + model.StreamType.Value.GetValueOrDefault()); + } + // Normal Game + return new Game(model.Name); + } + + // (Small, Large) + public static GameAsset[] ToEntity(this API.GameAssets model, ulong appId) + { + return new GameAsset[] + { + model.SmallImage.IsSpecified ? new GameAsset + { + ApplicationId = appId, + ImageId = model.SmallImage.GetValueOrDefault(), + Text = model.SmallText.GetValueOrDefault() + } : null, + model.LargeImage.IsSpecified ? new GameAsset + { + ApplicationId = appId, + ImageId = model.LargeImage.GetValueOrDefault(), + Text = model.LargeText.GetValueOrDefault() + } : null, + }; + } + + public static GameParty ToEntity(this API.GameParty model) + { + // Discord will probably send bad data since they don't validate anything + int current = 0, cap = 0; + if (model.Size?.Length == 2) + { + current = model.Size[0]; + cap = model.Size[1]; + } + return new GameParty + { + Id = model.Id, + Members = current, + Capacity = cap, + }; + } + + public static GameSecrets ToEntity(this API.GameSecrets model) + { + return new GameSecrets(model.Match, model.Join, model.Spectate); + } + + public static GameTimestamps ToEntity(this API.GameTimestamps model) + { + return new GameTimestamps(model.Start.ToNullable(), model.End.ToNullable()); } } } diff --git a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs index 282ae210a..a250acec9 100644 --- a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs +++ b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; +using System.Net; using System.Net.WebSockets; using System.Text; using System.Threading; @@ -23,18 +24,20 @@ namespace Discord.Net.WebSockets private readonly SemaphoreSlim _lock; private readonly Dictionary _headers; private ClientWebSocket _client; + private IWebProxy _proxy; private Task _task; private CancellationTokenSource _cancelTokenSource; private CancellationToken _cancelToken, _parentToken; private bool _isDisposed, _isDisconnecting; - public DefaultWebSocketClient() + public DefaultWebSocketClient(IWebProxy proxy = null) { _lock = new SemaphoreSlim(1, 1); _cancelTokenSource = new CancellationTokenSource(); _cancelToken = CancellationToken.None; _parentToken = CancellationToken.None; _headers = new Dictionary(); + _proxy = proxy; } private void Dispose(bool disposing) { @@ -70,7 +73,7 @@ namespace Discord.Net.WebSockets _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; _client = new ClientWebSocket(); - _client.Options.Proxy = null; + _client.Options.Proxy = _proxy; _client.Options.KeepAliveInterval = TimeSpan.Zero; foreach (var header in _headers) { diff --git a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs index 04b3f8388..68bd67c5b 100644 --- a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs +++ b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs @@ -1,21 +1,27 @@ using System; +using System.Net; namespace Discord.Net.WebSockets { public static class DefaultWebSocketProvider { #if DEFAULTWEBSOCKET - public static readonly WebSocketProvider Instance = () => + public static readonly WebSocketProvider Instance = Create(); + + public static WebSocketProvider Create(IWebProxy proxy = null) { - try - { - return new DefaultWebSocketClient(); - } - catch (PlatformNotSupportedException ex) + return () => { - throw new PlatformNotSupportedException("The default WebSocketProvider is not supported on this platform.", ex); - } - }; + try + { + return new DefaultWebSocketClient(proxy); + } + catch (PlatformNotSupportedException ex) + { + throw new PlatformNotSupportedException("The default WebSocketProvider is not supported on this platform.", ex); + } + }; + } #else public static readonly WebSocketProvider Instance = () => { diff --git a/src/Discord.Net.Webhook/DiscordWebhookClient.cs b/src/Discord.Net.Webhook/DiscordWebhookClient.cs index 3d8307da4..59cc8f3e7 100644 --- a/src/Discord.Net.Webhook/DiscordWebhookClient.cs +++ b/src/Discord.Net.Webhook/DiscordWebhookClient.cs @@ -1,32 +1,49 @@ -using Discord.API.Rest; -using Discord.Rest; using System; +using System.Collections.Generic; using System.IO; using System.Threading.Tasks; -using System.Linq; using Discord.Logging; +using Discord.Rest; namespace Discord.Webhook { - public partial class DiscordWebhookClient + public class DiscordWebhookClient : IDisposable { public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } internal readonly AsyncEvent> _logEvent = new AsyncEvent>(); private readonly ulong _webhookId; + internal IWebhook Webhook; internal readonly Logger _restLogger; internal API.DiscordRestApiClient ApiClient { get; } internal LogManager LogManager { get; } + /// Creates a new Webhook discord client. + public DiscordWebhookClient(IWebhook webhook) + : this(webhook.Id, webhook.Token, new DiscordRestConfig()) { } /// Creates a new Webhook discord client. public DiscordWebhookClient(ulong webhookId, string webhookToken) : this(webhookId, webhookToken, new DiscordRestConfig()) { } + /// Creates a new Webhook discord client. public DiscordWebhookClient(ulong webhookId, string webhookToken, DiscordRestConfig config) + : this(config) { _webhookId = webhookId; + ApiClient.LoginAsync(TokenType.Webhook, webhookToken).GetAwaiter().GetResult(); + Webhook = WebhookClientHelper.GetWebhookAsync(this, webhookId).GetAwaiter().GetResult(); + } + /// Creates a new Webhook discord client. + public DiscordWebhookClient(IWebhook webhook, DiscordRestConfig config) + : this(config) + { + Webhook = webhook; + _webhookId = Webhook.Id; + } + private DiscordWebhookClient(DiscordRestConfig config) + { ApiClient = CreateApiClient(config); LogManager = new LogManager(config.LogLevel); LogManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); @@ -41,42 +58,40 @@ namespace Discord.Webhook await _restLogger.WarningAsync($"Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); }; ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false); - ApiClient.LoginAsync(TokenType.Webhook, webhookToken).GetAwaiter().GetResult(); } private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config) => new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent); - - public async Task SendMessageAsync(string text, bool isTTS = false, Embed[] embeds = null, + + /// Sends a message using to the channel for this webhook. Returns the ID of the created message. + public Task SendMessageAsync(string text, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null) - { - var args = new CreateWebhookMessageParams(text) { IsTTS = isTTS }; - if (embeds != null) - args.Embeds = embeds.Select(x => x.ToModel()).ToArray(); - if (username != null) - args.Username = username; - if (avatarUrl != null) - args.AvatarUrl = avatarUrl; - await ApiClient.CreateWebhookMessageAsync(_webhookId, args, options).ConfigureAwait(false); - } + => WebhookClientHelper.SendMessageAsync(this, text, isTTS, embeds, username, avatarUrl, options); #if FILESYSTEM - public async Task SendFileAsync(string filePath, string text, bool isTTS = false, - string username = null, string avatarUrl = null, RequestOptions options = null) + /// Send a message to the channel for this webhook with an attachment. Returns the ID of the created message. + public Task SendFileAsync(string filePath, string text, bool isTTS = false, + IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null) + => WebhookClientHelper.SendFileAsync(this, filePath, text, isTTS, embeds, username, avatarUrl, options); +#endif + /// Send a message to the channel for this webhook with an attachment. Returns the ID of the created message. + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, + IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null) + => WebhookClientHelper.SendFileAsync(this, stream, filename, text, isTTS, embeds, username, avatarUrl, options); + + /// Modifies the properties of this webhook. + public Task ModifyWebhookAsync(Action func, RequestOptions options = null) + => Webhook.ModifyAsync(func, options); + + /// Deletes this webhook from Discord and disposes the client. + public async Task DeleteWebhookAsync(RequestOptions options = null) { - string filename = Path.GetFileName(filePath); - using (var file = File.OpenRead(filePath)) - await SendFileAsync(file, filename, text, isTTS, username, avatarUrl, options).ConfigureAwait(false); + await Webhook.DeleteAsync(options).ConfigureAwait(false); + Dispose(); } -#endif - public async Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, - string username = null, string avatarUrl = null, RequestOptions options = null) + + public void Dispose() { - var args = new UploadWebhookFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS }; - if (username != null) - args.Username = username; - if (avatarUrl != null) - args.AvatarUrl = username; - await ApiClient.UploadWebhookFileAsync(_webhookId, args, options).ConfigureAwait(false); + ApiClient?.Dispose(); } } } diff --git a/src/Discord.Net.Webhook/Entities/Webhooks/RestInternalWebhook.cs b/src/Discord.Net.Webhook/Entities/Webhooks/RestInternalWebhook.cs new file mode 100644 index 000000000..cd35d731c --- /dev/null +++ b/src/Discord.Net.Webhook/Entities/Webhooks/RestInternalWebhook.cs @@ -0,0 +1,66 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.Webhook; + +namespace Discord.Webhook +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + internal class RestInternalWebhook : IWebhook + { + private DiscordWebhookClient _client; + + public ulong Id { get; } + public ulong ChannelId { get; } + public string Token { get; } + + public string Name { get; private set; } + public string AvatarId { get; private set; } + public ulong? GuildId { get; private set; } + + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + + internal RestInternalWebhook(DiscordWebhookClient apiClient, Model model) + { + _client = apiClient; + Id = model.Id; + ChannelId = model.Id; + Token = model.Token; + } + internal static RestInternalWebhook Create(DiscordWebhookClient client, Model model) + { + var entity = new RestInternalWebhook(client, model); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + if (model.Avatar.IsSpecified) + AvatarId = model.Avatar.Value; + if (model.GuildId.IsSpecified) + GuildId = model.GuildId.Value; + if (model.Name.IsSpecified) + Name = model.Name.Value; + } + + public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + + public async Task ModifyAsync(Action func, RequestOptions options = null) + { + var model = await WebhookClientHelper.ModifyAsync(_client, func, options); + Update(model); + } + + public Task DeleteAsync(RequestOptions options = null) + => WebhookClientHelper.DeleteAsync(_client, options); + + public override string ToString() => $"Webhook: {Name}:{Id}"; + private string DebuggerDisplay => $"Webhook: {Name} ({Id})"; + + IUser IWebhook.Creator => null; + ITextChannel IWebhook.Channel => null; + IGuild IWebhook.Guild => null; + } +} diff --git a/src/Discord.Net.Webhook/WebhookClientHelper.cs b/src/Discord.Net.Webhook/WebhookClientHelper.cs new file mode 100644 index 000000000..f3a3984cf --- /dev/null +++ b/src/Discord.Net.Webhook/WebhookClientHelper.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Discord.API.Rest; +using Discord.Rest; +using ImageModel = Discord.API.Image; +using WebhookModel = Discord.API.Webhook; + +namespace Discord.Webhook +{ + internal static class WebhookClientHelper + { + public static async Task GetWebhookAsync(DiscordWebhookClient client, ulong webhookId) + { + var model = await client.ApiClient.GetWebhookAsync(webhookId); + if (model == null) + throw new InvalidOperationException("Could not find a webhook for the supplied credentials."); + return RestInternalWebhook.Create(client, model); + } + public static async Task SendMessageAsync(DiscordWebhookClient client, + string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, RequestOptions options) + { + var args = new CreateWebhookMessageParams(text) { IsTTS = isTTS }; + if (embeds != null) + args.Embeds = embeds.Select(x => x.ToModel()).ToArray(); + if (username != null) + args.Username = username; + if (avatarUrl != null) + args.AvatarUrl = avatarUrl; + + var model = await client.ApiClient.CreateWebhookMessageAsync(client.Webhook.Id, args, options: options).ConfigureAwait(false); + return model.Id; + } +#if FILESYSTEM + public static async Task SendFileAsync(DiscordWebhookClient client, string filePath, string text, bool isTTS, + IEnumerable embeds, string username, string avatarUrl, RequestOptions options) + { + string filename = Path.GetFileName(filePath); + using (var file = File.OpenRead(filePath)) + return await SendFileAsync(client, file, filename, text, isTTS, embeds, username, avatarUrl, options).ConfigureAwait(false); + } +#endif + public static async Task SendFileAsync(DiscordWebhookClient client, Stream stream, string filename, string text, bool isTTS, + IEnumerable embeds, string username, string avatarUrl, RequestOptions options) + { + var args = new UploadWebhookFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS }; + if (username != null) + args.Username = username; + if (avatarUrl != null) + args.AvatarUrl = username; + if (embeds != null) + args.Embeds = embeds.Select(x => x.ToModel()).ToArray(); + var msg = await client.ApiClient.UploadWebhookFileAsync(client.Webhook.Id, args, options).ConfigureAwait(false); + return msg.Id; + } + + public static async Task ModifyAsync(DiscordWebhookClient client, + Action func, RequestOptions options) + { + var args = new WebhookProperties(); + func(args); + var apiArgs = new ModifyWebhookParams + { + Avatar = args.Image.IsSpecified ? args.Image.Value?.ToModel() : Optional.Create(), + Name = args.Name + }; + + if (!apiArgs.Avatar.IsSpecified && client.Webhook.AvatarId != null) + apiArgs.Avatar = new ImageModel(client.Webhook.AvatarId); + + return await client.ApiClient.ModifyWebhookAsync(client.Webhook.Id, apiArgs, options).ConfigureAwait(false); + } + + public static async Task DeleteAsync(DiscordWebhookClient client, RequestOptions options) + { + await client.ApiClient.DeleteWebhookAsync(client.Webhook.Id, options).ConfigureAwait(false); + } + } +} diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index 309532615..cd57d2fcf 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,7 +2,7 @@ Discord.Net - 2.0.0-alpha$suffix$ + 2.0.0-beta$suffix$ Discord.Net Discord.Net Contributors RogueException @@ -13,28 +13,25 @@ false - - - - - - + + + + + - - - - - - + + + + + - - - - - - + + + + + diff --git a/test/Discord.Net.Tests/Tests.ChannelPermissions.cs b/test/Discord.Net.Tests/Tests.ChannelPermissions.cs new file mode 100644 index 000000000..ac8ede4e4 --- /dev/null +++ b/test/Discord.Net.Tests/Tests.ChannelPermissions.cs @@ -0,0 +1,327 @@ +using System; +using System.Threading.Tasks; +using Xunit; + +namespace Discord +{ + public partial class Tests + { + [Fact] + public Task TestChannelPermission() + { + var perm = new ChannelPermissions(); + + // check initial values + Assert.Equal((ulong)0, perm.RawValue); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // permissions list empty by default + Assert.Empty(perm.ToList()); + + // test modify with no parameters + var copy = perm.Modify(); + Assert.Equal((ulong)0, copy.RawValue); + + // test the values that are returned by ChannelPermission.All + Assert.Equal((ulong)0, ChannelPermissions.None.RawValue); + + // for text channels + ulong textChannel = (ulong)( ChannelPermission.CreateInstantInvite + | ChannelPermission.ManageChannels + | ChannelPermission.AddReactions + | ChannelPermission.ViewChannel + | ChannelPermission.SendMessages + | ChannelPermission.SendTTSMessages + | ChannelPermission.ManageMessages + | ChannelPermission.EmbedLinks + | ChannelPermission.AttachFiles + | ChannelPermission.ReadMessageHistory + | ChannelPermission.MentionEveryone + | ChannelPermission.UseExternalEmojis + | ChannelPermission.ManageRoles + | ChannelPermission.ManageWebhooks); + + Assert.Equal(textChannel, ChannelPermissions.Text.RawValue); + + // voice channels + ulong voiceChannel = (ulong)( + ChannelPermission.CreateInstantInvite + | ChannelPermission.ManageChannels + | ChannelPermission.Connect + | ChannelPermission.Speak + | ChannelPermission.MuteMembers + | ChannelPermission.DeafenMembers + | ChannelPermission.MoveMembers + | ChannelPermission.UseVAD + | ChannelPermission.ManageRoles); + + Assert.Equal(voiceChannel, ChannelPermissions.Voice.RawValue); + + // DM Channels + ulong dmChannel = (ulong)( + ChannelPermission.ViewChannel + | ChannelPermission.SendMessages + | ChannelPermission.EmbedLinks + | ChannelPermission.AttachFiles + | ChannelPermission.ReadMessageHistory + | ChannelPermission.UseExternalEmojis + | ChannelPermission.Connect + | ChannelPermission.Speak + | ChannelPermission.UseVAD + ); + Assert.Equal(dmChannel, ChannelPermissions.DM.RawValue); + + // group channel + ulong groupChannel = (ulong)( + ChannelPermission.SendMessages + | ChannelPermission.EmbedLinks + | ChannelPermission.AttachFiles + | ChannelPermission.SendTTSMessages + | ChannelPermission.Connect + | ChannelPermission.Speak + | ChannelPermission.UseVAD + ); + Assert.Equal(groupChannel, ChannelPermissions.Group.RawValue); + return Task.CompletedTask; + } + + public Task TestChannelPermissionModify() + { + // test channel permission modify + + var perm = new ChannelPermissions(); + + // ensure that the permission is initially false + Assert.False(perm.CreateInstantInvite); + + // ensure that when modified it works + perm = perm.Modify(createInstantInvite: true); + Assert.True(perm.CreateInstantInvite); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.CreateInstantInvite); + + // set false again, move on to next permission + perm = perm.Modify(createInstantInvite: false); + Assert.False(perm.CreateInstantInvite); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.ManageChannel); + + perm = perm.Modify(manageChannel: true); + Assert.True(perm.ManageChannel); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.ManageChannels); + + perm = perm.Modify(manageChannel: false); + Assert.False(perm.ManageChannel); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.AddReactions); + + perm = perm.Modify(addReactions: true); + Assert.True(perm.AddReactions); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.AddReactions); + + perm = perm.Modify(addReactions: false); + Assert.False(perm.AddReactions); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.ViewChannel); + + perm = perm.Modify(viewChannel: true); + Assert.True(perm.ViewChannel); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.ViewChannel); + + perm = perm.Modify(viewChannel: false); + Assert.False(perm.ViewChannel); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.SendMessages); + + perm = perm.Modify(sendMessages: true); + Assert.True(perm.SendMessages); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.SendMessages); + + perm = perm.Modify(sendMessages: false); + Assert.False(perm.SendMessages); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.SendTTSMessages); + + perm = perm.Modify(sendTTSMessages: true); + Assert.True(perm.SendTTSMessages); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.SendTTSMessages); + + perm = perm.Modify(sendTTSMessages: false); + Assert.False(perm.SendTTSMessages); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.ManageMessages); + + perm = perm.Modify(manageMessages: true); + Assert.True(perm.ManageMessages); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.ManageMessages); + + perm = perm.Modify(manageMessages: false); + Assert.False(perm.ManageMessages); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.EmbedLinks); + + perm = perm.Modify(embedLinks: true); + Assert.True(perm.EmbedLinks); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.EmbedLinks); + + perm = perm.Modify(embedLinks: false); + Assert.False(perm.EmbedLinks); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.AttachFiles); + + perm = perm.Modify(attachFiles: true); + Assert.True(perm.AttachFiles); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.AttachFiles); + + perm = perm.Modify(attachFiles: false); + Assert.False(perm.AttachFiles); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.ReadMessageHistory); + + perm = perm.Modify(readMessageHistory: true); + Assert.True(perm.ReadMessageHistory); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.ReadMessageHistory); + + perm = perm.Modify(readMessageHistory: false); + Assert.False(perm.ReadMessageHistory); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.MentionEveryone); + + perm = perm.Modify(mentionEveryone: true); + Assert.True(perm.MentionEveryone); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.MentionEveryone); + + perm = perm.Modify(mentionEveryone: false); + Assert.False(perm.MentionEveryone); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.UseExternalEmojis); + + perm = perm.Modify(useExternalEmojis: true); + Assert.True(perm.UseExternalEmojis); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.UseExternalEmojis); + + perm = perm.Modify(useExternalEmojis: false); + Assert.False(perm.UseExternalEmojis); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.Connect); + + perm = perm.Modify(connect: true); + Assert.True(perm.Connect); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.Connect); + + perm = perm.Modify(connect: false); + Assert.False(perm.Connect); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.Speak); + + perm = perm.Modify(speak: true); + Assert.True(perm.Speak); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.Speak); + + perm = perm.Modify(speak: false); + Assert.False(perm.Speak); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.MuteMembers); + + perm = perm.Modify(muteMembers: true); + Assert.True(perm.MuteMembers); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.MuteMembers); + + perm = perm.Modify(muteMembers: false); + Assert.False(perm.MuteMembers); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.DeafenMembers); + + perm = perm.Modify(deafenMembers: true); + Assert.True(perm.DeafenMembers); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.DeafenMembers); + + perm = perm.Modify(deafenMembers: false); + Assert.False(perm.DeafenMembers); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.MoveMembers); + + perm = perm.Modify(moveMembers: true); + Assert.True(perm.MoveMembers); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.MoveMembers); + + perm = perm.Modify(moveMembers: false); + Assert.False(perm.MoveMembers); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.UseVAD); + + perm = perm.Modify(useVoiceActivation: true); + Assert.True(perm.UseVAD); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.UseVAD); + + perm = perm.Modify(useVoiceActivation: false); + Assert.False(perm.UseVAD); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.ManageRoles); + + perm = perm.Modify(manageRoles: true); + Assert.True(perm.ManageRoles); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.ManageRoles); + + perm = perm.Modify(manageRoles: false); + Assert.False(perm.ManageRoles); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.ManageWebhooks); + + perm = perm.Modify(manageWebhooks: true); + Assert.True(perm.ManageWebhooks); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.ManageWebhooks); + + perm = perm.Modify(manageWebhooks: false); + Assert.False(perm.ManageWebhooks); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + return Task.CompletedTask; + } + + [Fact] + public Task TestChannelTypeResolution() + { + ITextChannel someChannel = null; + // null channels will throw exception + Assert.Throws(() => ChannelPermissions.All(someChannel)); + return Task.CompletedTask; + } + } +} diff --git a/test/Discord.Net.Tests/Tests.Emotes.cs b/test/Discord.Net.Tests/Tests.Emotes.cs new file mode 100644 index 000000000..334975ce4 --- /dev/null +++ b/test/Discord.Net.Tests/Tests.Emotes.cs @@ -0,0 +1,44 @@ +using System; +using Xunit; + +namespace Discord +{ + public class EmoteTests + { + [Fact] + public void Test_Emote_Parse() + { + Assert.True(Emote.TryParse("<:typingstatus:394207658351263745>", out Emote emote)); + Assert.NotNull(emote); + Assert.Equal("typingstatus", emote.Name); + Assert.Equal(394207658351263745UL, emote.Id); + Assert.False(emote.Animated); + Assert.Equal(DateTimeOffset.FromUnixTimeMilliseconds(1514056829775), emote.CreatedAt); + Assert.EndsWith("png", emote.Url); + } + [Fact] + public void Test_Invalid_Emote_Parse() + { + Assert.False(Emote.TryParse("invalid", out _)); + Assert.False(Emote.TryParse("<:typingstatus:not_a_number>", out _)); + Assert.Throws(() => Emote.Parse("invalid")); + } + [Fact] + public void Test_Animated_Emote_Parse() + { + Assert.True(Emote.TryParse("", out Emote emote)); + Assert.NotNull(emote); + Assert.Equal("typingstatus", emote.Name); + Assert.Equal(394207658351263745UL, emote.Id); + Assert.True(emote.Animated); + Assert.Equal(DateTimeOffset.FromUnixTimeMilliseconds(1514056829775), emote.CreatedAt); + Assert.EndsWith("gif", emote.Url); + } + public void Test_Invalid_Amimated_Emote_Parse() + { + Assert.False(Emote.TryParse("", out _)); + Assert.False(Emote.TryParse("", out _)); + Assert.False(Emote.TryParse("", out _)); + } + } +} diff --git a/test/Discord.Net.Tests/Tests.GuildPermissions.cs b/test/Discord.Net.Tests/Tests.GuildPermissions.cs new file mode 100644 index 000000000..bb113d221 --- /dev/null +++ b/test/Discord.Net.Tests/Tests.GuildPermissions.cs @@ -0,0 +1,307 @@ +using System; +using System.Threading.Tasks; +using Xunit; + +namespace Discord +{ + public partial class Tests + { + [Fact] + public Task TestGuildPermission() + { + // Test Guild Permission Constructors + var perm = new GuildPermissions(); + + // the default raw value is 0 + Assert.Equal((ulong)0, perm.RawValue); + // also check that it is the same as none + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // permissions list is empty by default + Assert.Empty(perm.ToList()); + Assert.NotNull(perm.ToList()); + + // Test modify with no parameters + var copy = perm.Modify(); + // ensure that the raw values match + Assert.Equal((ulong)0, copy.RawValue); + + // test GuildPermissions.All + ulong sumOfAllGuildPermissions = 0; + foreach(var v in Enum.GetValues(typeof(GuildPermission))) + { + sumOfAllGuildPermissions |= (ulong)v; + } + + // assert that the raw values match + Assert.Equal(sumOfAllGuildPermissions, GuildPermissions.All.RawValue); + Assert.Equal((ulong)0, GuildPermissions.None.RawValue); + + // assert that GuildPermissions.All contains the same number of permissions as the + // GuildPermissions enum + Assert.Equal(Enum.GetValues(typeof(GuildPermission)).Length, GuildPermissions.All.ToList().Count); + + // assert that webhook has the same raw value + ulong webHookPermissions = (ulong)( + GuildPermission.SendMessages | GuildPermission.SendTTSMessages | GuildPermission.EmbedLinks | + GuildPermission.AttachFiles); + Assert.Equal(webHookPermissions, GuildPermissions.Webhook.RawValue); + + return Task.CompletedTask; + } + + [Fact] + public Task TestGuildPermissionModify() + { + var perm = new GuildPermissions(); + + // tests each of the parameters of Modify one by one + + // test modify with each of the parameters + // test initially false state + Assert.False(perm.CreateInstantInvite); + + // ensure that when we modify it the parameter works + perm = perm.Modify(createInstantInvite: true); + Assert.True(perm.CreateInstantInvite); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.CreateInstantInvite); + + // set it false again, then move on to the next permission + perm = perm.Modify(createInstantInvite: false); + Assert.False(perm.CreateInstantInvite); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(kickMembers: true); + Assert.True(perm.KickMembers); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.KickMembers); + + perm = perm.Modify(kickMembers: false); + Assert.False(perm.KickMembers); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(banMembers: true); + Assert.True(perm.BanMembers); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.BanMembers); + + perm = perm.Modify(banMembers: false); + Assert.False(perm.BanMembers); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(administrator: true); + Assert.True(perm.Administrator); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.Administrator); + + perm = perm.Modify(administrator: false); + Assert.False(perm.Administrator); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(manageChannels: true); + Assert.True(perm.ManageChannels); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.ManageChannels); + + perm = perm.Modify(manageChannels: false); + Assert.False(perm.ManageChannels); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(manageGuild: true); + Assert.True(perm.ManageGuild); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.ManageGuild); + + perm = perm.Modify(manageGuild: false); + Assert.False(perm.ManageGuild); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + + // individual permission test + perm = perm.Modify(addReactions: true); + Assert.True(perm.AddReactions); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.AddReactions); + + perm = perm.Modify(addReactions: false); + Assert.False(perm.AddReactions); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + + // individual permission test + perm = perm.Modify(viewAuditLog: true); + Assert.True(perm.ViewAuditLog); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.ViewAuditLog); + + perm = perm.Modify(viewAuditLog: false); + Assert.False(perm.ViewAuditLog); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + + // individual permission test + perm = perm.Modify(readMessages: true); + Assert.True(perm.ReadMessages); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.ReadMessages); + + perm = perm.Modify(readMessages: false); + Assert.False(perm.ReadMessages); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + + // individual permission test + perm = perm.Modify(sendMessages: true); + Assert.True(perm.SendMessages); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.SendMessages); + + perm = perm.Modify(sendMessages: false); + Assert.False(perm.SendMessages); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(embedLinks: true); + Assert.True(perm.EmbedLinks); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.EmbedLinks); + + perm = perm.Modify(embedLinks: false); + Assert.False(perm.EmbedLinks); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(attachFiles: true); + Assert.True(perm.AttachFiles); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.AttachFiles); + + perm = perm.Modify(attachFiles: false); + Assert.False(perm.AttachFiles); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(readMessageHistory: true); + Assert.True(perm.ReadMessageHistory); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.ReadMessageHistory); + + perm = perm.Modify(readMessageHistory: false); + Assert.False(perm.ReadMessageHistory); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(mentionEveryone: true); + Assert.True(perm.MentionEveryone); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.MentionEveryone); + + perm = perm.Modify(mentionEveryone: false); + Assert.False(perm.MentionEveryone); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(useExternalEmojis: true); + Assert.True(perm.UseExternalEmojis); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.UseExternalEmojis); + + perm = perm.Modify(useExternalEmojis: false); + Assert.False(perm.UseExternalEmojis); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(connect: true); + Assert.True(perm.Connect); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.Connect); + + perm = perm.Modify(connect: false); + Assert.False(perm.Connect); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(speak: true); + Assert.True(perm.Speak); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.Speak); + + perm = perm.Modify(speak: false); + Assert.False(perm.Speak); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(muteMembers: true); + Assert.True(perm.MuteMembers); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.MuteMembers); + + perm = perm.Modify(muteMembers: false); + Assert.False(perm.MuteMembers); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(deafenMembers: true); + Assert.True(perm.DeafenMembers); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.DeafenMembers); + + perm = perm.Modify(deafenMembers: false); + Assert.False(perm.DeafenMembers); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(moveMembers: true); + Assert.True(perm.MoveMembers); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.MoveMembers); + + perm = perm.Modify(moveMembers: false); + Assert.False(perm.MoveMembers); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(useVoiceActivation: true); + Assert.True(perm.UseVAD); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.UseVAD); + + perm = perm.Modify(useVoiceActivation: false); + Assert.False(perm.UseVAD); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(changeNickname: true); + Assert.True(perm.ChangeNickname); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.ChangeNickname); + + perm = perm.Modify(changeNickname: false); + Assert.False(perm.ChangeNickname); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(manageNicknames: true); + Assert.True(perm.ManageNicknames); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.ManageNicknames); + + perm = perm.Modify(manageNicknames: false); + Assert.False(perm.ManageNicknames); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(manageRoles: true); + Assert.True(perm.ManageRoles); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.ManageRoles); + + perm = perm.Modify(manageRoles: false); + Assert.False(perm.ManageRoles); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(manageWebhooks: true); + Assert.True(perm.ManageWebhooks); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.ManageWebhooks); + + perm = perm.Modify(manageWebhooks: false); + Assert.False(perm.ManageWebhooks); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(manageEmojis: true); + Assert.True(perm.ManageEmojis); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.ManageEmojis); + + perm = perm.Modify(manageEmojis: false); + Assert.False(perm.ManageEmojis); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + return Task.CompletedTask; + } + + } +}