diff --git a/Discord.Net.sln b/Discord.Net.sln index a63606787..58bfcad86 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.26228.4 +VisualStudioVersion = 15.0.26730.12 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 @@ -142,4 +142,10 @@ Global {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012} {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A} EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D2404771-EEC8-45F2-9D71-F3373F6C1495} + EndGlobalSection + GlobalSection(CodealikeProperties) = postSolution + SolutionGuid = a45217b4-a401-4dbf-8654-34d2ec034cd9 + EndGlobalSection EndGlobal 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..3bf70c09c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -34,7 +34,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 e021b1eb3..6781764c9 100644 --- a/docs/guides/commands/commands.md +++ b/docs/guides/commands/commands.md @@ -1,24 +1,20 @@ # The Command Service ->[!WARNING] ->This article is out of date, and has not been rewritten yet. -Information is not guaranteed to be accurate. - [Discord.Commands](xref:Discord.Commands) provides an Attribute-based - Command Parser. +command parser. ## Setup -To use Commands, you must create a [Commands Service] and a -Command Handler. +To use Commands, you must create a [Command Service] and a Command +Handler. -Included below is a very bare-bones Command Handler. You can extend -your Command Handler as much as you like, however the below is the -bare minimum. +Included below is a very barebone Command Handler. You can extend your +Command Handler as much as you like; however, the below is the bare +minimum. -The CommandService optionally will accept a [CommandServiceConfig], +The `CommandService` will optionally accept a [CommandServiceConfig], which _does_ set a few default values for you. It is recommended to -look over the properties in [CommandServiceConfig], and their default +look over the properties in [CommandServiceConfig] and their default values. [!code-csharp[Command Handler](samples/command_handler.cs)] @@ -28,32 +24,32 @@ values. ## With Attributes -In 1.0, Commands can be defined ahead of time, with attributes, or -at runtime, with builders. +In 1.0, Commands can be defined ahead of time with attributes, or at +runtime with builders. -For most bots, ahead-of-time commands should be all you need, and this -is the recommended method of defining commands. +For most bots, ahead-of-time Commands should be all you need, and this +is the recommended method of defining Commands. ### Modules -The first step to creating commands is to create a _module_. +The first step to creating Commands is to create a _module_. -Modules are an organizational pattern that allow you to write your -commands in different classes, and have them automatically loaded. +A Module is an organizational pattern that allows you to write your +Commands in different classes and have them automatically loaded. Discord.Net's implementation of Modules is influenced heavily from -ASP.Net Core's Controller pattern. This means that the lifetime of a -module instance is only as long as the command being invoked. +ASP.NET Core's Controller pattern. This means that the lifetime of a +module instance is only as long as the Command is being invoked. **Avoid using long-running code** in your modules wherever possible. -You should **not** be implementing very much logic into your modules; -outsource to a service for that. +You should **not** be implementing very much logic into your modules, +instead, outsource to a service for that. If you are unfamiliar with Inversion of Control, it is recommended to read the MSDN article on [IoC] and [Dependency Injection]. -To begin, create a new class somewhere in your project, and -inherit the class from [ModuleBase]. This class **must** be `public`. +To begin, create a new class somewhere in your project and inherit the +class from [ModuleBase]. This class **must** be `public`. >[!NOTE] >[ModuleBase] is an _abstract_ class, meaning that you may extend it @@ -61,6 +57,7 @@ inherit the class from [ModuleBase]. This class **must** be `public`. >extension of ModuleBase. By now, your module should look like this: + [!code-csharp[Empty Module](samples/empty-module.cs)] [IoC]: https://msdn.microsoft.com/en-us/library/ff921087.aspx @@ -69,72 +66,75 @@ By now, your module should look like this: ### Adding Commands -The next step to creating commands, is actually creating commands. +The next step to creating Commands is actually creating the Commands. -To create a command, add a method to your module of type `Task`. -Typically, you will want to mark this method as `async`, although it is -not required. +To create a Command, add a method to your module of type `Task`. +Typically, you will want to mark this method as `async`, although it +is not required. -Adding parameters to a command is done by adding parameters to the +Adding parameters to a Command is done by adding parameters to the parent Task. -For example, to take an integer as an argument, add `int arg`. To take -a user as an argument, add `IUser user`. In 1.0, a command can accept -nearly any type of argument; a full list of types that are parsed by -default can be found in the below section on _Type Readers_. +For example, to take an integer as an argument from the user, add `int +arg`; to take a user as an argument from the user, add `IUser user`. +In 1.0, a Command can accept nearly any type of argument; a full list +of types that are parsed by default can be found in the below section +on _Type Readers_. Parameters, by default, are always required. To make a parameter optional, give it a default value. To accept a comma-separated list, set the parameter to `params Type[]`. Should a parameter include spaces, it **must** be wrapped in quotes. -For example, for a command with a parameter `string food`, you would +For example, for a Command with a parameter `string food`, you would execute it with `!favoritefood "Key Lime Pie"`. -If you would like a parameter to parse until the end of a command, +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. +user to invoke a Command without wrapping a parameter in quotes. -Finally, flag your command with the [CommandAttribute]. (You must -specify a name for this command, except for when it is part of a -module group - see below). +Finally, flag your Command with the [CommandAttribute] (you must +specify a name for this Command, except for when it is part of a +Module Group - see below). [RemainderAttribute]: xref:Discord.Commands.RemainderAttribute [CommandAttribute]: xref:Discord.Commands.CommandAttribute ### Command Overloads -You may add overloads of your commands, and the command parser will +You may add overloads to your Commands, and the Command parser will automatically pick up on it. -If, for whatever reason, you have too commands which are ambiguous to +If for whatever reason, you have two Commands which are ambiguous to each other, you may use the @Discord.Commands.PriorityAttribute to specify which should be tested before the other. -Priority's are sorted in ascending order; the higher priority will be -called first. +The `Priority` attributes are sorted in ascending order; the higher +priority will be called first. -### CommandContext +### Command Context -Every command can access the execution context through the [Context] -property on [ModuleBase]. CommandContext allows you to access the -message, channel, guild, and user that the command was invoked from, -as well as the underlying discord client the command was invoked from. +Every Command can access the execution context through the [Context] +property on [ModuleBase]. `ICommandContext` allows you to access the +message, channel, guild, and user that the Command was invoked from, +as well as the underlying Discord client that the Command was invoked +from. Different types of Contexts may be specified using the generic variant -of [ModuleBase]. When using a [SocketCommandContext], for example, -the properties on this context will already be Socket entities. You +of [ModuleBase]. When using a [SocketCommandContext], for example, the +properties on this context will already be Socket entities, so you will not need to cast them. To reply to messages, you may also invoke [ReplyAsync], instead of accessing the channel through the [Context] and sending a message. +> [!WARNING] +>Contexts should **NOT** be mixed! You cannot have one module that +>uses `CommandContext` and another that uses `SocketCommandContext`. + [Context]: xref:Discord.Commands.ModuleBase`1#Discord_Commands_ModuleBase_1_Context [SocketCommandContext]: xref:Discord.Commands.SocketCommandContext - ->![WARNING] ->Contexts should **NOT** be mixed! You cannot have one module that ->uses CommandContext, and another that uses SocketCommandContext. +[ReplyAsync]: xref:Discord.Commands.ModuleBase`1#Discord_Commands_ModuleBase_1_ReplyAsync_System_String_System_Boolean_Discord_Embed_Discord_RequestOptions_ ### Example Module @@ -144,50 +144,50 @@ At this point, your module should look comparable to this example: #### Loading Modules Automatically The Command Service can automatically discover all classes in an -Assembly that inherit [ModuleBase], and load them. +Assembly that inherit [ModuleBase] and load them. To opt a module out of auto-loading, flag it with -[DontAutoLoadAttribute] +[DontAutoLoadAttribute]. Invoke [CommandService.AddModulesAsync] to discover modules and install them. [DontAutoLoadAttribute]: xref:Discord.Commands.DontAutoLoadAttribute -[CommandService.AddModulesAsync]: xref:Discord_Commands_CommandService#Discord_Commands_CommandService_AddModulesAsync_Assembly_ +[CommandService.AddModulesAsync]: xref:Discord.Commands.CommandService#Discord_Commands_CommandService_AddModulesAsync_Assembly_ #### Loading Modules Manually -To manually load a module, invoke [CommandService.AddModuleAsync], -by passing in the generic type of your module, and optionally -a dependency map. +To manually load a module, invoke [CommandService.AddModuleAsync] by +passing in the generic type of your module and optionally, a +dependency map. [CommandService.AddModuleAsync]: xref:Discord.Commands.CommandService#Discord_Commands_CommandService_AddModuleAsync__1 ### Module Constructors Modules are constructed using Dependency Injection. Any parameters -that are placed in the constructor must be injected into an -@System.IServiceProvider. Alternatively, you may accept an -IServiceProvider as an argument and extract services yourself. +that are placed in the Module's constructor must be injected into an +@System.IServiceProvider first. Alternatively, you may accept an +`IServiceProvider` as an argument and extract services yourself. ### Module Properties -Modules with public settable properties will have them injected after module -construction. +Modules with `public` settable properties will have the dependencies +injected after the construction of the Module. ### Module Groups -Module Groups allow you to create a module where commands are prefixed. -To create a group, flag a module with the -@Discord.Commands.GroupAttribute +Module Groups allow you to create a module where Commands are +prefixed. To create a group, flag a module with the +@Discord.Commands.GroupAttribute. -Module groups also allow you to create **nameless commands**, where the -[CommandAttribute] is configured with no name. In this case, the -command will inherit the name of the group it belongs to. +Module groups also allow you to create **nameless Commands**, where +the [CommandAttribute] is configured with no name. In this case, the +Command will inherit the name of the group it belongs to. ### Submodules -Submodules are modules that reside within another module. Typically, +Submodules are Modules that reside within another one. Typically, submodules are used to create nested groups (although not required to create nested groups). @@ -199,54 +199,62 @@ create nested groups). ## Dependency Injection -The commands service is bundled with a very barebones Dependency -Injection service for your convienence. It is recommended that -you use DI when writing your modules. +The Command Service is bundled with a very barebone Dependency +Injection service for your convenience. It is recommended that you use +DI when writing your modules. ### Setup -First, you need to create an @System.IServiceProvider -You may create your own IServiceProvider if you wish. +First, you need to create an @System.IServiceProvider; you may create +your own one if you wish. -Next, add the dependencies your modules will use to the map. +Next, add the dependencies that your modules will use to the map. -Finally, pass the map into the `LoadAssembly` method. -Your modules will automatically be loaded with this dependency map. +Finally, pass the map into the `LoadAssembly` method. Your modules +will be automatically loaded with this dependency map. [!code-csharp[IServiceProvider Setup](samples/dependency_map_setup.cs)] ### Usage in Modules -In the constructor of your module, any parameters will be filled in by -the @System.IServiceProvider you pass into `LoadAssembly`. +In the constructor of your Module, any parameters will be filled in by +the @System.IServiceProvider that you've passed into `LoadAssembly`. -Any publicly settable properties will also be filled in the same manner. +Any publicly settable properties will also be filled in the same +manner. >[!NOTE] -> Annotating a property with the [DontInject] attribute will prevent it from -being injected. +> Annotating a property with a [DontInjectAttribute] attribute will prevent the +property from being injected. >[!NOTE] ->If you accept `CommandService` or `IServiceProvider` as a parameter in -your constructor or as an injectable property, these entries will be filled -by the CommandService the module was loaded from, and the ServiceProvider passed -into it, respectively. +>If you accept `CommandService` or `IServiceProvider` as a parameter +in your constructor or as an injectable property, these entries will +be filled by the `CommandService` that the Module is loaded from and +the `ServiceProvider` that is passed into it respectively. [!code-csharp[ServiceProvider in Modules](samples/dependency_module.cs)] +[DontInjectAttribute]: xref:Discord.Commands.DontInjectAttribute + # Preconditions -Preconditions serve as a permissions system for your commands. Keep in -mind, however, that they are not limited to _just_ permissions, and -can be as complex as you want them to be. +Precondition serve as a permissions system for your Commands. Keep in +mind, however, that they are not limited to _just_ permissions and can +be as complex as you want them to be. >[!NOTE] ->Preconditions can be applied to Modules, Groups, or Commands. +>There are two types of Preconditions. +[PreconditionAttribute] can be applied to Modules, Groups, or Commands; +[ParameterPreconditionAttribute] can be applied to Parameters. + +[PreconditionAttribute]: xref:Discord.Commands.PreconditionAttribute +[ParameterPreconditionAttribute]: xref:Discord.Commands.ParameterPreconditionAttribute ## Bundled Preconditions -Commands ships with four bundled preconditions; you may view their -usages on their API page. +Commands ship with four bundled Preconditions; you may view their +usages on their respective API pages. - @Discord.Commands.RequireContextAttribute - @Discord.Commands.RequireOwnerAttribute @@ -255,21 +263,23 @@ usages on their API page. ## Custom Preconditions -To write your own preconditions, create a new class that inherits from - @Discord.Commands.PreconditionAttribute +To write your own Precondition, create a new class that inherits from +either [PreconditionAttribute] or [ParameterPreconditionAttribute] +depending on your use. -In order for your precondition to function, you will need to override -[CheckPermissions]. +In order for your Precondition to function, you will need to override +the [CheckPermissions] method. Your IDE should provide an option to fill this in for you. -Return [PreconditionResult.FromSuccess] if the context met the -required parameters, otherwise return [PreconditionResult.FromError], -optionally including an error message. +If the context meets the required parameters, return +[PreconditionResult.FromSuccess], otherwise return +[PreconditionResult.FromError] and include an error message if +necessary. [!code-csharp[Custom Precondition](samples/require_owner.cs)] -[CheckPermissions]: xref:Discord.Commands.PreconditionAttribute#Discord_Commands_PreconditionAttribute_CheckPermissions_Discord_Commands_CommandContext_Discord_Commands_CommandInfo_Discord_Commands_IDependencyMap_ +[CheckPermissions]: xref:Discord.Commands.PreconditionAttribute#Discord_Commands_PreconditionAttribute_CheckPermissions_Discord_Commands_ICommandContext_Discord_Commands_CommandInfo_IServiceProvider_ [PreconditionResult.FromSuccess]: xref:Discord.Commands.PreconditionResult#Discord_Commands_PreconditionResult_FromSuccess [PreconditionResult.FromError]: xref:Discord.Commands.PreconditionResult#Discord_Commands_PreconditionResult_FromError_System_String_ @@ -296,22 +306,28 @@ By default, the following Types are supported arguments: ### Creating a Type Readers -To create a TypeReader, create a new class that imports @Discord and -@Discord.Commands. Ensure your class inherits from @Discord.Commands.TypeReader +To create a `TypeReader`, create a new class that imports @Discord and +@Discord.Commands and ensure the class inherits from +@Discord.Commands.TypeReader. -Next, satisfy the `TypeReader` class by overriding [Read]. +Next, satisfy the `TypeReader` class by overriding the [Read] method. >[!NOTE] >In many cases, Visual Studio can fill this in for you, using the >"Implement Abstract Class" IntelliSense hint. -Inside this task, add whatever logic you need to parse the input string. +Inside this task, add whatever logic you need to parse the input +string. -Finally, return a `TypeReaderResult`. If you were able to successfully -parse the input, return `TypeReaderResult.FromSuccess(parsedInput)`. -Otherwise, return `TypeReaderResult.FromError`. +If you are able to successfully parse the input, return +[TypeReaderResult.FromSuccess] with the parsed input, otherwise return +[TypeReaderResult.FromError] and include an error message if +necessary. -[Read]: xref:Discord.Commands.TypeReader#Discord_Commands_TypeReader_Read_Discord_Commands_CommandContext_System_String_ +[TypeReaderResult]: xref:Discord.Commands.TypeReaderResult +[TypeReaderResult.FromSuccess]: xref:Discord.Commands.TypeReaderResult#Discord_Commands_TypeReaderResult_FromSuccess_Discord_Commands_TypeReaderValue_ +[TypeReaderResult.FromError]: xref:Discord.Commands.TypeReaderResult#Discord_Commands_TypeReaderResult_FromError_Discord_Commands_CommandError_System_String_ +[Read]: xref:Discord.Commands.TypeReader#Discord_Commands_TypeReader_Read_Discord_Commands_ICommandContext_System_String_IServiceProvider_ #### Sample @@ -319,5 +335,9 @@ Otherwise, return `TypeReaderResult.FromError`. ### Installing TypeReaders -TypeReaders are not automatically discovered by the Command Service, -and must be explicitly added. To install a TypeReader, invoke [CommandService.AddTypeReader](xref:Discord.Commands.CommandService#Discord_Commands_CommandService_AddTypeReader__1_Discord_Commands_TypeReader_). +TypeReaders are not automatically discovered by the Command Service +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 diff --git a/docs/guides/commands/samples/command_handler.cs b/docs/guides/commands/samples/command_handler.cs index 6b5d4ad2b..da2453aa8 100644 --- a/docs/guides/commands/samples/command_handler.cs +++ b/docs/guides/commands/samples/command_handler.cs @@ -8,39 +8,42 @@ using Microsoft.Extensions.DependencyInjection; public class Program { - private CommandService commands; - private DiscordSocketClient client; - private IServiceProvider services; + private CommandService _commands; + private DiscordSocketClient _client; + private IServiceProvider _services; - static void Main(string[] args) => new Program().Start().GetAwaiter().GetResult(); + private static void Main(string[] args) => new Program().StartAsync().GetAwaiter().GetResult(); - public async Task Start() + public async Task StartAsync() { - client = new DiscordSocketClient(); - commands = new CommandService(); + _client = new DiscordSocketClient(); + _commands = new CommandService(); + // Avoid hard coding your token. Use an external source instead in your code. string token = "bot token here"; - services = new ServiceCollection() - .BuildServiceProvider(); + _services = new ServiceCollection() + .AddSingleton(_client) + .AddSingleton(_commands) + .BuildServiceProvider(); - await InstallCommands(); + await InstallCommandsAsync(); - await client.LoginAsync(TokenType.Bot, token); - await client.StartAsync(); + await _client.LoginAsync(TokenType.Bot, token); + await _client.StartAsync(); await Task.Delay(-1); } - public async Task InstallCommands() + public async Task InstallCommandsAsync() { // Hook the MessageReceived Event into our Command Handler - client.MessageReceived += HandleCommand; + _client.MessageReceived += HandleCommandAsync; // Discover all of the commands in this assembly and load them. - await commands.AddModulesAsync(Assembly.GetEntryAssembly()); + await _commands.AddModulesAsync(Assembly.GetEntryAssembly()); } - public async Task HandleCommand(SocketMessage messageParam) + private async Task HandleCommandAsync(SocketMessage messageParam) { // Don't process the command if it was a System Message var message = messageParam as SocketUserMessage; @@ -48,13 +51,13 @@ public class Program // Create a number to track where the prefix ends and the command begins int argPos = 0; // Determine if the message is a command, based on if it starts with '!' or a mention prefix - if (!(message.HasCharPrefix('!', ref argPos) || message.HasMentionPrefix(client.CurrentUser, ref argPos))) return; + if (!(message.HasCharPrefix('!', ref argPos) || message.HasMentionPrefix(_client.CurrentUser, ref argPos))) return; // Create a Command Context - var context = new CommandContext(client, message); + var context = new SocketCommandContext(_client, message); // Execute the command. (result does not indicate a return value, // rather an object stating if the command executed successfully) - var result = await commands.ExecuteAsync(context, argPos, service); + var result = await _commands.ExecuteAsync(context, argPos, _services); if (!result.IsSuccess) await context.Channel.SendMessageAsync(result.ErrorReason); } -} +} \ No newline at end of file diff --git a/docs/guides/commands/samples/dependency_map_setup.cs b/docs/guides/commands/samples/dependency_map_setup.cs index e205d891d..a36925904 100644 --- a/docs/guides/commands/samples/dependency_map_setup.cs +++ b/docs/guides/commands/samples/dependency_map_setup.cs @@ -1,18 +1,18 @@ -using Discord; -using Discord.Commands; -using Discord.WebSocket; -using foxboat.Services; +private IServiceProvider _services; +private CommandService _commands; -public class Commands +public async Task InstallAsync(DiscordSocketClient client) { - public async Task Install(DiscordSocketClient client) - { - // Here, we will inject the ServiceProvider with - // all of the services our client will use. - _serviceCollection.AddSingleton(client) - _serviceCollection.AddSingleton(new NotificationService()) - _serviceCollection.AddSingleton(new DatabaseService()) - // ... - await _commands.AddModulesAsync(Assembly.GetEntryAssembly()); - } -} + // Here, we will inject the ServiceProvider with + // all of the services our client will use. + _services = new ServiceCollection() + .AddSingleton(client) + .AddSingleton(_commands) + // You can pass in an instance of the desired type + .AddSingleton(new NotificationService()) + // ...or by using the generic method. + .AddSingleton() + .BuildServiceProvider(); + // ... + await _commands.AddModulesAsync(Assembly.GetEntryAssembly()); +} \ No newline at end of file diff --git a/docs/guides/commands/samples/empty-module.cs b/docs/guides/commands/samples/empty-module.cs index cac9922b5..6483c7cd2 100644 --- a/docs/guides/commands/samples/empty-module.cs +++ b/docs/guides/commands/samples/empty-module.cs @@ -1,6 +1,6 @@ using Discord.Commands; -public class InfoModule : ModuleBase +public class InfoModule : ModuleBase { } \ No newline at end of file diff --git a/docs/guides/commands/samples/groups.cs b/docs/guides/commands/samples/groups.cs index db6456c87..5f96c34e8 100644 --- a/docs/guides/commands/samples/groups.cs +++ b/docs/guides/commands/samples/groups.cs @@ -1,8 +1,8 @@ [Group("admin")] -public class AdminModule : ModuleBase +public class AdminModule : ModuleBase { [Group("clean")] - public class CleanModule : ModuleBase + public class CleanModule : ModuleBase { // ~admin clean 15 [Command] diff --git a/docs/guides/commands/samples/module.cs b/docs/guides/commands/samples/module.cs index 403acba06..5014619da 100644 --- a/docs/guides/commands/samples/module.cs +++ b/docs/guides/commands/samples/module.cs @@ -1,42 +1,41 @@ -using Discord; -using Discord.Commands; -using Discord.WebSocket; - // Create a module with no prefix -public class Info : ModuleBase +public class Info : ModuleBase { - // ~say hello -> hello - [Command("say"), Summary("Echos a message.")] - public async Task Say([Remainder, Summary("The text to echo")] string echo) - { - // ReplyAsync is a method on ModuleBase - await ReplyAsync(echo); - } + // ~say hello -> hello + [Command("say")] + [Summary("Echos a message.")] + public async Task SayAsync([Remainder] [Summary("The text to echo")] string echo) + { + // ReplyAsync is a method on ModuleBase + await ReplyAsync(echo); + } } // Create a module with the 'sample' prefix [Group("sample")] -public class Sample : ModuleBase +public class Sample : ModuleBase { - // ~sample square 20 -> 400 - [Command("square"), Summary("Squares a number.")] - public async Task Square([Summary("The number to square.")] int num) - { - // We can also access the channel from the Command Context. - await Context.Channel.SendMessageAsync($"{num}^2 = {Math.Pow(num, 2)}"); - } + // ~sample square 20 -> 400 + [Command("square")] + [Summary("Squares a number.")] + public async Task SquareAsync([Summary("The number to square.")] int num) + { + // We can also access the channel from the Command Context. + await Context.Channel.SendMessageAsync($"{num}^2 = {Math.Pow(num, 2)}"); + } - // ~sample userinfo --> foxbot#0282 + // ~sample userinfo --> foxbot#0282 // ~sample userinfo @Khionu --> Khionu#8708 // ~sample userinfo Khionu#8708 --> Khionu#8708 // ~sample userinfo Khionu --> Khionu#8708 // ~sample userinfo 96642168176807936 --> Khionu#8708 - // ~sample whois 96642168176807936 --> Khionu#8708 - [Command("userinfo"), Summary("Returns info about the current user, or the user parameter, if one passed.")] - [Alias("user", "whois")] - public async Task UserInfo([Summary("The (optional) user to get info for")] IUser user = null) - { - var userInfo = user ?? Context.Client.CurrentUser; - await ReplyAsync($"{userInfo.Username}#{userInfo.Discriminator}"); - } -} + // ~sample whois 96642168176807936 --> Khionu#8708 + [Command("userinfo")] + [Summary("Returns info about the current user, or the user parameter, if one passed.")] + [Alias("user", "whois")] + public async Task UserInfoAsync([Summary("The (optional) user to get info for")] SocketUser user = null) + { + var userInfo = user ?? Context.Client.CurrentUser; + await ReplyAsync($"{userInfo.Username}#{userInfo.Discriminator}"); + } +} \ No newline at end of file diff --git a/docs/guides/concepts/entities.md b/docs/guides/concepts/entities.md index a38651829..3a5d5496b 100644 --- a/docs/guides/concepts/entities.md +++ b/docs/guides/concepts/entities.md @@ -12,7 +12,7 @@ Discord API. ### Inheritance Due to the nature of the Discord API, some entities are designed with -multiple variants, for example, `SocketUser` and `SocketGuildUser`. +multiple variants; for example, `SocketUser` and `SocketGuildUser`. All models will contain the most detailed version of an entity possible, even if the type is less detailed. @@ -61,8 +61,11 @@ a variant of the type that you need. ### Tips Avoid using boxing-casts to coerce entities into a variant, use the -`as` keyword, and a null-conditional operator. +[`as`] keyword, and a null-conditional operator instead. -This allows you to write safer code, and avoid InvalidCastExceptions. +This allows you to write safer code and avoid [InvalidCastExceptions]. -For example, `(message.Author as SocketGuildUser)?.Nickname`. \ No newline at end of file +For example, `(message.Author as SocketGuildUser)?.Nickname`. + +[`as`]: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/as +[InvalidCastExceptions]: https://msdn.microsoft.com/en-us/library/system.invalidcastexception(v=vs.110).aspx \ No newline at end of file diff --git a/docs/guides/concepts/events.md b/docs/guides/concepts/events.md index f2dfb00f0..47db49aa8 100644 --- a/docs/guides/concepts/events.md +++ b/docs/guides/concepts/events.md @@ -4,27 +4,27 @@ title: Working with Events Events in Discord.Net are consumed in a similar manner to the standard convention, with the exception that every event must be of the type -`System.Threading.Tasks.Task`, and instead of using EventArgs, the +`System.Threading.Tasks.Task` and instead of using `EventArgs`, the event's parameters are passed directly into the handler. -This allows for events to be handled in an async context directly, -instead of relying on async void. +This allows for events to be handled in an async context directly +instead of relying on `async void`. ### Usage To receive data from an event, hook into it using C#'s delegate event pattern. -You may opt either to hook an event to an anonymous function (lambda) +You may either opt to hook an event to an anonymous function (lambda) or a named function. ### Safety -All events are designed to be thread-safe, in that events are executed -synchronously off the gateway task, in the same context as the gateway +All events are designed to be thread-safe; events are executed +synchronously off the gateway task in the same context as the gateway task. -As a side effect, this makes it possible to deadlock the gateway task, +As a side effect, this makes it possible to deadlock the gateway task and kill a connection. As a general rule of thumb, any task that takes longer than three seconds should **not** be awaited directly in the context of an event, but should be wrapped in a `Task.Run` or @@ -62,7 +62,7 @@ This pattern is typically only found on `EntityUpdated` events. An event handler with a signature of `Func` means that the `before` state of the entity was not provided by the -API, so it can either be pulled from the client's cache, or +API, so it can either be pulled from the client's cache or downloaded from the API. See the documentation for [Cacheable] for more information on this @@ -76,8 +76,8 @@ object. ### Tips -Many events relating to a Message entity, e.g. `MessageUpdated` -and `ReactionAdded` rely on the client's message cache, which is +Many events relating to a Message entity (i.e. `MessageUpdated` and +`ReactionAdded`) rely on the client's message cache, which is **not** enabled by default. Set the `MessageCacheSize` flag in [DiscordSocketConfig] to enable it. diff --git a/docs/guides/concepts/logging.md b/docs/guides/concepts/logging.md index 1592dfc72..50d2e9546 100644 --- a/docs/guides/concepts/logging.md +++ b/docs/guides/concepts/logging.md @@ -14,12 +14,14 @@ section. ### Usage To receive log events, simply hook the discord client's log method -to a Task with a single parameter of type [LogMessage] +to a `Task` with a single parameter of type [LogMessage]. It is recommended that you use an established function instead of a -lambda for handling logs, because most [addons] accept a reference +lambda for handling logs, because most addons accept a reference to a logging function to write their own messages. +[LogMessage]: xref:Discord.LogMessage + ### Usage in Commands Discord.Net's [CommandService] also provides a log event, identical @@ -29,6 +31,9 @@ Data logged through this event is typically coupled with a [CommandException], where information about the command's context and error can be found and handled. +[CommandService]: xref:Discord.Commands.CommandService +[CommandException]: xref:Discord.Commands.CommandException + #### Samples [!code-csharp[Logging Sample](samples/logging.cs)] diff --git a/docs/guides/getting_started/installing.md b/docs/guides/getting_started/installing.md index 82d242647..5d4c85d81 100644 --- a/docs/guides/getting_started/installing.md +++ b/docs/guides/getting_started/installing.md @@ -2,84 +2,87 @@ title: Installing Discord.Net --- -Discord.Net is distributed through the NuGet package manager, and it is -recommended to use NuGet to get started. +Discord.Net is distributed through the NuGet package manager, and it +is recommended to use NuGet to get started. Optionally, you may compile from source and install yourself. # Supported Platforms -Currently, Discord.Net targets [.NET Standard] 1.3, and offers support for -.NET Standard 1.1. If your application will be targeting .NET Standard 1.1, -please see the [additional steps](#installing-on-net-standard-11). +Currently, Discord.Net targets [.NET Standard] 1.3 and offers support +for .NET Standard 1.1. If your application will be targeting .NET +Standard 1.1, please see the [additional steps]. -Since Discord.Net is built on the .NET Standard, it is also recommended to -create applications using [.NET Core], though you are not required to. When -using .NET Framework, it is suggested to target `.NET 4.6.1` or higher. +Since Discord.Net is built on the .NET Standard, it is also +recommended to create applications using [.NET Core], though not +required. When using .NET Framework, it is suggested to target +`.NET Framework 4.6.1` or higher. [.NET Standard]: https://docs.microsoft.com/en-us/dotnet/articles/standard/library [.NET Core]: https://docs.microsoft.com/en-us/dotnet/articles/core/ +[additional steps]: #installing-on-net-standard-11 # Installing with NuGet Release builds of Discord.Net 1.0 will be published to the [official NuGet feed]. -Development builds of Discord.Net 1.0, as well as [addons](TODO) are published -to our development [MyGet feed]. +Development builds of Discord.Net 1.0, as well as addons *(TODO)* are +published to our development [MyGet feed]. Direct feed link: `https://www.myget.org/F/discord-net/api/v3/index.json` -Not sure how to add a direct feed? See how [with Visual Studio] -or [without Visual Studio](#configuring-nuget-without-visual-studio) +Not sure how to add a direct feed? See how [with Visual Studio] or +[without Visual Studio]. [official NuGet feed]: https://nuget.org [MyGet feed]: https://www.myget.org/feed/Packages/discord-net [with Visual Studio]: https://docs.microsoft.com/en-us/nuget/tools/package-manager-ui#package-sources - +[without Visual Studio]: #configuring-nuget-without-visual-studio ## Using Visual Studio -1. Create a solution for your bot -2. In Solution Explorer, find the 'Dependencies' element under your bot's -project -3. Right click on 'Dependencies', and select 'Manage NuGet packages' -![Step 3](images/install-vs-deps.png) -4. In the 'browse' tab, search for 'Discord.Net' - > [!TIP] -Don't forget to change your package source if you're installing from the -developer feed. -Also make sure to check 'Enable Prereleases' if installing a dev build! - -5. Install the 'Discord.Net' package - +>Don't forget to change your package source if you're installing from +the developer feed. +>Also make sure to check "Enable Prereleases" if installing a dev +build! + +1. Create a solution for your bot. +2. In Solution Explorer, find the "Dependencies" element under your +bot's project. +3. Right click on "Dependencies", and select "Manage NuGet packages." +![Step 3](images/install-vs-deps.png) +4. In the "Browse" tab, search for `Discord.Net`. +5. Install the `Discord.Net` package. ![Step 5](images/install-vs-nuget.png) ## Using JetBrains Rider -1. Create a new solution for your bot -2. Open the NuGet window (Tools > NuGet > Manage NuGet packages for Solution) -![Step 2](images/install-rider-nuget-manager.png) -3. In the 'Packages' tab, search for 'Discord.Net' -![Step 3](images/install-rider-search.png) - > [!TIP] -Make sure to check the 'Prerelease' box if installing a dev build! +Make sure to check the "Prerelease" box if installing a dev build! -4. Install by adding the package to your project +1. Create a new solution for your bot. +2. Open the NuGet window (Tools > NuGet > Manage NuGet packages for +Solution). +![Step 2](images/install-rider-nuget-manager.png) +3. In the "Packages" tab, search for `Discord.Net`. +![Step 3](images/install-rider-search.png) +4. Install by adding the package to your project. ![Step 4](images/install-rider-add.png) ## Using Visual Studio Code -1. Create a new project for your bot -2. Add Discord.Net to your .csproj +> [!TIP] +Don't forget to add the package source to a [NuGet.Config file] if +you're installing from the developer feed. + +1. Create a new project for your bot. +2. Add `Discord.Net` to your .csproj. [!code-xml[Sample .csproj](samples/project.csproj)] -> [!TIP] -Don't forget to add the package source to a [NuGet.Config file](#configuring-nuget-without-visual-studio) if you're installing from the -developer feed. +[NuGet.Config file]: #configuring-nuget-without-visual-studio # Compiling from Source @@ -90,8 +93,8 @@ In order to compile Discord.Net, you require the following: - [Visual Studio 2017](https://www.visualstudio.com/) - [.NET Core SDK 1.0](https://www.microsoft.com/net/download/core#/sdk) -The .NET Core and Docker (Preview) workload is required during Visual Studio -installation. +The .NET Core and Docker (Preview) workload is required during Visual +Studio installation. ### Using Command Line @@ -101,26 +104,27 @@ installation. ## Installing on .NET Standard 1.1 -For applications targeting a runtime corresponding with .NET Standard 1.1 or 1.2, -the builtin WebSocket and UDP provider will not work. For applications which -utilize a WebSocket connection to Discord (WebSocket or RPC), third-party -provider packages will need to be installed and configured. +For applications targeting a runtime corresponding with .NET Standard +1.1 or 1.2, the builtin WebSocket and UDP provider will not work. For +applications which utilize a WebSocket connection to Discord +(WebSocket or RPC), third-party provider packages will need to be +installed and configured. -First, install the following packages through NuGet, or compile yourself, if -you prefer: +First, install the following packages through NuGet, or compile +yourself, if you prefer: - Discord.Net.Providers.WS4Net - Discord.Net.Providers.UDPClient -Note that `Discord.Net.Providers.UDPClient` is _only_ required if your bot will -be utilizing voice chat. +Note that `Discord.Net.Providers.UDPClient` is _only_ required if your +bot will be utilizing voice chat. -Next, you will need to configure your [DiscordSocketClient] to use these custom -providers over the default ones. +Next, you will need to configure your [DiscordSocketClient] to use +these custom providers over the default ones. -To do this, set the `WebSocketProvider` and optionally `UdpSocketProvider` -properties on the [DiscordSocketConfig] that you are passing into your -client. +To do this, set the `WebSocketProvider` and the optional +`UdpSocketProvider` properties on the [DiscordSocketConfig] that you +are passing into your client. [!code-csharp[NET Standard 1.1 Example](samples/netstd11.cs)] @@ -129,13 +133,14 @@ client. ## Configuring NuGet without Visual Studio -If you plan on deploying your bot or developing outside of Visual Studio, you -will need to create a local NuGet configuration file for your project. +If you plan on deploying your bot or developing outside of Visual +Studio, you will need to create a local NuGet configuration file for +your project. -To do this, create a file named `nuget.config` alongside the root of your -application, where the project solution is located. +To do this, create a file named `nuget.config` alongside the root of +your application, where the project solution is located. -Paste the following snippets into this configuration file, adding any additional -feeds as necessary. +Paste the following snippets into this configuration file, adding any +additional feeds as necessary. [!code-xml[NuGet Configuration](samples/nuget.config)] diff --git a/docs/guides/getting_started/intro.md b/docs/guides/getting_started/intro.md index 837814511..db086df21 100644 --- a/docs/guides/getting_started/intro.md +++ b/docs/guides/getting_started/intro.md @@ -13,42 +13,46 @@ diverse commands later, but for now, it is a good starting point. Before you can begin writing your bot, it is necessary to create a bot account on Discord. -1. Visit the [Discord Applications Portal] -2. Create a New Application +1. Visit the [Discord Applications Portal]. +2. Create a New Application. 3. Give the application a name (this will be the bot's initial username). -4. Create the Application -![Step 4](images/intro-create-app.png) -5. In the application review page, click **Create a Bot User** -![Step 5](images/intro-create-bot.png) -6. Confirm the popup -7. If this bot will be public, check 'Public Bot'. -**Do not tick any other options!** +4. Create the Application. + + ![Step 4](images/intro-create-app.png) + +5. In the application review page, click **Create a Bot User**. + + ![Step 5](images/intro-create-bot.png) + +6. Confirm the popup. +7. If this bot will be public, check "Public Bot." **Do not tick any +other options!** [Discord Applications Portal]: https://discordapp.com/developers/applications/me ## Adding your bot to a server -Bots **can not** use invite links, they must be explicitly invited +Bots **cannot** use invite links, they must be explicitly invited through the OAuth2 flow. -1. Open your bot's application on the [Discord Applications Portal] +1. Open your bot's application on the [Discord Applications Portal]. 2. Retrieve the app's **Client ID**. - -![Step 2](images/intro-client-id.png) - + + ![Step 2](images/intro-client-id.png) + 3. Create an OAuth2 authorization URL `https://discordapp.com/oauth2/authorize?client_id=&scope=bot` -4. Open the authorization URL in your browser -5. Select a server - ->[!NOTE] -Only servers where you have the `MANAGE_SERVER` permission will be -present in this list. +4. Open the authorization URL in your browser. +5. Select a server. +6. Click on authorize. + + >[!NOTE] + Only servers where you have the `MANAGE_SERVER` permission will be + present in this list. + + ![Step 6](images/intro-add-bot.png) -6. Click authorize - -![Step 6](images/intro-add-bot.png) ## Connecting to Discord @@ -57,10 +61,10 @@ do that now. (see the [Installing](installing.md) section) ### Async -Discord.Net uses .NET's Task-based Asynchronous Pattern ([TAP]) +Discord.Net uses .NET's [Task-based Asynchronous Pattern (TAP)] extensively - nearly every operation is asynchronous. -It is highly recommended that these operations be awaited in a +It is highly recommended that these operations are awaited in a properly established async context whenever possible. Establishing an async context can be problematic, but not hard. @@ -70,27 +74,29 @@ async main. [!code-csharp[Async Context](samples/intro/async-context.cs)] -As a result of this, your program will now start, and immidiately -jump into an async context. This will allow us later on to create a -connection to Discord, without needing to worry about setting up the +As a result of this, your program will now start and immediately +jump into an async context. This will allow us to create a connection +to Discord later on without needing to worry about setting up the correct async implementation. >[!TIP] If your application throws any exceptions within an async context, -they will be thrown all the way back up to the first non-async method. -Since our first non-async method is the program's Main method, this +they will be thrown all the way back up to the first non-async method; +since our first non-async method is the program's `Main` method, this means that **all** unhandled exceptions will be thrown up there, which will crash your application. Discord.Net will prevent exceptions in event handlers from crashing your program, but any exceptions in your async main **will** cause the application to crash. +[Task-based Asynchronous Pattern (TAP)]: https://docs.microsoft.com/en-us/dotnet/articles/csharp/async + ### Creating a logging method Before we create and configure a Discord client, we will add a method to handle Discord.Net's log events. To allow agnostic support of as many log providers as possible, we -log information through a Log event, with a proprietary LogMessage +log information through a `Log` event with a proprietary `LogMessage` parameter. See the [API Documentation] for this event. If you are using your own logging framework, this is where you would @@ -99,10 +105,12 @@ the Console. [!code-csharp[Async Context](samples/intro/logging.cs)] +[API Documentation]: xref:Discord.Rest.BaseDiscordClient#Discord_Rest_BaseDiscordClient_Log + ### Creating a Discord Client Finally, we can create a connection to Discord. Since we are writing -a bot, we will be using a [DiscordSocketClient], along with socket +a bot, we will be using a [DiscordSocketClient] along with socket entities. See the [terminology](terminology.md) if you're unsure of the differences. @@ -110,22 +118,24 @@ To do so, create an instance of [DiscordSocketClient] in your async main, passing in a configuration object only if necessary. For most users, the default will work fine. -Before connecting, we should hook the client's log event to the +Before connecting, we should hook the client's `Log` event to the log handler that was just created. Events in Discord.Net work similarly to other events in C#, so hook this event the way that you typically would. -Next, you will need to 'login to Discord' with the `LoginAsync` method. +Next, you will need to "login to Discord" with the `LoginAsync` +method. You may create a variable to hold your bot's token (this can be found on your bot's application page on the [Discord Applications Portal]). + ![Token](images/intro-token.png) >[!IMPORTANT] Your bot's token can be used to gain total access to your bot, so -**do __NOT__ share this token with anyone!** It may behoove you to -store this token in an external file if you plan on distributing the -source code for your bot. +**do __NOT__ share this token with anyone else!** It may behoove you +to store this token in an external file if you plan on distributing +the source code for your bot. We may now invoke the client's `StartAsync` method, which will start connection/reconnection logic. It is important to note that @@ -134,14 +144,9 @@ start connection/reconnection logic. It is important to note that Any methods that rely on the client's state should go in an event handler. ->[!NOTE] -Connection logic is incomplete as of the current build. Events will -soon be added to indicate when the client's state is ready for use; -(rewrite this section when possible) - Finally, we will want to block the async main method from returning until after the application is exited. To do this, we can await an -infinite delay, or any other blocking method, such as reading from +infinite delay or any other blocking method, such as reading from the console. The following lines can now be added: @@ -154,51 +159,55 @@ online in Discord. >[!TIP] Encountering a `PlatformNotSupportedException` when starting your bot? This means that you are targeting a platform where .NET's default -WebSocket client is not supported. Refer to the [installing guide] +WebSocket client is not supported. Refer to the [installation guide] for how to fix this. -[TAP]: https://docs.microsoft.com/en-us/dotnet/articles/csharp/async -[API Documentation]: xref:Discord.Rest.BaseDiscordClient#Discord_Rest_BaseDiscordClient_Log [DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient -[installing guide]: installing.md#installing-on-net-standard-11 +[installation guide]: installing.md#installing-on-net-standard-11 ### Handling a 'ping' +>[!WARNING] +Please note that this is *not* a proper way to create a command. +Use the `CommandService` provided by the library instead, as explained +in the [Command Guide] section. + Now that we have learned how to open a connection to Discord, we can begin handling messages that users are sending. To start out, our bot will listen for any message where the content -is equal to `!ping`, and respond back with `Pong!`. +is equal to `!ping` and respond back with "Pong!". -Since we want to listen for new messages, the event to hook in to +Since we want to listen for new messages, the event to hook into is [MessageReceived]. In your program, add a method that matches the signature of the -MessageReceived event - it must be a method (`Func`) that returns the -type `Task`, and takes a single parameter, a [SocketMessage]. Also, +`MessageReceived` event - it must be a method (`Func`) that returns +the type `Task` and takes a single parameter, a [SocketMessage]. Also, since we will be sending data to Discord in this method, we will flag it as `async`. -In this method, we will add an `if` block, to determine if the message +In this method, we will add an `if` block to determine if the message content fits the rules of our scenario - recall that it must be equal to `!ping`. Inside the branch of this condition, we will want to send a message -back to the channel from which the message came - `Pong!`. To find the -channel, look for the `Channel` property on the message parameter. +back to the channel from which the message comes from - "Pong!". To +find the channel, look for the `Channel` property on the message +parameter. Next, we will want to send a message to this channel. Since the channel object is of type [SocketMessageChannel], we can invoke the `SendMessageAsync` instance method. For the message content, send back -a string containing 'Pong!'. +a string containing "Pong!". You should have now added the following lines: [!code-csharp[Message](samples/intro/message.cs)] -Now, your first bot is complete. You may continue to add on to this -if you desire, but for any bot that will be carrying out multiple -commands, it is strongly encouraged to use the command framework, as +Now your first bot is complete. You may continue to add on to this +if you desire, but for any bots that will be carrying out multiple +commands, it is strongly recommended to use the command framework as shown below. For your reference, you may view the [completed program]. @@ -207,13 +216,14 @@ For your reference, you may view the [completed program]. [SocketMessage]: xref:Discord.WebSocket.SocketMessage [SocketMessageChannel]: xref:Discord.WebSocket.ISocketMessageChannel [completed program]: samples/intro/complete.cs +[Command Guide]: ../commands/commands.md # Building a bot with commands This section will show you how to write a program that is ready for -[commands](commands/commands.md). Note that this will not be explaining _how_ -to write commands or services, it will only be covering the general -structure. +[Commands](../commands/commands.md). Note that we will not be +explaining _how_ to write Commands or Services, it will only be +covering the general structure. For reference, view an [annotated example] of this structure. @@ -224,4 +234,4 @@ should be to separate the program (initialization and command handler), the modules (handle commands), and the services (persistent storage, pure functions, data manipulation). -**todo:** diagram of bot structure +**todo:** diagram of bot structure \ No newline at end of file diff --git a/docs/guides/getting_started/samples/intro/message.cs b/docs/guides/getting_started/samples/intro/message.cs index d3cda46e5..d6fd90778 100644 --- a/docs/guides/getting_started/samples/intro/message.cs +++ b/docs/guides/getting_started/samples/intro/message.cs @@ -1,7 +1,7 @@ public async Task MainAsync() { // client.Log ... - client.MessageReceived += MessageReceived; + _client.MessageReceived += MessageReceived; // ... } @@ -11,4 +11,4 @@ private async Task MessageReceived(SocketMessage message) { await message.Channel.SendMessageAsync("Pong!"); } -} \ No newline at end of file +} diff --git a/docs/guides/getting_started/samples/intro/structure.cs b/docs/guides/getting_started/samples/intro/structure.cs index 00ce7a6c9..bdfc12b67 100644 --- a/docs/guides/getting_started/samples/intro/structure.cs +++ b/docs/guides/getting_started/samples/intro/structure.cs @@ -1,5 +1,6 @@ using System; using System.Reflection; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Discord; @@ -8,12 +9,6 @@ using Discord.WebSocket; class Program { - private readonly DiscordSocketClient _client; - - // Keep the CommandService and IServiceCollection around for use with commands. - private readonly IServiceCollection _map = new ServiceCollection(); - private readonly CommandService _commands = new CommandService(); - // Program entry point static void Main(string[] args) { @@ -22,6 +17,13 @@ class Program new Program().MainAsync().GetAwaiter().GetResult(); } + private readonly DiscordSocketClient _client; + + // Keep the CommandService and IServiceCollection around for use with commands. + // These two types require you install the Discord.Net.Commands package. + private readonly IServiceCollection _map = new ServiceCollection(); + private readonly CommandService _commands = new CommandService(); + private Program() { _client = new DiscordSocketClient(new DiscordSocketConfig @@ -48,7 +50,6 @@ class Program // that ask for a Func. private static Task Logger(LogMessage message) { - var cc = Console.ForegroundColor; switch (message.Severity) { case LogSeverity.Critical: @@ -66,8 +67,8 @@ class Program Console.ForegroundColor = ConsoleColor.DarkGray; break; } - Console.WriteLine($"{DateTime.Now,-19} [{message.Severity,8}] {message.Source}: {message.Message}"); - Console.ForegroundColor = cc; + Console.WriteLine($"{DateTime.Now,-19} [{message.Severity,8}] {message.Source}: {message.Message} {message.Exception}"); + Console.ResetColor(); // If you get an error saying 'CompletedTask' doesn't exist, // your project is targeting .NET 4.5.2 or lower. You'll need @@ -80,7 +81,7 @@ class Program private async Task MainAsync() { - // Centralize the logic for commands into a seperate method. + // Centralize the logic for commands into a separate method. await InitCommands(); // Login and connect. @@ -88,7 +89,7 @@ class Program await _client.StartAsync(); // Wait infinitely so your bot actually stays connected. - await Task.Delay(-1); + await Task.Delay(Timeout.Infinite); } private IServiceProvider _services; @@ -105,10 +106,11 @@ class Program _services = _map.BuildServiceProvider(); // Either search the program and add all Module classes that can be found. - // Module classes *must* be marked 'public' or they will be ignored. + // Module classes MUST be marked 'public' or they will be ignored. await _commands.AddModulesAsync(Assembly.GetEntryAssembly()); // Or add Modules manually if you prefer to be a little more explicit: await _commands.AddModuleAsync(); + // Note that the first one is 'Modules' (plural) and the second is 'Module' (singular). // Subscribe a handler to see if a message invokes a command. _client.MessageReceived += HandleCommandAsync; @@ -120,6 +122,11 @@ class Program var msg = arg as SocketUserMessage; if (msg == null) return; + // We don't want the bot to respond to itself or other bots. + // NOTE: Selfbots should invert this first check and remove the second + // as they should ONLY be allowed to respond to messages from the same account. + if (msg.Author.Id == _client.CurrentUser.Id || msg.Author.IsBot) return; + // Create a number to track where the prefix ends and the command begins int pos = 0; // Replace the '!' with whatever character @@ -132,7 +139,7 @@ class Program var context = new SocketCommandContext(_client, msg); // Execute the command. (result does not indicate a return value, - // rather an object stating if the command executed succesfully). + // rather an object stating if the command executed successfully). var result = await _commands.ExecuteAsync(context, pos, _services); // Uncomment the following lines if you want the bot diff --git a/docs/guides/getting_started/samples/project.csproj b/docs/guides/getting_started/samples/project.csproj index 8daf71877..feb0b0c40 100644 --- a/docs/guides/getting_started/samples/project.csproj +++ b/docs/guides/getting_started/samples/project.csproj @@ -7,7 +7,7 @@ - + diff --git a/docs/guides/getting_started/terminology.md b/docs/guides/getting_started/terminology.md index a51003e34..74f7a6259 100644 --- a/docs/guides/getting_started/terminology.md +++ b/docs/guides/getting_started/terminology.md @@ -7,32 +7,34 @@ title: Terminology ## Preface -Most terms for objects remain the same between 0.9 and 1.0. The major difference is that the ``Server`` is now called ``Guild``, to stay in line with Discord internally +Most terms for objects remain the same between 0.9 and 1.0. The major +difference is that the ``Server`` is now called ``Guild`` to stay in +line with Discord internally. ## Implementation Specific Entities -Discord.Net 1.0 is split into a core library, and three different -implementations - Discord.Net.Core, Discord.Net.Rest, Discord.Net.Rpc, -and Discord.Net.WebSockets. +Discord.Net 1.0 is split into a core library and three different +implementations - `Discord.Net.Core`, `Discord.Net.Rest`, +`Discord.Net.Rpc`, and `Discord.Net.WebSockets`. -As a bot developer, you will only need to use Discord.Net.WebSockets, +As a bot developer, you will only need to use `Discord.Net.WebSockets`, but you should be aware of the differences between them. -`Discord.Net.Core` provides a set of interfaces that model Discord's +`Discord.Net.Core` provides a set of interfaces that models Discord's API. These interfaces are consistent throughout all implementations of Discord.Net, and if you are writing an implementation-agnostic library or addon, you can rely on the core interfaces to ensure that your addon will run on all platforms. `Discord.Net.Rest` provides a set of concrete classes to be used -**strictly** with the REST portion of Discord's API. Entities in -this implementation are prefixed with `Rest`, e.g. `RestChannel`. +**strictly** with the REST portion of Discord's API. Entities in this +implementation are prefixed with `Rest` (e.g. `RestChannel`). -`Discord.Net.Rpc` provides a set of concrete classes that are used with -Discord's RPC API. Entities in this implementation are prefixed with -`Rpc`, e.g. `RpcChannel`. +`Discord.Net.Rpc` provides a set of concrete classes that are used +with Discord's RPC API. Entities in this implementation are prefixed +with `Rpc` (e.g. `RpcChannel`). -`Discord.Net.WebSocket` provides a set of concrete classes that are used -primarily with Discord's WebSocket API, or entities that are kept in -cache. When developing bots, you will be using this implementation. All -entities are prefixed with `Socket`, e.g. `SocketChannel`. \ No newline at end of file +`Discord.Net.WebSocket` provides a set of concrete classes that are +used primarily with Discord's WebSocket API or entities that are kept +in cache. When developing bots, you will be using this implementation. +All entities are prefixed with `Socket` (e.g. `SocketChannel`). \ No newline at end of file diff --git a/docs/guides/voice/samples/audio_create_ffmpeg.cs b/docs/guides/voice/samples/audio_create_ffmpeg.cs index e24af088b..dda560efe 100644 --- a/docs/guides/voice/samples/audio_create_ffmpeg.cs +++ b/docs/guides/voice/samples/audio_create_ffmpeg.cs @@ -1,11 +1,10 @@ private Process CreateStream(string path) { - var ffmpeg = new ProcessStartInfo + return Process.Start(new ProcessStartInfo { FileName = "ffmpeg", - Arguments = $"-i {path} -ac 2 -f s16le -ar 48000 pipe:1", + Arguments = $"-hide_banner -loglevel panic -i \"{path}\" -ac 2 -f s16le -ar 48000 pipe:1", UseShellExecute = false, RedirectStandardOutput = true, - }; - return Process.Start(ffmpeg); -} \ No newline at end of file + }); +} diff --git a/docs/guides/voice/samples/audio_ffmpeg.cs b/docs/guides/voice/samples/audio_ffmpeg.cs index b9430ac11..d36fbbc20 100644 --- a/docs/guides/voice/samples/audio_ffmpeg.cs +++ b/docs/guides/voice/samples/audio_ffmpeg.cs @@ -1,9 +1,11 @@ private async Task SendAsync(IAudioClient client, string path) { // Create FFmpeg using the previous example - var ffmpeg = CreateStream(path); - var output = ffmpeg.StandardOutput.BaseStream; - var discord = client.CreatePCMStream(AudioApplication.Mixed); - await output.CopyToAsync(discord); - await discord.FlushAsync(); + using (var ffmpeg = CreateStream(path)) + using (var output = ffmpeg.StandardOutput.BaseStream) + using (var discord = client.CreatePCMStream(AudioApplication.Mixed)) + { + try { await output.CopyToAsync(discord); } + finally { await discord.FlushAsync(); } + } } diff --git a/docs/guides/voice/samples/joining_audio.cs b/docs/guides/voice/samples/joining_audio.cs index 0cc36978a..4cec67540 100644 --- a/docs/guides/voice/samples/joining_audio.cs +++ b/docs/guides/voice/samples/joining_audio.cs @@ -7,4 +7,4 @@ public async Task JoinChannel(IVoiceChannel channel = null) // For the next step with transmitting audio, you would want to pass this Audio Client in to a service. var audioClient = await channel.ConnectAsync(); -} \ No newline at end of file +} diff --git a/docs/guides/voice/sending-voice.md b/docs/guides/voice/sending-voice.md index c3ec8d9d7..024a98b95 100644 --- a/docs/guides/voice/sending-voice.md +++ b/docs/guides/voice/sending-voice.md @@ -17,7 +17,7 @@ when developing on .NET Core, this is where you execute `dotnet run` from; typically the same directory as your csproj). For Windows Users, precompiled binaries are available for your -convienence [here](https://discord.foxbot.me/binaries/) +convienence [here](https://discord.foxbot.me/binaries/). For Linux Users, you will need to compile [Sodium] and [Opus] from source, or install them from your package manager. @@ -31,7 +31,7 @@ Joining a channel is the first step to sending audio, and will return an [IAudioClient] to send data with. To join a channel, simply await [ConnectAsync] on any instance of an -@Discord.IVoiceChannel. +@Discord.IAudioChannel. [!code-csharp[Joining a Channel](samples/joining_audio.cs)] @@ -44,7 +44,7 @@ guild. To switch channels within a guild, invoke [ConnectAsync] on another voice channel in the guild. [IAudioClient]: xref:Discord.Audio.IAudioClient -[ConnectAsync]: xref:Discord.IVoiceChannel#Discord_IVoiceChannel_ConnectAsync +[ConnectAsync]: xref:Discord.IAudioChannel#Discord_IAudioChannel_ConnectAsync_Action_IAudioClient__ ## Transmitting Audio @@ -84,7 +84,7 @@ Channels should be left at `2`, unless you specified a different value for `-ac 2` when creating FFmpeg. [AudioOutStream]: xref:Discord.Audio.AudioOutStream -[IAudioClient.CreatePCMStream]: xref:Discord.Audio.IAudioClient#Discord_Audio_IAudioClient_CreatePCMStream_System_Int32_System_Int32_System_Nullable_System_Int32__System_Int32_ +[IAudioClient.CreatePCMStream]: xref:Discord.Audio.IAudioClient#Discord_Audio_IAudioClient_CreateDirectPCMStream_Discord_Audio_AudioApplication_System_Nullable_System_Int32__System_Int32_ Finally, audio will need to be piped from FFmpeg's stdout into your AudioOutStream. This step can be as complex as you'd like it to be, but diff --git a/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs b/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs index 49dae6080..209822583 100644 --- a/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs @@ -7,6 +7,6 @@ namespace Discord.Commands [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true, Inherited = true)] public abstract class ParameterPreconditionAttribute : Attribute { - public abstract Task CheckPermissions(ICommandContext context, ParameterInfo parameter, object value, IServiceProvider services); + public abstract Task CheckPermissionsAsync(ICommandContext context, ParameterInfo parameter, object value, IServiceProvider services); } } \ No newline at end of file diff --git a/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs b/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs index 3727510d9..367adebf0 100644 --- a/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs @@ -8,11 +8,11 @@ namespace Discord.Commands { /// /// Specify a group that this precondition belongs to. Preconditions of the same group require only one - /// of the preconditions to pass in order to be successful (A || B). Specifying = + /// of the preconditions to pass in order to be successful (A || B). Specifying = /// or not at all will require *all* preconditions to pass, just like normal (A && B). /// public string Group { get; set; } = null; - public abstract Task CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services); + public abstract Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services); } } diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs index b2cd3811c..104252799 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs @@ -41,7 +41,7 @@ namespace Discord.Commands GuildPermission = null; } - public override async Task CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services) + public override async Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) { IGuildUser guildUser = null; if (context.Guild != null) @@ -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/RequireContextAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs index a221eb4a9..5fa0fb1b9 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs @@ -38,7 +38,7 @@ namespace Discord.Commands Contexts = contexts; } - public override Task CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services) + public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) { bool isValid = false; diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs index b3cf25365..c8e3bfa82 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs @@ -9,7 +9,7 @@ namespace Discord.Commands [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class RequireNsfwAttribute : PreconditionAttribute { - public override Task CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services) + public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) { if (context.Channel is ITextChannel text && text.IsNsfw) return Task.FromResult(PreconditionResult.FromSuccess()); diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs index 0852ce39c..e370aeec4 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs @@ -1,4 +1,5 @@ -using System; +#pragma warning disable CS0618 +using System; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -11,7 +12,7 @@ namespace Discord.Commands [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class RequireOwnerAttribute : PreconditionAttribute { - public override async Task CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services) + public override async Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) { switch (context.Client.TokenType) { diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs index f5e3a9fc5..14121f35b 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs @@ -42,7 +42,7 @@ namespace Discord.Commands GuildPermission = null; } - public override Task CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services) + public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) { var guildUser = context.User as IGuildUser; @@ -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 394f8589d..28e36d54d 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 ParseArgs(CommandInfo command, ICommandContext context, IServiceProvider services, string input, int startPos) + public static async Task ParseArgsAsync(CommandInfo command, ICommandContext context, IServiceProvider services, string input, int startPos) { ParameterInfo curParam = null; StringBuilder argBuilder = new StringBuilder(input.Length); @@ -111,7 +111,7 @@ namespace Discord.Commands if (curParam == null) return ParseResult.FromError(CommandError.BadArgCount, "The input text has too many parameters."); - var typeReaderResult = await curParam.Parse(context, argString, services).ConfigureAwait(false); + var typeReaderResult = await curParam.ParseAsync(context, argString, services).ConfigureAwait(false); if (!typeReaderResult.IsSuccess && typeReaderResult.Error != CommandError.MultipleMatches) return ParseResult.FromError(typeReaderResult); @@ -134,7 +134,7 @@ namespace Discord.Commands if (curParam != null && curParam.IsRemainder) { - var typeReaderResult = await curParam.Parse(context, argBuilder.ToString(), services).ConfigureAwait(false); + var typeReaderResult = await curParam.ParseAsync(context, argBuilder.ToString(), services).ConfigureAwait(false); if (!typeReaderResult.IsSuccess) return ParseResult.FromError(typeReaderResult); argList.Add(typeReaderResult); diff --git a/src/Discord.Net.Commands/CommandServiceConfig.cs b/src/Discord.Net.Commands/CommandServiceConfig.cs index 5dcd50cd8..b53b0248c 100644 --- a/src/Discord.Net.Commands/CommandServiceConfig.cs +++ b/src/Discord.Net.Commands/CommandServiceConfig.cs @@ -2,17 +2,18 @@ { public class CommandServiceConfig { - /// The default RunMode commands should have, if one is not specified on the Command attribute or builder. + /// Gets or sets the default RunMode commands should have, if one is not specified on the Command attribute or builder. public RunMode DefaultRunMode { get; set; } = RunMode.Sync; public char SeparatorChar { get; set; } = ' '; - /// Should commands be case-sensitive? + + /// Determines whether commands should be case-sensitive. public bool CaseSensitiveCommands { get; set; } = false; /// Gets or sets the minimum log level severity that will be sent to the Log event. public LogSeverity LogLevel { get; set; } = LogSeverity.Info; - /// Gets or sets whether RunMode.Sync commands should push exceptions up to the caller. + /// Determines whether RunMode.Sync commands should push exceptions up to the caller. public bool ThrowOnError { get; set; } = true; } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index c94be525f..6bb621f94 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -78,7 +78,7 @@ namespace Discord.Commands { foreach (PreconditionAttribute precondition in preconditionGroup) { - var result = await precondition.CheckPermissions(context, this, services).ConfigureAwait(false); + var result = await precondition.CheckPermissionsAsync(context, this, services).ConfigureAwait(false); if (!result.IsSuccess) return result; } @@ -87,7 +87,7 @@ namespace Discord.Commands { var results = new List(); foreach (PreconditionAttribute precondition in preconditionGroup) - results.Add(await precondition.CheckPermissions(context, this, services).ConfigureAwait(false)); + results.Add(await precondition.CheckPermissionsAsync(context, this, services).ConfigureAwait(false)); if (!results.Any(p => p.IsSuccess)) return PreconditionGroupResult.FromError($"{type} precondition group {preconditionGroup.Key} failed.", results); @@ -117,7 +117,7 @@ namespace Discord.Commands return ParseResult.FromError(preconditionResult); string input = searchResult.Text.Substring(startIndex); - return await CommandParser.ParseArgs(this, context, services, input, 0).ConfigureAwait(false); + return await CommandParser.ParseArgsAsync(this, context, services, input, 0).ConfigureAwait(false); } public Task ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) @@ -163,11 +163,11 @@ namespace Discord.Commands switch (RunMode) { case RunMode.Sync: //Always sync - return await ExecuteAsyncInternal(context, args, services).ConfigureAwait(false); + return await ExecuteAsyncInternalAsync(context, args, services).ConfigureAwait(false); case RunMode.Async: //Always async var t2 = Task.Run(async () => { - await ExecuteAsyncInternal(context, args, services).ConfigureAwait(false); + await ExecuteAsyncInternalAsync(context, args, services).ConfigureAwait(false); }); break; } @@ -179,7 +179,7 @@ namespace Discord.Commands } } - private async Task ExecuteAsyncInternal(ICommandContext context, object[] args, IServiceProvider services) + private async Task ExecuteAsyncInternalAsync(ICommandContext context, object[] args, IServiceProvider services) { await Module.Service._cmdLogger.DebugAsync($"Executing {GetLogText(context)}").ConfigureAwait(false); try @@ -199,10 +199,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/Info/ParameterInfo.cs b/src/Discord.Net.Commands/Info/ParameterInfo.cs index e417b1ab6..4a56415e5 100644 --- a/src/Discord.Net.Commands/Info/ParameterInfo.cs +++ b/src/Discord.Net.Commands/Info/ParameterInfo.cs @@ -48,7 +48,7 @@ namespace Discord.Commands foreach (var precondition in Preconditions) { - var result = await precondition.CheckPermissions(context, this, arg, services).ConfigureAwait(false); + var result = await precondition.CheckPermissionsAsync(context, this, arg, services).ConfigureAwait(false); if (!result.IsSuccess) return result; } @@ -56,10 +56,10 @@ namespace Discord.Commands return PreconditionResult.FromSuccess(); } - public async Task Parse(ICommandContext context, string input, IServiceProvider services = null) + public async Task ParseAsync(ICommandContext context, string input, IServiceProvider services = null) { services = services ?? EmptyServiceProvider.Instance; - return await _reader.Read(context, input, services).ConfigureAwait(false); + return await _reader.ReadAsync(context, input, services).ConfigureAwait(false); } public override string ToString() => Name; diff --git a/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs b/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs index 72c62282e..3136eb2cb 100644 --- a/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs @@ -9,7 +9,7 @@ namespace Discord.Commands internal class ChannelTypeReader : TypeReader where T : class, IChannel { - public override async Task Read(ICommandContext context, string input, IServiceProvider services) + public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { if (context.Guild != null) { diff --git a/src/Discord.Net.Commands/Readers/EnumTypeReader.cs b/src/Discord.Net.Commands/Readers/EnumTypeReader.cs index 383b8e63c..c097e6189 100644 --- a/src/Discord.Net.Commands/Readers/EnumTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/EnumTypeReader.cs @@ -44,7 +44,7 @@ namespace Discord.Commands _enumsByValue = byValueBuilder.ToImmutable(); } - public override Task Read(ICommandContext context, string input, IServiceProvider services) + public override Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { object enumValue; diff --git a/src/Discord.Net.Commands/Readers/MessageTypeReader.cs b/src/Discord.Net.Commands/Readers/MessageTypeReader.cs index 895713e4f..fe576c3c6 100644 --- a/src/Discord.Net.Commands/Readers/MessageTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/MessageTypeReader.cs @@ -7,7 +7,7 @@ namespace Discord.Commands internal class MessageTypeReader : TypeReader where T : class, IMessage { - public override async Task Read(ICommandContext context, string input, IServiceProvider services) + public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { ulong id; diff --git a/src/Discord.Net.Commands/Readers/NullableTypeReader.cs b/src/Discord.Net.Commands/Readers/NullableTypeReader.cs index 07976fb69..109689e15 100644 --- a/src/Discord.Net.Commands/Readers/NullableTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/NullableTypeReader.cs @@ -24,11 +24,11 @@ namespace Discord.Commands _baseTypeReader = baseTypeReader; } - public override async Task Read(ICommandContext context, string input, IServiceProvider services) + public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { if (string.Equals(input, "null", StringComparison.OrdinalIgnoreCase) || string.Equals(input, "nothing", StringComparison.OrdinalIgnoreCase)) return TypeReaderResult.FromSuccess(new T?()); - return await _baseTypeReader.Read(context, input, services); ; + return await _baseTypeReader.ReadAsync(context, input, services); } } } diff --git a/src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs b/src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs index 2656741f0..b19a6bd69 100644 --- a/src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs @@ -30,7 +30,7 @@ namespace Discord.Commands _score = score; } - public override Task Read(ICommandContext context, string input, IServiceProvider services) + public override Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { if (_tryParse(input, out T value)) return Task.FromResult(TypeReaderResult.FromSuccess(new TypeReaderValue(value, _score))); diff --git a/src/Discord.Net.Commands/Readers/RoleTypeReader.cs b/src/Discord.Net.Commands/Readers/RoleTypeReader.cs index 17786e6f0..703374c05 100644 --- a/src/Discord.Net.Commands/Readers/RoleTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/RoleTypeReader.cs @@ -9,7 +9,7 @@ namespace Discord.Commands internal class RoleTypeReader : TypeReader where T : class, IRole { - public override Task Read(ICommandContext context, string input, IServiceProvider services) + public override Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { ulong id; diff --git a/src/Discord.Net.Commands/Readers/TypeReader.cs b/src/Discord.Net.Commands/Readers/TypeReader.cs index 2c4644376..af45a0aac 100644 --- a/src/Discord.Net.Commands/Readers/TypeReader.cs +++ b/src/Discord.Net.Commands/Readers/TypeReader.cs @@ -5,6 +5,6 @@ namespace Discord.Commands { public abstract class TypeReader { - public abstract Task Read(ICommandContext context, string input, IServiceProvider services); + public abstract Task ReadAsync(ICommandContext context, string input, IServiceProvider services); } } diff --git a/src/Discord.Net.Commands/Readers/UserTypeReader.cs b/src/Discord.Net.Commands/Readers/UserTypeReader.cs index c71dac2d2..ca337aaf6 100644 --- a/src/Discord.Net.Commands/Readers/UserTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/UserTypeReader.cs @@ -10,7 +10,7 @@ namespace Discord.Commands internal class UserTypeReader : TypeReader where T : class, IUser { - public override async Task Read(ICommandContext context, string input, IServiceProvider services) + 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? diff --git a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs index 90d1175d4..c9841cb15 100644 --- a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs @@ -24,7 +24,7 @@ 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); diff --git a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs index a465b3ad8..a1df5b4c7 100644 --- a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs @@ -29,10 +29,6 @@ namespace Discord CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// Gets a collection of pinned messages in this channel. Task> GetPinnedMessagesAsync(RequestOptions options = null); - /// Bulk deletes multiple messages. - Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null); - /// Bulk deletes multiple messages. - Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null); /// Broadcasts the "user is typing" message to all users in this channel, lasting 10 seconds. Task TriggerTypingAsync(RequestOptions options = null); diff --git a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs index b2b7e491f..be4dd0260 100644 --- a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; namespace Discord @@ -11,6 +12,11 @@ namespace Discord /// Gets the current topic for this text channel. string Topic { get; } + /// Bulk deletes multiple messages. + Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null); + /// Bulk deletes multiple messages. + Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null); + /// Modifies this text channel. Task ModifyAsync(Action func, RequestOptions options = null); } 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/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 94eb67b61..3882bcf96 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -120,5 +120,14 @@ 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 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 a93f02497..3e438f43f 100644 --- a/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs @@ -1,40 +1,36 @@ -namespace Discord +using System; + +namespace Discord { - public enum ChannelPermission : byte + [FlagsAttribute] + public enum ChannelPermission : ulong { - //General - CreateInstantInvite = 0, - //KickMembers = 1, - //BanMembers = 2, - //Administrator = 3, - ManageChannel = 4, - //ManageGuild = 5, + // General + CreateInstantInvite = 0x00_00_00_01, + ManageChannels = 0x00_00_00_10, - //Text - AddReactions = 6, - ReadMessages = 10, - SendMessages = 11, - SendTTSMessages = 12, - ManageMessages = 13, - EmbedLinks = 14, - AttachFiles = 15, - ReadMessageHistory = 16, - MentionEveryone = 17, - UseExternalEmojis = 18, + // Text + AddReactions = 0x00_00_00_40, + ReadMessages = 0x00_00_04_00, + SendMessages = 0x00_00_08_00, + SendTTSMessages = 0x00_00_10_00, + ManageMessages = 0x00_00_20_00, + EmbedLinks = 0x00_00_40_00, + AttachFiles = 0x00_00_80_00, + ReadMessageHistory = 0x00_01_00_00, + MentionEveryone = 0x00_02_00_00, + UseExternalEmojis = 0x00_04_00_00, - //Voice - Connect = 20, - Speak = 21, - MuteMembers = 22, - DeafenMembers = 23, - MoveMembers = 24, - UseVAD = 25, + // Voice + Connect = 0x00_10_00_00, + Speak = 0x00_20_00_00, + MuteMembers = 0x00_40_00_00, + DeafenMembers = 0x00_80_00_00, + MoveMembers = 0x01_00_00_00, + UseVAD = 0x02_00_00_00, - //General2 - //ChangeNickname = 26, - //ManageNicknames = 27, - ManagePermissions = 28, - ManageWebhooks = 29, - //ManageEmojis = 30 + // More General + ManageRoles = 0x10_00_00_00, + ManageWebhooks = 0x20_00_00_00, } } diff --git a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs index 94596e0e6..4c11d0db0 100644 --- a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs @@ -10,7 +10,7 @@ namespace Discord /// Gets a blank ChannelPermissions that grants no permissions. public static readonly ChannelPermissions None = new ChannelPermissions(); /// Gets a ChannelPermissions that grants all permissions for text channels. - public static readonly ChannelPermissions Text = new ChannelPermissions(0b00100_0000000_1111111110001_010001); + public static readonly ChannelPermissions Text = new ChannelPermissions(0b01100_0000000_1111111110001_010001); /// Gets a ChannelPermissions that grants all permissions for voice channels. public static readonly ChannelPermissions Voice = new ChannelPermissions(0b00100_1111110_0000000000000_010001); /// Gets a ChannelPermissions that grants all permissions for direct message channels. @@ -36,7 +36,7 @@ namespace Discord /// If True, a user may create invites. public bool CreateInstantInvite => Permissions.GetValue(RawValue, ChannelPermission.CreateInstantInvite); /// If True, a user may create, delete and modify this channel. - public bool ManageChannel => Permissions.GetValue(RawValue, ChannelPermission.ManageChannel); + public bool ManageChannel => Permissions.GetValue(RawValue, ChannelPermission.ManageChannels); /// If true, a user may add reactions. public bool AddReactions => Permissions.GetValue(RawValue, ChannelPermission.AddReactions); @@ -72,8 +72,8 @@ namespace Discord /// If True, a user may use voice-activity-detection rather than push-to-talk. public bool UseVAD => Permissions.GetValue(RawValue, ChannelPermission.UseVAD); - /// If True, a user may adjust permissions. This also implictly grants all other permissions. - public bool ManagePermissions => Permissions.GetValue(RawValue, ChannelPermission.ManagePermissions); + /// If True, a user may adjust role permissions. This also implictly grants all other permissions. + public bool ManageRoles => Permissions.GetValue(RawValue, ChannelPermission.ManageRoles); /// If True, a user may edit the webhooks for this channel. public bool ManageWebhooks => Permissions.GetValue(RawValue, ChannelPermission.ManageWebhooks); @@ -85,12 +85,12 @@ namespace Discord bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, bool? useExternalEmojis = null, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, - bool? moveMembers = null, bool? useVoiceActivation = null, bool? managePermissions = null, bool? manageWebhooks = null) + bool? moveMembers = null, bool? useVoiceActivation = null, bool? manageRoles = null, bool? manageWebhooks = null) { ulong value = initialValue; Permissions.SetValue(ref value, createInstantInvite, ChannelPermission.CreateInstantInvite); - Permissions.SetValue(ref value, manageChannel, ChannelPermission.ManageChannel); + Permissions.SetValue(ref value, manageChannel, ChannelPermission.ManageChannels); Permissions.SetValue(ref value, addReactions, ChannelPermission.AddReactions); Permissions.SetValue(ref value, readMessages, ChannelPermission.ReadMessages); Permissions.SetValue(ref value, sendMessages, ChannelPermission.SendMessages); @@ -107,7 +107,7 @@ namespace Discord Permissions.SetValue(ref value, deafenMembers, ChannelPermission.DeafenMembers); Permissions.SetValue(ref value, moveMembers, ChannelPermission.MoveMembers); Permissions.SetValue(ref value, useVoiceActivation, ChannelPermission.UseVAD); - Permissions.SetValue(ref value, managePermissions, ChannelPermission.ManagePermissions); + Permissions.SetValue(ref value, manageRoles, ChannelPermission.ManageRoles); Permissions.SetValue(ref value, manageWebhooks, ChannelPermission.ManageWebhooks); RawValue = value; @@ -119,10 +119,10 @@ namespace Discord bool readMessages = false, bool sendMessages = false, bool sendTTSMessages = false, bool manageMessages = false, bool embedLinks = false, bool attachFiles = false, bool readMessageHistory = false, bool mentionEveryone = false, bool useExternalEmojis = false, bool connect = false, bool speak = false, bool muteMembers = false, bool deafenMembers = false, - bool moveMembers = false, bool useVoiceActivation = false, bool managePermissions = false, bool manageWebhooks = false) + bool moveMembers = false, bool useVoiceActivation = false, bool manageRoles = false, bool manageWebhooks = false) : this(0, createInstantInvite, manageChannel, addReactions, readMessages, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, - speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, managePermissions, manageWebhooks) + speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, manageRoles, manageWebhooks) { } /// Creates a new ChannelPermissions from this one, changing the provided non-null permissions. @@ -131,21 +131,21 @@ namespace Discord bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, bool useExternalEmojis = false, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, - bool? moveMembers = null, bool? useVoiceActivation = null, bool? managePermissions = null, bool? manageWebhooks = null) + bool? moveMembers = null, bool? useVoiceActivation = null, bool? manageRoles = null, bool? manageWebhooks = null) => new ChannelPermissions(RawValue, createInstantInvite, manageChannel, addReactions, readMessages, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, - speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, managePermissions, manageWebhooks); + speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, manageRoles, manageWebhooks); public bool Has(ChannelPermission permission) => Permissions.GetValue(RawValue, permission); public List ToList() { var perms = new List(); - ulong x = 1; - for (byte i = 0; i < Permissions.MaxBits; i++, x <<= 1) + for (byte i = 0; i < Permissions.MaxBits; i++) { - if ((RawValue & x) != 0) - perms.Add((ChannelPermission)i); + ulong flag = ((ulong)1 << i); + if ((RawValue & flag) != 0) + perms.Add((ChannelPermission)flag); } return perms; } diff --git a/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs b/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs index 3975c1b8b..8469fd304 100644 --- a/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs +++ b/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs @@ -1,40 +1,44 @@ -namespace Discord +using System; + +namespace Discord { - public enum GuildPermission : byte + [FlagsAttribute] + public enum GuildPermission : ulong { - //General - CreateInstantInvite = 0, - KickMembers = 1, - BanMembers = 2, - Administrator = 3, - ManageChannels = 4, - ManageGuild = 5, + // General + CreateInstantInvite = 0x00_00_00_01, + KickMembers = 0x00_00_00_02, + BanMembers = 0x00_00_00_04, + Administrator = 0x00_00_00_08, + ManageChannels = 0x00_00_00_10, + ManageGuild = 0x00_00_00_20, - //Text - AddReactions = 6, - ReadMessages = 10, - SendMessages = 11, - SendTTSMessages = 12, - ManageMessages = 13, - EmbedLinks = 14, - AttachFiles = 15, - ReadMessageHistory = 16, - MentionEveryone = 17, - UseExternalEmojis = 18, + // Text + AddReactions = 0x00_00_00_40, + ViewAuditLog = 0x00_00_00_80, + ReadMessages = 0x00_00_04_00, + SendMessages = 0x00_00_08_00, + SendTTSMessages = 0x00_00_10_00, + ManageMessages = 0x00_00_20_00, + EmbedLinks = 0x00_00_40_00, + AttachFiles = 0x00_00_80_00, + ReadMessageHistory = 0x00_01_00_00, + MentionEveryone = 0x00_02_00_00, + UseExternalEmojis = 0x00_04_00_00, - //Voice - Connect = 20, - Speak = 21, - MuteMembers = 22, - DeafenMembers = 23, - MoveMembers = 24, - UseVAD = 25, + // Voice + Connect = 0x00_10_00_00, + Speak = 0x00_20_00_00, + MuteMembers = 0x00_40_00_00, + DeafenMembers = 0x00_80_00_00, + MoveMembers = 0x01_00_00_00, + UseVAD = 0x02_00_00_00, - //General2 - ChangeNickname = 26, - ManageNicknames = 27, - ManageRoles = 28, - ManageWebhooks = 29, - ManageEmojis = 30 + // General 2 + ChangeNickname = 0x04_00_00_00, + ManageNicknames = 0x08_00_00_00, + ManageRoles = 0x10_00_00_00, + ManageWebhooks = 0x20_00_00_00, + ManageEmojis = 0x40_00_00_00 } } diff --git a/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs index c5f1efab0..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_0111111110001_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; } @@ -28,9 +28,12 @@ namespace Discord public bool ManageChannels => Permissions.GetValue(RawValue, GuildPermission.ManageChannels); /// If True, a user may adjust guild properties. public bool ManageGuild => Permissions.GetValue(RawValue, GuildPermission.ManageGuild); - + /// If true, a user may add reactions. public bool AddReactions => Permissions.GetValue(RawValue, GuildPermission.AddReactions); + /// If true, a user may view the audit log. + public bool ViewAuditLog => Permissions.GetValue(RawValue, GuildPermission.ViewAuditLog); + /// If True, a user may join channels. public bool ReadMessages => Permissions.GetValue(RawValue, GuildPermission.ReadMessages); /// If True, a user may send messages. @@ -77,13 +80,13 @@ namespace Discord /// Creates a new GuildPermissions with the provided packed value. public GuildPermissions(ulong rawValue) { RawValue = rawValue; } - private GuildPermissions(ulong initialValue, bool? createInstantInvite = null, bool? kickMembers = null, - bool? banMembers = null, bool? administrator = null, bool? manageChannel = null, bool? manageGuild = null, - bool? addReactions = null, - bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, - bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, - bool? userExternalEmojis = null, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, - bool? moveMembers = null, bool? useVoiceActivation = null, bool? changeNickname = null, bool? manageNicknames = null, + private GuildPermissions(ulong initialValue, bool? createInstantInvite = null, bool? kickMembers = null, + bool? banMembers = null, bool? administrator = null, bool? manageChannels = null, bool? manageGuild = null, + bool? addReactions = null, bool? viewAuditLog = null, + bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, + bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, + bool? useExternalEmojis = null, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, + bool? moveMembers = null, bool? useVoiceActivation = null, bool? changeNickname = null, bool? manageNicknames = null, bool? manageRoles = null, bool? manageWebhooks = null, bool? manageEmojis = null) { ulong value = initialValue; @@ -92,9 +95,10 @@ namespace Discord Permissions.SetValue(ref value, banMembers, GuildPermission.BanMembers); Permissions.SetValue(ref value, kickMembers, GuildPermission.KickMembers); Permissions.SetValue(ref value, administrator, GuildPermission.Administrator); - Permissions.SetValue(ref value, manageChannel, GuildPermission.ManageChannels); + Permissions.SetValue(ref value, manageChannels, GuildPermission.ManageChannels); Permissions.SetValue(ref value, manageGuild, GuildPermission.ManageGuild); Permissions.SetValue(ref value, addReactions, GuildPermission.AddReactions); + Permissions.SetValue(ref value, viewAuditLog, GuildPermission.ViewAuditLog); Permissions.SetValue(ref value, readMessages, GuildPermission.ReadMessages); Permissions.SetValue(ref value, sendMessages, GuildPermission.SendMessages); Permissions.SetValue(ref value, sendTTSMessages, GuildPermission.SendTTSMessages); @@ -103,7 +107,7 @@ namespace Discord Permissions.SetValue(ref value, attachFiles, GuildPermission.AttachFiles); Permissions.SetValue(ref value, readMessageHistory, GuildPermission.ReadMessageHistory); Permissions.SetValue(ref value, mentionEveryone, GuildPermission.MentionEveryone); - Permissions.SetValue(ref value, userExternalEmojis, GuildPermission.UseExternalEmojis); + Permissions.SetValue(ref value, useExternalEmojis, GuildPermission.UseExternalEmojis); Permissions.SetValue(ref value, connect, GuildPermission.Connect); Permissions.SetValue(ref value, speak, GuildPermission.Speak); Permissions.SetValue(ref value, muteMembers, GuildPermission.MuteMembers); @@ -120,42 +124,50 @@ namespace Discord } /// Creates a new GuildPermissions with the provided permissions. - public GuildPermissions(bool createInstantInvite = false, bool kickMembers = false, + public GuildPermissions(bool createInstantInvite = false, bool kickMembers = false, bool banMembers = false, bool administrator = false, bool manageChannels = false, bool manageGuild = false, - bool addReactions = false, + bool addReactions = false, bool viewAuditLog = false, bool readMessages = false, bool sendMessages = false, bool sendTTSMessages = false, bool manageMessages = false, bool embedLinks = false, bool attachFiles = false, bool readMessageHistory = false, bool mentionEveryone = false, bool useExternalEmojis = false, bool connect = false, bool speak = false, bool muteMembers = false, bool deafenMembers = false, - bool moveMembers = false, bool useVoiceActivation = false, bool? changeNickname = false, bool? manageNicknames = false, + bool moveMembers = false, bool useVoiceActivation = false, bool? changeNickname = false, bool? manageNicknames = false, bool manageRoles = false, bool manageWebhooks = false, bool manageEmojis = false) - : this(0, createInstantInvite, manageRoles, kickMembers, banMembers, manageChannels, manageGuild, addReactions, - readMessages, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, mentionEveryone, useExternalEmojis, connect, - manageWebhooks, manageEmojis) { } + : this(0, createInstantInvite: createInstantInvite, manageRoles: manageRoles, kickMembers: kickMembers, banMembers: banMembers, + administrator: administrator, manageChannels: manageChannels, manageGuild: manageGuild, addReactions: addReactions, + viewAuditLog: viewAuditLog, readMessages: readMessages, sendMessages: sendMessages, sendTTSMessages: sendTTSMessages, + manageMessages: manageMessages, embedLinks: embedLinks, attachFiles: attachFiles, readMessageHistory: readMessageHistory, + mentionEveryone: mentionEveryone, useExternalEmojis: useExternalEmojis, connect: connect, speak: speak, muteMembers: muteMembers, + deafenMembers: deafenMembers, moveMembers: moveMembers, useVoiceActivation: useVoiceActivation, changeNickname: changeNickname, + manageNicknames: manageNicknames, manageWebhooks: manageWebhooks, manageEmojis: manageEmojis) + { } /// Creates a new GuildPermissions from this one, changing the provided non-null permissions. - public GuildPermissions Modify(bool? createInstantInvite = null, bool? kickMembers = null, + public GuildPermissions Modify(bool? createInstantInvite = null, bool? kickMembers = null, bool? banMembers = null, bool? administrator = null, bool? manageChannels = null, bool? manageGuild = null, - bool? addReactions = null, + bool? addReactions = null, bool? viewAuditLog = null, bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, bool? useExternalEmojis = null, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, - bool? moveMembers = null, bool? useVoiceActivation = null, bool? changeNickname = null, bool? manageNicknames = null, + bool? moveMembers = null, bool? useVoiceActivation = null, bool? changeNickname = null, bool? manageNicknames = null, bool? manageRoles = null, bool? manageWebhooks = null, bool? manageEmojis = null) - => new GuildPermissions(RawValue, createInstantInvite, manageRoles, kickMembers, banMembers, manageChannels, manageGuild, addReactions, - readMessages, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, mentionEveryone, useExternalEmojis, connect, - speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, changeNickname, manageNicknames, manageRoles, - manageWebhooks, manageEmojis); + => new GuildPermissions(RawValue, createInstantInvite, kickMembers, banMembers, administrator, manageChannels, manageGuild, addReactions, + viewAuditLog, readMessages, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, + readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, moveMembers, + useVoiceActivation, changeNickname, manageNicknames, manageRoles, manageWebhooks, manageEmojis); public bool Has(GuildPermission permission) => Permissions.GetValue(RawValue, permission); public List ToList() { var perms = new List(); - ulong x = 1; - for (byte i = 0; i < Permissions.MaxBits; i++, x <<= 1) + + // bitwise operations on raw value + // each of the GuildPermissions increments by 2^i from 0 to MaxBits + for (byte i = 0; i < Permissions.MaxBits; i++) { - if ((RawValue & x) != 0) - perms.Add((GuildPermission)i); + ulong flag = ((ulong)1 << i); + if ((RawValue & flag) != 0) + perms.Add((GuildPermission)flag); } return perms; } diff --git a/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs b/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs index c3f8b2bab..c3e296e2c 100644 --- a/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs @@ -23,7 +23,7 @@ namespace Discord /// If Allowed, a user may create invites. public PermValue CreateInstantInvite => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.CreateInstantInvite); /// If Allowed, a user may create, delete and modify this channel. - public PermValue ManageChannel => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ManageChannel); + public PermValue ManageChannel => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ManageChannels); /// If Allowed, a user may add reactions. public PermValue AddReactions => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.AddReactions); /// If Allowed, a user may join channels. @@ -58,8 +58,8 @@ namespace Discord /// If Allowed, a user may use voice-activity-detection rather than push-to-talk. public PermValue UseVAD => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.UseVAD); - /// If Allowed, a user may adjust permissions. This also implictly grants all other permissions. - public PermValue ManagePermissions => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ManagePermissions); + /// If Allowed, a user may adjust role permissions. This also implictly grants all other permissions. + public PermValue ManageRoles => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ManageRoles); /// If True, a user may edit the webhooks for this channel. public PermValue ManageWebhooks => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ManageWebhooks); @@ -75,11 +75,11 @@ namespace Discord PermValue? readMessages = null, PermValue? sendMessages = null, PermValue? sendTTSMessages = null, PermValue? manageMessages = null, PermValue? embedLinks = null, PermValue? attachFiles = null, PermValue? readMessageHistory = null, PermValue? mentionEveryone = null, PermValue? useExternalEmojis = null, PermValue? connect = null, PermValue? speak = null, PermValue? muteMembers = null, - PermValue? deafenMembers = null, PermValue? moveMembers = null, PermValue? useVoiceActivation = null, PermValue? managePermissions = null, + PermValue? deafenMembers = null, PermValue? moveMembers = null, PermValue? useVoiceActivation = null, PermValue? manageRoles = null, PermValue? manageWebhooks = null) { Permissions.SetValue(ref allowValue, ref denyValue, createInstantInvite, ChannelPermission.CreateInstantInvite); - Permissions.SetValue(ref allowValue, ref denyValue, manageChannel, ChannelPermission.ManageChannel); + Permissions.SetValue(ref allowValue, ref denyValue, 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, sendMessages, ChannelPermission.SendMessages); @@ -96,7 +96,7 @@ namespace Discord Permissions.SetValue(ref allowValue, ref denyValue, deafenMembers, ChannelPermission.DeafenMembers); Permissions.SetValue(ref allowValue, ref denyValue, moveMembers, ChannelPermission.MoveMembers); Permissions.SetValue(ref allowValue, ref denyValue, useVoiceActivation, ChannelPermission.UseVAD); - Permissions.SetValue(ref allowValue, ref denyValue, managePermissions, ChannelPermission.ManagePermissions); + Permissions.SetValue(ref allowValue, ref denyValue, manageRoles, ChannelPermission.ManageRoles); Permissions.SetValue(ref allowValue, ref denyValue, manageWebhooks, ChannelPermission.ManageWebhooks); AllowValue = allowValue; @@ -109,10 +109,10 @@ namespace Discord PermValue readMessages = PermValue.Inherit, PermValue sendMessages = PermValue.Inherit, PermValue sendTTSMessages = PermValue.Inherit, PermValue manageMessages = PermValue.Inherit, PermValue embedLinks = PermValue.Inherit, PermValue attachFiles = PermValue.Inherit, PermValue readMessageHistory = PermValue.Inherit, PermValue mentionEveryone = PermValue.Inherit, PermValue useExternalEmojis = PermValue.Inherit, PermValue connect = PermValue.Inherit, PermValue speak = PermValue.Inherit, PermValue muteMembers = PermValue.Inherit, PermValue deafenMembers = PermValue.Inherit, - PermValue moveMembers = PermValue.Inherit, PermValue useVoiceActivation = PermValue.Inherit, PermValue managePermissions = PermValue.Inherit, PermValue manageWebhooks = PermValue.Inherit) + PermValue moveMembers = PermValue.Inherit, PermValue useVoiceActivation = PermValue.Inherit, PermValue manageRoles = PermValue.Inherit, PermValue manageWebhooks = PermValue.Inherit) : this(0, 0, createInstantInvite, manageChannel, addReactions, readMessages, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, - moveMembers, useVoiceActivation, managePermissions, manageWebhooks) { } + moveMembers, useVoiceActivation, manageRoles, manageWebhooks) { } /// Creates a new OverwritePermissions from this one, changing the provided non-null permissions. public OverwritePermissions Modify(PermValue? createInstantInvite = null, PermValue? manageChannel = null, @@ -120,30 +120,31 @@ namespace Discord PermValue? readMessages = null, PermValue? sendMessages = null, PermValue? sendTTSMessages = null, PermValue? manageMessages = null, PermValue? embedLinks = null, PermValue? attachFiles = null, PermValue? readMessageHistory = null, PermValue? mentionEveryone = null, PermValue? useExternalEmojis = null, PermValue? connect = null, PermValue? speak = null, PermValue? muteMembers = null, PermValue? deafenMembers = null, - PermValue? moveMembers = null, PermValue? useVoiceActivation = null, PermValue? managePermissions = null, PermValue? manageWebhooks = null) + PermValue? moveMembers = null, PermValue? useVoiceActivation = null, PermValue? manageRoles = null, PermValue? manageWebhooks = null) => new OverwritePermissions(AllowValue, DenyValue, createInstantInvite, manageChannel, addReactions, readMessages, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, - moveMembers, useVoiceActivation, managePermissions, manageWebhooks); + moveMembers, useVoiceActivation, manageRoles, manageWebhooks); public List ToAllowList() { var perms = new List(); - ulong x = 1; - for (byte i = 0; i < Permissions.MaxBits; i++, x <<= 1) + for (byte i = 0; i < Permissions.MaxBits; i++) { - if ((AllowValue & x) != 0) - perms.Add((ChannelPermission)i); + // first operand must be long or ulong to shift >31 bits + ulong flag = ((ulong)1 << i); + if ((AllowValue & flag) != 0) + perms.Add((ChannelPermission)flag); } return perms; } public List ToDenyList() { var perms = new List(); - ulong x = 1; - for (byte i = 0; i < Permissions.MaxBits; i++, x <<= 1) + for (byte i = 0; i < Permissions.MaxBits; i++) { - if ((DenyValue & x) != 0) - perms.Add((ChannelPermission)i); + ulong flag = ((ulong)1 << i); + if ((DenyValue & flag) != 0) + perms.Add((ChannelPermission)flag); } return perms; } diff --git a/src/Discord.Net.Core/TokenType.cs b/src/Discord.Net.Core/TokenType.cs index e19197cd6..c351b1c19 100644 --- a/src/Discord.Net.Core/TokenType.cs +++ b/src/Discord.Net.Core/TokenType.cs @@ -1,7 +1,10 @@ -namespace Discord +using System; + +namespace Discord { public enum TokenType { + [Obsolete("User logins are being deprecated and may result in a ToS strike against your account - please see https://github.com/RogueException/Discord.Net/issues/827")] User, Bearer, Bot, diff --git a/src/Discord.Net.Core/Utils/Optional.cs b/src/Discord.Net.Core/Utils/Optional.cs index df927b7ea..eb3cbdca2 100644 --- a/src/Discord.Net.Core/Utils/Optional.cs +++ b/src/Discord.Net.Core/Utils/Optional.cs @@ -10,7 +10,7 @@ namespace Discord public static Optional Unspecified => default(Optional); private readonly T _value; - /// Gets the value for this paramter. + /// Gets the value for this parameter. public T Value { get diff --git a/src/Discord.Net.Core/Utils/Permissions.cs b/src/Discord.Net.Core/Utils/Permissions.cs index c2b7e83ea..a7de90623 100644 --- a/src/Discord.Net.Core/Utils/Permissions.cs +++ b/src/Discord.Net.Core/Utils/Permissions.cs @@ -7,84 +7,84 @@ namespace Discord public const int MaxBits = 53; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static PermValue GetValue(ulong allow, ulong deny, ChannelPermission bit) - => GetValue(allow, deny, (byte)bit); + public static PermValue GetValue(ulong allow, ulong deny, ChannelPermission flag) + => GetValue(allow, deny, (ulong)flag); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static PermValue GetValue(ulong allow, ulong deny, GuildPermission bit) - => GetValue(allow, deny, (byte)bit); + public static PermValue GetValue(ulong allow, ulong deny, GuildPermission flag) + => GetValue(allow, deny, (ulong)flag); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static PermValue GetValue(ulong allow, ulong deny, byte bit) + public static PermValue GetValue(ulong allow, ulong deny, ulong flag) { - if (HasBit(allow, bit)) + if (HasFlag(allow, flag)) return PermValue.Allow; - else if (HasBit(deny, bit)) + else if (HasFlag(deny, flag)) return PermValue.Deny; else return PermValue.Inherit; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool GetValue(ulong value, ChannelPermission bit) - => GetValue(value, (byte)bit); + public static bool GetValue(ulong value, ChannelPermission flag) + => GetValue(value, (ulong)flag); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool GetValue(ulong value, GuildPermission bit) - => GetValue(value, (byte)bit); + public static bool GetValue(ulong value, GuildPermission flag) + => GetValue(value, (ulong)flag); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool GetValue(ulong value, byte bit) => HasBit(value, bit); + public static bool GetValue(ulong value, ulong flag) => HasFlag(value, flag); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetValue(ref ulong rawValue, bool? value, ChannelPermission bit) - => SetValue(ref rawValue, value, (byte)bit); + public static void SetValue(ref ulong rawValue, bool? value, ChannelPermission flag) + => SetValue(ref rawValue, value, (ulong)flag); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetValue(ref ulong rawValue, bool? value, GuildPermission bit) - => SetValue(ref rawValue, value, (byte)bit); + public static void SetValue(ref ulong rawValue, bool? value, GuildPermission flag) + => SetValue(ref rawValue, value, (ulong)flag); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetValue(ref ulong rawValue, bool? value, byte bit) + public static void SetValue(ref ulong rawValue, bool? value, ulong flag) { if (value.HasValue) { if (value == true) - SetBit(ref rawValue, bit); + SetFlag(ref rawValue, flag); else - UnsetBit(ref rawValue, bit); + UnsetFlag(ref rawValue, flag); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetValue(ref ulong allow, ref ulong deny, PermValue? value, ChannelPermission bit) - => SetValue(ref allow, ref deny, value, (byte)bit); + public static void SetValue(ref ulong allow, ref ulong deny, PermValue? value, ChannelPermission flag) + => SetValue(ref allow, ref deny, value, (ulong)flag); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetValue(ref ulong allow, ref ulong deny, PermValue? value, GuildPermission bit) - => SetValue(ref allow, ref deny, value, (byte)bit); + public static void SetValue(ref ulong allow, ref ulong deny, PermValue? value, GuildPermission flag) + => SetValue(ref allow, ref deny, value, (ulong)flag); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetValue(ref ulong allow, ref ulong deny, PermValue? value, byte bit) + public static void SetValue(ref ulong allow, ref ulong deny, PermValue? value, ulong flag) { if (value.HasValue) { switch (value) { case PermValue.Allow: - SetBit(ref allow, bit); - UnsetBit(ref deny, bit); + SetFlag(ref allow, flag); + UnsetFlag(ref deny, flag); break; case PermValue.Deny: - UnsetBit(ref allow, bit); - SetBit(ref deny, bit); + UnsetFlag(ref allow, flag); + SetFlag(ref deny, flag); break; default: - UnsetBit(ref allow, bit); - UnsetBit(ref deny, bit); + UnsetFlag(ref allow, flag); + UnsetFlag(ref deny, flag); break; } } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool HasBit(ulong value, byte bit) => (value & (1U << bit)) != 0; + private static bool HasFlag(ulong value, ulong flag) => (value & flag) != 0; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetBit(ref ulong value, byte bit) => value |= (1U << bit); + public static void SetFlag(ref ulong value, ulong flag) => value |= flag; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void UnsetBit(ref ulong value, byte bit) => value &= ~(1U << bit); + public static void UnsetFlag(ref ulong value, ulong flag) => value &= ~flag; public static ChannelPermissions ToChannelPerms(IGuildChannel channel, ulong guildPermissions) => new ChannelPermissions(guildPermissions & ChannelPermissions.All(channel).RawValue); diff --git a/src/Discord.Net.Core/Utils/Preconditions.cs b/src/Discord.Net.Core/Utils/Preconditions.cs index bec8de9dc..300f584e4 100644 --- a/src/Discord.Net.Core/Utils/Preconditions.cs +++ b/src/Discord.Net.Core/Utils/Preconditions.cs @@ -188,7 +188,8 @@ namespace Discord var minimum = SnowflakeUtils.ToSnowflake(DateTimeOffset.UtcNow.Subtract(TimeSpan.FromDays(14))); for (var i = 0; i < collection.Length; i++) { - if (collection[i] <= minimum) + if (collection[i] == 0) continue; + if (collection[i] <= minimum) throw new ArgumentOutOfRangeException(name, "Messages must be younger than two weeks old."); } } 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/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/BaseDiscordClient.cs b/src/Discord.Net.Rest/BaseDiscordClient.cs index 87f974dc2..47a946f20 100644 --- a/src/Discord.Net.Rest/BaseDiscordClient.cs +++ b/src/Discord.Net.Rest/BaseDiscordClient.cs @@ -85,7 +85,8 @@ namespace Discord.Rest await _loggedInEvent.InvokeAsync().ConfigureAwait(false); } - internal virtual Task OnLoginAsync(TokenType tokenType, string token) { return Task.Delay(0); } + internal virtual Task OnLoginAsync(TokenType tokenType, string token) + => Task.Delay(0); /// public async Task LogoutAsync() @@ -110,7 +111,8 @@ namespace Discord.Rest await _loggedOutEvent.InvokeAsync().ConfigureAwait(false); } - internal virtual Task OnLogoutAsync() { return Task.Delay(0); } + internal virtual Task OnLogoutAsync() + => Task.Delay(0); internal virtual void Dispose(bool disposing) { @@ -127,7 +129,8 @@ namespace Discord.Rest ConnectionState IDiscordClient.ConnectionState => ConnectionState.Disconnected; ISelfUser IDiscordClient.CurrentUser => CurrentUser; - Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) { throw new NotSupportedException(); } + Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) + => throw new NotSupportedException(); Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); @@ -148,7 +151,8 @@ namespace Discord.Rest => Task.FromResult(null); Task> IDiscordClient.GetGuildsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(ImmutableArray.Create()); - Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) { throw new NotSupportedException(); } + Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) + => throw new NotSupportedException(); Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index a6c42782a..4e65b19d2 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -1,4 +1,5 @@ #pragma warning disable CS1591 +#pragma warning disable CS0618 using Discord.API.Rest; using Discord.Net; using Discord.Net.Converters; @@ -1065,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) { diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index 4c265f3aa..0d272a004 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -184,7 +184,7 @@ namespace Discord.Rest return RestUserMessage.Create(client, channel, client.CurrentUser, model); } - public static async Task DeleteMessagesAsync(IMessageChannel channel, BaseDiscordClient client, + public static async Task DeleteMessagesAsync(ITextChannel channel, BaseDiscordClient client, IEnumerable messageIds, RequestOptions options) { var msgs = messageIds.ToArray(); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs index 342e57717..04cc5a937 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs @@ -48,8 +48,8 @@ namespace Discord.Rest string IChannel.Name => null; Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(null); //Overriden + => Task.FromResult(null); //Overridden IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => AsyncEnumerable.Empty>(); //Overriden + => AsyncEnumerable.Empty>(); //Overridden } } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs index 8a31da3f1..d41441967 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs @@ -72,11 +72,6 @@ namespace Discord.Rest public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); - public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); - public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); - public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); public IDisposable EnterTypingState(RequestOptions options = null) diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs index 44c118fee..aa5c0f7dc 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs @@ -85,11 +85,6 @@ namespace Discord.Rest public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); - public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); - public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); - public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); public IDisposable EnterTypingState(RequestOptions options = null) diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs index 2f6b6a174..7656063e1 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs @@ -123,7 +123,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; @@ -158,14 +158,14 @@ namespace Discord.Rest => await RemovePermissionOverwriteAsync(user, options).ConfigureAwait(false); IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => AsyncEnumerable.Empty>(); //Overriden //Overriden in Text/Voice + => AsyncEnumerable.Empty>(); //Overridden //Overridden in Text/Voice Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(null); //Overriden in Text/Voice + => Task.FromResult(null); //Overridden in Text/Voice //IChannel IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => AsyncEnumerable.Empty>(); //Overriden in Text/Voice + => AsyncEnumerable.Empty>(); //Overridden in Text/Voice Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(null); //Overriden in Text/Voice + => Task.FromResult(null); //Overridden in Text/Voice } } diff --git a/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs index 7043c8c76..eb807423f 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs @@ -42,11 +42,6 @@ namespace Discord.Rest public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); - public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); - public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); - public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); public IDisposable EnterTypingState(RequestOptions options = null) diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index a33f27243..6248b7941 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -262,5 +262,46 @@ namespace Discord.Rest model = await client.ApiClient.BeginGuildPruneAsync(guild.Id, args, options).ConfigureAwait(false); return model.Pruned; } + + //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 f0e95a9a6..423c2119d 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -267,6 +267,16 @@ namespace Discord.Rest 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; diff --git a/src/Discord.Net.Rest/Extensions/EmbedBuilderExtensions.cs b/src/Discord.Net.Rest/Extensions/EmbedBuilderExtensions.cs index cee9a136e..2eb4ed473 100644 --- a/src/Discord.Net.Rest/Extensions/EmbedBuilderExtensions.cs +++ b/src/Discord.Net.Rest/Extensions/EmbedBuilderExtensions.cs @@ -1,3 +1,5 @@ +using System; + namespace Discord { public static class EmbedBuilderExtensions @@ -19,5 +21,38 @@ namespace Discord public static EmbedBuilder WithAuthor(this EmbedBuilder builder, IGuildUser user) => builder.WithAuthor($"{user.Nickname ?? user.Username}#{user.Discriminator}", user.GetAvatarUrl()); + + public static EmbedBuilder ToEmbedBuilder(this IEmbed embed) + { + if (embed.Type != EmbedType.Rich) + throw new InvalidOperationException($"Only {nameof(EmbedType.Rich)} embeds may be built."); + + var builder = new EmbedBuilder + { + Author = new EmbedAuthorBuilder + { + Name = embed.Author?.Name, + IconUrl = embed.Author?.IconUrl, + Url = embed.Author?.Url + }, + Color = embed.Color ?? Color.Default, + Description = embed.Description, + Footer = new EmbedFooterBuilder + { + Text = embed.Footer?.Text, + IconUrl = embed.Footer?.IconUrl + }, + ImageUrl = embed.Image?.Url, + ThumbnailUrl = embed.Thumbnail?.Url, + Timestamp = embed.Timestamp, + Title = embed.Title, + Url = embed.Url + }; + + foreach (var field in embed.Fields) + builder.AddField(field.Name, field.Value, field.Inline); + + return builder; + } } } 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/RpcDMChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs index da9bce700..42e590aca 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs @@ -53,11 +53,6 @@ namespace Discord.Rpc public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); - public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); - public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); - public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); public IDisposable EnterTypingState(RequestOptions options = null) diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs index d449688a4..1c9355047 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs @@ -56,11 +56,6 @@ namespace Discord.Rpc public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); - public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); - public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); - public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); public IDisposable EnterTypingState(RequestOptions options = null) diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs index 983f30106..576a0489c 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs @@ -52,7 +52,7 @@ 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; diff --git a/src/Discord.Net.WebSocket/API/Gateway/IdentifyParams.cs b/src/Discord.Net.WebSocket/API/Gateway/IdentifyParams.cs index e87c58221..af16f22f5 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/IdentifyParams.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/IdentifyParams.cs @@ -13,8 +13,6 @@ namespace Discord.API.Gateway public IDictionary Properties { get; set; } [JsonProperty("large_threshold")] public int LargeThreshold { get; set; } - [JsonProperty("compress")] - public bool UseCompression { get; set; } [JsonProperty("shard")] public Optional ShardingParams { get; set; } } diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs new file mode 100644 index 000000000..e881a7855 --- /dev/null +++ b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs @@ -0,0 +1,193 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.WebSocket +{ + public partial class BaseSocketClient + { + //Channels + /// Fired when a channel is created. + public event Func ChannelCreated + { + add { _channelCreatedEvent.Add(value); } + remove { _channelCreatedEvent.Remove(value); } + } + internal readonly AsyncEvent> _channelCreatedEvent = new AsyncEvent>(); + /// Fired when a channel is destroyed. + public event Func ChannelDestroyed { + add { _channelDestroyedEvent.Add(value); } + remove { _channelDestroyedEvent.Remove(value); } + } + internal readonly AsyncEvent> _channelDestroyedEvent = new AsyncEvent>(); + /// Fired when a channel is updated. + public event Func ChannelUpdated { + add { _channelUpdatedEvent.Add(value); } + remove { _channelUpdatedEvent.Remove(value); } + } + internal readonly AsyncEvent> _channelUpdatedEvent = new AsyncEvent>(); + + //Messages + /// Fired when a message is received. + public event Func MessageReceived { + add { _messageReceivedEvent.Add(value); } + remove { _messageReceivedEvent.Remove(value); } + } + internal readonly AsyncEvent> _messageReceivedEvent = new AsyncEvent>(); + /// Fired when a message is deleted. + public event Func, ISocketMessageChannel, Task> MessageDeleted { + add { _messageDeletedEvent.Add(value); } + remove { _messageDeletedEvent.Remove(value); } + } + internal readonly AsyncEvent, ISocketMessageChannel, Task>> _messageDeletedEvent = new AsyncEvent, ISocketMessageChannel, Task>>(); + /// Fired when a message is updated. + public event Func, SocketMessage, ISocketMessageChannel, Task> MessageUpdated { + add { _messageUpdatedEvent.Add(value); } + remove { _messageUpdatedEvent.Remove(value); } + } + internal readonly AsyncEvent, SocketMessage, ISocketMessageChannel, Task>> _messageUpdatedEvent = new AsyncEvent, SocketMessage, ISocketMessageChannel, Task>>(); + /// Fired when a reaction is added to a message. + public event Func, ISocketMessageChannel, SocketReaction, Task> ReactionAdded { + add { _reactionAddedEvent.Add(value); } + remove { _reactionAddedEvent.Remove(value); } + } + internal readonly AsyncEvent, ISocketMessageChannel, SocketReaction, Task>> _reactionAddedEvent = new AsyncEvent, ISocketMessageChannel, SocketReaction, Task>>(); + /// Fired when a reaction is removed from a message. + public event Func, ISocketMessageChannel, SocketReaction, Task> ReactionRemoved { + add { _reactionRemovedEvent.Add(value); } + remove { _reactionRemovedEvent.Remove(value); } + } + internal readonly AsyncEvent, ISocketMessageChannel, SocketReaction, Task>> _reactionRemovedEvent = new AsyncEvent, ISocketMessageChannel, SocketReaction, Task>>(); + /// Fired when all reactions to a message are cleared. + public event Func, ISocketMessageChannel, Task> ReactionsCleared { + add { _reactionsClearedEvent.Add(value); } + remove { _reactionsClearedEvent.Remove(value); } + } + internal readonly AsyncEvent, ISocketMessageChannel, Task>> _reactionsClearedEvent = new AsyncEvent, ISocketMessageChannel, Task>>(); + + //Roles + /// Fired when a role is created. + public event Func RoleCreated { + add { _roleCreatedEvent.Add(value); } + remove { _roleCreatedEvent.Remove(value); } + } + internal readonly AsyncEvent> _roleCreatedEvent = new AsyncEvent>(); + /// Fired when a role is deleted. + public event Func RoleDeleted { + add { _roleDeletedEvent.Add(value); } + remove { _roleDeletedEvent.Remove(value); } + } + internal readonly AsyncEvent> _roleDeletedEvent = new AsyncEvent>(); + /// Fired when a role is updated. + public event Func RoleUpdated { + add { _roleUpdatedEvent.Add(value); } + remove { _roleUpdatedEvent.Remove(value); } + } + internal readonly AsyncEvent> _roleUpdatedEvent = new AsyncEvent>(); + + //Guilds + /// Fired when the connected account joins a guild. + public event Func JoinedGuild { + add { _joinedGuildEvent.Add(value); } + remove { _joinedGuildEvent.Remove(value); } + } + internal readonly AsyncEvent> _joinedGuildEvent = new AsyncEvent>(); + /// Fired when the connected account leaves a guild. + public event Func LeftGuild { + add { _leftGuildEvent.Add(value); } + remove { _leftGuildEvent.Remove(value); } + } + internal readonly AsyncEvent> _leftGuildEvent = new AsyncEvent>(); + /// Fired when a guild becomes available. + public event Func GuildAvailable { + add { _guildAvailableEvent.Add(value); } + remove { _guildAvailableEvent.Remove(value); } + } + internal readonly AsyncEvent> _guildAvailableEvent = new AsyncEvent>(); + /// Fired when a guild becomes unavailable. + public event Func GuildUnavailable { + add { _guildUnavailableEvent.Add(value); } + remove { _guildUnavailableEvent.Remove(value); } + } + internal readonly AsyncEvent> _guildUnavailableEvent = new AsyncEvent>(); + /// Fired when offline guild members are downloaded. + public event Func GuildMembersDownloaded { + add { _guildMembersDownloadedEvent.Add(value); } + remove { _guildMembersDownloadedEvent.Remove(value); } + } + internal readonly AsyncEvent> _guildMembersDownloadedEvent = new AsyncEvent>(); + /// Fired when a guild is updated. + public event Func GuildUpdated { + add { _guildUpdatedEvent.Add(value); } + remove { _guildUpdatedEvent.Remove(value); } + } + internal readonly AsyncEvent> _guildUpdatedEvent = new AsyncEvent>(); + + //Users + /// Fired when a user joins a guild. + public event Func UserJoined { + add { _userJoinedEvent.Add(value); } + remove { _userJoinedEvent.Remove(value); } + } + internal readonly AsyncEvent> _userJoinedEvent = new AsyncEvent>(); + /// Fired when a user leaves a guild. + public event Func UserLeft { + add { _userLeftEvent.Add(value); } + remove { _userLeftEvent.Remove(value); } + } + internal readonly AsyncEvent> _userLeftEvent = new AsyncEvent>(); + /// Fired when a user is banned from a guild. + public event Func UserBanned { + add { _userBannedEvent.Add(value); } + remove { _userBannedEvent.Remove(value); } + } + internal readonly AsyncEvent> _userBannedEvent = new AsyncEvent>(); + /// Fired when a user is unbanned from a guild. + public event Func UserUnbanned { + add { _userUnbannedEvent.Add(value); } + remove { _userUnbannedEvent.Remove(value); } + } + internal readonly AsyncEvent> _userUnbannedEvent = new AsyncEvent>(); + /// Fired when a user is updated. + public event Func UserUpdated { + add { _userUpdatedEvent.Add(value); } + remove { _userUpdatedEvent.Remove(value); } + } + internal readonly AsyncEvent> _userUpdatedEvent = new AsyncEvent>(); + /// Fired when a guild member is updated, or a member presence is updated. + public event Func GuildMemberUpdated { + add { _guildMemberUpdatedEvent.Add(value); } + remove { _guildMemberUpdatedEvent.Remove(value); } + } + internal readonly AsyncEvent> _guildMemberUpdatedEvent = new AsyncEvent>(); + /// Fired when a user joins, leaves, or moves voice channels. + public event Func UserVoiceStateUpdated { + add { _userVoiceStateUpdatedEvent.Add(value); } + remove { _userVoiceStateUpdatedEvent.Remove(value); } + } + internal readonly AsyncEvent> _userVoiceStateUpdatedEvent = new AsyncEvent>(); + /// Fired when the connected account is updated. + public event Func CurrentUserUpdated { + add { _selfUpdatedEvent.Add(value); } + remove { _selfUpdatedEvent.Remove(value); } + } + internal readonly AsyncEvent> _selfUpdatedEvent = new AsyncEvent>(); + /// Fired when a user starts typing. + public event Func UserIsTyping { + add { _userIsTypingEvent.Add(value); } + remove { _userIsTypingEvent.Remove(value); } + } + internal readonly AsyncEvent> _userIsTypingEvent = new AsyncEvent>(); + /// Fired when a user joins a group channel. + public event Func RecipientAdded { + add { _recipientAddedEvent.Add(value); } + remove { _recipientAddedEvent.Remove(value); } + } + internal readonly AsyncEvent> _recipientAddedEvent = new AsyncEvent>(); + /// Fired when a user is removed from a group channel. + public event Func RecipientRemoved { + add { _recipientRemovedEvent.Add(value); } + remove { _recipientRemovedEvent.Remove(value); } + } + internal readonly AsyncEvent> _recipientRemovedEvent = new AsyncEvent>(); + } +} diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.cs b/src/Discord.Net.WebSocket/BaseSocketClient.cs new file mode 100644 index 000000000..d248285cd --- /dev/null +++ b/src/Discord.Net.WebSocket/BaseSocketClient.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Discord.API; +using Discord.Rest; + +namespace Discord.WebSocket +{ + public abstract partial class BaseSocketClient : BaseDiscordClient, IDiscordClient + { + protected readonly DiscordSocketConfig _baseconfig; + + /// 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; } + + internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; + + public new SocketSelfUser CurrentUser { get => base.CurrentUser as SocketSelfUser; protected set => base.CurrentUser = value; } + public abstract IReadOnlyCollection Guilds { get; } + public abstract IReadOnlyCollection PrivateChannels { get; } + public abstract IReadOnlyCollection VoiceRegions { get; } + + internal BaseSocketClient(DiscordSocketConfig config, DiscordRestApiClient client) + : base(config, client) => _baseconfig = config; + private static DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config) + => new DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent); + + /// + public abstract Task GetApplicationInfoAsync(RequestOptions options = null); + /// + public abstract SocketUser GetUser(ulong id); + /// + public abstract SocketUser GetUser(string username, string discriminator); + /// + public abstract SocketChannel GetChannel(ulong id); + /// + public abstract SocketGuild GetGuild(ulong id); + /// + public abstract RestVoiceRegion GetVoiceRegion(string id); + /// + public abstract Task StartAsync(); + /// + 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 DownloadUsersAsync(IEnumerable guilds); + + /// + public Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null, RequestOptions options = null) + => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon, options ?? RequestOptions.Default); + /// + public Task> GetConnectionsAsync(RequestOptions options = null) + => ClientHelper.GetConnectionsAsync(this, options ?? RequestOptions.Default); + /// + public Task GetInviteAsync(string inviteId, RequestOptions options = null) + => ClientHelper.GetInviteAsync(this, inviteId, options ?? RequestOptions.Default); + + // IDiscordClient + async Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) + => await GetApplicationInfoAsync(options).ConfigureAwait(false); + + Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(GetChannel(id)); + Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode, RequestOptions options) + => Task.FromResult>(PrivateChannels); + + async Task> IDiscordClient.GetConnectionsAsync(RequestOptions options) + => await GetConnectionsAsync(options).ConfigureAwait(false); + + async Task IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options) + => await GetInviteAsync(inviteId, options).ConfigureAwait(false); + + Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(GetGuild(id)); + Task> IDiscordClient.GetGuildsAsync(CacheMode mode, RequestOptions options) + => Task.FromResult>(Guilds); + + async Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) + => await CreateGuildAsync(name, region, jpegIcon, options).ConfigureAwait(false); + + Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(GetUser(id)); + Task IDiscordClient.GetUserAsync(string username, string discriminator, RequestOptions options) + => Task.FromResult(GetUser(username, discriminator)); + + Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) + => Task.FromResult(GetVoiceRegion(id)); + Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) + => Task.FromResult>(VoiceRegions); + } +} diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.Events.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.Events.cs index c52675e70..c9e679669 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.Events.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.Events.cs @@ -3,197 +3,36 @@ using System.Threading.Tasks; namespace Discord.WebSocket { - //TODO: Add event docstrings public partial class DiscordShardedClient { - //Channels - public event Func ChannelCreated + //General + /// Fired when a shard is connected to the Discord gateway. + public event Func ShardConnected { - add { _channelCreatedEvent.Add(value); } - remove { _channelCreatedEvent.Remove(value); } + add { _shardConnectedEvent.Add(value); } + remove { _shardConnectedEvent.Remove(value); } } - private readonly AsyncEvent> _channelCreatedEvent = new AsyncEvent>(); - public event Func ChannelDestroyed + private readonly AsyncEvent> _shardConnectedEvent = new AsyncEvent>(); + /// Fired when a shard is disconnected from the Discord gateway. + public event Func ShardDisconnected { - add { _channelDestroyedEvent.Add(value); } - remove { _channelDestroyedEvent.Remove(value); } + add { _shardDisconnectedEvent.Add(value); } + remove { _shardDisconnectedEvent.Remove(value); } } - private readonly AsyncEvent> _channelDestroyedEvent = new AsyncEvent>(); - public event Func ChannelUpdated + private readonly AsyncEvent> _shardDisconnectedEvent = new AsyncEvent>(); + /// Fired when a guild data for a shard has finished downloading. + public event Func ShardReady { - add { _channelUpdatedEvent.Add(value); } - remove { _channelUpdatedEvent.Remove(value); } + add { _shardReadyEvent.Add(value); } + remove { _shardReadyEvent.Remove(value); } } - private readonly AsyncEvent> _channelUpdatedEvent = new AsyncEvent>(); - - //Messages - public event Func MessageReceived - { - add { _messageReceivedEvent.Add(value); } - remove { _messageReceivedEvent.Remove(value); } - } - private readonly AsyncEvent> _messageReceivedEvent = new AsyncEvent>(); - public event Func, ISocketMessageChannel, Task> MessageDeleted - { - add { _messageDeletedEvent.Add(value); } - remove { _messageDeletedEvent.Remove(value); } - } - private readonly AsyncEvent, ISocketMessageChannel, Task>> _messageDeletedEvent = new AsyncEvent, ISocketMessageChannel, Task>>(); - public event Func, SocketMessage, ISocketMessageChannel, Task> MessageUpdated - { - add { _messageUpdatedEvent.Add(value); } - remove { _messageUpdatedEvent.Remove(value); } - } - private readonly AsyncEvent, SocketMessage, ISocketMessageChannel, Task>> _messageUpdatedEvent = new AsyncEvent, SocketMessage, ISocketMessageChannel, Task>>(); - public event Func, ISocketMessageChannel, SocketReaction, Task> ReactionAdded - { - add { _reactionAddedEvent.Add(value); } - remove { _reactionAddedEvent.Remove(value); } - } - private readonly AsyncEvent, ISocketMessageChannel, SocketReaction, Task>> _reactionAddedEvent = new AsyncEvent, ISocketMessageChannel, SocketReaction, Task>>(); - public event Func, ISocketMessageChannel, SocketReaction, Task> ReactionRemoved - { - add { _reactionRemovedEvent.Add(value); } - remove { _reactionRemovedEvent.Remove(value); } - } - private readonly AsyncEvent, ISocketMessageChannel, SocketReaction, Task>> _reactionRemovedEvent = new AsyncEvent, ISocketMessageChannel, SocketReaction, Task>>(); - public event Func, ISocketMessageChannel, Task> ReactionsCleared - { - add { _reactionsClearedEvent.Add(value); } - remove { _reactionsClearedEvent.Remove(value); } - } - private readonly AsyncEvent, ISocketMessageChannel, Task>> _reactionsClearedEvent = new AsyncEvent, ISocketMessageChannel, Task>>(); - - //Roles - public event Func RoleCreated - { - add { _roleCreatedEvent.Add(value); } - remove { _roleCreatedEvent.Remove(value); } - } - private readonly AsyncEvent> _roleCreatedEvent = new AsyncEvent>(); - public event Func RoleDeleted - { - add { _roleDeletedEvent.Add(value); } - remove { _roleDeletedEvent.Remove(value); } - } - private readonly AsyncEvent> _roleDeletedEvent = new AsyncEvent>(); - public event Func RoleUpdated - { - add { _roleUpdatedEvent.Add(value); } - remove { _roleUpdatedEvent.Remove(value); } - } - private readonly AsyncEvent> _roleUpdatedEvent = new AsyncEvent>(); - - //Guilds - public event Func JoinedGuild - { - add { _joinedGuildEvent.Add(value); } - remove { _joinedGuildEvent.Remove(value); } - } - private AsyncEvent> _joinedGuildEvent = new AsyncEvent>(); - public event Func LeftGuild - { - add { _leftGuildEvent.Add(value); } - remove { _leftGuildEvent.Remove(value); } - } - private AsyncEvent> _leftGuildEvent = new AsyncEvent>(); - public event Func GuildAvailable - { - add { _guildAvailableEvent.Add(value); } - remove { _guildAvailableEvent.Remove(value); } - } - private AsyncEvent> _guildAvailableEvent = new AsyncEvent>(); - public event Func GuildUnavailable - { - add { _guildUnavailableEvent.Add(value); } - remove { _guildUnavailableEvent.Remove(value); } - } - private AsyncEvent> _guildUnavailableEvent = new AsyncEvent>(); - public event Func GuildMembersDownloaded - { - add { _guildMembersDownloadedEvent.Add(value); } - remove { _guildMembersDownloadedEvent.Remove(value); } - } - private AsyncEvent> _guildMembersDownloadedEvent = new AsyncEvent>(); - public event Func GuildUpdated - { - add { _guildUpdatedEvent.Add(value); } - remove { _guildUpdatedEvent.Remove(value); } - } - private AsyncEvent> _guildUpdatedEvent = new AsyncEvent>(); - - //Users - public event Func UserJoined - { - add { _userJoinedEvent.Add(value); } - remove { _userJoinedEvent.Remove(value); } - } - private readonly AsyncEvent> _userJoinedEvent = new AsyncEvent>(); - public event Func UserLeft - { - add { _userLeftEvent.Add(value); } - remove { _userLeftEvent.Remove(value); } - } - private readonly AsyncEvent> _userLeftEvent = new AsyncEvent>(); - public event Func UserBanned - { - add { _userBannedEvent.Add(value); } - remove { _userBannedEvent.Remove(value); } - } - private readonly AsyncEvent> _userBannedEvent = new AsyncEvent>(); - public event Func UserUnbanned - { - add { _userUnbannedEvent.Add(value); } - remove { _userUnbannedEvent.Remove(value); } - } - private readonly AsyncEvent> _userUnbannedEvent = new AsyncEvent>(); - public event Func UserUpdated - { - add { _userUpdatedEvent.Add(value); } - remove { _userUpdatedEvent.Remove(value); } - } - private readonly AsyncEvent> _userUpdatedEvent = new AsyncEvent>(); - public event Func GuildMemberUpdated - { - add { _guildMemberUpdatedEvent.Add(value); } - remove { _guildMemberUpdatedEvent.Remove(value); } - } - private readonly AsyncEvent> _guildMemberUpdatedEvent = new AsyncEvent>(); - public event Func, SocketUser, SocketPresence, SocketPresence, Task> UserPresenceUpdated - { - add { _userPresenceUpdatedEvent.Add(value); } - remove { _userPresenceUpdatedEvent.Remove(value); } - } - private readonly AsyncEvent, SocketUser, SocketPresence, SocketPresence, Task>> _userPresenceUpdatedEvent = new AsyncEvent, SocketUser, SocketPresence, SocketPresence, Task>>(); - public event Func UserVoiceStateUpdated - { - add { _userVoiceStateUpdatedEvent.Add(value); } - remove { _userVoiceStateUpdatedEvent.Remove(value); } - } - private readonly AsyncEvent> _userVoiceStateUpdatedEvent = new AsyncEvent>(); - public event Func CurrentUserUpdated - { - add { _selfUpdatedEvent.Add(value); } - remove { _selfUpdatedEvent.Remove(value); } - } - private readonly AsyncEvent> _selfUpdatedEvent = new AsyncEvent>(); - public event Func UserIsTyping - { - add { _userIsTypingEvent.Add(value); } - remove { _userIsTypingEvent.Remove(value); } - } - private readonly AsyncEvent> _userIsTypingEvent = new AsyncEvent>(); - public event Func RecipientAdded - { - add { _recipientAddedEvent.Add(value); } - remove { _recipientAddedEvent.Remove(value); } - } - private readonly AsyncEvent> _recipientAddedEvent = new AsyncEvent>(); - public event Func RecipientRemoved + private readonly AsyncEvent> _shardReadyEvent = new AsyncEvent>(); + /// Fired when a shard receives a heartbeat from the Discord gateway. + public event Func ShardLatencyUpdated { - add { _recipientRemovedEvent.Add(value); } - remove { _recipientRemovedEvent.Remove(value); } + add { _shardLatencyUpdatedEvent.Add(value); } + remove { _shardLatencyUpdatedEvent.Remove(value); } } - private readonly AsyncEvent> _recipientRemovedEvent = new AsyncEvent>(); + private readonly AsyncEvent> _shardLatencyUpdatedEvent = new AsyncEvent>(); } -} +} \ No newline at end of file diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index ab2cb9266..6c2a0f3b9 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -9,7 +9,7 @@ using System.Threading; namespace Discord.WebSocket { - public partial class DiscordShardedClient : BaseDiscordClient, IDiscordClient + public partial class DiscordShardedClient : BaseSocketClient, IDiscordClient { private readonly DiscordSocketConfig _baseConfig; private readonly SemaphoreSlim _connectionGroupLock; @@ -20,16 +20,15 @@ namespace Discord.WebSocket private bool _automaticShards; /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. - public int Latency => GetLatency(); - public UserStatus Status => _shards[0].Status; - public Game? Game => _shards[0].Game; + 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 { } } internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; - public new SocketSelfUser CurrentUser { get { return base.CurrentUser as SocketSelfUser; } private set { base.CurrentUser = value; } } - public IReadOnlyCollection Guilds => GetGuilds().ToReadOnlyCollection(() => GetGuildCount()); - public IReadOnlyCollection PrivateChannels => GetPrivateChannels().ToReadOnlyCollection(() => GetPrivateChannelCount()); + public override IReadOnlyCollection Guilds => GetGuilds().ToReadOnlyCollection(() => GetGuildCount()); + public override IReadOnlyCollection PrivateChannels => GetPrivateChannels().ToReadOnlyCollection(() => GetPrivateChannelCount()); public IReadOnlyCollection Shards => _shards; - public IReadOnlyCollection VoiceRegions => _shards[0].VoiceRegions; + public override IReadOnlyCollection VoiceRegions => _shards[0].VoiceRegions; /// Creates a new REST/WebSocket discord client. public DiscordShardedClient() : this(null, new DiscordSocketConfig()) { } @@ -115,15 +114,11 @@ namespace Discord.WebSocket } /// - public async Task StartAsync() - { - await Task.WhenAll(_shards.Select(x => x.StartAsync())).ConfigureAwait(false); - } + public override async Task StartAsync() + => await Task.WhenAll(_shards.Select(x => x.StartAsync())).ConfigureAwait(false); /// - public async Task StopAsync() - { - await Task.WhenAll(_shards.Select(x => x.StopAsync())).ConfigureAwait(false); - } + public override async Task StopAsync() + => await Task.WhenAll(_shards.Select(x => x.StopAsync())).ConfigureAwait(false); public DiscordSocketClient GetShard(int id) { @@ -141,17 +136,15 @@ namespace Discord.WebSocket => GetShardFor(guild.Id); /// - public async Task GetApplicationInfoAsync() - => await _shards[0].GetApplicationInfoAsync().ConfigureAwait(false); + public override async Task GetApplicationInfoAsync(RequestOptions options = null) + => await _shards[0].GetApplicationInfoAsync(options).ConfigureAwait(false); /// - public SocketGuild GetGuild(ulong id) => GetShardFor(id).GetGuild(id); - /// - public Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null) - => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon, new RequestOptions()); + public override SocketGuild GetGuild(ulong id) + => GetShardFor(id).GetGuild(id); /// - public SocketChannel GetChannel(ulong id) + public override SocketChannel GetChannel(ulong id) { for (int i = 0; i < _shards.Length; i++) { @@ -175,11 +168,7 @@ namespace Discord.WebSocket for (int i = 0; i < _shards.Length; i++) result += _shards[i].PrivateChannels.Count; return result; - } - - /// - public Task> GetConnectionsAsync() - => ClientHelper.GetConnectionsAsync(this, new RequestOptions()); + } private IEnumerable GetGuilds() { @@ -195,14 +184,10 @@ namespace Discord.WebSocket for (int i = 0; i < _shards.Length; i++) result += _shards[i].Guilds.Count; return result; - } + } /// - public Task GetInviteAsync(string inviteId) - => ClientHelper.GetInviteAsync(this, inviteId, new RequestOptions()); - - /// - public SocketUser GetUser(ulong id) + public override SocketUser GetUser(ulong id) { for (int i = 0; i < _shards.Length; i++) { @@ -213,7 +198,7 @@ namespace Discord.WebSocket return null; } /// - public SocketUser GetUser(string username, string discriminator) + public override SocketUser GetUser(string username, string discriminator) { for (int i = 0; i < _shards.Length; i++) { @@ -225,11 +210,11 @@ namespace Discord.WebSocket } /// - public RestVoiceRegion GetVoiceRegion(string id) + public override RestVoiceRegion GetVoiceRegion(string id) => _shards[0].GetVoiceRegion(id); /// Downloads the users list for the provided guilds, if they don't have a complete list. - public async Task DownloadUsersAsync(IEnumerable guilds) + public override async Task DownloadUsersAsync(IEnumerable guilds) { for (int i = 0; i < _shards.Length; i++) { @@ -248,12 +233,12 @@ namespace Discord.WebSocket return (int)Math.Round(total / (double)_shards.Length); } - public async Task SetStatusAsync(UserStatus status) + public override async Task SetStatusAsync(UserStatus status) { for (int i = 0; i < _shards.Length; i++) await _shards[i].SetStatusAsync(status).ConfigureAwait(false); } - public async Task SetGameAsync(string name, string streamUrl = null, StreamType streamType = StreamType.NotStreaming) + public override async Task SetGameAsync(string name, string streamUrl = null, StreamType streamType = StreamType.NotStreaming) { for (int i = 0; i < _shards.Length; i++) await _shards[i].SetGameAsync(name, streamUrl, streamType).ConfigureAwait(false); @@ -281,6 +266,11 @@ namespace Discord.WebSocket }; } + client.Connected += () => _shardConnectedEvent.InvokeAsync(client); + client.Disconnected += (exception) => _shardDisconnectedEvent.InvokeAsync(exception, client); + client.Ready += () => _shardReadyEvent.InvokeAsync(client); + client.LatencyUpdated += (oldLatency, newLatency) => _shardLatencyUpdatedEvent.InvokeAsync(oldLatency, newLatency, client); + client.ChannelCreated += (channel) => _channelCreatedEvent.InvokeAsync(channel); client.ChannelDestroyed += (channel) => _channelDestroyedEvent.InvokeAsync(channel); client.ChannelUpdated += (oldChannel, newChannel) => _channelUpdatedEvent.InvokeAsync(oldChannel, newChannel); diff --git a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs index 7d680eaf2..72781204c 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs @@ -29,7 +29,11 @@ namespace Discord.API private CancellationTokenSource _connectCancelToken; private string _gatewayUrl; private bool _isExplicitUrl; - + + //Store our decompression streams for zlib shared state + private MemoryStream _compressed; + private DeflateStream _decompressor; + internal IWebSocketClient WebSocketClient { get; } public ConnectionState ConnectionState { get; private set; } @@ -43,14 +47,29 @@ namespace Discord.API _isExplicitUrl = true; WebSocketClient = webSocketProvider(); //WebSocketClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .NET Framework 4.6+) + WebSocketClient.BinaryMessage += async (data, index, count) => { - using (var compressed = new MemoryStream(data, index + 2, count - 2)) using (var decompressed = new MemoryStream()) { - using (var zlib = new DeflateStream(compressed, CompressionMode.Decompress)) - zlib.CopyTo(decompressed); + if (data[0] == 0x78) + { + //Strip the zlib header + _compressed.Write(data, index + 2, count - 2); + _compressed.SetLength(count - 2); + } + else + { + _compressed.Write(data, index, count); + _compressed.SetLength(count); + } + + //Reset positions so we don't run out of memory + _compressed.Position = 0; + _decompressor.CopyTo(decompressed); + _compressed.Position = 0; decompressed.Position = 0; + using (var reader = new StreamReader(decompressed)) using (var jsonReader = new JsonTextReader(reader)) { @@ -76,6 +95,7 @@ namespace Discord.API await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); }; } + internal override void Dispose(bool disposing) { if (!_isDisposed) @@ -84,6 +104,8 @@ namespace Discord.API { _connectCancelToken?.Dispose(); (WebSocketClient as IDisposable)?.Dispose(); + _decompressor?.Dispose(); + _compressed?.Dispose(); } _isDisposed = true; } @@ -105,6 +127,12 @@ namespace Discord.API if (WebSocketClient == null) throw new NotSupportedException("This client is not configured with websocket support."); + //Re-create streams to reset the zlib state + _compressed?.Dispose(); + _decompressor?.Dispose(); + _compressed = new MemoryStream(); + _decompressor = new DeflateStream(_compressed, CompressionMode.Decompress); + ConnectionState = ConnectionState.Connecting; try { @@ -115,7 +143,7 @@ namespace Discord.API if (!_isExplicitUrl) { var gatewayResponse = await GetGatewayAsync().ConfigureAwait(false); - _gatewayUrl = $"{gatewayResponse.Url}?v={DiscordConfig.APIVersion}&encoding={DiscordSocketConfig.GatewayEncoding}"; + _gatewayUrl = $"{gatewayResponse.Url}?v={DiscordConfig.APIVersion}&encoding={DiscordSocketConfig.GatewayEncoding}&compress=zlib-stream"; } await WebSocketClient.ConnectAsync(_gatewayUrl).ConfigureAwait(false); @@ -191,7 +219,7 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); return await SendAsync("GET", () => "gateway/bot", new BucketIds(), options: options).ConfigureAwait(false); } - public async Task SendIdentifyAsync(int largeThreshold = 100, bool useCompression = true, int shardID = 0, int totalShards = 1, RequestOptions options = null) + public async Task SendIdentifyAsync(int largeThreshold = 100, int shardID = 0, int totalShards = 1, RequestOptions options = null) { options = RequestOptions.CreateOrClone(options); var props = new Dictionary @@ -202,8 +230,7 @@ namespace Discord.API { Token = AuthToken, Properties = props, - LargeThreshold = largeThreshold, - UseCompression = useCompression, + LargeThreshold = largeThreshold }; if (totalShards > 1) msg.ShardingParams = new int[] { shardID, totalShards }; diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs index fb155e535..1222b270e 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs @@ -2,218 +2,37 @@ using System.Threading.Tasks; namespace Discord.WebSocket -{ - //TODO: Add event docstrings +{ public partial class DiscordSocketClient { //General + /// Fired when connected to the Discord gateway. public event Func Connected { add { _connectedEvent.Add(value); } remove { _connectedEvent.Remove(value); } } private readonly AsyncEvent> _connectedEvent = new AsyncEvent>(); + /// Fired when disconnected to the Discord gateway. public event Func Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); + /// Fired when guild data has finished downloading. public event Func Ready { add { _readyEvent.Add(value); } remove { _readyEvent.Remove(value); } } private readonly AsyncEvent> _readyEvent = new AsyncEvent>(); + /// Fired when a heartbeat is received from the Discord gateway. public event Func LatencyUpdated { add { _latencyUpdatedEvent.Add(value); } remove { _latencyUpdatedEvent.Remove(value); } } private readonly AsyncEvent> _latencyUpdatedEvent = new AsyncEvent>(); - - //Channels - public event Func ChannelCreated - { - add { _channelCreatedEvent.Add(value); } - remove { _channelCreatedEvent.Remove(value); } - } - private readonly AsyncEvent> _channelCreatedEvent = new AsyncEvent>(); - public event Func ChannelDestroyed - { - add { _channelDestroyedEvent.Add(value); } - remove { _channelDestroyedEvent.Remove(value); } - } - private readonly AsyncEvent> _channelDestroyedEvent = new AsyncEvent>(); - public event Func ChannelUpdated - { - add { _channelUpdatedEvent.Add(value); } - remove { _channelUpdatedEvent.Remove(value); } - } - private readonly AsyncEvent> _channelUpdatedEvent = new AsyncEvent>(); - - //Messages - public event Func MessageReceived - { - add { _messageReceivedEvent.Add(value); } - remove { _messageReceivedEvent.Remove(value); } - } - private readonly AsyncEvent> _messageReceivedEvent = new AsyncEvent>(); - public event Func, ISocketMessageChannel, Task> MessageDeleted - { - add { _messageDeletedEvent.Add(value); } - remove { _messageDeletedEvent.Remove(value); } - } - private readonly AsyncEvent, ISocketMessageChannel, Task>> _messageDeletedEvent = new AsyncEvent, ISocketMessageChannel, Task>>(); - public event Func, SocketMessage, ISocketMessageChannel, Task> MessageUpdated - { - add { _messageUpdatedEvent.Add(value); } - remove { _messageUpdatedEvent.Remove(value); } - } - private readonly AsyncEvent, SocketMessage, ISocketMessageChannel, Task>> _messageUpdatedEvent = new AsyncEvent, SocketMessage, ISocketMessageChannel, Task>>(); - public event Func, ISocketMessageChannel, SocketReaction, Task> ReactionAdded - { - add { _reactionAddedEvent.Add(value); } - remove { _reactionAddedEvent.Remove(value); } - } - private readonly AsyncEvent, ISocketMessageChannel, SocketReaction, Task>> _reactionAddedEvent = new AsyncEvent, ISocketMessageChannel, SocketReaction, Task>>(); - public event Func, ISocketMessageChannel, SocketReaction, Task> ReactionRemoved - { - add { _reactionRemovedEvent.Add(value); } - remove { _reactionRemovedEvent.Remove(value); } - } - private readonly AsyncEvent, ISocketMessageChannel, SocketReaction, Task>> _reactionRemovedEvent = new AsyncEvent, ISocketMessageChannel, SocketReaction, Task>>(); - public event Func, ISocketMessageChannel, Task> ReactionsCleared - { - add { _reactionsClearedEvent.Add(value); } - remove { _reactionsClearedEvent.Remove(value); } - } - private readonly AsyncEvent, ISocketMessageChannel, Task>> _reactionsClearedEvent = new AsyncEvent, ISocketMessageChannel, Task>>(); - - //Roles - public event Func RoleCreated - { - add { _roleCreatedEvent.Add(value); } - remove { _roleCreatedEvent.Remove(value); } - } - private readonly AsyncEvent> _roleCreatedEvent = new AsyncEvent>(); - public event Func RoleDeleted - { - add { _roleDeletedEvent.Add(value); } - remove { _roleDeletedEvent.Remove(value); } - } - private readonly AsyncEvent> _roleDeletedEvent = new AsyncEvent>(); - public event Func RoleUpdated - { - add { _roleUpdatedEvent.Add(value); } - remove { _roleUpdatedEvent.Remove(value); } - } - private readonly AsyncEvent> _roleUpdatedEvent = new AsyncEvent>(); - - //Guilds - public event Func JoinedGuild - { - add { _joinedGuildEvent.Add(value); } - remove { _joinedGuildEvent.Remove(value); } - } - private readonly AsyncEvent> _joinedGuildEvent = new AsyncEvent>(); - public event Func LeftGuild - { - add { _leftGuildEvent.Add(value); } - remove { _leftGuildEvent.Remove(value); } - } - private readonly AsyncEvent> _leftGuildEvent = new AsyncEvent>(); - public event Func GuildAvailable - { - add { _guildAvailableEvent.Add(value); } - remove { _guildAvailableEvent.Remove(value); } - } - private readonly AsyncEvent> _guildAvailableEvent = new AsyncEvent>(); - public event Func GuildUnavailable - { - add { _guildUnavailableEvent.Add(value); } - remove { _guildUnavailableEvent.Remove(value); } - } - private readonly AsyncEvent> _guildUnavailableEvent = new AsyncEvent>(); - public event Func GuildMembersDownloaded - { - add { _guildMembersDownloadedEvent.Add(value); } - remove { _guildMembersDownloadedEvent.Remove(value); } - } - private readonly AsyncEvent> _guildMembersDownloadedEvent = new AsyncEvent>(); - public event Func GuildUpdated - { - add { _guildUpdatedEvent.Add(value); } - remove { _guildUpdatedEvent.Remove(value); } - } - private readonly AsyncEvent> _guildUpdatedEvent = new AsyncEvent>(); - - //Users - public event Func UserJoined - { - add { _userJoinedEvent.Add(value); } - remove { _userJoinedEvent.Remove(value); } - } - private readonly AsyncEvent> _userJoinedEvent = new AsyncEvent>(); - public event Func UserLeft - { - add { _userLeftEvent.Add(value); } - remove { _userLeftEvent.Remove(value); } - } - private readonly AsyncEvent> _userLeftEvent = new AsyncEvent>(); - public event Func UserBanned - { - add { _userBannedEvent.Add(value); } - remove { _userBannedEvent.Remove(value); } - } - private readonly AsyncEvent> _userBannedEvent = new AsyncEvent>(); - public event Func UserUnbanned - { - add { _userUnbannedEvent.Add(value); } - remove { _userUnbannedEvent.Remove(value); } - } - private readonly AsyncEvent> _userUnbannedEvent = new AsyncEvent>(); - public event Func UserUpdated - { - add { _userUpdatedEvent.Add(value); } - remove { _userUpdatedEvent.Remove(value); } - } - private readonly AsyncEvent> _userUpdatedEvent = new AsyncEvent>(); - public event Func GuildMemberUpdated - { - add { _guildMemberUpdatedEvent.Add(value); } - remove { _guildMemberUpdatedEvent.Remove(value); } - } - private readonly AsyncEvent> _guildMemberUpdatedEvent = new AsyncEvent>(); - public event Func UserVoiceStateUpdated - { - add { _userVoiceStateUpdatedEvent.Add(value); } - remove { _userVoiceStateUpdatedEvent.Remove(value); } - } - private readonly AsyncEvent> _userVoiceStateUpdatedEvent = new AsyncEvent>(); - public event Func CurrentUserUpdated - { - add { _selfUpdatedEvent.Add(value); } - remove { _selfUpdatedEvent.Remove(value); } - } - private readonly AsyncEvent> _selfUpdatedEvent = new AsyncEvent>(); - public event Func UserIsTyping - { - add { _userIsTypingEvent.Add(value); } - remove { _userIsTypingEvent.Remove(value); } - } - private readonly AsyncEvent> _userIsTypingEvent = new AsyncEvent>(); - public event Func RecipientAdded - { - add { _recipientAddedEvent.Add(value); } - remove { _recipientAddedEvent.Remove(value); } - } - private readonly AsyncEvent> _recipientAddedEvent = new AsyncEvent>(); - public event Func RecipientRemoved - { - add { _recipientRemovedEvent.Add(value); } - remove { _recipientRemovedEvent.Remove(value); } - } - private readonly AsyncEvent> _recipientRemovedEvent = new AsyncEvent>(); } } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 5256e0efd..d152bbc03 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1,4 +1,5 @@ -using Discord.API; +#pragma warning disable CS0618 +using Discord.API; using Discord.API.Gateway; using Discord.Logging; using Discord.Net.Converters; @@ -19,7 +20,7 @@ using GameModel = Discord.API.Game; namespace Discord.WebSocket { - public partial class DiscordSocketClient : BaseDiscordClient, IDiscordClient + public partial class DiscordSocketClient : BaseSocketClient, IDiscordClient { private readonly ConcurrentQueue _largeGuilds; private readonly JsonSerializer _serializer; @@ -44,10 +45,10 @@ namespace Discord.WebSocket public int ShardId { get; } /// Gets the current connection state of this client. public ConnectionState ConnectionState => _connection.State; - /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. - public int Latency { get; private set; } - internal UserStatus Status { get; private set; } = UserStatus.Online; - internal Game? Game { get; private set; } + /// + public override int Latency { get; protected set; } + public override UserStatus Status { get; protected set; } = UserStatus.Online; + public override Game? Game { get; protected set; } //From DiscordSocketConfig internal int TotalShards { get; private set; } @@ -60,14 +61,13 @@ namespace Discord.WebSocket internal int? HandlerTimeout { get; private set; } internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; - public new SocketSelfUser CurrentUser { get => base.CurrentUser as SocketSelfUser; private set => base.CurrentUser = value; } - public IReadOnlyCollection Guilds => State.Guilds; - public IReadOnlyCollection PrivateChannels => State.PrivateChannels; + public override IReadOnlyCollection Guilds => State.Guilds; + public override IReadOnlyCollection PrivateChannels => State.PrivateChannels; public IReadOnlyCollection DMChannels => State.PrivateChannels.Select(x => x as SocketDMChannel).Where(x => x != null).ToImmutableArray(); public IReadOnlyCollection GroupChannels => State.PrivateChannels.Select(x => x as SocketGroupChannel).Where(x => x != null).ToImmutableArray(); - public IReadOnlyCollection VoiceRegions => _voiceRegions.ToReadOnlyCollection(); + public override IReadOnlyCollection VoiceRegions => _voiceRegions.ToReadOnlyCollection(); /// Creates a new REST/WebSocket discord client. public DiscordSocketClient() : this(new DiscordSocketConfig()) { } @@ -155,9 +155,9 @@ namespace Discord.WebSocket _voiceRegions = ImmutableDictionary.Create(); } - public async Task StartAsync() + public override async Task StartAsync() => await _connection.StartAsync().ConfigureAwait(false); - public async Task StopAsync() + public override async Task StopAsync() => await _connection.StopAsync().ConfigureAwait(false); private async Task OnConnectingAsync() @@ -231,44 +231,23 @@ namespace Discord.WebSocket } /// - public async Task GetApplicationInfoAsync() - { - return _applicationInfo ?? (_applicationInfo = await ClientHelper.GetApplicationInfoAsync(this, new RequestOptions())); - } - - /// - public SocketGuild GetGuild(ulong id) - { - return State.GetGuild(id); - } - /// - public Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null) - => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon, new RequestOptions()); - - /// - public SocketChannel GetChannel(ulong id) - { - return State.GetChannel(id); - } + public override async Task GetApplicationInfoAsync(RequestOptions options = null) + => _applicationInfo ?? (_applicationInfo = await ClientHelper.GetApplicationInfoAsync(this, options ?? RequestOptions.Default).ConfigureAwait(false)); /// - public Task> GetConnectionsAsync() - => ClientHelper.GetConnectionsAsync(this, new RequestOptions()); + public override SocketGuild GetGuild(ulong id) + => State.GetGuild(id); /// - public Task GetInviteAsync(string inviteId) - => ClientHelper.GetInviteAsync(this, inviteId, new RequestOptions()); - + public override SocketChannel GetChannel(ulong id) + => State.GetChannel(id); + /// - public SocketUser GetUser(ulong id) - { - return State.GetUser(id); - } + public override SocketUser GetUser(ulong id) + => State.GetUser(id); /// - public SocketUser GetUser(string username, string discriminator) - { - return State.Users.FirstOrDefault(x => x.Discriminator == discriminator && x.Username == username); - } + public override SocketUser GetUser(string username, string discriminator) + => State.Users.FirstOrDefault(x => x.Discriminator == discriminator && x.Username == username); internal SocketGlobalUser GetOrCreateUser(ClientState state, Discord.API.User model) { return state.GetOrAddUser(model.Id, x => @@ -288,13 +267,11 @@ namespace Discord.WebSocket return user; }); } - internal void RemoveUser(ulong id) - { - State.RemoveUser(id); - } + internal void RemoveUser(ulong id) + => State.RemoveUser(id); /// - public RestVoiceRegion GetVoiceRegion(string id) + public override RestVoiceRegion GetVoiceRegion(string id) { if (_voiceRegions.TryGetValue(id, out RestVoiceRegion region)) return region; @@ -302,7 +279,7 @@ namespace Discord.WebSocket } /// Downloads the users list for the provided guilds, if they don't have a complete list. - public async Task DownloadUsersAsync(IEnumerable guilds) + public override async Task DownloadUsersAsync(IEnumerable guilds) { if (ConnectionState == ConnectionState.Connected) { @@ -340,7 +317,7 @@ namespace Discord.WebSocket } } - public async Task SetStatusAsync(UserStatus status) + public override async Task SetStatusAsync(UserStatus status) { Status = status; if (status == UserStatus.AFK) @@ -349,7 +326,7 @@ namespace Discord.WebSocket _statusSince = null; await SendStatusAsync().ConfigureAwait(false); } - public async Task SetGameAsync(string name, string streamUrl = null, StreamType streamType = StreamType.NotStreaming) + public override async Task SetGameAsync(string name, string streamUrl = null, StreamType streamType = StreamType.NotStreaming) { if (name != null) Game = new Game(name, streamUrl, streamType); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs index 322a99496..00cef60f8 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs @@ -76,11 +76,6 @@ namespace Discord.WebSocket public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); - public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); - public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); - public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); public IDisposable EnterTypingState(RequestOptions options = null) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index dc1853e73..92a93a903 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -104,11 +104,6 @@ namespace Discord.WebSocket public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); - public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); - public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); - public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); public IDisposable EnterTypingState(RequestOptions options = null) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs index 27edf3441..43e3e50cb 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -119,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; @@ -160,8 +160,8 @@ namespace Discord.WebSocket //IChannel IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => ImmutableArray.Create>(Users).ToAsyncEnumerable(); //Overriden in Text/Voice + => ImmutableArray.Create>(Users).ToAsyncEnumerable(); //Overridden in Text/Voice Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetUser(id)); //Overriden in Text/Voice + => Task.FromResult(GetUser(id)); //Overridden in Text/Voice } } diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 3df2d5192..a136437a3 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -1,4 +1,5 @@ -using Discord.Audio; +#pragma warning disable CS0618 +using Discord.Audio; using Discord.Rest; using System; using System.Collections.Concurrent; @@ -437,6 +438,16 @@ namespace Discord.WebSocket _downloaderPromise.TrySetResultAsync(true); } + //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) { diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index 844b0c7f4..66af20bb6 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -43,7 +43,7 @@ namespace Discord.WebSocket public SocketVoiceState? VoiceState => Guild.GetVoiceState(Id); public AudioInStream AudioStream => Guild.GetAudioStream(Id); - /// The position of the user within the role hirearchy. + /// The position of the user within the role hierarchy. /// The returned value equal to the position of the highest role the user has, /// or int.MaxValue if user is the server owner. public int Hierarchy 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/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index 309532615..f904f4126 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,28 @@ 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..92234e88b --- /dev/null +++ b/test/Discord.Net.Tests/Tests.ChannelPermissions.cs @@ -0,0 +1,324 @@ +using System; +using System.Threading.Tasks; +using Xunit; + +namespace Discord +{ + public partial class Tests + { + [Fact] + public void 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.ReadMessages + | 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.ReadMessages + | 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); + } + + public void 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)GuildPermission.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)GuildPermission.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)GuildPermission.AddReactions); + + perm = perm.Modify(addReactions: false); + Assert.False(perm.AddReactions); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.ReadMessages); + + 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(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)GuildPermission.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)GuildPermission.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)GuildPermission.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)GuildPermission.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)GuildPermission.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)GuildPermission.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)GuildPermission.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)GuildPermission.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)GuildPermission.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)GuildPermission.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)GuildPermission.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)GuildPermission.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)GuildPermission.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)GuildPermission.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)GuildPermission.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)GuildPermission.ManageWebhooks); + + perm = perm.Modify(manageWebhooks: false); + Assert.False(perm.ManageWebhooks); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + } + + [Fact] + public void TestChannelTypeResolution() + { + ITextChannel someChannel = null; + // null channels will throw exception + Assert.Throws(() => ChannelPermissions.All(someChannel)); + } + } +} diff --git a/test/Discord.Net.Tests/Tests.GuildPermissions.cs b/test/Discord.Net.Tests/Tests.GuildPermissions.cs new file mode 100644 index 000000000..dc51600cf --- /dev/null +++ b/test/Discord.Net.Tests/Tests.GuildPermissions.cs @@ -0,0 +1,304 @@ +using System; +using System.Threading.Tasks; +using Xunit; + +namespace Discord +{ + public partial class Tests + { + [Fact] + public void 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); + } + + [Fact] + public void 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); + + } + + } +}