| @@ -1,7 +1,7 @@ | |||||
| <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | ||||
| <PropertyGroup> | <PropertyGroup> | ||||
| <VersionPrefix>1.0.0</VersionPrefix> | |||||
| <VersionSuffix>rc3</VersionSuffix> | |||||
| <VersionPrefix>1.0.1</VersionPrefix> | |||||
| <VersionSuffix></VersionSuffix> | |||||
| <Authors>RogueException</Authors> | <Authors>RogueException</Authors> | ||||
| <PackageTags>discord;discordapp</PackageTags> | <PackageTags>discord;discordapp</PackageTags> | ||||
| <PackageProjectUrl>https://github.com/RogueException/Discord.Net</PackageProjectUrl> | <PackageProjectUrl>https://github.com/RogueException/Discord.Net</PackageProjectUrl> | ||||
| @@ -1,4 +1,4 @@ | |||||
| # Discord.Net v1.0.0-rc | |||||
| # Discord.Net | |||||
| [](https://www.nuget.org/packages/Discord.Net) | [](https://www.nuget.org/packages/Discord.Net) | ||||
| [](https://www.myget.org/feed/Packages/discord-net) | [](https://www.myget.org/feed/Packages/discord-net) | ||||
| [](https://ci.appveyor.com/project/RogueException/discord-net/branch/dev) | [](https://ci.appveyor.com/project/RogueException/discord-net/branch/dev) | ||||
| @@ -34,7 +34,7 @@ after_build: | |||||
| if ($Env:APPVEYOR_REPO_TAG -eq "true") { | if ($Env:APPVEYOR_REPO_TAG -eq "true") { | ||||
| nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="" | nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="" | ||||
| } else { | } else { | ||||
| nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="-$Env:BUILD" | |||||
| nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="-build-$Env:BUILD" | |||||
| } | } | ||||
| - ps: Get-ChildItem artifacts\*.nupkg | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } | - ps: Get-ChildItem artifacts\*.nupkg | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } | ||||
| @@ -45,7 +45,7 @@ Discord.Net's implementation of Modules is influenced heavily from | |||||
| ASP.Net Core's Controller pattern. This means that the lifetime of a | ASP.Net Core's Controller pattern. This means that the lifetime of a | ||||
| module instance is only as long as the command being invoked. | module instance is only as long as the command being invoked. | ||||
| **Avoid using long-running code** in your modules whereever possible. | |||||
| **Avoid using long-running code** in your modules wherever possible. | |||||
| You should **not** be implementing very much logic into your modules; | You should **not** be implementing very much logic into your modules; | ||||
| outsource to a service for that. | outsource to a service for that. | ||||
| @@ -167,8 +167,8 @@ a dependency map. | |||||
| Modules are constructed using Dependency Injection. Any parameters | Modules are constructed using Dependency Injection. Any parameters | ||||
| that are placed in the constructor must be injected into an | that are placed in the constructor must be injected into an | ||||
| @Discord.Commands.IDependencyMap. Alternatively, you may accept an | |||||
| IDependencyMap as an argument and extract services yourself. | |||||
| @System.IServiceProvider. Alternatively, you may accept an | |||||
| IServiceProvider as an argument and extract services yourself. | |||||
| ### Module Properties | ### Module Properties | ||||
| @@ -205,21 +205,20 @@ you use DI when writing your modules. | |||||
| ### Setup | ### Setup | ||||
| First, you need to create an @Discord.Commands.IDependencyMap. | |||||
| The library includes @Discord.Commands.DependencyMap to help with | |||||
| this, however you may create your own IDependencyMap if you wish. | |||||
| First, you need to create an @System.IServiceProvider | |||||
| You may create your own IServiceProvider if you wish. | |||||
| Next, add the dependencies your modules will use to the map. | Next, add the dependencies your modules will use to the map. | ||||
| Finally, pass the map into the `LoadAssembly` method. | Finally, pass the map into the `LoadAssembly` method. | ||||
| Your modules will automatically be loaded with this dependency map. | Your modules will automatically be loaded with this dependency map. | ||||
| [!code-csharp[DependencyMap Setup](samples/dependency_map_setup.cs)] | |||||
| [!code-csharp[IServiceProvider Setup](samples/dependency_map_setup.cs)] | |||||
| ### Usage in Modules | ### Usage in Modules | ||||
| In the constructor of your module, any parameters will be filled in by | In the constructor of your module, any parameters will be filled in by | ||||
| the @Discord.Commands.IDependencyMap you pass into `LoadAssembly`. | |||||
| the @System.IServiceProvider you pass 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. | ||||
| @@ -228,12 +227,12 @@ Any publicly settable properties will also be filled in the same manner. | |||||
| being injected. | being injected. | ||||
| >[!NOTE] | >[!NOTE] | ||||
| >If you accept `CommandService` or `IDependencyMap` as a parameter in | |||||
| >If you accept `CommandService` or `IServiceProvider` as a parameter in | |||||
| your constructor or as an injectable property, these entries will be filled | your constructor or as an injectable property, these entries will be filled | ||||
| by the CommandService the module was loaded from, and the DependencyMap passed | |||||
| by the CommandService the module was loaded from, and the ServiceProvider passed | |||||
| into it, respectively. | into it, respectively. | ||||
| [!code-csharp[DependencyMap in Modules](samples/dependency_module.cs)] | |||||
| [!code-csharp[ServiceProvider in Modules](samples/dependency_module.cs)] | |||||
| # Preconditions | # Preconditions | ||||
| @@ -1,14 +1,16 @@ | |||||
| using System; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using System.Reflection; | using System.Reflection; | ||||
| using Discord; | using Discord; | ||||
| using Discord.WebSocket; | using Discord.WebSocket; | ||||
| using Discord.Commands; | using Discord.Commands; | ||||
| using Microsoft.Extensions.DependencyInjection; | |||||
| public class Program | public class Program | ||||
| { | { | ||||
| private CommandService commands; | private CommandService commands; | ||||
| private DiscordSocketClient client; | private DiscordSocketClient client; | ||||
| private DependencyMap map; | |||||
| private IServiceProvider services; | |||||
| static void Main(string[] args) => new Program().Start().GetAwaiter().GetResult(); | static void Main(string[] args) => new Program().Start().GetAwaiter().GetResult(); | ||||
| @@ -19,38 +21,40 @@ public class Program | |||||
| string token = "bot token here"; | string token = "bot token here"; | ||||
| map = new DependencyMap(); | |||||
| services = new ServiceCollection() | |||||
| .BuildServiceProvider(); | |||||
| await InstallCommands(); | await InstallCommands(); | ||||
| await client.LoginAsync(TokenType.Bot, token); | await client.LoginAsync(TokenType.Bot, token); | ||||
| await client.ConnectAsync(); | |||||
| await client.StartAsync(); | |||||
| await Task.Delay(-1); | await Task.Delay(-1); | ||||
| } | } | ||||
| public async Task InstallCommands() | public async Task InstallCommands() | ||||
| { | { | ||||
| // Hook the MessageReceived Event into our Command Handler | // Hook the MessageReceived Event into our Command Handler | ||||
| client.MessageReceived += HandleCommand; | client.MessageReceived += HandleCommand; | ||||
| // Discover all of the commands in this assembly and load them. | |||||
| // Discover all of the commands in this assembly and load them. | |||||
| await commands.AddModulesAsync(Assembly.GetEntryAssembly()); | await commands.AddModulesAsync(Assembly.GetEntryAssembly()); | ||||
| } | } | ||||
| public async Task HandleCommand(SocketMessage messageParam) | public async Task HandleCommand(SocketMessage messageParam) | ||||
| { | |||||
| { | |||||
| // Don't process the command if it was a System Message | // Don't process the command if it was a System Message | ||||
| var message = messageParam as SocketUserMessage; | var message = messageParam as SocketUserMessage; | ||||
| if (message == null) return; | if (message == null) return; | ||||
| // Create a number to track where the prefix ends and the command begins | |||||
| int argPos = 0; | |||||
| // Determine if the message is a command, based on if it starts with '!' or a mention prefix | |||||
| if (!(message.HasCharPrefix('!', ref argPos) || message.HasMentionPrefix(client.CurrentUser, ref argPos))) return; | |||||
| // Create a number to track where the prefix ends and the command begins | |||||
| int argPos = 0; | |||||
| // Determine if the message is a command, based on if it starts with '!' or a mention prefix | |||||
| if (!(message.HasCharPrefix('!', ref argPos) || message.HasMentionPrefix(client.CurrentUser, ref argPos))) return; | |||||
| // Create a Command Context | // Create a Command Context | ||||
| var context = new CommandContext(client, message); | var context = new CommandContext(client, message); | ||||
| // Execute the command. (result does not indicate a return value, | // Execute the command. (result does not indicate a return value, | ||||
| // rather an object stating if the command executed succesfully) | |||||
| var result = await commands.ExecuteAsync(context, argPos, map); | |||||
| // rather an object stating if the command executed successfully) | |||||
| var result = await commands.ExecuteAsync(context, argPos, service); | |||||
| if (!result.IsSuccess) | if (!result.IsSuccess) | ||||
| await context.Channel.SendMessageAsync(result.ErrorReason); | await context.Channel.SendMessageAsync(result.ErrorReason); | ||||
| } | |||||
| } | |||||
| } | } | ||||
| @@ -7,12 +7,11 @@ public class Commands | |||||
| { | { | ||||
| public async Task Install(DiscordSocketClient client) | public async Task Install(DiscordSocketClient client) | ||||
| { | { | ||||
| // Here, we will inject the Dependency Map with | |||||
| // Here, we will inject the ServiceProvider with | |||||
| // all of the services our client will use. | // all of the services our client will use. | ||||
| _map.Add(client); | |||||
| _map.Add(commands); | |||||
| _map.Add(new NotificationService(_map)); | |||||
| _map.Add(new DatabaseService(_map)); | |||||
| _serviceCollection.AddSingleton(client) | |||||
| _serviceCollection.AddSingleton(new NotificationService()) | |||||
| _serviceCollection.AddSingleton(new DatabaseService()) | |||||
| // ... | // ... | ||||
| await _commands.AddModulesAsync(Assembly.GetEntryAssembly()); | await _commands.AddModulesAsync(Assembly.GetEntryAssembly()); | ||||
| } | } | ||||
| @@ -2,16 +2,18 @@ | |||||
| using Discord.Commands; | using Discord.Commands; | ||||
| using Discord.WebSocket; | using Discord.WebSocket; | ||||
| using Microsoft.Extensions.DependencyInjection; | |||||
| using System; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| // Inherit from PreconditionAttribute | // Inherit from PreconditionAttribute | ||||
| public class RequireOwnerAttribute : PreconditionAttribute | public class RequireOwnerAttribute : PreconditionAttribute | ||||
| { | { | ||||
| // Override the CheckPermissions method | // Override the CheckPermissions method | ||||
| public async override Task<PreconditionResult> CheckPermissions(ICommandContext context, CommandInfo command, IDependencyMap map) | |||||
| public async override Task<PreconditionResult> CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services) | |||||
| { | { | ||||
| // Get the ID of the bot's owner | // Get the ID of the bot's owner | ||||
| var ownerId = (await map.Get<DiscordSocketClient>().GetApplicationInfoAsync()).Owner.Id; | |||||
| var ownerId = (await services.GetService<DiscordSocketClient>().GetApplicationInfoAsync()).Owner.Id; | |||||
| // If this command was executed by that user, return a success | // If this command was executed by that user, return a success | ||||
| if (context.User.Id == ownerId) | if (context.User.Id == ownerId) | ||||
| return PreconditionResult.FromSuccess(); | return PreconditionResult.FromSuccess(); | ||||
| @@ -8,7 +8,11 @@ public class Program | |||||
| public async Task MainAsync() | public async Task MainAsync() | ||||
| { | { | ||||
| _client = new DiscordSocketClient(); | |||||
| // When working with events that have Cacheable<IMessage, ulong> parameters, | |||||
| // you must enable the message cache in your config settings if you plan to | |||||
| // use the cached message entity. | |||||
| var _config = new DiscordSocketConfig { MessageCacheSize = 100 }; | |||||
| _client = new DiscordSocketClient(_config); | |||||
| await _client.LoginAsync(TokenType.Bot, "bot token"); | await _client.LoginAsync(TokenType.Bot, "bot token"); | ||||
| await _client.StartAsync(); | await _client.StartAsync(); | ||||
| @@ -25,7 +29,8 @@ public class Program | |||||
| private async Task MessageUpdated(Cacheable<IMessage, ulong> before, SocketMessage after, ISocketMessageChannel channel) | private async Task MessageUpdated(Cacheable<IMessage, ulong> before, SocketMessage after, ISocketMessageChannel channel) | ||||
| { | { | ||||
| // If the message was not in the cache, downloading it will result in getting a copy of `after`. | |||||
| var message = await before.GetOrDownloadAsync(); | var message = await before.GetOrDownloadAsync(); | ||||
| Console.WriteLine($"{message} -> {after}"); | Console.WriteLine($"{message} -> {after}"); | ||||
| } | } | ||||
| } | |||||
| } | |||||
| @@ -211,7 +211,7 @@ For your reference, you may view the [completed program]. | |||||
| # Building a bot with commands | # Building a bot with commands | ||||
| This section will show you how to write a program that is ready for | This section will show you how to write a program that is ready for | ||||
| [commands](commands.md). Note that this will not be explaining _how_ | |||||
| [commands](commands/commands.md). Note that this will not be explaining _how_ | |||||
| to write commands or services, it will only be covering the general | to write commands or services, it will only be covering the general | ||||
| structure. | structure. | ||||
| @@ -224,4 +224,4 @@ should be to separate the program (initialization and command handler), | |||||
| the modules (handle commands), and the services (persistent storage, | the modules (handle commands), and the services (persistent storage, | ||||
| pure functions, data manipulation). | pure functions, data manipulation). | ||||
| **todo:** diagram of bot structure | |||||
| **todo:** diagram of bot structure | |||||
| @@ -30,8 +30,8 @@ class Program | |||||
| LogLevel = LogSeverity.Info, | LogLevel = LogSeverity.Info, | ||||
| // If you or another service needs to do anything with messages | // If you or another service needs to do anything with messages | ||||
| // (eg. checking Reactions), you should probably | |||||
| // set the MessageCacheSize here. | |||||
| // (eg. checking Reactions, checking the content of edited/deleted messages), | |||||
| // you must set the MessageCacheSize. You may adjust the number as needed. | |||||
| //MessageCacheSize = 50, | //MessageCacheSize = 50, | ||||
| // If your platform doesn't have native websockets, | // If your platform doesn't have native websockets, | ||||
| @@ -41,7 +41,7 @@ class Program | |||||
| }); | }); | ||||
| } | } | ||||
| // Create a named logging handler, so it can be re-used by addons | |||||
| // Example of a logging handler. This can be re-used by addons | |||||
| // that ask for a Func<LogMessage, Task>. | // that ask for a Func<LogMessage, Task>. | ||||
| private static Task Logger(LogMessage message) | private static Task Logger(LogMessage message) | ||||
| { | { | ||||
| @@ -65,6 +65,13 @@ class Program | |||||
| } | } | ||||
| Console.WriteLine($"{DateTime.Now,-19} [{message.Severity,8}] {message.Source}: {message.Message}"); | Console.WriteLine($"{DateTime.Now,-19} [{message.Severity,8}] {message.Source}: {message.Message}"); | ||||
| Console.ForegroundColor = cc; | Console.ForegroundColor = cc; | ||||
| // If you get an error saying 'CompletedTask' doesn't exist, | |||||
| // your project is targeting .NET 4.5.2 or lower. You'll need | |||||
| // to adjust your project's target framework to 4.6 or higher | |||||
| // (instructions for this are easily Googled). | |||||
| // If you *need* to run on .NET 4.5 for compat/other reasons, | |||||
| // the alternative is to 'return Task.Delay(0);' instead. | |||||
| return Task.CompletedTask; | return Task.CompletedTask; | ||||
| } | } | ||||
| @@ -92,16 +99,17 @@ class Program | |||||
| // and other dependencies that your commands might need. | // and other dependencies that your commands might need. | ||||
| _map.AddSingleton(new SomeServiceClass()); | _map.AddSingleton(new SomeServiceClass()); | ||||
| // Either search the program and add all Module classes that can be found: | |||||
| await _commands.AddModulesAsync(Assembly.GetEntryAssembly()); | |||||
| // Or add Modules manually if you prefer to be a little more explicit: | |||||
| await _commands.AddModuleAsync<SomeModule>(); | |||||
| // When all your required services are in the collection, build the container. | // When all your required services are in the collection, build the container. | ||||
| // Tip: There's an overload taking in a 'validateScopes' bool to make sure | // Tip: There's an overload taking in a 'validateScopes' bool to make sure | ||||
| // you haven't made any mistakes in your dependency graph. | // you haven't made any mistakes in your dependency graph. | ||||
| _services = _map.BuildServiceProvider(); | _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. | |||||
| await _commands.AddModulesAsync(Assembly.GetEntryAssembly()); | |||||
| // Or add Modules manually if you prefer to be a little more explicit: | |||||
| await _commands.AddModuleAsync<SomeModule>(); | |||||
| // Subscribe a handler to see if a message invokes a command. | // Subscribe a handler to see if a message invokes a command. | ||||
| _client.MessageReceived += HandleCommandAsync; | _client.MessageReceived += HandleCommandAsync; | ||||
| } | } | ||||
| @@ -120,7 +128,7 @@ class Program | |||||
| // commands to be invoked by mentioning the bot instead. | // commands to be invoked by mentioning the bot instead. | ||||
| if (msg.HasCharPrefix('!', ref pos) /* || msg.HasMentionPrefix(_client.CurrentUser, ref pos) */) | if (msg.HasCharPrefix('!', ref pos) /* || msg.HasMentionPrefix(_client.CurrentUser, ref pos) */) | ||||
| { | { | ||||
| // Create a Command Context | |||||
| // Create a Command Context. | |||||
| var context = new SocketCommandContext(_client, msg); | var context = new SocketCommandContext(_client, msg); | ||||
| // Execute the command. (result does not indicate a return value, | // Execute the command. (result does not indicate a return value, | ||||
| @@ -42,7 +42,7 @@ events are delegates, but are still registered the same. | |||||
| For example, let's look at [DiscordSocketClient.MessageReceived](xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_MessageReceived) | For example, let's look at [DiscordSocketClient.MessageReceived](xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_MessageReceived) | ||||
| To hook an event into MessageReceived, we now use the following code: | To hook an event into MessageReceived, we now use the following code: | ||||
| [!code-csharp[Event Registration](guides/samples/migrating/event.cs)] | |||||
| [!code-csharp[Event Registration](samples/event.cs)] | |||||
| > **All Event Handlers in 1.0 MUST return Task!** | > **All Event Handlers in 1.0 MUST return Task!** | ||||
| @@ -50,7 +50,7 @@ If your event handler is marked as `async`, it will automatically return `Task`. | |||||
| if you do not need to execute asynchronus code, do _not_ mark your handler as `async`, and instead, | if you do not need to execute asynchronus code, do _not_ mark your handler as `async`, and instead, | ||||
| stick a `return Task.CompletedTask` at the bottom. | stick a `return Task.CompletedTask` at the bottom. | ||||
| [!code-csharp[Sync Event Registration](guides/samples/migrating/sync_event.cs)] | |||||
| [!code-csharp[Sync Event Registration](samples/sync_event.cs)] | |||||
| **Event handlers no longer require a sender.** The only arguments your event handler needs to accept | **Event handlers no longer require a sender.** The only arguments your event handler needs to accept | ||||
| are the parameters used by the event. It is recommended to look at the event in IntelliSense or on the | are the parameters used by the event. It is recommended to look at the event in IntelliSense or on the | ||||
| @@ -3,7 +3,7 @@ private async Task SendAsync(IAudioClient client, string path) | |||||
| // Create FFmpeg using the previous example | // Create FFmpeg using the previous example | ||||
| var ffmpeg = CreateStream(path); | var ffmpeg = CreateStream(path); | ||||
| var output = ffmpeg.StandardOutput.BaseStream; | var output = ffmpeg.StandardOutput.BaseStream; | ||||
| var discord = client.CreatePCMStream(AudioApplication.Mixed, 1920); | |||||
| var discord = client.CreatePCMStream(AudioApplication.Mixed); | |||||
| await output.CopyToAsync(discord); | await output.CopyToAsync(discord); | ||||
| await discord.FlushAsync(); | await discord.FlushAsync(); | ||||
| } | } | ||||
| @@ -6,6 +6,13 @@ namespace Discord.Commands | |||||
| [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = true)] | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = true)] | ||||
| public abstract class PreconditionAttribute : Attribute | public abstract class PreconditionAttribute : Attribute | ||||
| { | { | ||||
| /// <summary> | |||||
| /// 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 <see cref="Group"/> = <see cref="null"/> | |||||
| /// or not at all will require *all* preconditions to pass, just like normal (A && B). | |||||
| /// </summary> | |||||
| public string Group { get; set; } = null; | |||||
| public abstract Task<PreconditionResult> CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services); | public abstract Task<PreconditionResult> CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services); | ||||
| } | } | ||||
| } | } | ||||
| @@ -44,14 +44,16 @@ namespace Discord.Commands | |||||
| public override async Task<PreconditionResult> CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services) | public override async Task<PreconditionResult> CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services) | ||||
| { | { | ||||
| var guildUser = await context.Guild.GetCurrentUserAsync(); | |||||
| IGuildUser guildUser = null; | |||||
| if (context.Guild != null) | |||||
| guildUser = await context.Guild.GetCurrentUserAsync().ConfigureAwait(false); | |||||
| if (GuildPermission.HasValue) | if (GuildPermission.HasValue) | ||||
| { | { | ||||
| if (guildUser == null) | if (guildUser == null) | ||||
| return PreconditionResult.FromError("Command must be used in a guild channel"); | return PreconditionResult.FromError("Command must be used in a guild channel"); | ||||
| if (!guildUser.GuildPermissions.Has(GuildPermission.Value)) | if (!guildUser.GuildPermissions.Has(GuildPermission.Value)) | ||||
| return PreconditionResult.FromError($"Command requires guild permission {GuildPermission.Value}"); | |||||
| return PreconditionResult.FromError($"Bot requires guild permission {GuildPermission.Value}"); | |||||
| } | } | ||||
| if (ChannelPermission.HasValue) | if (ChannelPermission.HasValue) | ||||
| @@ -65,7 +67,7 @@ namespace Discord.Commands | |||||
| perms = ChannelPermissions.All(guildChannel); | perms = ChannelPermissions.All(guildChannel); | ||||
| if (!perms.Has(ChannelPermission.Value)) | if (!perms.Has(ChannelPermission.Value)) | ||||
| return PreconditionResult.FromError($"Command requires channel permission {ChannelPermission.Value}"); | |||||
| return PreconditionResult.FromError($"Bot requires channel permission {ChannelPermission.Value}"); | |||||
| } | } | ||||
| return PreconditionResult.FromSuccess(); | return PreconditionResult.FromSuccess(); | ||||
| @@ -52,7 +52,7 @@ namespace Discord.Commands | |||||
| if (guildUser == null) | if (guildUser == null) | ||||
| return Task.FromResult(PreconditionResult.FromError("Command must be used in a guild channel")); | return Task.FromResult(PreconditionResult.FromError("Command must be used in a guild channel")); | ||||
| if (!guildUser.GuildPermissions.Has(GuildPermission.Value)) | if (!guildUser.GuildPermissions.Has(GuildPermission.Value)) | ||||
| return Task.FromResult(PreconditionResult.FromError($"Command requires guild permission {GuildPermission.Value}")); | |||||
| return Task.FromResult(PreconditionResult.FromError($"User requires guild permission {GuildPermission.Value}")); | |||||
| } | } | ||||
| if (ChannelPermission.HasValue) | if (ChannelPermission.HasValue) | ||||
| @@ -66,7 +66,7 @@ namespace Discord.Commands | |||||
| perms = ChannelPermissions.All(guildChannel); | perms = ChannelPermissions.All(guildChannel); | ||||
| if (!perms.Has(ChannelPermission.Value)) | if (!perms.Has(ChannelPermission.Value)) | ||||
| return Task.FromResult(PreconditionResult.FromError($"Command requires channel permission {ChannelPermission.Value}")); | |||||
| return Task.FromResult(PreconditionResult.FromError($"User requires channel permission {ChannelPermission.Value}")); | |||||
| } | } | ||||
| return Task.FromResult(PreconditionResult.FromSuccess()); | return Task.FromResult(PreconditionResult.FromSuccess()); | ||||
| @@ -10,10 +10,11 @@ namespace Discord.Commands.Builders | |||||
| { | { | ||||
| private readonly List<PreconditionAttribute> _preconditions; | private readonly List<PreconditionAttribute> _preconditions; | ||||
| private readonly List<ParameterBuilder> _parameters; | private readonly List<ParameterBuilder> _parameters; | ||||
| private readonly List<Attribute> _attributes; | |||||
| private readonly List<string> _aliases; | private readonly List<string> _aliases; | ||||
| public ModuleBuilder Module { get; } | public ModuleBuilder Module { get; } | ||||
| internal Func<ICommandContext, object[], IServiceProvider, Task> Callback { get; set; } | |||||
| internal Func<ICommandContext, object[], IServiceProvider, CommandInfo, Task> Callback { get; set; } | |||||
| public string Name { get; set; } | public string Name { get; set; } | ||||
| public string Summary { get; set; } | public string Summary { get; set; } | ||||
| @@ -24,6 +25,7 @@ namespace Discord.Commands.Builders | |||||
| public IReadOnlyList<PreconditionAttribute> Preconditions => _preconditions; | public IReadOnlyList<PreconditionAttribute> Preconditions => _preconditions; | ||||
| public IReadOnlyList<ParameterBuilder> Parameters => _parameters; | public IReadOnlyList<ParameterBuilder> Parameters => _parameters; | ||||
| public IReadOnlyList<Attribute> Attributes => _attributes; | |||||
| public IReadOnlyList<string> Aliases => _aliases; | public IReadOnlyList<string> Aliases => _aliases; | ||||
| //Automatic | //Automatic | ||||
| @@ -33,10 +35,11 @@ namespace Discord.Commands.Builders | |||||
| _preconditions = new List<PreconditionAttribute>(); | _preconditions = new List<PreconditionAttribute>(); | ||||
| _parameters = new List<ParameterBuilder>(); | _parameters = new List<ParameterBuilder>(); | ||||
| _attributes = new List<Attribute>(); | |||||
| _aliases = new List<string>(); | _aliases = new List<string>(); | ||||
| } | } | ||||
| //User-defined | //User-defined | ||||
| internal CommandBuilder(ModuleBuilder module, string primaryAlias, Func<ICommandContext, object[], IServiceProvider, Task> callback) | |||||
| internal CommandBuilder(ModuleBuilder module, string primaryAlias, Func<ICommandContext, object[], IServiceProvider, CommandInfo, Task> callback) | |||||
| : this(module) | : this(module) | ||||
| { | { | ||||
| Discord.Preconditions.NotNull(primaryAlias, nameof(primaryAlias)); | Discord.Preconditions.NotNull(primaryAlias, nameof(primaryAlias)); | ||||
| @@ -77,12 +80,17 @@ namespace Discord.Commands.Builders | |||||
| { | { | ||||
| for (int i = 0; i < aliases.Length; i++) | for (int i = 0; i < aliases.Length; i++) | ||||
| { | { | ||||
| var alias = aliases[i] ?? ""; | |||||
| string alias = aliases[i] ?? ""; | |||||
| if (!_aliases.Contains(alias)) | if (!_aliases.Contains(alias)) | ||||
| _aliases.Add(alias); | _aliases.Add(alias); | ||||
| } | } | ||||
| return this; | return this; | ||||
| } | } | ||||
| public CommandBuilder AddAttributes(params Attribute[] attributes) | |||||
| { | |||||
| _attributes.AddRange(attributes); | |||||
| return this; | |||||
| } | |||||
| public CommandBuilder AddPrecondition(PreconditionAttribute precondition) | public CommandBuilder AddPrecondition(PreconditionAttribute precondition) | ||||
| { | { | ||||
| _preconditions.Add(precondition); | _preconditions.Add(precondition); | ||||
| @@ -122,11 +130,11 @@ namespace Discord.Commands.Builders | |||||
| var firstMultipleParam = _parameters.FirstOrDefault(x => x.IsMultiple); | var firstMultipleParam = _parameters.FirstOrDefault(x => x.IsMultiple); | ||||
| if ((firstMultipleParam != null) && (firstMultipleParam != lastParam)) | if ((firstMultipleParam != null) && (firstMultipleParam != lastParam)) | ||||
| throw new InvalidOperationException("Only the last parameter in a command may have the Multiple flag."); | |||||
| throw new InvalidOperationException($"Only the last parameter in a command may have the Multiple flag. Parameter: {firstMultipleParam.Name} in {PrimaryAlias}"); | |||||
| var firstRemainderParam = _parameters.FirstOrDefault(x => x.IsRemainder); | var firstRemainderParam = _parameters.FirstOrDefault(x => x.IsRemainder); | ||||
| if ((firstRemainderParam != null) && (firstRemainderParam != lastParam)) | if ((firstRemainderParam != null) && (firstRemainderParam != lastParam)) | ||||
| throw new InvalidOperationException("Only the last parameter in a command may have the Remainder flag."); | |||||
| throw new InvalidOperationException($"Only the last parameter in a command may have the Remainder flag. Parameter: {firstRemainderParam.Name} in {PrimaryAlias}"); | |||||
| } | } | ||||
| return new CommandInfo(this, info, service); | return new CommandInfo(this, info, service); | ||||
| @@ -10,6 +10,7 @@ namespace Discord.Commands.Builders | |||||
| private readonly List<CommandBuilder> _commands; | private readonly List<CommandBuilder> _commands; | ||||
| private readonly List<ModuleBuilder> _submodules; | private readonly List<ModuleBuilder> _submodules; | ||||
| private readonly List<PreconditionAttribute> _preconditions; | private readonly List<PreconditionAttribute> _preconditions; | ||||
| private readonly List<Attribute> _attributes; | |||||
| private readonly List<string> _aliases; | private readonly List<string> _aliases; | ||||
| public CommandService Service { get; } | public CommandService Service { get; } | ||||
| @@ -21,6 +22,7 @@ namespace Discord.Commands.Builders | |||||
| public IReadOnlyList<CommandBuilder> Commands => _commands; | public IReadOnlyList<CommandBuilder> Commands => _commands; | ||||
| public IReadOnlyList<ModuleBuilder> Modules => _submodules; | public IReadOnlyList<ModuleBuilder> Modules => _submodules; | ||||
| public IReadOnlyList<PreconditionAttribute> Preconditions => _preconditions; | public IReadOnlyList<PreconditionAttribute> Preconditions => _preconditions; | ||||
| public IReadOnlyList<Attribute> Attributes => _attributes; | |||||
| public IReadOnlyList<string> Aliases => _aliases; | public IReadOnlyList<string> Aliases => _aliases; | ||||
| //Automatic | //Automatic | ||||
| @@ -32,6 +34,7 @@ namespace Discord.Commands.Builders | |||||
| _commands = new List<CommandBuilder>(); | _commands = new List<CommandBuilder>(); | ||||
| _submodules = new List<ModuleBuilder>(); | _submodules = new List<ModuleBuilder>(); | ||||
| _preconditions = new List<PreconditionAttribute>(); | _preconditions = new List<PreconditionAttribute>(); | ||||
| _attributes = new List<Attribute>(); | |||||
| _aliases = new List<string>(); | _aliases = new List<string>(); | ||||
| } | } | ||||
| //User-defined | //User-defined | ||||
| @@ -63,18 +66,23 @@ namespace Discord.Commands.Builders | |||||
| { | { | ||||
| for (int i = 0; i < aliases.Length; i++) | for (int i = 0; i < aliases.Length; i++) | ||||
| { | { | ||||
| var alias = aliases[i] ?? ""; | |||||
| string alias = aliases[i] ?? ""; | |||||
| if (!_aliases.Contains(alias)) | if (!_aliases.Contains(alias)) | ||||
| _aliases.Add(alias); | _aliases.Add(alias); | ||||
| } | } | ||||
| return this; | return this; | ||||
| } | } | ||||
| public ModuleBuilder AddAttributes(params Attribute[] attributes) | |||||
| { | |||||
| _attributes.AddRange(attributes); | |||||
| return this; | |||||
| } | |||||
| public ModuleBuilder AddPrecondition(PreconditionAttribute precondition) | public ModuleBuilder AddPrecondition(PreconditionAttribute precondition) | ||||
| { | { | ||||
| _preconditions.Add(precondition); | _preconditions.Add(precondition); | ||||
| return this; | return this; | ||||
| } | } | ||||
| public ModuleBuilder AddCommand(string primaryAlias, Func<ICommandContext, object[], IServiceProvider, Task> callback, Action<CommandBuilder> createFunc) | |||||
| public ModuleBuilder AddCommand(string primaryAlias, Func<ICommandContext, object[], IServiceProvider, CommandInfo, Task> callback, Action<CommandBuilder> createFunc) | |||||
| { | { | ||||
| var builder = new CommandBuilder(this, primaryAlias, callback); | var builder = new CommandBuilder(this, primaryAlias, callback); | ||||
| createFunc(builder); | createFunc(builder); | ||||
| @@ -12,25 +12,42 @@ namespace Discord.Commands | |||||
| { | { | ||||
| private static readonly TypeInfo _moduleTypeInfo = typeof(IModuleBase).GetTypeInfo(); | private static readonly TypeInfo _moduleTypeInfo = typeof(IModuleBase).GetTypeInfo(); | ||||
| public static IEnumerable<TypeInfo> Search(Assembly assembly) | |||||
| public static async Task<IReadOnlyList<TypeInfo>> SearchAsync(Assembly assembly, CommandService service) | |||||
| { | { | ||||
| foreach (var type in assembly.ExportedTypes) | |||||
| bool IsLoadableModule(TypeInfo info) | |||||
| { | { | ||||
| var typeInfo = type.GetTypeInfo(); | |||||
| if (IsValidModuleDefinition(typeInfo) && | |||||
| !typeInfo.IsDefined(typeof(DontAutoLoadAttribute))) | |||||
| return info.DeclaredMethods.Any(x => x.GetCustomAttribute<CommandAttribute>() != null) && | |||||
| info.GetCustomAttribute<DontAutoLoadAttribute>() == null; | |||||
| } | |||||
| var result = new List<TypeInfo>(); | |||||
| foreach (var typeInfo in assembly.DefinedTypes) | |||||
| { | |||||
| if (typeInfo.IsPublic || typeInfo.IsNestedPublic) | |||||
| { | { | ||||
| yield return typeInfo; | |||||
| if (IsValidModuleDefinition(typeInfo) && | |||||
| !typeInfo.IsDefined(typeof(DontAutoLoadAttribute))) | |||||
| { | |||||
| result.Add(typeInfo); | |||||
| } | |||||
| } | |||||
| else if (IsLoadableModule(typeInfo)) | |||||
| { | |||||
| await service._cmdLogger.WarningAsync($"Class {typeInfo.FullName} is not public and cannot be loaded. To suppress this message, mark the class with {nameof(DontAutoLoadAttribute)}."); | |||||
| } | } | ||||
| } | } | ||||
| return result; | |||||
| } | } | ||||
| public static Dictionary<Type, ModuleInfo> Build(CommandService service, params TypeInfo[] validTypes) => Build(validTypes, service); | |||||
| public static Dictionary<Type, ModuleInfo> Build(IEnumerable<TypeInfo> validTypes, CommandService service) | |||||
| public static Task<Dictionary<Type, ModuleInfo>> BuildAsync(CommandService service, params TypeInfo[] validTypes) => BuildAsync(validTypes, service); | |||||
| public static async Task<Dictionary<Type, ModuleInfo>> BuildAsync(IEnumerable<TypeInfo> validTypes, CommandService service) | |||||
| { | { | ||||
| /*if (!validTypes.Any()) | /*if (!validTypes.Any()) | ||||
| throw new InvalidOperationException("Could not find any valid modules from the given selection");*/ | throw new InvalidOperationException("Could not find any valid modules from the given selection");*/ | ||||
| var topLevelGroups = validTypes.Where(x => x.DeclaringType == null); | var topLevelGroups = validTypes.Where(x => x.DeclaringType == null); | ||||
| var subGroups = validTypes.Intersect(topLevelGroups); | var subGroups = validTypes.Intersect(topLevelGroups); | ||||
| @@ -48,10 +65,13 @@ namespace Discord.Commands | |||||
| BuildModule(module, typeInfo, service); | BuildModule(module, typeInfo, service); | ||||
| BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service); | BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service); | ||||
| builtTypes.Add(typeInfo); | |||||
| result[typeInfo.AsType()] = module.Build(service); | result[typeInfo.AsType()] = module.Build(service); | ||||
| } | } | ||||
| await service._cmdLogger.DebugAsync($"Successfully built {builtTypes.Count} modules.").ConfigureAwait(false); | |||||
| return result; | return result; | ||||
| } | } | ||||
| @@ -102,6 +122,9 @@ namespace Discord.Commands | |||||
| case PreconditionAttribute precondition: | case PreconditionAttribute precondition: | ||||
| builder.AddPrecondition(precondition); | builder.AddPrecondition(precondition); | ||||
| break; | break; | ||||
| default: | |||||
| builder.AddAttributes(attribute); | |||||
| break; | |||||
| } | } | ||||
| } | } | ||||
| @@ -128,26 +151,35 @@ namespace Discord.Commands | |||||
| foreach (var attribute in attributes) | foreach (var attribute in attributes) | ||||
| { | { | ||||
| // TODO: C#7 type switch | |||||
| if (attribute is CommandAttribute) | |||||
| switch (attribute) | |||||
| { | { | ||||
| var cmdAttr = attribute as CommandAttribute; | |||||
| builder.AddAliases(cmdAttr.Text); | |||||
| builder.RunMode = cmdAttr.RunMode; | |||||
| builder.Name = builder.Name ?? cmdAttr.Text; | |||||
| case CommandAttribute command: | |||||
| builder.AddAliases(command.Text); | |||||
| builder.RunMode = command.RunMode; | |||||
| builder.Name = builder.Name ?? command.Text; | |||||
| break; | |||||
| case NameAttribute name: | |||||
| builder.Name = name.Text; | |||||
| break; | |||||
| case PriorityAttribute priority: | |||||
| builder.Priority = priority.Priority; | |||||
| break; | |||||
| case SummaryAttribute summary: | |||||
| builder.Summary = summary.Text; | |||||
| break; | |||||
| case RemarksAttribute remarks: | |||||
| builder.Remarks = remarks.Text; | |||||
| break; | |||||
| case AliasAttribute alias: | |||||
| builder.AddAliases(alias.Aliases); | |||||
| break; | |||||
| case PreconditionAttribute precondition: | |||||
| builder.AddPrecondition(precondition); | |||||
| break; | |||||
| default: | |||||
| builder.AddAttributes(attribute); | |||||
| break; | |||||
| } | } | ||||
| else if (attribute is NameAttribute) | |||||
| builder.Name = (attribute as NameAttribute).Text; | |||||
| else if (attribute is PriorityAttribute) | |||||
| builder.Priority = (attribute as PriorityAttribute).Priority; | |||||
| else if (attribute is SummaryAttribute) | |||||
| builder.Summary = (attribute as SummaryAttribute).Text; | |||||
| else if (attribute is RemarksAttribute) | |||||
| builder.Remarks = (attribute as RemarksAttribute).Text; | |||||
| else if (attribute is AliasAttribute) | |||||
| builder.AddAliases((attribute as AliasAttribute).Aliases); | |||||
| else if (attribute is PreconditionAttribute) | |||||
| builder.AddPrecondition(attribute as PreconditionAttribute); | |||||
| } | } | ||||
| if (builder.Name == null) | if (builder.Name == null) | ||||
| @@ -165,22 +197,34 @@ namespace Discord.Commands | |||||
| var createInstance = ReflectionUtils.CreateBuilder<IModuleBase>(typeInfo, service); | var createInstance = ReflectionUtils.CreateBuilder<IModuleBase>(typeInfo, service); | ||||
| builder.Callback = async (ctx, args, map) => | |||||
| async Task<IResult> ExecuteCallback(ICommandContext context, object[] args, IServiceProvider services, CommandInfo cmd) | |||||
| { | { | ||||
| var instance = createInstance(map); | |||||
| instance.SetContext(ctx); | |||||
| var instance = createInstance(services); | |||||
| instance.SetContext(context); | |||||
| try | try | ||||
| { | { | ||||
| instance.BeforeExecute(); | |||||
| instance.BeforeExecute(cmd); | |||||
| var task = method.Invoke(instance, args) as Task ?? Task.Delay(0); | var task = method.Invoke(instance, args) as Task ?? Task.Delay(0); | ||||
| await task.ConfigureAwait(false); | |||||
| if (task is Task<RuntimeResult> resultTask) | |||||
| { | |||||
| return await resultTask.ConfigureAwait(false); | |||||
| } | |||||
| else | |||||
| { | |||||
| await task.ConfigureAwait(false); | |||||
| return ExecuteResult.FromSuccess(); | |||||
| } | |||||
| } | } | ||||
| finally | finally | ||||
| { | { | ||||
| instance.AfterExecute(); | |||||
| instance.AfterExecute(cmd); | |||||
| (instance as IDisposable)?.Dispose(); | (instance as IDisposable)?.Dispose(); | ||||
| } | } | ||||
| }; | |||||
| } | |||||
| builder.Callback = ExecuteCallback; | |||||
| } | } | ||||
| private static void BuildParameter(ParameterBuilder builder, System.Reflection.ParameterInfo paramInfo, int position, int count, CommandService service) | private static void BuildParameter(ParameterBuilder builder, System.Reflection.ParameterInfo paramInfo, int position, int count, CommandService service) | ||||
| @@ -195,24 +239,30 @@ namespace Discord.Commands | |||||
| foreach (var attribute in attributes) | foreach (var attribute in attributes) | ||||
| { | { | ||||
| // TODO: C#7 type switch | |||||
| if (attribute is SummaryAttribute) | |||||
| builder.Summary = (attribute as SummaryAttribute).Text; | |||||
| else if (attribute is OverrideTypeReaderAttribute) | |||||
| builder.TypeReader = GetTypeReader(service, paramType, (attribute as OverrideTypeReaderAttribute).TypeReader); | |||||
| else if (attribute is ParameterPreconditionAttribute) | |||||
| builder.AddPrecondition(attribute as ParameterPreconditionAttribute); | |||||
| else if (attribute is ParamArrayAttribute) | |||||
| { | |||||
| builder.IsMultiple = true; | |||||
| paramType = paramType.GetElementType(); | |||||
| } | |||||
| else if (attribute is RemainderAttribute) | |||||
| switch (attribute) | |||||
| { | { | ||||
| if (position != count-1) | |||||
| throw new InvalidOperationException("Remainder parameters must be the last parameter in a command."); | |||||
| builder.IsRemainder = true; | |||||
| case SummaryAttribute summary: | |||||
| builder.Summary = summary.Text; | |||||
| break; | |||||
| case OverrideTypeReaderAttribute typeReader: | |||||
| builder.TypeReader = GetTypeReader(service, paramType, typeReader.TypeReader); | |||||
| break; | |||||
| case ParamArrayAttribute _: | |||||
| builder.IsMultiple = true; | |||||
| paramType = paramType.GetElementType(); | |||||
| break; | |||||
| case ParameterPreconditionAttribute precon: | |||||
| builder.AddPrecondition(precon); | |||||
| break; | |||||
| case RemainderAttribute _: | |||||
| if (position != count - 1) | |||||
| throw new InvalidOperationException($"Remainder parameters must be the last parameter in a command. Parameter: {paramInfo.Name} in {paramInfo.Member.DeclaringType.Name}.{paramInfo.Member.Name}"); | |||||
| builder.IsRemainder = true; | |||||
| break; | |||||
| default: | |||||
| builder.AddAttributes(attribute); | |||||
| break; | |||||
| } | } | ||||
| } | } | ||||
| @@ -258,9 +308,9 @@ namespace Discord.Commands | |||||
| private static bool IsValidCommandDefinition(MethodInfo methodInfo) | private static bool IsValidCommandDefinition(MethodInfo methodInfo) | ||||
| { | { | ||||
| return methodInfo.IsDefined(typeof(CommandAttribute)) && | return methodInfo.IsDefined(typeof(CommandAttribute)) && | ||||
| (methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(void)) && | |||||
| (methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task<RuntimeResult>)) && | |||||
| !methodInfo.IsStatic && | !methodInfo.IsStatic && | ||||
| !methodInfo.IsGenericMethod; | !methodInfo.IsGenericMethod; | ||||
| } | } | ||||
| } | } | ||||
| } | |||||
| } | |||||
| @@ -8,7 +8,8 @@ namespace Discord.Commands.Builders | |||||
| { | { | ||||
| public class ParameterBuilder | public class ParameterBuilder | ||||
| { | { | ||||
| private readonly List<ParameterPreconditionAttribute> _preconditions; | |||||
| private readonly List<ParameterPreconditionAttribute> _preconditions; | |||||
| private readonly List<Attribute> _attributes; | |||||
| public CommandBuilder Command { get; } | public CommandBuilder Command { get; } | ||||
| public string Name { get; internal set; } | public string Name { get; internal set; } | ||||
| @@ -22,11 +23,13 @@ namespace Discord.Commands.Builders | |||||
| public string Summary { get; set; } | public string Summary { get; set; } | ||||
| public IReadOnlyList<ParameterPreconditionAttribute> Preconditions => _preconditions; | public IReadOnlyList<ParameterPreconditionAttribute> Preconditions => _preconditions; | ||||
| public IReadOnlyList<Attribute> Attributes => _attributes; | |||||
| //Automatic | //Automatic | ||||
| internal ParameterBuilder(CommandBuilder command) | internal ParameterBuilder(CommandBuilder command) | ||||
| { | { | ||||
| _preconditions = new List<ParameterPreconditionAttribute>(); | _preconditions = new List<ParameterPreconditionAttribute>(); | ||||
| _attributes = new List<Attribute>(); | |||||
| Command = command; | Command = command; | ||||
| } | } | ||||
| @@ -49,7 +52,7 @@ namespace Discord.Commands.Builders | |||||
| TypeReader = Command.Module.Service.GetDefaultTypeReader(type); | TypeReader = Command.Module.Service.GetDefaultTypeReader(type); | ||||
| if (TypeReader == null) | if (TypeReader == null) | ||||
| throw new InvalidOperationException($"{type} does not have a TypeReader registered for it"); | |||||
| throw new InvalidOperationException($"{type} does not have a TypeReader registered for it. Parameter: {Name} in {Command.PrimaryAlias}"); | |||||
| if (type.GetTypeInfo().IsValueType) | if (type.GetTypeInfo().IsValueType) | ||||
| DefaultValue = Activator.CreateInstance(type); | DefaultValue = Activator.CreateInstance(type); | ||||
| @@ -84,6 +87,11 @@ namespace Discord.Commands.Builders | |||||
| return this; | return this; | ||||
| } | } | ||||
| public ParameterBuilder AddAttributes(params Attribute[] attributes) | |||||
| { | |||||
| _attributes.AddRange(attributes); | |||||
| return this; | |||||
| } | |||||
| public ParameterBuilder AddPrecondition(ParameterPreconditionAttribute precondition) | public ParameterBuilder AddPrecondition(ParameterPreconditionAttribute precondition) | ||||
| { | { | ||||
| _preconditions.Add(precondition); | _preconditions.Add(precondition); | ||||
| @@ -18,6 +18,9 @@ | |||||
| UnmetPrecondition, | UnmetPrecondition, | ||||
| //Execute | //Execute | ||||
| Exception | |||||
| Exception, | |||||
| //Runtime | |||||
| Unsuccessful | |||||
| } | } | ||||
| } | } | ||||
| @@ -18,11 +18,11 @@ namespace Discord.Commands | |||||
| public Task<PreconditionResult> CheckPreconditionsAsync(ICommandContext context, IServiceProvider services = null) | public Task<PreconditionResult> CheckPreconditionsAsync(ICommandContext context, IServiceProvider services = null) | ||||
| => Command.CheckPreconditionsAsync(context, services); | => Command.CheckPreconditionsAsync(context, services); | ||||
| public Task<ParseResult> ParseAsync(ICommandContext context, SearchResult searchResult, PreconditionResult? preconditionResult = null) | |||||
| => Command.ParseAsync(context, Alias.Length, searchResult, preconditionResult); | |||||
| public Task<ExecuteResult> ExecuteAsync(ICommandContext context, IEnumerable<object> argList, IEnumerable<object> paramList, IServiceProvider services) | |||||
| public Task<ParseResult> ParseAsync(ICommandContext context, SearchResult searchResult, PreconditionResult preconditionResult = null, IServiceProvider services = null) | |||||
| => Command.ParseAsync(context, Alias.Length, searchResult, preconditionResult, services); | |||||
| public Task<IResult> ExecuteAsync(ICommandContext context, IEnumerable<object> argList, IEnumerable<object> paramList, IServiceProvider services) | |||||
| => Command.ExecuteAsync(context, argList, paramList, services); | => Command.ExecuteAsync(context, argList, paramList, services); | ||||
| public Task<ExecuteResult> ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) | |||||
| public Task<IResult> ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) | |||||
| => Command.ExecuteAsync(context, parseResult, services); | => Command.ExecuteAsync(context, parseResult, services); | ||||
| } | } | ||||
| } | } | ||||
| @@ -1,4 +1,5 @@ | |||||
| using System.Collections.Immutable; | |||||
| using System; | |||||
| using System.Collections.Immutable; | |||||
| using System.Text; | using System.Text; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| @@ -13,7 +14,7 @@ namespace Discord.Commands | |||||
| QuotedParameter | QuotedParameter | ||||
| } | } | ||||
| public static async Task<ParseResult> ParseArgs(CommandInfo command, ICommandContext context, string input, int startPos) | |||||
| public static async Task<ParseResult> ParseArgs(CommandInfo command, ICommandContext context, IServiceProvider services, string input, int startPos) | |||||
| { | { | ||||
| ParameterInfo curParam = null; | ParameterInfo curParam = null; | ||||
| StringBuilder argBuilder = new StringBuilder(input.Length); | StringBuilder argBuilder = new StringBuilder(input.Length); | ||||
| @@ -110,7 +111,7 @@ namespace Discord.Commands | |||||
| if (curParam == null) | if (curParam == null) | ||||
| return ParseResult.FromError(CommandError.BadArgCount, "The input text has too many parameters."); | return ParseResult.FromError(CommandError.BadArgCount, "The input text has too many parameters."); | ||||
| var typeReaderResult = await curParam.Parse(context, argString).ConfigureAwait(false); | |||||
| var typeReaderResult = await curParam.Parse(context, argString, services).ConfigureAwait(false); | |||||
| if (!typeReaderResult.IsSuccess && typeReaderResult.Error != CommandError.MultipleMatches) | if (!typeReaderResult.IsSuccess && typeReaderResult.Error != CommandError.MultipleMatches) | ||||
| return ParseResult.FromError(typeReaderResult); | return ParseResult.FromError(typeReaderResult); | ||||
| @@ -133,7 +134,7 @@ namespace Discord.Commands | |||||
| if (curParam != null && curParam.IsRemainder) | if (curParam != null && curParam.IsRemainder) | ||||
| { | { | ||||
| var typeReaderResult = await curParam.Parse(context, argBuilder.ToString()).ConfigureAwait(false); | |||||
| var typeReaderResult = await curParam.Parse(context, argBuilder.ToString(), services).ConfigureAwait(false); | |||||
| if (!typeReaderResult.IsSuccess) | if (!typeReaderResult.IsSuccess) | ||||
| return ParseResult.FromError(typeReaderResult); | return ParseResult.FromError(typeReaderResult); | ||||
| argList.Add(typeReaderResult); | argList.Add(typeReaderResult); | ||||
| @@ -33,7 +33,7 @@ namespace Discord.Commands | |||||
| public IEnumerable<ModuleInfo> Modules => _moduleDefs.Select(x => x); | public IEnumerable<ModuleInfo> Modules => _moduleDefs.Select(x => x); | ||||
| public IEnumerable<CommandInfo> Commands => _moduleDefs.SelectMany(x => x.Commands); | public IEnumerable<CommandInfo> Commands => _moduleDefs.SelectMany(x => x.Commands); | ||||
| public ILookup<Type, TypeReader> TypeReaders => _typeReaders.SelectMany(x => x.Value.Select(y => new {y.Key, y.Value})).ToLookup(x => x.Key, x => x.Value); | |||||
| public ILookup<Type, TypeReader> TypeReaders => _typeReaders.SelectMany(x => x.Value.Select(y => new { y.Key, y.Value })).ToLookup(x => x.Key, x => x.Value); | |||||
| public CommandService() : this(new CommandServiceConfig()) { } | public CommandService() : this(new CommandServiceConfig()) { } | ||||
| public CommandService(CommandServiceConfig config) | public CommandService(CommandServiceConfig config) | ||||
| @@ -59,6 +59,9 @@ namespace Discord.Commands | |||||
| foreach (var type in PrimitiveParsers.SupportedTypes) | foreach (var type in PrimitiveParsers.SupportedTypes) | ||||
| _defaultTypeReaders[type] = PrimitiveTypeReader.Create(type); | _defaultTypeReaders[type] = PrimitiveTypeReader.Create(type); | ||||
| _defaultTypeReaders[typeof(string)] = | |||||
| new PrimitiveTypeReader<string>((string x, out string y) => { y = x; return true; }, 0); | |||||
| var entityTypeReaders = ImmutableList.CreateBuilder<Tuple<Type, Type>>(); | var entityTypeReaders = ImmutableList.CreateBuilder<Tuple<Type, Type>>(); | ||||
| entityTypeReaders.Add(new Tuple<Type, Type>(typeof(IMessage), typeof(MessageTypeReader<>))); | entityTypeReaders.Add(new Tuple<Type, Type>(typeof(IMessage), typeof(MessageTypeReader<>))); | ||||
| entityTypeReaders.Add(new Tuple<Type, Type>(typeof(IChannel), typeof(ChannelTypeReader<>))); | entityTypeReaders.Add(new Tuple<Type, Type>(typeof(IChannel), typeof(ChannelTypeReader<>))); | ||||
| @@ -95,7 +98,7 @@ namespace Discord.Commands | |||||
| if (_typedModuleDefs.ContainsKey(type)) | if (_typedModuleDefs.ContainsKey(type)) | ||||
| throw new ArgumentException($"This module has already been added."); | throw new ArgumentException($"This module has already been added."); | ||||
| var module = ModuleClassBuilder.Build(this, typeInfo).FirstOrDefault(); | |||||
| var module = (await ModuleClassBuilder.BuildAsync(this, typeInfo).ConfigureAwait(false)).FirstOrDefault(); | |||||
| if (module.Value == default(ModuleInfo)) | if (module.Value == default(ModuleInfo)) | ||||
| throw new InvalidOperationException($"Could not build the module {type.FullName}, did you pass an invalid type?"); | throw new InvalidOperationException($"Could not build the module {type.FullName}, did you pass an invalid type?"); | ||||
| @@ -114,8 +117,8 @@ namespace Discord.Commands | |||||
| await _moduleLock.WaitAsync().ConfigureAwait(false); | await _moduleLock.WaitAsync().ConfigureAwait(false); | ||||
| try | try | ||||
| { | { | ||||
| var types = ModuleClassBuilder.Search(assembly).ToArray(); | |||||
| var moduleDefs = ModuleClassBuilder.Build(types, this); | |||||
| var types = await ModuleClassBuilder.SearchAsync(assembly, this).ConfigureAwait(false); | |||||
| var moduleDefs = await ModuleClassBuilder.BuildAsync(types, this).ConfigureAwait(false); | |||||
| foreach (var info in moduleDefs) | foreach (var info in moduleDefs) | ||||
| { | { | ||||
| @@ -161,8 +164,7 @@ namespace Discord.Commands | |||||
| await _moduleLock.WaitAsync().ConfigureAwait(false); | await _moduleLock.WaitAsync().ConfigureAwait(false); | ||||
| try | try | ||||
| { | { | ||||
| ModuleInfo module; | |||||
| if (!_typedModuleDefs.TryRemove(type, out module)) | |||||
| if (!_typedModuleDefs.TryRemove(type, out var module)) | |||||
| return false; | return false; | ||||
| return RemoveModuleInternal(module); | return RemoveModuleInternal(module); | ||||
| @@ -196,20 +198,18 @@ namespace Discord.Commands | |||||
| } | } | ||||
| public void AddTypeReader(Type type, TypeReader reader) | public void AddTypeReader(Type type, TypeReader reader) | ||||
| { | { | ||||
| var readers = _typeReaders.GetOrAdd(type, x=> new ConcurrentDictionary<Type, TypeReader>()); | |||||
| var readers = _typeReaders.GetOrAdd(type, x => new ConcurrentDictionary<Type, TypeReader>()); | |||||
| readers[reader.GetType()] = reader; | readers[reader.GetType()] = reader; | ||||
| } | } | ||||
| internal IDictionary<Type, TypeReader> GetTypeReaders(Type type) | internal IDictionary<Type, TypeReader> GetTypeReaders(Type type) | ||||
| { | { | ||||
| ConcurrentDictionary<Type, TypeReader> definedTypeReaders; | |||||
| if (_typeReaders.TryGetValue(type, out definedTypeReaders)) | |||||
| if (_typeReaders.TryGetValue(type, out var definedTypeReaders)) | |||||
| return definedTypeReaders; | return definedTypeReaders; | ||||
| return null; | return null; | ||||
| } | } | ||||
| internal TypeReader GetDefaultTypeReader(Type type) | internal TypeReader GetDefaultTypeReader(Type type) | ||||
| { | { | ||||
| TypeReader reader; | |||||
| if (_defaultTypeReaders.TryGetValue(type, out reader)) | |||||
| if (_defaultTypeReaders.TryGetValue(type, out var reader)) | |||||
| return reader; | return reader; | ||||
| var typeInfo = type.GetTypeInfo(); | var typeInfo = type.GetTypeInfo(); | ||||
| @@ -235,13 +235,13 @@ namespace Discord.Commands | |||||
| } | } | ||||
| //Execution | //Execution | ||||
| public SearchResult Search(ICommandContext context, int argPos) | |||||
| public SearchResult Search(ICommandContext context, int argPos) | |||||
| => Search(context, context.Message.Content.Substring(argPos)); | => Search(context, context.Message.Content.Substring(argPos)); | ||||
| public SearchResult Search(ICommandContext context, string input) | public SearchResult Search(ICommandContext context, string input) | ||||
| { | { | ||||
| string searchInput = _caseSensitive ? input : input.ToLowerInvariant(); | string searchInput = _caseSensitive ? input : input.ToLowerInvariant(); | ||||
| var matches = _map.GetCommands(searchInput).OrderByDescending(x => x.Command.Priority).ToImmutableArray(); | var matches = _map.GetCommands(searchInput).OrderByDescending(x => x.Command.Priority).ToImmutableArray(); | ||||
| if (matches.Length > 0) | if (matches.Length > 0) | ||||
| return SearchResult.FromSuccess(input, matches); | return SearchResult.FromSuccess(input, matches); | ||||
| else | else | ||||
| @@ -259,46 +259,86 @@ namespace Discord.Commands | |||||
| return searchResult; | return searchResult; | ||||
| var commands = searchResult.Commands; | var commands = searchResult.Commands; | ||||
| for (int i = 0; i < commands.Count; i++) | |||||
| var preconditionResults = new Dictionary<CommandMatch, PreconditionResult>(); | |||||
| foreach (var match in commands) | |||||
| { | { | ||||
| var preconditionResult = await commands[i].CheckPreconditionsAsync(context, services).ConfigureAwait(false); | |||||
| if (!preconditionResult.IsSuccess) | |||||
| { | |||||
| if (commands.Count == 1) | |||||
| return preconditionResult; | |||||
| else | |||||
| continue; | |||||
| } | |||||
| preconditionResults[match] = await match.Command.CheckPreconditionsAsync(context, services).ConfigureAwait(false); | |||||
| } | |||||
| var successfulPreconditions = preconditionResults | |||||
| .Where(x => x.Value.IsSuccess) | |||||
| .ToArray(); | |||||
| if (successfulPreconditions.Length == 0) | |||||
| { | |||||
| //All preconditions failed, return the one from the highest priority command | |||||
| var bestCandidate = preconditionResults | |||||
| .OrderByDescending(x => x.Key.Command.Priority) | |||||
| .FirstOrDefault(x => !x.Value.IsSuccess); | |||||
| return bestCandidate.Value; | |||||
| } | |||||
| //If we get this far, at least one precondition was successful. | |||||
| var parseResultsDict = new Dictionary<CommandMatch, ParseResult>(); | |||||
| foreach (var pair in successfulPreconditions) | |||||
| { | |||||
| var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, services).ConfigureAwait(false); | |||||
| var parseResult = await commands[i].ParseAsync(context, searchResult, preconditionResult).ConfigureAwait(false); | |||||
| if (!parseResult.IsSuccess) | |||||
| if (parseResult.Error == CommandError.MultipleMatches) | |||||
| { | { | ||||
| if (parseResult.Error == CommandError.MultipleMatches) | |||||
| IReadOnlyList<TypeReaderValue> argList, paramList; | |||||
| switch (multiMatchHandling) | |||||
| { | { | ||||
| IReadOnlyList<TypeReaderValue> argList, paramList; | |||||
| switch (multiMatchHandling) | |||||
| { | |||||
| case MultiMatchHandling.Best: | |||||
| argList = parseResult.ArgValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); | |||||
| paramList = parseResult.ParamValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); | |||||
| parseResult = ParseResult.FromSuccess(argList, paramList); | |||||
| break; | |||||
| } | |||||
| case MultiMatchHandling.Best: | |||||
| argList = parseResult.ArgValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); | |||||
| paramList = parseResult.ParamValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); | |||||
| parseResult = ParseResult.FromSuccess(argList, paramList); | |||||
| break; | |||||
| } | } | ||||
| } | |||||
| if (!parseResult.IsSuccess) | |||||
| { | |||||
| if (commands.Count == 1) | |||||
| return parseResult; | |||||
| else | |||||
| continue; | |||||
| } | |||||
| parseResultsDict[pair.Key] = parseResult; | |||||
| } | |||||
| // Calculates the 'score' of a command given a parse result | |||||
| float CalculateScore(CommandMatch match, ParseResult parseResult) | |||||
| { | |||||
| float argValuesScore = 0, paramValuesScore = 0; | |||||
| if (match.Command.Parameters.Count > 0) | |||||
| { | |||||
| var argValuesSum = parseResult.ArgValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; | |||||
| var paramValuesSum = parseResult.ParamValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; | |||||
| argValuesScore = argValuesSum / match.Command.Parameters.Count; | |||||
| paramValuesScore = paramValuesSum / match.Command.Parameters.Count; | |||||
| } | } | ||||
| return await commands[i].ExecuteAsync(context, parseResult, services).ConfigureAwait(false); | |||||
| var totalArgsScore = (argValuesScore + paramValuesScore) / 2; | |||||
| return match.Command.Priority + totalArgsScore * 0.99f; | |||||
| } | |||||
| //Order the parse results by their score so that we choose the most likely result to execute | |||||
| var parseResults = parseResultsDict | |||||
| .OrderByDescending(x => CalculateScore(x.Key, x.Value)); | |||||
| var successfulParses = parseResults | |||||
| .Where(x => x.Value.IsSuccess) | |||||
| .ToArray(); | |||||
| if (successfulParses.Length == 0) | |||||
| { | |||||
| //All parses failed, return the one from the highest priority command, using score as a tie breaker | |||||
| var bestMatch = parseResults | |||||
| .FirstOrDefault(x => !x.Value.IsSuccess); | |||||
| return bestMatch.Value; | |||||
| } | } | ||||
| return SearchResult.FromError(CommandError.UnknownCommand, "This input does not match any overload."); | |||||
| //If we get this far, at least one parse was successful. Execute the most likely overload. | |||||
| var chosenOverload = successfulParses[0]; | |||||
| return await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, services).ConfigureAwait(false); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -4,8 +4,8 @@ | |||||
| { | { | ||||
| void SetContext(ICommandContext context); | void SetContext(ICommandContext context); | ||||
| void BeforeExecute(); | |||||
| void BeforeExecute(CommandInfo command); | |||||
| void AfterExecute(); | |||||
| void AfterExecute(CommandInfo command); | |||||
| } | } | ||||
| } | } | ||||
| @@ -18,7 +18,7 @@ namespace Discord.Commands | |||||
| private static readonly System.Reflection.MethodInfo _convertParamsMethod = typeof(CommandInfo).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList)); | private static readonly System.Reflection.MethodInfo _convertParamsMethod = typeof(CommandInfo).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList)); | ||||
| private static readonly ConcurrentDictionary<Type, Func<IEnumerable<object>, object>> _arrayConverters = new ConcurrentDictionary<Type, Func<IEnumerable<object>, object>>(); | private static readonly ConcurrentDictionary<Type, Func<IEnumerable<object>, object>> _arrayConverters = new ConcurrentDictionary<Type, Func<IEnumerable<object>, object>>(); | ||||
| private readonly Func<ICommandContext, object[], IServiceProvider, Task> _action; | |||||
| private readonly Func<ICommandContext, object[], IServiceProvider, CommandInfo, Task> _action; | |||||
| public ModuleInfo Module { get; } | public ModuleInfo Module { get; } | ||||
| public string Name { get; } | public string Name { get; } | ||||
| @@ -31,18 +31,19 @@ namespace Discord.Commands | |||||
| public IReadOnlyList<string> Aliases { get; } | public IReadOnlyList<string> Aliases { get; } | ||||
| public IReadOnlyList<ParameterInfo> Parameters { get; } | public IReadOnlyList<ParameterInfo> Parameters { get; } | ||||
| public IReadOnlyList<PreconditionAttribute> Preconditions { get; } | public IReadOnlyList<PreconditionAttribute> Preconditions { get; } | ||||
| public IReadOnlyList<Attribute> Attributes { get; } | |||||
| internal CommandInfo(CommandBuilder builder, ModuleInfo module, CommandService service) | internal CommandInfo(CommandBuilder builder, ModuleInfo module, CommandService service) | ||||
| { | { | ||||
| Module = module; | Module = module; | ||||
| Name = builder.Name; | Name = builder.Name; | ||||
| Summary = builder.Summary; | Summary = builder.Summary; | ||||
| Remarks = builder.Remarks; | Remarks = builder.Remarks; | ||||
| RunMode = (builder.RunMode == RunMode.Default ? service._defaultRunMode : builder.RunMode); | RunMode = (builder.RunMode == RunMode.Default ? service._defaultRunMode : builder.RunMode); | ||||
| Priority = builder.Priority; | Priority = builder.Priority; | ||||
| Aliases = module.Aliases | Aliases = module.Aliases | ||||
| .Permutate(builder.Aliases, (first, second) => | .Permutate(builder.Aliases, (first, second) => | ||||
| { | { | ||||
| @@ -57,6 +58,7 @@ namespace Discord.Commands | |||||
| .ToImmutableArray(); | .ToImmutableArray(); | ||||
| Preconditions = builder.Preconditions.ToImmutableArray(); | Preconditions = builder.Preconditions.ToImmutableArray(); | ||||
| Attributes = builder.Attributes.ToImmutableArray(); | |||||
| Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray(); | Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray(); | ||||
| HasVarArgs = builder.Parameters.Count > 0 ? builder.Parameters[builder.Parameters.Count - 1].IsMultiple : false; | HasVarArgs = builder.Parameters.Count > 0 ? builder.Parameters[builder.Parameters.Count - 1].IsMultiple : false; | ||||
| @@ -68,58 +70,80 @@ namespace Discord.Commands | |||||
| { | { | ||||
| services = services ?? EmptyServiceProvider.Instance; | services = services ?? EmptyServiceProvider.Instance; | ||||
| foreach (PreconditionAttribute precondition in Module.Preconditions) | |||||
| async Task<PreconditionResult> CheckGroups(IEnumerable<PreconditionAttribute> preconditions, string type) | |||||
| { | { | ||||
| var result = await precondition.CheckPermissions(context, this, services).ConfigureAwait(false); | |||||
| if (!result.IsSuccess) | |||||
| return result; | |||||
| } | |||||
| foreach (IGrouping<string, PreconditionAttribute> preconditionGroup in preconditions.GroupBy(p => p.Group, StringComparer.Ordinal)) | |||||
| { | |||||
| if (preconditionGroup.Key == null) | |||||
| { | |||||
| foreach (PreconditionAttribute precondition in preconditionGroup) | |||||
| { | |||||
| var result = await precondition.CheckPermissions(context, this, services).ConfigureAwait(false); | |||||
| if (!result.IsSuccess) | |||||
| return result; | |||||
| } | |||||
| } | |||||
| else | |||||
| { | |||||
| var results = new List<PreconditionResult>(); | |||||
| foreach (PreconditionAttribute precondition in preconditionGroup) | |||||
| results.Add(await precondition.CheckPermissions(context, this, services).ConfigureAwait(false)); | |||||
| foreach (PreconditionAttribute precondition in Preconditions) | |||||
| { | |||||
| var result = await precondition.CheckPermissions(context, this, services).ConfigureAwait(false); | |||||
| if (!result.IsSuccess) | |||||
| return result; | |||||
| if (!results.Any(p => p.IsSuccess)) | |||||
| return PreconditionGroupResult.FromError($"{type} precondition group {preconditionGroup.Key} failed.", results); | |||||
| } | |||||
| } | |||||
| return PreconditionGroupResult.FromSuccess(); | |||||
| } | } | ||||
| var moduleResult = await CheckGroups(Module.Preconditions, "Module"); | |||||
| if (!moduleResult.IsSuccess) | |||||
| return moduleResult; | |||||
| var commandResult = await CheckGroups(Preconditions, "Command"); | |||||
| if (!commandResult.IsSuccess) | |||||
| return commandResult; | |||||
| return PreconditionResult.FromSuccess(); | return PreconditionResult.FromSuccess(); | ||||
| } | } | ||||
| public async Task<ParseResult> ParseAsync(ICommandContext context, int startIndex, SearchResult searchResult, PreconditionResult? preconditionResult = null) | |||||
| public async Task<ParseResult> ParseAsync(ICommandContext context, int startIndex, SearchResult searchResult, PreconditionResult preconditionResult = null, IServiceProvider services = null) | |||||
| { | { | ||||
| services = services ?? EmptyServiceProvider.Instance; | |||||
| if (!searchResult.IsSuccess) | if (!searchResult.IsSuccess) | ||||
| return ParseResult.FromError(searchResult); | return ParseResult.FromError(searchResult); | ||||
| if (preconditionResult != null && !preconditionResult.Value.IsSuccess) | |||||
| return ParseResult.FromError(preconditionResult.Value); | |||||
| if (preconditionResult != null && !preconditionResult.IsSuccess) | |||||
| return ParseResult.FromError(preconditionResult); | |||||
| string input = searchResult.Text.Substring(startIndex); | string input = searchResult.Text.Substring(startIndex); | ||||
| return await CommandParser.ParseArgs(this, context, input, 0).ConfigureAwait(false); | |||||
| return await CommandParser.ParseArgs(this, context, services, input, 0).ConfigureAwait(false); | |||||
| } | } | ||||
| public Task<ExecuteResult> ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) | |||||
| public Task<IResult> ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) | |||||
| { | { | ||||
| if (!parseResult.IsSuccess) | if (!parseResult.IsSuccess) | ||||
| return Task.FromResult(ExecuteResult.FromError(parseResult)); | |||||
| return Task.FromResult((IResult)ExecuteResult.FromError(parseResult)); | |||||
| var argList = new object[parseResult.ArgValues.Count]; | var argList = new object[parseResult.ArgValues.Count]; | ||||
| for (int i = 0; i < parseResult.ArgValues.Count; i++) | for (int i = 0; i < parseResult.ArgValues.Count; i++) | ||||
| { | { | ||||
| if (!parseResult.ArgValues[i].IsSuccess) | if (!parseResult.ArgValues[i].IsSuccess) | ||||
| return Task.FromResult(ExecuteResult.FromError(parseResult.ArgValues[i])); | |||||
| return Task.FromResult((IResult)ExecuteResult.FromError(parseResult.ArgValues[i])); | |||||
| argList[i] = parseResult.ArgValues[i].Values.First().Value; | argList[i] = parseResult.ArgValues[i].Values.First().Value; | ||||
| } | } | ||||
| var paramList = new object[parseResult.ParamValues.Count]; | var paramList = new object[parseResult.ParamValues.Count]; | ||||
| for (int i = 0; i < parseResult.ParamValues.Count; i++) | for (int i = 0; i < parseResult.ParamValues.Count; i++) | ||||
| { | { | ||||
| if (!parseResult.ParamValues[i].IsSuccess) | if (!parseResult.ParamValues[i].IsSuccess) | ||||
| return Task.FromResult(ExecuteResult.FromError(parseResult.ParamValues[i])); | |||||
| return Task.FromResult((IResult)ExecuteResult.FromError(parseResult.ParamValues[i])); | |||||
| paramList[i] = parseResult.ParamValues[i].Values.First().Value; | paramList[i] = parseResult.ParamValues[i].Values.First().Value; | ||||
| } | } | ||||
| return ExecuteAsync(context, argList, paramList, services); | return ExecuteAsync(context, argList, paramList, services); | ||||
| } | } | ||||
| public async Task<ExecuteResult> ExecuteAsync(ICommandContext context, IEnumerable<object> argList, IEnumerable<object> paramList, IServiceProvider services) | |||||
| public async Task<IResult> ExecuteAsync(ICommandContext context, IEnumerable<object> argList, IEnumerable<object> paramList, IServiceProvider services) | |||||
| { | { | ||||
| services = services ?? EmptyServiceProvider.Instance; | services = services ?? EmptyServiceProvider.Instance; | ||||
| @@ -130,7 +154,7 @@ namespace Discord.Commands | |||||
| for (int position = 0; position < Parameters.Count; position++) | for (int position = 0; position < Parameters.Count; position++) | ||||
| { | { | ||||
| var parameter = Parameters[position]; | var parameter = Parameters[position]; | ||||
| var argument = args[position]; | |||||
| object argument = args[position]; | |||||
| var result = await parameter.CheckPreconditionsAsync(context, argument, services).ConfigureAwait(false); | var result = await parameter.CheckPreconditionsAsync(context, argument, services).ConfigureAwait(false); | ||||
| if (!result.IsSuccess) | if (!result.IsSuccess) | ||||
| return ExecuteResult.FromError(result); | return ExecuteResult.FromError(result); | ||||
| @@ -139,10 +163,9 @@ namespace Discord.Commands | |||||
| switch (RunMode) | switch (RunMode) | ||||
| { | { | ||||
| case RunMode.Sync: //Always sync | case RunMode.Sync: //Always sync | ||||
| await ExecuteAsyncInternal(context, args, services).ConfigureAwait(false); | |||||
| break; | |||||
| return await ExecuteAsyncInternal(context, args, services).ConfigureAwait(false); | |||||
| case RunMode.Async: //Always async | case RunMode.Async: //Always async | ||||
| var t2 = Task.Run(async () => | |||||
| var t2 = Task.Run(async () => | |||||
| { | { | ||||
| await ExecuteAsyncInternal(context, args, services).ConfigureAwait(false); | await ExecuteAsyncInternal(context, args, services).ConfigureAwait(false); | ||||
| }); | }); | ||||
| @@ -156,12 +179,26 @@ namespace Discord.Commands | |||||
| } | } | ||||
| } | } | ||||
| private async Task ExecuteAsyncInternal(ICommandContext context, object[] args, IServiceProvider services) | |||||
| private async Task<IResult> ExecuteAsyncInternal(ICommandContext context, object[] args, IServiceProvider services) | |||||
| { | { | ||||
| await Module.Service._cmdLogger.DebugAsync($"Executing {GetLogText(context)}").ConfigureAwait(false); | await Module.Service._cmdLogger.DebugAsync($"Executing {GetLogText(context)}").ConfigureAwait(false); | ||||
| try | try | ||||
| { | { | ||||
| await _action(context, args, services).ConfigureAwait(false); | |||||
| var task = _action(context, args, services, this); | |||||
| if (task is Task<IResult> resultTask) | |||||
| { | |||||
| var result = await resultTask.ConfigureAwait(false); | |||||
| if (result is RuntimeResult execResult) | |||||
| return execResult; | |||||
| } | |||||
| else if (task is Task<ExecuteResult> execTask) | |||||
| { | |||||
| return await execTask.ConfigureAwait(false); | |||||
| } | |||||
| else | |||||
| await task.ConfigureAwait(false); | |||||
| return ExecuteResult.FromSuccess(); | |||||
| } | } | ||||
| catch (Exception ex) | catch (Exception ex) | ||||
| { | { | ||||
| @@ -178,8 +215,13 @@ namespace Discord.Commands | |||||
| else | else | ||||
| ExceptionDispatchInfo.Capture(ex).Throw(); | ExceptionDispatchInfo.Capture(ex).Throw(); | ||||
| } | } | ||||
| return ExecuteResult.FromError(CommandError.Exception, ex.Message); | |||||
| } | |||||
| finally | |||||
| { | |||||
| await Module.Service._cmdLogger.VerboseAsync($"Executed {GetLogText(context)}").ConfigureAwait(false); | |||||
| } | } | ||||
| await Module.Service._cmdLogger.VerboseAsync($"Executed {GetLogText(context)}").ConfigureAwait(false); | |||||
| } | } | ||||
| private object[] GenerateArgs(IEnumerable<object> argList, IEnumerable<object> paramsList) | private object[] GenerateArgs(IEnumerable<object> argList, IEnumerable<object> paramsList) | ||||
| @@ -190,7 +232,7 @@ namespace Discord.Commands | |||||
| argCount--; | argCount--; | ||||
| int i = 0; | int i = 0; | ||||
| foreach (var arg in argList) | |||||
| foreach (object arg in argList) | |||||
| { | { | ||||
| if (i == argCount) | if (i == argCount) | ||||
| throw new InvalidOperationException("Command was invoked with too many parameters"); | throw new InvalidOperationException("Command was invoked with too many parameters"); | ||||
| @@ -216,11 +258,11 @@ namespace Discord.Commands | |||||
| => paramsList.Cast<T>().ToArray(); | => paramsList.Cast<T>().ToArray(); | ||||
| internal string GetLogText(ICommandContext context) | internal string GetLogText(ICommandContext context) | ||||
| { | |||||
| { | |||||
| if (context.Guild != null) | if (context.Guild != null) | ||||
| return $"\"{Name}\" for {context.User} in {context.Guild}/{context.Channel}"; | return $"\"{Name}\" for {context.User} in {context.Guild}/{context.Channel}"; | ||||
| else | else | ||||
| return $"\"{Name}\" for {context.User} in {context.Channel}"; | return $"\"{Name}\" for {context.User} in {context.Channel}"; | ||||
| } | } | ||||
| } | } | ||||
| } | |||||
| } | |||||
| @@ -1,3 +1,4 @@ | |||||
| using System; | |||||
| using System.Linq; | using System.Linq; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
| @@ -16,6 +17,7 @@ namespace Discord.Commands | |||||
| public IReadOnlyList<string> Aliases { get; } | public IReadOnlyList<string> Aliases { get; } | ||||
| public IReadOnlyList<CommandInfo> Commands { get; } | public IReadOnlyList<CommandInfo> Commands { get; } | ||||
| public IReadOnlyList<PreconditionAttribute> Preconditions { get; } | public IReadOnlyList<PreconditionAttribute> Preconditions { get; } | ||||
| public IReadOnlyList<Attribute> Attributes { get; } | |||||
| public IReadOnlyList<ModuleInfo> Submodules { get; } | public IReadOnlyList<ModuleInfo> Submodules { get; } | ||||
| public ModuleInfo Parent { get; } | public ModuleInfo Parent { get; } | ||||
| public bool IsSubmodule => Parent != null; | public bool IsSubmodule => Parent != null; | ||||
| @@ -32,6 +34,7 @@ namespace Discord.Commands | |||||
| Aliases = BuildAliases(builder, service).ToImmutableArray(); | Aliases = BuildAliases(builder, service).ToImmutableArray(); | ||||
| Commands = builder.Commands.Select(x => x.Build(this, service)).ToImmutableArray(); | Commands = builder.Commands.Select(x => x.Build(this, service)).ToImmutableArray(); | ||||
| Preconditions = BuildPreconditions(builder).ToImmutableArray(); | Preconditions = BuildPreconditions(builder).ToImmutableArray(); | ||||
| Attributes = BuildAttributes(builder).ToImmutableArray(); | |||||
| Submodules = BuildSubmodules(builder, service).ToImmutableArray(); | Submodules = BuildSubmodules(builder, service).ToImmutableArray(); | ||||
| } | } | ||||
| @@ -86,5 +89,19 @@ namespace Discord.Commands | |||||
| return result; | return result; | ||||
| } | } | ||||
| private static List<Attribute> BuildAttributes(ModuleBuilder builder) | |||||
| { | |||||
| var result = new List<Attribute>(); | |||||
| ModuleBuilder parent = builder; | |||||
| while (parent != null) | |||||
| { | |||||
| result.AddRange(parent.Attributes); | |||||
| parent = parent.Parent; | |||||
| } | |||||
| return result; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -21,6 +21,7 @@ namespace Discord.Commands | |||||
| public object DefaultValue { get; } | public object DefaultValue { get; } | ||||
| public IReadOnlyList<ParameterPreconditionAttribute> Preconditions { get; } | public IReadOnlyList<ParameterPreconditionAttribute> Preconditions { get; } | ||||
| public IReadOnlyList<Attribute> Attributes { get; } | |||||
| internal ParameterInfo(ParameterBuilder builder, CommandInfo command, CommandService service) | internal ParameterInfo(ParameterBuilder builder, CommandInfo command, CommandService service) | ||||
| { | { | ||||
| @@ -36,6 +37,7 @@ namespace Discord.Commands | |||||
| DefaultValue = builder.DefaultValue; | DefaultValue = builder.DefaultValue; | ||||
| Preconditions = builder.Preconditions.ToImmutableArray(); | Preconditions = builder.Preconditions.ToImmutableArray(); | ||||
| Attributes = builder.Attributes.ToImmutableArray(); | |||||
| _reader = builder.TypeReader; | _reader = builder.TypeReader; | ||||
| } | } | ||||
| @@ -54,9 +56,10 @@ namespace Discord.Commands | |||||
| return PreconditionResult.FromSuccess(); | return PreconditionResult.FromSuccess(); | ||||
| } | } | ||||
| public async Task<TypeReaderResult> Parse(ICommandContext context, string input) | |||||
| public async Task<TypeReaderResult> Parse(ICommandContext context, string input, IServiceProvider services = null) | |||||
| { | { | ||||
| return await _reader.Read(context, input).ConfigureAwait(false); | |||||
| services = services ?? EmptyServiceProvider.Instance; | |||||
| return await _reader.Read(context, input, services).ConfigureAwait(false); | |||||
| } | } | ||||
| public override string ToString() => Name; | public override string ToString() => Name; | ||||
| @@ -15,11 +15,11 @@ namespace Discord.Commands | |||||
| return await Context.Channel.SendMessageAsync(message, isTTS, embed, options).ConfigureAwait(false); | return await Context.Channel.SendMessageAsync(message, isTTS, embed, options).ConfigureAwait(false); | ||||
| } | } | ||||
| protected virtual void BeforeExecute() | |||||
| protected virtual void BeforeExecute(CommandInfo command) | |||||
| { | { | ||||
| } | } | ||||
| protected virtual void AfterExecute() | |||||
| protected virtual void AfterExecute(CommandInfo command) | |||||
| { | { | ||||
| } | } | ||||
| @@ -27,13 +27,11 @@ namespace Discord.Commands | |||||
| void IModuleBase.SetContext(ICommandContext context) | void IModuleBase.SetContext(ICommandContext context) | ||||
| { | { | ||||
| var newValue = context as T; | var newValue = context as T; | ||||
| if (newValue == null) | |||||
| throw new InvalidOperationException($"Invalid context type. Expected {typeof(T).Name}, got {context.GetType().Name}"); | |||||
| Context = newValue; | |||||
| Context = newValue ?? throw new InvalidOperationException($"Invalid context type. Expected {typeof(T).Name}, got {context.GetType().Name}"); | |||||
| } | } | ||||
| void IModuleBase.BeforeExecute() => BeforeExecute(); | |||||
| void IModuleBase.BeforeExecute(CommandInfo command) => BeforeExecute(command); | |||||
| void IModuleBase.AfterExecute() => AfterExecute(); | |||||
| void IModuleBase.AfterExecute(CommandInfo command) => AfterExecute(command); | |||||
| } | } | ||||
| } | } | ||||
| @@ -31,11 +31,6 @@ namespace Discord.Commands | |||||
| parserBuilder[typeof(DateTimeOffset)] = (TryParseDelegate<DateTimeOffset>)DateTimeOffset.TryParse; | parserBuilder[typeof(DateTimeOffset)] = (TryParseDelegate<DateTimeOffset>)DateTimeOffset.TryParse; | ||||
| parserBuilder[typeof(TimeSpan)] = (TryParseDelegate<TimeSpan>)TimeSpan.TryParse; | parserBuilder[typeof(TimeSpan)] = (TryParseDelegate<TimeSpan>)TimeSpan.TryParse; | ||||
| parserBuilder[typeof(char)] = (TryParseDelegate<char>)char.TryParse; | parserBuilder[typeof(char)] = (TryParseDelegate<char>)char.TryParse; | ||||
| parserBuilder[typeof(string)] = (TryParseDelegate<string>)delegate (string str, out string value) | |||||
| { | |||||
| value = str; | |||||
| return true; | |||||
| }; | |||||
| return parserBuilder.ToImmutable(); | return parserBuilder.ToImmutable(); | ||||
| } | } | ||||
| @@ -9,7 +9,7 @@ namespace Discord.Commands | |||||
| internal class ChannelTypeReader<T> : TypeReader | internal class ChannelTypeReader<T> : TypeReader | ||||
| where T : class, IChannel | where T : class, IChannel | ||||
| { | { | ||||
| public override async Task<TypeReaderResult> Read(ICommandContext context, string input) | |||||
| public override async Task<TypeReaderResult> Read(ICommandContext context, string input, IServiceProvider services) | |||||
| { | { | ||||
| if (context.Guild != null) | if (context.Guild != null) | ||||
| { | { | ||||
| @@ -44,12 +44,11 @@ namespace Discord.Commands | |||||
| _enumsByValue = byValueBuilder.ToImmutable(); | _enumsByValue = byValueBuilder.ToImmutable(); | ||||
| } | } | ||||
| public override Task<TypeReaderResult> Read(ICommandContext context, string input) | |||||
| public override Task<TypeReaderResult> Read(ICommandContext context, string input, IServiceProvider services) | |||||
| { | { | ||||
| T baseValue; | |||||
| object enumValue; | object enumValue; | ||||
| if (_tryParse(input, out baseValue)) | |||||
| if (_tryParse(input, out T baseValue)) | |||||
| { | { | ||||
| if (_enumsByValue.TryGetValue(baseValue, out enumValue)) | if (_enumsByValue.TryGetValue(baseValue, out enumValue)) | ||||
| return Task.FromResult(TypeReaderResult.FromSuccess(enumValue)); | return Task.FromResult(TypeReaderResult.FromSuccess(enumValue)); | ||||
| @@ -1,4 +1,5 @@ | |||||
| using System.Globalization; | |||||
| using System; | |||||
| using System.Globalization; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| namespace Discord.Commands | namespace Discord.Commands | ||||
| @@ -6,15 +7,14 @@ namespace Discord.Commands | |||||
| internal class MessageTypeReader<T> : TypeReader | internal class MessageTypeReader<T> : TypeReader | ||||
| where T : class, IMessage | where T : class, IMessage | ||||
| { | { | ||||
| public override async Task<TypeReaderResult> Read(ICommandContext context, string input) | |||||
| public override async Task<TypeReaderResult> Read(ICommandContext context, string input, IServiceProvider services) | |||||
| { | { | ||||
| ulong id; | ulong id; | ||||
| //By Id (1.0) | //By Id (1.0) | ||||
| if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id)) | if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id)) | ||||
| { | { | ||||
| var msg = await context.Channel.GetMessageAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T; | |||||
| if (msg != null) | |||||
| if (await context.Channel.GetMessageAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) is T msg) | |||||
| return TypeReaderResult.FromSuccess(msg); | return TypeReaderResult.FromSuccess(msg); | ||||
| } | } | ||||
| @@ -15,17 +15,25 @@ namespace Discord.Commands | |||||
| internal class PrimitiveTypeReader<T> : TypeReader | internal class PrimitiveTypeReader<T> : TypeReader | ||||
| { | { | ||||
| private readonly TryParseDelegate<T> _tryParse; | private readonly TryParseDelegate<T> _tryParse; | ||||
| private readonly float _score; | |||||
| public PrimitiveTypeReader() | public PrimitiveTypeReader() | ||||
| : this(PrimitiveParsers.Get<T>(), 1) | |||||
| { } | |||||
| public PrimitiveTypeReader(TryParseDelegate<T> tryParse, float score) | |||||
| { | { | ||||
| _tryParse = PrimitiveParsers.Get<T>(); | |||||
| if (score < 0 || score > 1) | |||||
| throw new ArgumentOutOfRangeException(nameof(score), score, "Scores must be within the range [0, 1]"); | |||||
| _tryParse = tryParse; | |||||
| _score = score; | |||||
| } | } | ||||
| public override Task<TypeReaderResult> Read(ICommandContext context, string input) | |||||
| public override Task<TypeReaderResult> Read(ICommandContext context, string input, IServiceProvider services) | |||||
| { | { | ||||
| T value; | |||||
| if (_tryParse(input, out value)) | |||||
| return Task.FromResult(TypeReaderResult.FromSuccess(value)); | |||||
| if (_tryParse(input, out T value)) | |||||
| return Task.FromResult(TypeReaderResult.FromSuccess(new TypeReaderValue(value, _score))); | |||||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, $"Failed to parse {typeof(T).Name}")); | return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, $"Failed to parse {typeof(T).Name}")); | ||||
| } | } | ||||
| } | } | ||||
| @@ -9,7 +9,7 @@ namespace Discord.Commands | |||||
| internal class RoleTypeReader<T> : TypeReader | internal class RoleTypeReader<T> : TypeReader | ||||
| where T : class, IRole | where T : class, IRole | ||||
| { | { | ||||
| public override Task<TypeReaderResult> Read(ICommandContext context, string input) | |||||
| public override Task<TypeReaderResult> Read(ICommandContext context, string input, IServiceProvider services) | |||||
| { | { | ||||
| ulong id; | ulong id; | ||||
| @@ -1,9 +1,10 @@ | |||||
| using System.Threading.Tasks; | |||||
| using System; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.Commands | namespace Discord.Commands | ||||
| { | { | ||||
| public abstract class TypeReader | public abstract class TypeReader | ||||
| { | { | ||||
| public abstract Task<TypeReaderResult> Read(ICommandContext context, string input); | |||||
| public abstract Task<TypeReaderResult> Read(ICommandContext context, string input, IServiceProvider services); | |||||
| } | } | ||||
| } | } | ||||
| @@ -10,7 +10,7 @@ namespace Discord.Commands | |||||
| internal class UserTypeReader<T> : TypeReader | internal class UserTypeReader<T> : TypeReader | ||||
| where T : class, IUser | where T : class, IUser | ||||
| { | { | ||||
| public override async Task<TypeReaderResult> Read(ICommandContext context, string input) | |||||
| public override async Task<TypeReaderResult> Read(ICommandContext context, string input, IServiceProvider services) | |||||
| { | { | ||||
| var results = new Dictionary<ulong, TypeReaderValue>(); | var results = new Dictionary<ulong, TypeReaderValue>(); | ||||
| IReadOnlyCollection<IUser> channelUsers = await context.Channel.GetUsersAsync(CacheMode.CacheOnly).ToArray().ConfigureAwait(false); //TODO: must be a better way? | IReadOnlyCollection<IUser> channelUsers = await context.Channel.GetUsersAsync(CacheMode.CacheOnly).ToArray().ConfigureAwait(false); //TODO: must be a better way? | ||||
| @@ -43,8 +43,7 @@ namespace Discord.Commands | |||||
| if (index >= 0) | if (index >= 0) | ||||
| { | { | ||||
| string username = input.Substring(0, index); | string username = input.Substring(0, index); | ||||
| ushort discriminator; | |||||
| if (ushort.TryParse(input.Substring(index + 1), out discriminator)) | |||||
| if (ushort.TryParse(input.Substring(index + 1), out ushort discriminator)) | |||||
| { | { | ||||
| var channelUser = channelUsers.FirstOrDefault(x => x.DiscriminatorValue == discriminator && | var channelUser = channelUsers.FirstOrDefault(x => x.DiscriminatorValue == discriminator && | ||||
| string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase)); | string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase)); | ||||
| @@ -0,0 +1,27 @@ | |||||
| using System.Collections.Generic; | |||||
| using System.Diagnostics; | |||||
| namespace Discord.Commands | |||||
| { | |||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||||
| public class PreconditionGroupResult : PreconditionResult | |||||
| { | |||||
| public IReadOnlyCollection<PreconditionResult> PreconditionResults { get; } | |||||
| protected PreconditionGroupResult(CommandError? error, string errorReason, ICollection<PreconditionResult> preconditions) | |||||
| : base(error, errorReason) | |||||
| { | |||||
| PreconditionResults = (preconditions ?? new List<PreconditionResult>(0)).ToReadOnlyCollection(); | |||||
| } | |||||
| public static new PreconditionGroupResult FromSuccess() | |||||
| => new PreconditionGroupResult(null, null, null); | |||||
| public static PreconditionGroupResult FromError(string reason, ICollection<PreconditionResult> preconditions) | |||||
| => new PreconditionGroupResult(CommandError.UnmetPrecondition, reason, preconditions); | |||||
| public static new PreconditionGroupResult FromError(IResult result) //needed? | |||||
| => new PreconditionGroupResult(result.Error, result.ErrorReason, null); | |||||
| public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; | |||||
| private string DebuggerDisplay => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; | |||||
| } | |||||
| } | |||||
| @@ -3,14 +3,14 @@ | |||||
| namespace Discord.Commands | namespace Discord.Commands | ||||
| { | { | ||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| public struct PreconditionResult : IResult | |||||
| public class PreconditionResult : IResult | |||||
| { | { | ||||
| public CommandError? Error { get; } | public CommandError? Error { get; } | ||||
| public string ErrorReason { get; } | public string ErrorReason { get; } | ||||
| public bool IsSuccess => !Error.HasValue; | public bool IsSuccess => !Error.HasValue; | ||||
| private PreconditionResult(CommandError? error, string errorReason) | |||||
| protected PreconditionResult(CommandError? error, string errorReason) | |||||
| { | { | ||||
| Error = error; | Error = error; | ||||
| ErrorReason = errorReason; | ErrorReason = errorReason; | ||||
| @@ -0,0 +1,27 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Diagnostics; | |||||
| using System.Text; | |||||
| namespace Discord.Commands | |||||
| { | |||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||||
| public abstract class RuntimeResult : IResult | |||||
| { | |||||
| protected RuntimeResult(CommandError? error, string reason) | |||||
| { | |||||
| Error = error; | |||||
| Reason = reason; | |||||
| } | |||||
| public CommandError? Error { get; } | |||||
| public string Reason { get; } | |||||
| public bool IsSuccess => !Error.HasValue; | |||||
| string IResult.ErrorReason => Reason; | |||||
| public override string ToString() => Reason ?? (IsSuccess ? "Successful" : "Unsuccessful"); | |||||
| private string DebuggerDisplay => IsSuccess ? $"Success: {Reason ?? "No Reason"}" : $"{Error}: {Reason}"; | |||||
| } | |||||
| } | |||||
| @@ -58,7 +58,7 @@ namespace Discord.Commands | |||||
| { | { | ||||
| foreach (var prop in ownerType.DeclaredProperties) | foreach (var prop in ownerType.DeclaredProperties) | ||||
| { | { | ||||
| if (prop.SetMethod?.IsPublic == true && prop.GetCustomAttribute<DontInjectAttribute>() == null) | |||||
| if (prop.SetMethod?.IsStatic == false && prop.SetMethod?.IsPublic == true && prop.GetCustomAttribute<DontInjectAttribute>() == null) | |||||
| result.Add(prop); | result.Add(prop); | ||||
| } | } | ||||
| ownerType = ownerType.BaseType.GetTypeInfo(); | ownerType = ownerType.BaseType.GetTypeInfo(); | ||||
| @@ -11,7 +11,10 @@ namespace Discord.Audio | |||||
| public override bool CanSeek => false; | public override bool CanSeek => false; | ||||
| public override bool CanWrite => false; | public override bool CanWrite => false; | ||||
| public virtual void WriteHeader(ushort seq, uint timestamp, bool missed) { } | |||||
| public virtual void WriteHeader(ushort seq, uint timestamp, bool missed) | |||||
| { | |||||
| throw new InvalidOperationException("This stream does not accept headers"); | |||||
| } | |||||
| public override void Write(byte[] buffer, int offset, int count) | public override void Write(byte[] buffer, int offset, int count) | ||||
| { | { | ||||
| WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); | WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); | ||||
| @@ -28,8 +28,8 @@ namespace Discord.Audio | |||||
| /// <summary>Creates a new outgoing stream accepting Opus-encoded data. This is a direct stream with no internal timer.</summary> | /// <summary>Creates a new outgoing stream accepting Opus-encoded data. This is a direct stream with no internal timer.</summary> | ||||
| AudioOutStream CreateDirectOpusStream(); | AudioOutStream CreateDirectOpusStream(); | ||||
| /// <summary>Creates a new outgoing stream accepting PCM (raw) data.</summary> | /// <summary>Creates a new outgoing stream accepting PCM (raw) data.</summary> | ||||
| AudioOutStream CreatePCMStream(AudioApplication application, int? bitrate = null, int bufferMillis = 1000); | |||||
| AudioOutStream CreatePCMStream(AudioApplication application, int? bitrate = null, int bufferMillis = 1000, int packetLoss = 30); | |||||
| /// <summary>Creates a new direct outgoing stream accepting PCM (raw) data. This is a direct stream with no internal timer.</summary> | /// <summary>Creates a new direct outgoing stream accepting PCM (raw) data. This is a direct stream with no internal timer.</summary> | ||||
| AudioOutStream CreateDirectPCMStream(AudioApplication application, int? bitrate = null); | |||||
| AudioOutStream CreateDirectPCMStream(AudioApplication application, int? bitrate = null, int packetLoss = 30); | |||||
| } | } | ||||
| } | } | ||||
| @@ -30,7 +30,9 @@ namespace Discord | |||||
| /// <summary> Gets a collection of pinned messages in this channel. </summary> | /// <summary> Gets a collection of pinned messages in this channel. </summary> | ||||
| Task<IReadOnlyCollection<IMessage>> GetPinnedMessagesAsync(RequestOptions options = null); | Task<IReadOnlyCollection<IMessage>> GetPinnedMessagesAsync(RequestOptions options = null); | ||||
| /// <summary> Bulk deletes multiple messages. </summary> | /// <summary> Bulk deletes multiple messages. </summary> | ||||
| Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null); | |||||
| Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null); | |||||
| /// <summary> Bulk deletes multiple messages. </summary> | |||||
| Task DeleteMessagesAsync(IEnumerable<ulong> messageIds, RequestOptions options = null); | |||||
| /// <summary> Broadcasts the "user is typing" message to all users in this channel, lasting 10 seconds. </summary> | /// <summary> Broadcasts the "user is typing" message to all users in this channel, lasting 10 seconds. </summary> | ||||
| Task TriggerTypingAsync(RequestOptions options = null); | Task TriggerTypingAsync(RequestOptions options = null); | ||||
| @@ -6,8 +6,16 @@ | |||||
| public class Emoji : IEmote | public class Emoji : IEmote | ||||
| { | { | ||||
| // TODO: need to constrain this to unicode-only emojis somehow | // TODO: need to constrain this to unicode-only emojis somehow | ||||
| /// <summary> | |||||
| /// The unicode representation of this emote. | |||||
| /// </summary> | |||||
| public string Name { get; } | |||||
| public override string ToString() => Name; | |||||
| /// <summary> | /// <summary> | ||||
| /// Creates a unciode emoji. | |||||
| /// Creates a unicode emoji. | |||||
| /// </summary> | /// </summary> | ||||
| /// <param name="unicode">The pure UTF-8 encoding of an emoji</param> | /// <param name="unicode">The pure UTF-8 encoding of an emoji</param> | ||||
| public Emoji(string unicode) | public Emoji(string unicode) | ||||
| @@ -15,9 +23,17 @@ | |||||
| Name = unicode; | Name = unicode; | ||||
| } | } | ||||
| /// <summary> | |||||
| /// The unicode representation of this emote. | |||||
| /// </summary> | |||||
| public string Name { get; } | |||||
| public override bool Equals(object other) | |||||
| { | |||||
| if (other == null) return false; | |||||
| if (other == this) return true; | |||||
| var otherEmoji = other as Emoji; | |||||
| if (otherEmoji == null) return false; | |||||
| return string.Equals(Name, otherEmoji.Name); | |||||
| } | |||||
| public override int GetHashCode() => Name.GetHashCode(); | |||||
| } | } | ||||
| } | } | ||||
| @@ -25,6 +25,25 @@ namespace Discord | |||||
| Name = name; | Name = name; | ||||
| } | } | ||||
| public override bool Equals(object other) | |||||
| { | |||||
| if (other == null) return false; | |||||
| if (other == this) return true; | |||||
| var otherEmote = other as Emote; | |||||
| if (otherEmote == null) return false; | |||||
| return string.Equals(Name, otherEmote.Name) && Id == otherEmote.Id; | |||||
| } | |||||
| public override int GetHashCode() | |||||
| { | |||||
| unchecked | |||||
| { | |||||
| return (Name.GetHashCode() * 397) ^ Id.GetHashCode(); | |||||
| } | |||||
| } | |||||
| /// <summary> | /// <summary> | ||||
| /// Parse an Emote from its raw format | /// Parse an Emote from its raw format | ||||
| /// </summary> | /// </summary> | ||||
| @@ -58,6 +77,6 @@ namespace Discord | |||||
| } | } | ||||
| private string DebuggerDisplay => $"{Name} ({Id})"; | private string DebuggerDisplay => $"{Name} ({Id})"; | ||||
| public override string ToString() => Name; | |||||
| public override string ToString() => $"<:{Name}:{Id}>"; | |||||
| } | } | ||||
| } | } | ||||
| @@ -20,7 +20,7 @@ namespace Discord | |||||
| RoleIds = roleIds; | RoleIds = roleIds; | ||||
| } | } | ||||
| public override string ToString() => Name; | |||||
| private string DebuggerDisplay => $"{Name} ({Id})"; | private string DebuggerDisplay => $"{Name} ({Id})"; | ||||
| public override string ToString() => $"<:{Name}:{Id}>"; | |||||
| } | } | ||||
| } | } | ||||
| @@ -66,10 +66,10 @@ namespace Discord | |||||
| Task<IReadOnlyCollection<IBan>> GetBansAsync(RequestOptions options = null); | Task<IReadOnlyCollection<IBan>> GetBansAsync(RequestOptions options = null); | ||||
| /// <summary> Bans the provided user from this guild and optionally prunes their recent messages. </summary> | /// <summary> Bans the provided user from this guild and optionally prunes their recent messages. </summary> | ||||
| /// <param name="pruneDays">The number of days to remove messages from this user for - must be between [0, 7]</param> | /// <param name="pruneDays">The number of days to remove messages from this user for - must be between [0, 7]</param> | ||||
| Task AddBanAsync(IUser user, int pruneDays = 0, RequestOptions options = null); | |||||
| Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null); | |||||
| /// <summary> Bans the provided user id from this guild and optionally prunes their recent messages. </summary> | /// <summary> Bans the provided user id from this guild and optionally prunes their recent messages. </summary> | ||||
| /// <param name="pruneDays">The number of days to remove messages from this user for - must be between [0, 7]</param> | /// <param name="pruneDays">The number of days to remove messages from this user for - must be between [0, 7]</param> | ||||
| Task AddBanAsync(ulong userId, int pruneDays = 0, RequestOptions options = null); | |||||
| Task AddBanAsync(ulong userId, int pruneDays = 0, string reason = null, RequestOptions options = null); | |||||
| /// <summary> Unbans the provided user if it is currently banned. </summary> | /// <summary> Unbans the provided user if it is currently banned. </summary> | ||||
| Task RemoveBanAsync(IUser user, RequestOptions options = null); | Task RemoveBanAsync(IUser user, RequestOptions options = null); | ||||
| /// <summary> Unbans the provided user id if it is currently banned. </summary> | /// <summary> Unbans the provided user id if it is currently banned. </summary> | ||||
| @@ -9,6 +9,8 @@ | |||||
| /// <summary> Users must fulfill the requirements of Low, and be registered on Discord for at least 5 minutes. </summary> | /// <summary> Users must fulfill the requirements of Low, and be registered on Discord for at least 5 minutes. </summary> | ||||
| Medium = 2, | Medium = 2, | ||||
| /// <summary> Users must fulfill the requirements of Medium, and be a member of this guild for at least 10 minutes. </summary> | /// <summary> Users must fulfill the requirements of Medium, and be a member of this guild for at least 10 minutes. </summary> | ||||
| High = 3 | |||||
| High = 3, | |||||
| /// <summary> Users must fulfill the requirements of High, and must have a verified phone on their Discord account. </summary> | |||||
| Extreme = 4 | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,13 +1,14 @@ | |||||
| using System; | using System; | ||||
| using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
| using System.Diagnostics; | using System.Diagnostics; | ||||
| using System.Linq; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| public class Embed : IEmbed | public class Embed : IEmbed | ||||
| { | { | ||||
| public string Type { get; } | |||||
| public EmbedType Type { get; } | |||||
| public string Description { get; internal set; } | public string Description { get; internal set; } | ||||
| public string Url { get; internal set; } | public string Url { get; internal set; } | ||||
| @@ -22,12 +23,12 @@ namespace Discord | |||||
| public EmbedThumbnail? Thumbnail { get; internal set; } | public EmbedThumbnail? Thumbnail { get; internal set; } | ||||
| public ImmutableArray<EmbedField> Fields { get; internal set; } | public ImmutableArray<EmbedField> Fields { get; internal set; } | ||||
| internal Embed(string type) | |||||
| internal Embed(EmbedType type) | |||||
| { | { | ||||
| Type = type; | Type = type; | ||||
| Fields = ImmutableArray.Create<EmbedField>(); | Fields = ImmutableArray.Create<EmbedField>(); | ||||
| } | } | ||||
| internal Embed(string type, | |||||
| internal Embed(EmbedType type, | |||||
| string title, | string title, | ||||
| string description, | string description, | ||||
| string url, | string url, | ||||
| @@ -56,6 +57,8 @@ namespace Discord | |||||
| Fields = fields; | Fields = fields; | ||||
| } | } | ||||
| public int Length => Title?.Length + Author?.Name?.Length + Description?.Length + Footer?.Text?.Length + Fields.Sum(f => f.Name.Length + f.Value.ToString().Length) ?? 0; | |||||
| public override string ToString() => Title; | public override string ToString() => Title; | ||||
| private string DebuggerDisplay => $"{Title} ({Type})"; | private string DebuggerDisplay => $"{Title} ({Type})"; | ||||
| } | } | ||||
| @@ -1,4 +1,5 @@ | |||||
| using System.Diagnostics; | |||||
| using System; | |||||
| using System.Diagnostics; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| @@ -1,4 +1,5 @@ | |||||
| using System.Diagnostics; | |||||
| using System; | |||||
| using System.Diagnostics; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| @@ -1,4 +1,5 @@ | |||||
| using System.Diagnostics; | |||||
| using System; | |||||
| using System.Diagnostics; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| @@ -19,6 +20,6 @@ namespace Discord | |||||
| } | } | ||||
| private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})"; | private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})"; | ||||
| public override string ToString() => Url; | |||||
| public override string ToString() => Url.ToString(); | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,4 +1,5 @@ | |||||
| using System.Diagnostics; | |||||
| using System; | |||||
| using System.Diagnostics; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| @@ -1,4 +1,5 @@ | |||||
| using System.Diagnostics; | |||||
| using System; | |||||
| using System.Diagnostics; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| @@ -19,6 +20,6 @@ namespace Discord | |||||
| } | } | ||||
| private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})"; | private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})"; | ||||
| public override string ToString() => Url; | |||||
| public override string ToString() => Url.ToString(); | |||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,13 @@ | |||||
| namespace Discord | |||||
| { | |||||
| public enum EmbedType | |||||
| { | |||||
| Rich, | |||||
| Link, | |||||
| Video, | |||||
| Image, | |||||
| Gifv, | |||||
| Article, | |||||
| Tweet | |||||
| } | |||||
| } | |||||
| @@ -1,4 +1,5 @@ | |||||
| using System.Diagnostics; | |||||
| using System; | |||||
| using System.Diagnostics; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| @@ -17,6 +18,6 @@ namespace Discord | |||||
| } | } | ||||
| private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})"; | private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})"; | ||||
| public override string ToString() => Url; | |||||
| public override string ToString() => Url.ToString(); | |||||
| } | } | ||||
| } | } | ||||
| @@ -6,9 +6,9 @@ namespace Discord | |||||
| public interface IEmbed | public interface IEmbed | ||||
| { | { | ||||
| string Url { get; } | string Url { get; } | ||||
| string Type { get; } | |||||
| string Title { get; } | string Title { get; } | ||||
| string Description { get; } | string Description { get; } | ||||
| EmbedType Type { get; } | |||||
| DateTimeOffset? Timestamp { get; } | DateTimeOffset? Timestamp { get; } | ||||
| Color? Color { get; } | Color? Color { get; } | ||||
| EmbedImage? Image { get; } | EmbedImage? Image { get; } | ||||
| @@ -8,6 +8,46 @@ namespace Discord | |||||
| { | { | ||||
| /// <summary> Gets the default user color value. </summary> | /// <summary> Gets the default user color value. </summary> | ||||
| public static readonly Color Default = new Color(0); | public static readonly Color Default = new Color(0); | ||||
| /// <summary> Gets the teal color value </summary> | |||||
| public static readonly Color Teal = new Color(0x1ABC9C); | |||||
| /// <summary> Gets the dark teal color value </summary> | |||||
| public static readonly Color DarkTeal = new Color(0x11806A); | |||||
| /// <summary> Gets the green color value </summary> | |||||
| public static readonly Color Green = new Color(0x2ECC71); | |||||
| /// <summary> Gets the dark green color value </summary> | |||||
| public static readonly Color DarkGreen = new Color(0x1F8B4C); | |||||
| /// <summary> Gets the blue color value </summary> | |||||
| public static readonly Color Blue = new Color(0x3498DB); | |||||
| /// <summary> Gets the dark blue color value </summary> | |||||
| public static readonly Color DarkBlue = new Color(0x206694); | |||||
| /// <summary> Gets the purple color value </summary> | |||||
| public static readonly Color Purple = new Color(0x9B59B6); | |||||
| /// <summary> Gets the dark purple color value </summary> | |||||
| public static readonly Color DarkPurple = new Color(0x71368A); | |||||
| /// <summary> Gets the magenta color value </summary> | |||||
| public static readonly Color Magenta = new Color(0xE91E63); | |||||
| /// <summary> Gets the dark magenta color value </summary> | |||||
| public static readonly Color DarkMagenta = new Color(0xAD1457); | |||||
| /// <summary> Gets the gold color value </summary> | |||||
| public static readonly Color Gold = new Color(0xF1C40F); | |||||
| /// <summary> Gets the light orange color value </summary> | |||||
| public static readonly Color LightOrange = new Color(0xC27C0E); | |||||
| /// <summary> Gets the orange color value </summary> | |||||
| public static readonly Color Orange = new Color(0xE67E22); | |||||
| /// <summary> Gets the dark orange color value </summary> | |||||
| public static readonly Color DarkOrange = new Color(0xA84300); | |||||
| /// <summary> Gets the red color value </summary> | |||||
| public static readonly Color Red = new Color(0xE74C3C); | |||||
| /// <summary> Gets the dark red color value </summary> | |||||
| public static readonly Color DarkRed = new Color(0x992D22); | |||||
| /// <summary> Gets the light grey color value </summary> | |||||
| public static readonly Color LightGrey = new Color(0x979C9F); | |||||
| /// <summary> Gets the lighter grey color value </summary> | |||||
| public static readonly Color LighterGrey = new Color(0x95A5A6); | |||||
| /// <summary> Gets the dark grey color value </summary> | |||||
| public static readonly Color DarkGrey = new Color(0x607D8B); | |||||
| /// <summary> Gets the darker grey color value </summary> | |||||
| public static readonly Color DarkerGrey = new Color(0x546E7A); | |||||
| /// <summary> Gets the encoded value for this color. </summary> | /// <summary> Gets the encoded value for this color. </summary> | ||||
| public uint RawValue { get; } | public uint RawValue { get; } | ||||
| @@ -25,7 +25,7 @@ namespace Discord | |||||
| ChannelPermissions GetPermissions(IGuildChannel channel); | ChannelPermissions GetPermissions(IGuildChannel channel); | ||||
| /// <summary> Kicks this user from this guild. </summary> | /// <summary> Kicks this user from this guild. </summary> | ||||
| Task KickAsync(RequestOptions options = null); | |||||
| Task KickAsync(string reason = null, RequestOptions options = null); | |||||
| /// <summary> Modifies this user's properties in this guild. </summary> | /// <summary> Modifies this user's properties in this guild. </summary> | ||||
| Task ModifyAsync(Action<GuildUserProperties> func, RequestOptions options = null); | Task ModifyAsync(Action<GuildUserProperties> func, RequestOptions options = null); | ||||
| @@ -20,8 +20,6 @@ namespace Discord | |||||
| string Username { get; } | string Username { get; } | ||||
| /// <summary> Returns a private message channel to this user, creating one if it does not already exist. </summary> | /// <summary> Returns a private message channel to this user, creating one if it does not already exist. </summary> | ||||
| Task<IDMChannel> GetDMChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); | |||||
| /// <summary> Returns a private message channel to this user, creating one if it does not already exist. </summary> | |||||
| Task<IDMChannel> CreateDMChannelAsync(RequestOptions options = null); | |||||
| Task<IDMChannel> GetOrCreateDMChannelAsync(RequestOptions options = null); | |||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,10 @@ | |||||
| using System; | |||||
| namespace Discord | |||||
| { | |||||
| internal static class StringExtensions | |||||
| { | |||||
| public static bool IsNullOrUri(this string url) => | |||||
| string.IsNullOrEmpty(url) || Uri.IsWellFormedUriString(url, UriKind.Absolute); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,16 @@ | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| public static class UserExtensions | |||||
| { | |||||
| public static async Task<IUserMessage> SendMessageAsync(this IUser user, | |||||
| string text, | |||||
| bool isTTS = false, | |||||
| Embed embed = null, | |||||
| RequestOptions options = null) | |||||
| { | |||||
| return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -9,8 +9,8 @@ namespace Discord.Net.Rest | |||||
| void SetHeader(string key, string value); | void SetHeader(string key, string value); | ||||
| void SetCancelToken(CancellationToken cancelToken); | void SetCancelToken(CancellationToken cancelToken); | ||||
| Task<RestResponse> SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly = false); | |||||
| Task<RestResponse> SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly = false); | |||||
| Task<RestResponse> SendAsync(string method, string endpoint, IReadOnlyDictionary<string, object> multipartParams, CancellationToken cancelToken, bool headerOnly = false); | |||||
| Task<RestResponse> SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly = false, string reason = null); | |||||
| Task<RestResponse> SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly = false, string reason = null); | |||||
| Task<RestResponse> SendAsync(string method, string endpoint, IReadOnlyDictionary<string, object> multipartParams, CancellationToken cancelToken, bool headerOnly = false, string reason = null); | |||||
| } | } | ||||
| } | } | ||||
| @@ -14,6 +14,10 @@ namespace Discord | |||||
| public CancellationToken CancelToken { get; set; } = CancellationToken.None; | public CancellationToken CancelToken { get; set; } = CancellationToken.None; | ||||
| public RetryMode? RetryMode { get; set; } | public RetryMode? RetryMode { get; set; } | ||||
| public bool HeaderOnly { get; internal set; } | public bool HeaderOnly { get; internal set; } | ||||
| /// <summary> | |||||
| /// The reason for this action in the guild's audit log | |||||
| /// </summary> | |||||
| public string AuditLogReason { get; set; } | |||||
| internal bool IgnoreState { get; set; } | internal bool IgnoreState { get; set; } | ||||
| internal string BucketId { get; set; } | internal string BucketId { get; set; } | ||||
| @@ -1,6 +1,7 @@ | |||||
| #pragma warning disable CS1591 | #pragma warning disable CS1591 | ||||
| using System; | using System; | ||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| using Newtonsoft.Json.Converters; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| @@ -8,14 +9,14 @@ namespace Discord.API | |||||
| { | { | ||||
| [JsonProperty("title")] | [JsonProperty("title")] | ||||
| public string Title { get; set; } | public string Title { get; set; } | ||||
| [JsonProperty("type")] | |||||
| public string Type { get; set; } | |||||
| [JsonProperty("description")] | [JsonProperty("description")] | ||||
| public string Description { get; set; } | public string Description { get; set; } | ||||
| [JsonProperty("url")] | [JsonProperty("url")] | ||||
| public string Url { get; set; } | public string Url { get; set; } | ||||
| [JsonProperty("color")] | [JsonProperty("color")] | ||||
| public uint? Color { get; set; } | public uint? Color { get; set; } | ||||
| [JsonProperty("type"), JsonConverter(typeof(StringEnumConverter))] | |||||
| public EmbedType Type { get; set; } | |||||
| [JsonProperty("timestamp")] | [JsonProperty("timestamp")] | ||||
| public DateTimeOffset? Timestamp { get; set; } | public DateTimeOffset? Timestamp { get; set; } | ||||
| [JsonProperty("author")] | [JsonProperty("author")] | ||||
| @@ -1,4 +1,5 @@ | |||||
| using Newtonsoft.Json; | |||||
| using System; | |||||
| using Newtonsoft.Json; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| @@ -1,4 +1,5 @@ | |||||
| using Newtonsoft.Json; | |||||
| using System; | |||||
| using Newtonsoft.Json; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| @@ -1,4 +1,5 @@ | |||||
| #pragma warning disable CS1591 | #pragma warning disable CS1591 | ||||
| using System; | |||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| namespace Discord.API | namespace Discord.API | ||||
| @@ -1,4 +1,5 @@ | |||||
| #pragma warning disable CS1591 | #pragma warning disable CS1591 | ||||
| using System; | |||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| namespace Discord.API | namespace Discord.API | ||||
| @@ -1,4 +1,5 @@ | |||||
| #pragma warning disable CS1591 | #pragma warning disable CS1591 | ||||
| using System; | |||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| namespace Discord.API | namespace Discord.API | ||||
| @@ -1,4 +1,5 @@ | |||||
| #pragma warning disable CS1591 | #pragma warning disable CS1591 | ||||
| using System; | |||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| namespace Discord.API | namespace Discord.API | ||||
| @@ -4,5 +4,6 @@ namespace Discord.API.Rest | |||||
| internal class CreateGuildBanParams | internal class CreateGuildBanParams | ||||
| { | { | ||||
| public Optional<int> DeleteMessageDays { get; set; } | public Optional<int> DeleteMessageDays { get; set; } | ||||
| public string Reason { get; set; } | |||||
| } | } | ||||
| } | } | ||||
| @@ -30,7 +30,7 @@ namespace Discord.API | |||||
| protected readonly JsonSerializer _serializer; | protected readonly JsonSerializer _serializer; | ||||
| protected readonly SemaphoreSlim _stateLock; | protected readonly SemaphoreSlim _stateLock; | ||||
| private readonly RestClientProvider RestClientProvider; | |||||
| private readonly RestClientProvider _restClientProvider; | |||||
| protected bool _isDisposed; | protected bool _isDisposed; | ||||
| private CancellationTokenSource _loginCancelToken; | private CancellationTokenSource _loginCancelToken; | ||||
| @@ -48,7 +48,7 @@ namespace Discord.API | |||||
| public DiscordRestApiClient(RestClientProvider restClientProvider, string userAgent, RetryMode defaultRetryMode = RetryMode.AlwaysRetry, | public DiscordRestApiClient(RestClientProvider restClientProvider, string userAgent, RetryMode defaultRetryMode = RetryMode.AlwaysRetry, | ||||
| JsonSerializer serializer = null) | JsonSerializer serializer = null) | ||||
| { | { | ||||
| RestClientProvider = restClientProvider; | |||||
| _restClientProvider = restClientProvider; | |||||
| UserAgent = userAgent; | UserAgent = userAgent; | ||||
| DefaultRetryMode = defaultRetryMode; | DefaultRetryMode = defaultRetryMode; | ||||
| _serializer = serializer ?? new JsonSerializer { DateFormatString = "yyyy-MM-ddTHH:mm:ssZ", ContractResolver = new DiscordContractResolver() }; | _serializer = serializer ?? new JsonSerializer { DateFormatString = "yyyy-MM-ddTHH:mm:ssZ", ContractResolver = new DiscordContractResolver() }; | ||||
| @@ -60,7 +60,7 @@ namespace Discord.API | |||||
| } | } | ||||
| internal void SetBaseUrl(string baseUrl) | internal void SetBaseUrl(string baseUrl) | ||||
| { | { | ||||
| RestClient = RestClientProvider(baseUrl); | |||||
| RestClient = _restClientProvider(baseUrl); | |||||
| RestClient.SetHeader("accept", "*/*"); | RestClient.SetHeader("accept", "*/*"); | ||||
| RestClient.SetHeader("user-agent", UserAgent); | RestClient.SetHeader("user-agent", UserAgent); | ||||
| RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken)); | RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken)); | ||||
| @@ -189,7 +189,7 @@ namespace Discord.API | |||||
| options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; | options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; | ||||
| options.IsClientBucket = AuthTokenType == TokenType.User; | options.IsClientBucket = AuthTokenType == TokenType.User; | ||||
| var json = payload != null ? SerializeJson(payload) : null; | |||||
| string json = payload != null ? SerializeJson(payload) : null; | |||||
| var request = new JsonRestRequest(RestClient, method, endpoint, json, options); | var request = new JsonRestRequest(RestClient, method, endpoint, json, options); | ||||
| await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); | await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); | ||||
| } | } | ||||
| @@ -233,7 +233,7 @@ namespace Discord.API | |||||
| options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; | options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; | ||||
| options.IsClientBucket = AuthTokenType == TokenType.User; | options.IsClientBucket = AuthTokenType == TokenType.User; | ||||
| var json = payload != null ? SerializeJson(payload) : null; | |||||
| string json = payload != null ? SerializeJson(payload) : null; | |||||
| var request = new JsonRestRequest(RestClient, method, endpoint, json, options); | var request = new JsonRestRequest(RestClient, method, endpoint, json, options); | ||||
| return DeserializeJson<TResponse>(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false)); | return DeserializeJson<TResponse>(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false)); | ||||
| } | } | ||||
| @@ -803,7 +803,8 @@ namespace Discord.API | |||||
| options = RequestOptions.CreateOrClone(options); | options = RequestOptions.CreateOrClone(options); | ||||
| var ids = new BucketIds(guildId: guildId); | var ids = new BucketIds(guildId: guildId); | ||||
| await SendAsync("PUT", () => $"guilds/{guildId}/bans/{userId}?delete-message-days={args.DeleteMessageDays}", ids, options: options).ConfigureAwait(false); | |||||
| string reason = string.IsNullOrWhiteSpace(args.Reason) ? "" : $"&reason={args.Reason}"; | |||||
| await SendAsync("PUT", () => $"guilds/{guildId}/bans/{userId}?delete-message-days={args.DeleteMessageDays}{reason}", ids, options: options).ConfigureAwait(false); | |||||
| } | } | ||||
| public async Task RemoveGuildBanAsync(ulong guildId, ulong userId, RequestOptions options = null) | public async Task RemoveGuildBanAsync(ulong guildId, ulong userId, RequestOptions options = null) | ||||
| { | { | ||||
| @@ -980,14 +981,15 @@ namespace Discord.API | |||||
| Expression<Func<string>> endpoint = () => $"guilds/{guildId}/members?limit={limit}&after={afterUserId}"; | Expression<Func<string>> endpoint = () => $"guilds/{guildId}/members?limit={limit}&after={afterUserId}"; | ||||
| return await SendAsync<IReadOnlyCollection<GuildMember>>("GET", endpoint, ids, options: options).ConfigureAwait(false); | return await SendAsync<IReadOnlyCollection<GuildMember>>("GET", endpoint, ids, options: options).ConfigureAwait(false); | ||||
| } | } | ||||
| public async Task RemoveGuildMemberAsync(ulong guildId, ulong userId, RequestOptions options = null) | |||||
| public async Task RemoveGuildMemberAsync(ulong guildId, ulong userId, string reason, RequestOptions options = null) | |||||
| { | { | ||||
| Preconditions.NotEqual(guildId, 0, nameof(guildId)); | Preconditions.NotEqual(guildId, 0, nameof(guildId)); | ||||
| Preconditions.NotEqual(userId, 0, nameof(userId)); | Preconditions.NotEqual(userId, 0, nameof(userId)); | ||||
| options = RequestOptions.CreateOrClone(options); | options = RequestOptions.CreateOrClone(options); | ||||
| var ids = new BucketIds(guildId: guildId); | var ids = new BucketIds(guildId: guildId); | ||||
| await SendAsync("DELETE", () => $"guilds/{guildId}/members/{userId}", ids, options: options).ConfigureAwait(false); | |||||
| reason = string.IsNullOrWhiteSpace(reason) ? "" : $"?reason={reason}"; | |||||
| await SendAsync("DELETE", () => $"guilds/{guildId}/members/{userId}{reason}", ids, options: options).ConfigureAwait(false); | |||||
| } | } | ||||
| public async Task ModifyGuildMemberAsync(ulong guildId, ulong userId, Rest.ModifyGuildMemberParams args, RequestOptions options = null) | public async Task ModifyGuildMemberAsync(ulong guildId, ulong userId, Rest.ModifyGuildMemberParams args, RequestOptions options = null) | ||||
| { | { | ||||
| @@ -178,12 +178,12 @@ namespace Discord.Rest | |||||
| var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS }; | var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS }; | ||||
| var model = await client.ApiClient.UploadFileAsync(channel.Id, args, options).ConfigureAwait(false); | var model = await client.ApiClient.UploadFileAsync(channel.Id, args, options).ConfigureAwait(false); | ||||
| return RestUserMessage.Create(client, channel, client.CurrentUser, model); | return RestUserMessage.Create(client, channel, client.CurrentUser, model); | ||||
| } | |||||
| } | |||||
| public static async Task DeleteMessagesAsync(IMessageChannel channel, BaseDiscordClient client, | |||||
| IEnumerable<IMessage> messages, RequestOptions options) | |||||
| public static async Task DeleteMessagesAsync(IMessageChannel channel, BaseDiscordClient client, | |||||
| IEnumerable<ulong> messageIds, RequestOptions options) | |||||
| { | { | ||||
| var msgs = messages.Select(x => x.Id).ToArray(); | |||||
| var msgs = messageIds.ToArray(); | |||||
| if (msgs.Length < 100) | if (msgs.Length < 100) | ||||
| { | { | ||||
| var args = new DeleteMessagesParams(msgs); | var args = new DeleteMessagesParams(msgs); | ||||
| @@ -73,7 +73,9 @@ namespace Discord.Rest | |||||
| => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); | => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); | ||||
| public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null) | public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null) | ||||
| => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); | |||||
| => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); | |||||
| public Task DeleteMessagesAsync(IEnumerable<ulong> messageIds, RequestOptions options = null) | |||||
| => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); | |||||
| public Task TriggerTypingAsync(RequestOptions options = null) | public Task TriggerTypingAsync(RequestOptions options = null) | ||||
| => ChannelHelper.TriggerTypingAsync(this, Discord, options); | => ChannelHelper.TriggerTypingAsync(this, Discord, options); | ||||
| @@ -86,7 +86,9 @@ namespace Discord.Rest | |||||
| => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); | => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); | ||||
| public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null) | public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null) | ||||
| => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); | |||||
| => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); | |||||
| public Task DeleteMessagesAsync(IEnumerable<ulong> messageIds, RequestOptions options = null) | |||||
| => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); | |||||
| public Task TriggerTypingAsync(RequestOptions options = null) | public Task TriggerTypingAsync(RequestOptions options = null) | ||||
| => ChannelHelper.TriggerTypingAsync(this, Discord, options); | => ChannelHelper.TriggerTypingAsync(this, Discord, options); | ||||
| @@ -64,7 +64,9 @@ namespace Discord.Rest | |||||
| => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); | => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); | ||||
| public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null) | public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null) | ||||
| => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); | |||||
| => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); | |||||
| public Task DeleteMessagesAsync(IEnumerable<ulong> messageIds, RequestOptions options = null) | |||||
| => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); | |||||
| public Task TriggerTypingAsync(RequestOptions options = null) | public Task TriggerTypingAsync(RequestOptions options = null) | ||||
| => ChannelHelper.TriggerTypingAsync(this, Discord, options); | => ChannelHelper.TriggerTypingAsync(this, Discord, options); | ||||
| @@ -43,7 +43,9 @@ namespace Discord.Rest | |||||
| => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); | => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); | ||||
| public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null) | public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null) | ||||
| => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); | |||||
| => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); | |||||
| public Task DeleteMessagesAsync(IEnumerable<ulong> messageIds, RequestOptions options = null) | |||||
| => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); | |||||
| public Task TriggerTypingAsync(RequestOptions options = null) | public Task TriggerTypingAsync(RequestOptions options = null) | ||||
| => ChannelHelper.TriggerTypingAsync(this, Discord, options); | => ChannelHelper.TriggerTypingAsync(this, Discord, options); | ||||
| @@ -107,9 +107,9 @@ namespace Discord.Rest | |||||
| } | } | ||||
| public static async Task AddBanAsync(IGuild guild, BaseDiscordClient client, | public static async Task AddBanAsync(IGuild guild, BaseDiscordClient client, | ||||
| ulong userId, int pruneDays, RequestOptions options) | |||||
| ulong userId, int pruneDays, string reason, RequestOptions options) | |||||
| { | { | ||||
| var args = new CreateGuildBanParams { DeleteMessageDays = pruneDays }; | |||||
| var args = new CreateGuildBanParams { DeleteMessageDays = pruneDays, Reason = reason }; | |||||
| await client.ApiClient.CreateGuildBanAsync(guild.Id, userId, args, options).ConfigureAwait(false); | await client.ApiClient.CreateGuildBanAsync(guild.Id, userId, args, options).ConfigureAwait(false); | ||||
| } | } | ||||
| public static async Task RemoveBanAsync(IGuild guild, BaseDiscordClient client, | public static async Task RemoveBanAsync(IGuild guild, BaseDiscordClient client, | ||||
| @@ -137,10 +137,10 @@ namespace Discord.Rest | |||||
| public Task<IReadOnlyCollection<RestBan>> GetBansAsync(RequestOptions options = null) | public Task<IReadOnlyCollection<RestBan>> GetBansAsync(RequestOptions options = null) | ||||
| => GuildHelper.GetBansAsync(this, Discord, options); | => GuildHelper.GetBansAsync(this, Discord, options); | ||||
| public Task AddBanAsync(IUser user, int pruneDays = 0, RequestOptions options = null) | |||||
| => GuildHelper.AddBanAsync(this, Discord, user.Id, pruneDays, options); | |||||
| public Task AddBanAsync(ulong userId, int pruneDays = 0, RequestOptions options = null) | |||||
| => GuildHelper.AddBanAsync(this, Discord, userId, pruneDays, options); | |||||
| public Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null) | |||||
| => GuildHelper.AddBanAsync(this, Discord, user.Id, pruneDays, reason, options); | |||||
| public Task AddBanAsync(ulong userId, int pruneDays = 0, string reason = null, RequestOptions options = null) | |||||
| => GuildHelper.AddBanAsync(this, Discord, userId, pruneDays, reason, options); | |||||
| public Task RemoveBanAsync(IUser user, RequestOptions options = null) | public Task RemoveBanAsync(IUser user, RequestOptions options = null) | ||||
| => GuildHelper.RemoveBanAsync(this, Discord, user.Id, options); | => GuildHelper.RemoveBanAsync(this, Discord, user.Id, options); | ||||
| @@ -8,19 +8,66 @@ namespace Discord | |||||
| { | { | ||||
| private readonly Embed _embed; | private readonly Embed _embed; | ||||
| public const int MaxFieldCount = 25; | |||||
| public const int MaxTitleLength = 256; | |||||
| public const int MaxDescriptionLength = 2048; | |||||
| public const int MaxEmbedLength = 6000; // user bot limit is 2000, but we don't validate that here. | |||||
| public EmbedBuilder() | public EmbedBuilder() | ||||
| { | { | ||||
| _embed = new Embed("rich"); | |||||
| _embed = new Embed(EmbedType.Rich); | |||||
| Fields = new List<EmbedFieldBuilder>(); | Fields = new List<EmbedFieldBuilder>(); | ||||
| } | } | ||||
| public string Title { get { return _embed.Title; } set { _embed.Title = value; } } | |||||
| public string Description { get { return _embed.Description; } set { _embed.Description = value; } } | |||||
| public string Url { get { return _embed.Url; } set { _embed.Url = value; } } | |||||
| public string ThumbnailUrl { get { return _embed.Thumbnail?.Url; } set { _embed.Thumbnail = new EmbedThumbnail(value, null, null, null); } } | |||||
| public string ImageUrl { get { return _embed.Image?.Url; } set { _embed.Image = new EmbedImage(value, null, null, null); } } | |||||
| public DateTimeOffset? Timestamp { get { return _embed.Timestamp; } set { _embed.Timestamp = value; } } | |||||
| public Color? Color { get { return _embed.Color; } set { _embed.Color = value; } } | |||||
| public string Title | |||||
| { | |||||
| get => _embed.Title; | |||||
| set | |||||
| { | |||||
| if (value?.Length > MaxTitleLength) throw new ArgumentException($"Title length must be less than or equal to {MaxTitleLength}.", nameof(Title)); | |||||
| _embed.Title = value; | |||||
| } | |||||
| } | |||||
| public string Description | |||||
| { | |||||
| get => _embed.Description; | |||||
| set | |||||
| { | |||||
| if (value?.Length > MaxDescriptionLength) throw new ArgumentException($"Description length must be less than or equal to {MaxDescriptionLength}.", nameof(Description)); | |||||
| _embed.Description = value; | |||||
| } | |||||
| } | |||||
| public string Url | |||||
| { | |||||
| get => _embed.Url; | |||||
| set | |||||
| { | |||||
| if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(Url)); | |||||
| _embed.Url = value; | |||||
| } | |||||
| } | |||||
| public string ThumbnailUrl | |||||
| { | |||||
| get => _embed.Thumbnail?.Url; | |||||
| set | |||||
| { | |||||
| if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(ThumbnailUrl)); | |||||
| _embed.Thumbnail = new EmbedThumbnail(value, null, null, null); | |||||
| } | |||||
| } | |||||
| public string ImageUrl | |||||
| { | |||||
| get => _embed.Image?.Url; | |||||
| set | |||||
| { | |||||
| if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(ImageUrl)); | |||||
| _embed.Image = new EmbedImage(value, null, null, null); | |||||
| } | |||||
| } | |||||
| public DateTimeOffset? Timestamp { get => _embed.Timestamp; set { _embed.Timestamp = value; } } | |||||
| public Color? Color { get => _embed.Color; set { _embed.Color = value; } } | |||||
| public EmbedAuthorBuilder Author { get; set; } | public EmbedAuthorBuilder Author { get; set; } | ||||
| public EmbedFooterBuilder Footer { get; set; } | public EmbedFooterBuilder Footer { get; set; } | ||||
| @@ -30,8 +77,10 @@ namespace Discord | |||||
| get => _fields; | get => _fields; | ||||
| set | set | ||||
| { | { | ||||
| if (value != null) _fields = value; | |||||
| else throw new ArgumentNullException("Cannot set an embed builder's fields collection to null", nameof(value)); | |||||
| if (value == null) throw new ArgumentNullException("Cannot set an embed builder's fields collection to null", nameof(Fields)); | |||||
| if (value.Count > MaxFieldCount) throw new ArgumentException($"Field count must be less than or equal to {MaxFieldCount}.", nameof(Fields)); | |||||
| _fields = value; | |||||
| } | } | ||||
| } | } | ||||
| @@ -88,6 +137,17 @@ namespace Discord | |||||
| Author = author; | Author = author; | ||||
| return this; | return this; | ||||
| } | } | ||||
| public EmbedBuilder WithAuthor(string name, string iconUrl = null, string url = null) | |||||
| { | |||||
| var author = new EmbedAuthorBuilder | |||||
| { | |||||
| Name = name, | |||||
| IconUrl = iconUrl, | |||||
| Url = url | |||||
| }; | |||||
| Author = author; | |||||
| return this; | |||||
| } | |||||
| public EmbedBuilder WithFooter(EmbedFooterBuilder footer) | public EmbedBuilder WithFooter(EmbedFooterBuilder footer) | ||||
| { | { | ||||
| Footer = footer; | Footer = footer; | ||||
| @@ -100,6 +160,16 @@ namespace Discord | |||||
| Footer = footer; | Footer = footer; | ||||
| return this; | return this; | ||||
| } | } | ||||
| public EmbedBuilder WithFooter(string text, string iconUrl = null) | |||||
| { | |||||
| var footer = new EmbedFooterBuilder | |||||
| { | |||||
| Text = text, | |||||
| IconUrl = iconUrl | |||||
| }; | |||||
| Footer = footer; | |||||
| return this; | |||||
| } | |||||
| public EmbedBuilder AddField(string name, object value) | public EmbedBuilder AddField(string name, object value) | ||||
| { | { | ||||
| @@ -107,7 +177,7 @@ namespace Discord | |||||
| .WithIsInline(false) | .WithIsInline(false) | ||||
| .WithName(name) | .WithName(name) | ||||
| .WithValue(value); | .WithValue(value); | ||||
| Fields.Add(field); | |||||
| AddField(field); | |||||
| return this; | return this; | ||||
| } | } | ||||
| public EmbedBuilder AddInlineField(string name, object value) | public EmbedBuilder AddInlineField(string name, object value) | ||||
| @@ -116,11 +186,16 @@ namespace Discord | |||||
| .WithIsInline(true) | .WithIsInline(true) | ||||
| .WithName(name) | .WithName(name) | ||||
| .WithValue(value); | .WithValue(value); | ||||
| Fields.Add(field); | |||||
| AddField(field); | |||||
| return this; | return this; | ||||
| } | } | ||||
| public EmbedBuilder AddField(EmbedFieldBuilder field) | public EmbedBuilder AddField(EmbedFieldBuilder field) | ||||
| { | { | ||||
| if (Fields.Count >= MaxFieldCount) | |||||
| { | |||||
| throw new ArgumentException($"Field count must be less than or equal to {MaxFieldCount}.", nameof(field)); | |||||
| } | |||||
| Fields.Add(field); | Fields.Add(field); | ||||
| return this; | return this; | ||||
| } | } | ||||
| @@ -128,7 +203,18 @@ namespace Discord | |||||
| { | { | ||||
| var field = new EmbedFieldBuilder(); | var field = new EmbedFieldBuilder(); | ||||
| action(field); | action(field); | ||||
| Fields.Add(field); | |||||
| this.AddField(field); | |||||
| return this; | |||||
| } | |||||
| public EmbedBuilder AddField(string title, string text, bool inline = false) | |||||
| { | |||||
| var field = new EmbedFieldBuilder | |||||
| { | |||||
| Name = title, | |||||
| Value = text, | |||||
| IsInline = inline | |||||
| }; | |||||
| _fields.Add(field); | |||||
| return this; | return this; | ||||
| } | } | ||||
| @@ -140,6 +226,12 @@ namespace Discord | |||||
| for (int i = 0; i < Fields.Count; i++) | for (int i = 0; i < Fields.Count; i++) | ||||
| fields.Add(Fields[i].Build()); | fields.Add(Fields[i].Build()); | ||||
| _embed.Fields = fields.ToImmutable(); | _embed.Fields = fields.ToImmutable(); | ||||
| if (_embed.Length > MaxEmbedLength) | |||||
| { | |||||
| throw new InvalidOperationException($"Total embed length must be less than or equal to {MaxEmbedLength}"); | |||||
| } | |||||
| return _embed; | return _embed; | ||||
| } | } | ||||
| public static implicit operator Embed(EmbedBuilder builder) => builder?.Build(); | public static implicit operator Embed(EmbedBuilder builder) => builder?.Build(); | ||||
| @@ -149,9 +241,32 @@ namespace Discord | |||||
| { | { | ||||
| private EmbedField _field; | private EmbedField _field; | ||||
| public string Name { get { return _field.Name; } set { _field.Name = value; } } | |||||
| public object Value { get { return _field.Value; } set { _field.Value = value.ToString(); } } | |||||
| public bool IsInline { get { return _field.Inline; } set { _field.Inline = value; } } | |||||
| public const int MaxFieldNameLength = 256; | |||||
| public const int MaxFieldValueLength = 1024; | |||||
| public string Name | |||||
| { | |||||
| get => _field.Name; | |||||
| set | |||||
| { | |||||
| if (string.IsNullOrEmpty(value)) throw new ArgumentException($"Field name must not be null or empty.", nameof(Name)); | |||||
| if (value.Length > MaxFieldNameLength) throw new ArgumentException($"Field name length must be less than or equal to {MaxFieldNameLength}.", nameof(Name)); | |||||
| _field.Name = value; | |||||
| } | |||||
| } | |||||
| public object Value | |||||
| { | |||||
| get => _field.Value; | |||||
| set | |||||
| { | |||||
| var stringValue = value?.ToString(); | |||||
| if (string.IsNullOrEmpty(stringValue)) throw new ArgumentException($"Field value must not be null or empty.", nameof(Value)); | |||||
| if (stringValue.Length > MaxFieldValueLength) throw new ArgumentException($"Field value length must be less than or equal to {MaxFieldValueLength}.", nameof(Value)); | |||||
| _field.Value = stringValue; | |||||
| } | |||||
| } | |||||
| public bool IsInline { get => _field.Inline; set { _field.Inline = value; } } | |||||
| public EmbedFieldBuilder() | public EmbedFieldBuilder() | ||||
| { | { | ||||
| @@ -182,9 +297,35 @@ namespace Discord | |||||
| { | { | ||||
| private EmbedAuthor _author; | private EmbedAuthor _author; | ||||
| public string Name { get { return _author.Name; } set { _author.Name = value; } } | |||||
| public string Url { get { return _author.Url; } set { _author.Url = value; } } | |||||
| public string IconUrl { get { return _author.IconUrl; } set { _author.IconUrl = value; } } | |||||
| public const int MaxAuthorNameLength = 256; | |||||
| public string Name | |||||
| { | |||||
| get => _author.Name; | |||||
| set | |||||
| { | |||||
| if (value?.Length > MaxAuthorNameLength) throw new ArgumentException($"Author name length must be less than or equal to {MaxAuthorNameLength}.", nameof(Name)); | |||||
| _author.Name = value; | |||||
| } | |||||
| } | |||||
| public string Url | |||||
| { | |||||
| get => _author.Url; | |||||
| set | |||||
| { | |||||
| if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(Url)); | |||||
| _author.Url = value; | |||||
| } | |||||
| } | |||||
| public string IconUrl | |||||
| { | |||||
| get => _author.IconUrl; | |||||
| set | |||||
| { | |||||
| if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(IconUrl)); | |||||
| _author.IconUrl = value; | |||||
| } | |||||
| } | |||||
| public EmbedAuthorBuilder() | public EmbedAuthorBuilder() | ||||
| { | { | ||||
| @@ -215,8 +356,26 @@ namespace Discord | |||||
| { | { | ||||
| private EmbedFooter _footer; | private EmbedFooter _footer; | ||||
| public string Text { get { return _footer.Text; } set { _footer.Text = value; } } | |||||
| public string IconUrl { get { return _footer.IconUrl; } set { _footer.IconUrl = value; } } | |||||
| public const int MaxFooterTextLength = 2048; | |||||
| public string Text | |||||
| { | |||||
| get => _footer.Text; | |||||
| set | |||||
| { | |||||
| if (value?.Length > MaxFooterTextLength) throw new ArgumentException($"Footer text length must be less than or equal to {MaxFooterTextLength}.", nameof(Text)); | |||||
| _footer.Text = value; | |||||
| } | |||||
| } | |||||
| public string IconUrl | |||||
| { | |||||
| get => _footer.IconUrl; | |||||
| set | |||||
| { | |||||
| if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(IconUrl)); | |||||
| _footer.IconUrl = value; | |||||
| } | |||||
| } | |||||
| public EmbedFooterBuilder() | public EmbedFooterBuilder() | ||||
| { | { | ||||
| @@ -85,8 +85,8 @@ namespace Discord.Rest | |||||
| else if (args.RoleIds.IsSpecified) | else if (args.RoleIds.IsSpecified) | ||||
| UpdateRoles(args.RoleIds.Value.ToArray()); | UpdateRoles(args.RoleIds.Value.ToArray()); | ||||
| } | } | ||||
| public Task KickAsync(RequestOptions options = null) | |||||
| => UserHelper.KickAsync(this, Discord, options); | |||||
| public Task KickAsync(string reason = null, RequestOptions options = null) | |||||
| => UserHelper.KickAsync(this, Discord, reason, options); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public Task AddRoleAsync(IRole role, RequestOptions options = null) | public Task AddRoleAsync(IRole role, RequestOptions options = null) | ||||
| => AddRolesAsync(new[] { role }, options); | => AddRolesAsync(new[] { role }, options); | ||||
| @@ -54,7 +54,7 @@ namespace Discord.Rest | |||||
| Update(model); | Update(model); | ||||
| } | } | ||||
| public Task<RestDMChannel> CreateDMChannelAsync(RequestOptions options = null) | |||||
| public Task<RestDMChannel> GetOrCreateDMChannelAsync(RequestOptions options = null) | |||||
| => UserHelper.CreateDMChannelAsync(this, Discord, options); | => UserHelper.CreateDMChannelAsync(this, Discord, options); | ||||
| public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) | public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) | ||||
| @@ -64,9 +64,7 @@ namespace Discord.Rest | |||||
| private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; | private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; | ||||
| //IUser | //IUser | ||||
| Task<IDMChannel> IUser.GetDMChannelAsync(CacheMode mode, RequestOptions options) | |||||
| => Task.FromResult<IDMChannel>(null); | |||||
| async Task<IDMChannel> IUser.CreateDMChannelAsync(RequestOptions options) | |||||
| => await CreateDMChannelAsync(options).ConfigureAwait(false); | |||||
| async Task<IDMChannel> IUser.GetOrCreateDMChannelAsync(RequestOptions options) | |||||
| => await GetOrCreateDMChannelAsync(options); | |||||
| } | } | ||||
| } | } | ||||
| @@ -45,7 +45,7 @@ namespace Discord.Rest | |||||
| GuildPermissions IGuildUser.GuildPermissions => GuildPermissions.Webhook; | GuildPermissions IGuildUser.GuildPermissions => GuildPermissions.Webhook; | ||||
| ChannelPermissions IGuildUser.GetPermissions(IGuildChannel channel) => Permissions.ToChannelPerms(channel, GuildPermissions.Webhook.RawValue); | ChannelPermissions IGuildUser.GetPermissions(IGuildChannel channel) => Permissions.ToChannelPerms(channel, GuildPermissions.Webhook.RawValue); | ||||
| Task IGuildUser.KickAsync(RequestOptions options) | |||||
| Task IGuildUser.KickAsync(string reason, RequestOptions options) | |||||
| { | { | ||||
| throw new NotSupportedException("Webhook users cannot be kicked."); | throw new NotSupportedException("Webhook users cannot be kicked."); | ||||
| } | } | ||||
| @@ -53,9 +53,9 @@ namespace Discord.Rest | |||||
| } | } | ||||
| public static async Task KickAsync(IGuildUser user, BaseDiscordClient client, | public static async Task KickAsync(IGuildUser user, BaseDiscordClient client, | ||||
| RequestOptions options) | |||||
| string reason, RequestOptions options) | |||||
| { | { | ||||
| await client.ApiClient.RemoveGuildMemberAsync(user.GuildId, user.Id, options).ConfigureAwait(false); | |||||
| await client.ApiClient.RemoveGuildMemberAsync(user.GuildId, user.Id, reason, options).ConfigureAwait(false); | |||||
| } | } | ||||
| public static async Task<RestDMChannel> CreateDMChannelAsync(IUser user, BaseDiscordClient client, | public static async Task<RestDMChannel> CreateDMChannelAsync(IUser user, BaseDiscordClient client, | ||||
| @@ -0,0 +1,23 @@ | |||||
| namespace Discord | |||||
| { | |||||
| public static class EmbedBuilderExtensions | |||||
| { | |||||
| public static EmbedBuilder WithColor(this EmbedBuilder builder, uint rawValue) => | |||||
| builder.WithColor(new Color(rawValue)); | |||||
| public static EmbedBuilder WithColor(this EmbedBuilder builder, byte r, byte g, byte b) => | |||||
| builder.WithColor(new Color(r, g, b)); | |||||
| public static EmbedBuilder WithColor(this EmbedBuilder builder, int r, int g, int b) => | |||||
| builder.WithColor(new Color(r, g, b)); | |||||
| public static EmbedBuilder WithColor(this EmbedBuilder builder, float r, float g, float b) => | |||||
| builder.WithColor(new Color(r, g, b)); | |||||
| public static EmbedBuilder WithAuthor(this EmbedBuilder builder, IUser user) => | |||||
| builder.WithAuthor($"{user.Username}#{user.Discriminator}", user.GetAvatarUrl()); | |||||
| public static EmbedBuilder WithAuthor(this EmbedBuilder builder, IGuildUser user) => | |||||
| builder.WithAuthor($"{user.Nickname ?? user.Username}#{user.Discriminator}", user.GetAvatarUrl()); | |||||
| } | |||||
| } | |||||
| @@ -62,26 +62,31 @@ namespace Discord.Net.Rest | |||||
| _cancelToken = cancelToken; | _cancelToken = cancelToken; | ||||
| } | } | ||||
| public async Task<RestResponse> SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly) | |||||
| public async Task<RestResponse> SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly, string reason = null) | |||||
| { | { | ||||
| string uri = Path.Combine(_baseUrl, endpoint); | string uri = Path.Combine(_baseUrl, endpoint); | ||||
| using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) | using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) | ||||
| { | |||||
| if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); | |||||
| return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); | return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); | ||||
| } | |||||
| } | } | ||||
| public async Task<RestResponse> SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly) | |||||
| public async Task<RestResponse> SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly, string reason = null) | |||||
| { | { | ||||
| string uri = Path.Combine(_baseUrl, endpoint); | string uri = Path.Combine(_baseUrl, endpoint); | ||||
| using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) | using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) | ||||
| { | { | ||||
| if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); | |||||
| restRequest.Content = new StringContent(json, Encoding.UTF8, "application/json"); | restRequest.Content = new StringContent(json, Encoding.UTF8, "application/json"); | ||||
| return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); | return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); | ||||
| } | } | ||||
| } | } | ||||
| public async Task<RestResponse> SendAsync(string method, string endpoint, IReadOnlyDictionary<string, object> multipartParams, CancellationToken cancelToken, bool headerOnly) | |||||
| public async Task<RestResponse> SendAsync(string method, string endpoint, IReadOnlyDictionary<string, object> multipartParams, CancellationToken cancelToken, bool headerOnly, string reason = null) | |||||
| { | { | ||||
| string uri = Path.Combine(_baseUrl, endpoint); | string uri = Path.Combine(_baseUrl, endpoint); | ||||
| using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) | using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) | ||||
| { | { | ||||
| if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); | |||||
| var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture)); | var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture)); | ||||
| if (multipartParams != null) | if (multipartParams != null) | ||||
| { | { | ||||
| @@ -15,7 +15,7 @@ namespace Discord.Net.Queue | |||||
| public override async Task<RestResponse> SendAsync() | public override async Task<RestResponse> SendAsync() | ||||
| { | { | ||||
| return await Client.SendAsync(Method, Endpoint, Json, Options.CancelToken, Options.HeaderOnly).ConfigureAwait(false); | |||||
| return await Client.SendAsync(Method, Endpoint, Json, Options.CancelToken, Options.HeaderOnly, Options.AuditLogReason).ConfigureAwait(false); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -16,7 +16,7 @@ namespace Discord.Net.Queue | |||||
| public override async Task<RestResponse> SendAsync() | public override async Task<RestResponse> SendAsync() | ||||
| { | { | ||||
| return await Client.SendAsync(Method, Endpoint, MultipartParams, Options.CancelToken, Options.HeaderOnly).ConfigureAwait(false); | |||||
| return await Client.SendAsync(Method, Endpoint, MultipartParams, Options.CancelToken, Options.HeaderOnly, Options.AuditLogReason).ConfigureAwait(false); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -28,7 +28,7 @@ namespace Discord.Net.Queue | |||||
| public virtual async Task<RestResponse> SendAsync() | public virtual async Task<RestResponse> SendAsync() | ||||
| { | { | ||||
| return await Client.SendAsync(Method, Endpoint, Options.CancelToken, Options.HeaderOnly).ConfigureAwait(false); | |||||
| return await Client.SendAsync(Method, Endpoint, Options.CancelToken, Options.HeaderOnly, Options.AuditLogReason).ConfigureAwait(false); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -54,7 +54,9 @@ namespace Discord.Rpc | |||||
| => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); | => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); | ||||
| public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null) | public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null) | ||||
| => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); | |||||
| => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); | |||||
| public Task DeleteMessagesAsync(IEnumerable<ulong> messageIds, RequestOptions options = null) | |||||
| => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); | |||||
| public Task TriggerTypingAsync(RequestOptions options = null) | public Task TriggerTypingAsync(RequestOptions options = null) | ||||
| => ChannelHelper.TriggerTypingAsync(this, Discord, options); | => ChannelHelper.TriggerTypingAsync(this, Discord, options); | ||||
| @@ -57,7 +57,9 @@ namespace Discord.Rpc | |||||
| => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); | => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); | ||||
| public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null) | public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null) | ||||
| => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); | |||||
| => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); | |||||
| public Task DeleteMessagesAsync(IEnumerable<ulong> messageIds, RequestOptions options = null) | |||||
| => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); | |||||
| public Task TriggerTypingAsync(RequestOptions options = null) | public Task TriggerTypingAsync(RequestOptions options = null) | ||||
| => ChannelHelper.TriggerTypingAsync(this, Discord, options); | => ChannelHelper.TriggerTypingAsync(this, Discord, options); | ||||
| @@ -58,7 +58,9 @@ namespace Discord.Rpc | |||||
| => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); | => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); | ||||
| public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null) | public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null) | ||||
| => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); | |||||
| => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); | |||||
| public Task DeleteMessagesAsync(IEnumerable<ulong> messageIds, RequestOptions options = null) | |||||
| => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); | |||||
| public Task TriggerTypingAsync(RequestOptions options = null) | public Task TriggerTypingAsync(RequestOptions options = null) | ||||
| => ChannelHelper.TriggerTypingAsync(this, Discord, options); | => ChannelHelper.TriggerTypingAsync(this, Discord, options); | ||||
| @@ -49,7 +49,7 @@ namespace Discord.Rpc | |||||
| Username = model.Username.Value; | Username = model.Username.Value; | ||||
| } | } | ||||
| public Task<RestDMChannel> CreateDMChannelAsync(RequestOptions options = null) | |||||
| public Task<RestDMChannel> GetOrCreateDMChannelAsync(RequestOptions options = null) | |||||
| => UserHelper.CreateDMChannelAsync(this, Discord, options); | => UserHelper.CreateDMChannelAsync(this, Discord, options); | ||||
| public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) | public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) | ||||
| @@ -59,9 +59,7 @@ namespace Discord.Rpc | |||||
| private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; | private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; | ||||
| //IUser | //IUser | ||||
| Task<IDMChannel> IUser.GetDMChannelAsync(CacheMode mode, RequestOptions options) | |||||
| => Task.FromResult<IDMChannel>(null); | |||||
| async Task<IDMChannel> IUser.CreateDMChannelAsync(RequestOptions options) | |||||
| => await CreateDMChannelAsync(options).ConfigureAwait(false); | |||||
| async Task<IDMChannel> IUser.GetOrCreateDMChannelAsync(RequestOptions options) | |||||
| => await GetOrCreateDMChannelAsync(options); | |||||
| } | } | ||||
| } | } | ||||
| @@ -142,31 +142,31 @@ namespace Discord.Audio | |||||
| public AudioOutStream CreateOpusStream(int bufferMillis) | public AudioOutStream CreateOpusStream(int bufferMillis) | ||||
| { | { | ||||
| var outputStream = new OutputStream(ApiClient); | |||||
| var sodiumEncrypter = new SodiumEncryptStream( outputStream, this); | |||||
| var rtpWriter = new RTPWriteStream(sodiumEncrypter, _ssrc); | |||||
| return new BufferedWriteStream(rtpWriter, this, bufferMillis, _connection.CancelToken, _audioLogger); | |||||
| var outputStream = new OutputStream(ApiClient); //Ignores header | |||||
| var sodiumEncrypter = new SodiumEncryptStream( outputStream, this); //Passes header | |||||
| var rtpWriter = new RTPWriteStream(sodiumEncrypter, _ssrc); //Consumes header, passes | |||||
| return new BufferedWriteStream(rtpWriter, this, bufferMillis, _connection.CancelToken, _audioLogger); //Generates header | |||||
| } | } | ||||
| public AudioOutStream CreateDirectOpusStream() | public AudioOutStream CreateDirectOpusStream() | ||||
| { | { | ||||
| var outputStream = new OutputStream(ApiClient); | |||||
| var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); | |||||
| return new RTPWriteStream(sodiumEncrypter, _ssrc); | |||||
| var outputStream = new OutputStream(ApiClient); //Ignores header | |||||
| var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); //Passes header | |||||
| return new RTPWriteStream(sodiumEncrypter, _ssrc); //Consumes header (external input), passes | |||||
| } | } | ||||
| public AudioOutStream CreatePCMStream(AudioApplication application, int? bitrate, int bufferMillis) | |||||
| public AudioOutStream CreatePCMStream(AudioApplication application, int? bitrate, int bufferMillis, int packetLoss) | |||||
| { | { | ||||
| var outputStream = new OutputStream(ApiClient); | |||||
| var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); | |||||
| var rtpWriter = new RTPWriteStream(sodiumEncrypter, _ssrc); | |||||
| var bufferedStream = new BufferedWriteStream(rtpWriter, this, bufferMillis, _connection.CancelToken, _audioLogger); | |||||
| return new OpusEncodeStream(bufferedStream, bitrate ?? (96 * 1024), application); | |||||
| var outputStream = new OutputStream(ApiClient); //Ignores header | |||||
| var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); //Passes header | |||||
| var rtpWriter = new RTPWriteStream(sodiumEncrypter, _ssrc); //Consumes header, passes | |||||
| var bufferedStream = new BufferedWriteStream(rtpWriter, this, bufferMillis, _connection.CancelToken, _audioLogger); //Ignores header, generates header | |||||
| return new OpusEncodeStream(bufferedStream, bitrate ?? (96 * 1024), application, packetLoss); //Generates header | |||||
| } | } | ||||
| public AudioOutStream CreateDirectPCMStream(AudioApplication application, int? bitrate) | |||||
| public AudioOutStream CreateDirectPCMStream(AudioApplication application, int? bitrate, int packetLoss) | |||||
| { | { | ||||
| var outputStream = new OutputStream(ApiClient); | |||||
| var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); | |||||
| var rtpWriter = new RTPWriteStream(sodiumEncrypter, _ssrc); | |||||
| return new OpusEncodeStream(rtpWriter, bitrate ?? (96 * 1024), application); | |||||
| var outputStream = new OutputStream(ApiClient); //Ignores header | |||||
| var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); //Passes header | |||||
| var rtpWriter = new RTPWriteStream(sodiumEncrypter, _ssrc); //Consumes header, passes | |||||
| return new OpusEncodeStream(rtpWriter, bitrate ?? (96 * 1024), application, packetLoss); //Generates header | |||||
| } | } | ||||
| internal async Task CreateInputStreamAsync(ulong userId) | internal async Task CreateInputStreamAsync(ulong userId) | ||||
| @@ -174,11 +174,11 @@ namespace Discord.Audio | |||||
| //Assume Thread-safe | //Assume Thread-safe | ||||
| if (!_streams.ContainsKey(userId)) | if (!_streams.ContainsKey(userId)) | ||||
| { | { | ||||
| var readerStream = new InputStream(); | |||||
| var opusDecoder = new OpusDecodeStream(readerStream); | |||||
| var readerStream = new InputStream(); //Consumes header | |||||
| var opusDecoder = new OpusDecodeStream(readerStream); //Passes header | |||||
| //var jitterBuffer = new JitterBuffer(opusDecoder, _audioLogger); | //var jitterBuffer = new JitterBuffer(opusDecoder, _audioLogger); | ||||
| var rtpReader = new RTPReadStream(opusDecoder); | |||||
| var decryptStream = new SodiumDecryptStream(rtpReader, this); | |||||
| var rtpReader = new RTPReadStream(opusDecoder); //Generates header | |||||
| var decryptStream = new SodiumDecryptStream(rtpReader, this); //No header | |||||
| _streams.TryAdd(userId, new StreamPair(readerStream, decryptStream)); | _streams.TryAdd(userId, new StreamPair(readerStream, decryptStream)); | ||||
| await _streamCreatedEvent.InvokeAsync(userId, readerStream); | await _streamCreatedEvent.InvokeAsync(userId, readerStream); | ||||
| } | } | ||||
| @@ -17,7 +17,7 @@ namespace Discord.Audio | |||||
| public AudioApplication Application { get; } | public AudioApplication Application { get; } | ||||
| public int BitRate { get;} | public int BitRate { get;} | ||||
| public OpusEncoder(int bitrate, AudioApplication application) | |||||
| public OpusEncoder(int bitrate, AudioApplication application, int packetLoss) | |||||
| { | { | ||||
| if (bitrate < 1 || bitrate > DiscordVoiceAPIClient.MaxBitrate) | if (bitrate < 1 || bitrate > DiscordVoiceAPIClient.MaxBitrate) | ||||
| throw new ArgumentOutOfRangeException(nameof(bitrate)); | throw new ArgumentOutOfRangeException(nameof(bitrate)); | ||||
| @@ -48,7 +48,7 @@ namespace Discord.Audio | |||||
| _ptr = CreateEncoder(SamplingRate, Channels, (int)opusApplication, out var error); | _ptr = CreateEncoder(SamplingRate, Channels, (int)opusApplication, out var error); | ||||
| CheckError(error); | CheckError(error); | ||||
| CheckError(EncoderCtl(_ptr, OpusCtl.SetSignal, (int)opusSignal)); | CheckError(EncoderCtl(_ptr, OpusCtl.SetSignal, (int)opusSignal)); | ||||
| CheckError(EncoderCtl(_ptr, OpusCtl.SetPacketLossPercent, 30)); //% | |||||
| CheckError(EncoderCtl(_ptr, OpusCtl.SetPacketLossPercent, packetLoss)); //% | |||||
| CheckError(EncoderCtl(_ptr, OpusCtl.SetInbandFEC, 1)); //True | CheckError(EncoderCtl(_ptr, OpusCtl.SetInbandFEC, 1)); //True | ||||
| CheckError(EncoderCtl(_ptr, OpusCtl.SetBitrate, bitrate)); | CheckError(EncoderCtl(_ptr, OpusCtl.SetBitrate, bitrate)); | ||||
| } | } | ||||
| @@ -88,11 +88,12 @@ namespace Discord.Audio.Streams | |||||
| if (_queuedFrames.TryDequeue(out Frame frame)) | if (_queuedFrames.TryDequeue(out Frame frame)) | ||||
| { | { | ||||
| await _client.SetSpeakingAsync(true).ConfigureAwait(false); | await _client.SetSpeakingAsync(true).ConfigureAwait(false); | ||||
| _next.WriteHeader(seq++, timestamp, false); | |||||
| _next.WriteHeader(seq, timestamp, false); | |||||
| await _next.WriteAsync(frame.Buffer, 0, frame.Bytes).ConfigureAwait(false); | await _next.WriteAsync(frame.Buffer, 0, frame.Bytes).ConfigureAwait(false); | ||||
| _bufferPool.Enqueue(frame.Buffer); | _bufferPool.Enqueue(frame.Buffer); | ||||
| _queueLock.Release(); | _queueLock.Release(); | ||||
| nextTick += _ticksPerFrame; | nextTick += _ticksPerFrame; | ||||
| seq++; | |||||
| timestamp += OpusEncoder.FrameSamplesPerChannel; | timestamp += OpusEncoder.FrameSamplesPerChannel; | ||||
| _silenceFrames = 0; | _silenceFrames = 0; | ||||
| #if DEBUG | #if DEBUG | ||||
| @@ -105,12 +106,13 @@ namespace Discord.Audio.Streams | |||||
| { | { | ||||
| if (_silenceFrames++ < MaxSilenceFrames) | if (_silenceFrames++ < MaxSilenceFrames) | ||||
| { | { | ||||
| _next.WriteHeader(seq++, timestamp, false); | |||||
| _next.WriteHeader(seq, timestamp, false); | |||||
| await _next.WriteAsync(_silenceFrame, 0, _silenceFrame.Length).ConfigureAwait(false); | await _next.WriteAsync(_silenceFrame, 0, _silenceFrame.Length).ConfigureAwait(false); | ||||
| } | } | ||||
| else | else | ||||
| await _client.SetSpeakingAsync(false).ConfigureAwait(false); | await _client.SetSpeakingAsync(false).ConfigureAwait(false); | ||||
| nextTick += _ticksPerFrame; | nextTick += _ticksPerFrame; | ||||
| seq++; | |||||
| timestamp += OpusEncoder.FrameSamplesPerChannel; | timestamp += OpusEncoder.FrameSamplesPerChannel; | ||||
| } | } | ||||
| #if DEBUG | #if DEBUG | ||||
| @@ -126,6 +128,7 @@ namespace Discord.Audio.Streams | |||||
| }); | }); | ||||
| } | } | ||||
| public override void WriteHeader(ushort seq, uint timestamp, bool missed) { } //Ignore, we use our own timing | |||||
| public override async Task WriteAsync(byte[] data, int offset, int count, CancellationToken cancelToken) | public override async Task WriteAsync(byte[] data, int offset, int count, CancellationToken cancelToken) | ||||
| { | { | ||||
| if (cancelToken.CanBeCanceled) | if (cancelToken.CanBeCanceled) | ||||
| @@ -1,4 +1,4 @@ | |||||
| using Discord.Logging; | |||||
| /*using Discord.Logging; | |||||
| using System; | using System; | ||||
| using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
| using System.Threading; | using System.Threading; | ||||
| @@ -243,4 +243,4 @@ namespace Discord.Audio.Streams | |||||
| return Task.Delay(0); | return Task.Delay(0); | ||||
| } | } | ||||
| } | } | ||||
| } | |||||
| }*/ | |||||
| @@ -25,12 +25,13 @@ namespace Discord.Audio.Streams | |||||
| public override void WriteHeader(ushort seq, uint timestamp, bool missed) | public override void WriteHeader(ushort seq, uint timestamp, bool missed) | ||||
| { | { | ||||
| if (_hasHeader) | if (_hasHeader) | ||||
| throw new InvalidOperationException("Header received with no payload"); | |||||
| _nextMissed = missed; | |||||
| throw new InvalidOperationException("Header received with no payload"); | |||||
| _hasHeader = true; | _hasHeader = true; | ||||
| _nextMissed = missed; | |||||
| _next.WriteHeader(seq, timestamp, missed); | _next.WriteHeader(seq, timestamp, missed); | ||||
| } | } | ||||
| public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) | |||||
| public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) | |||||
| { | { | ||||
| if (!_hasHeader) | if (!_hasHeader) | ||||
| throw new InvalidOperationException("Received payload without an RTP header"); | throw new InvalidOperationException("Received payload without an RTP header"); | ||||
| @@ -39,17 +40,17 @@ namespace Discord.Audio.Streams | |||||
| if (!_nextMissed) | if (!_nextMissed) | ||||
| { | { | ||||
| count = _decoder.DecodeFrame(buffer, offset, count, _buffer, 0, false); | count = _decoder.DecodeFrame(buffer, offset, count, _buffer, 0, false); | ||||
| await _next.WriteAsync(_buffer, 0, count, cancellationToken).ConfigureAwait(false); | |||||
| await _next.WriteAsync(_buffer, 0, count, cancelToken).ConfigureAwait(false); | |||||
| } | } | ||||
| else if (count > 0) | else if (count > 0) | ||||
| { | { | ||||
| count = _decoder.DecodeFrame(buffer, offset, count, _buffer, 0, true); | |||||
| await _next.WriteAsync(_buffer, 0, count, cancellationToken).ConfigureAwait(false); | |||||
| count = _decoder.DecodeFrame(buffer, offset, count, _buffer, 0, true); | |||||
| await _next.WriteAsync(_buffer, 0, count, cancelToken).ConfigureAwait(false); | |||||
| } | } | ||||
| else | else | ||||
| { | { | ||||
| count = _decoder.DecodeFrame(null, 0, 0, _buffer, 0, true); | |||||
| await _next.WriteAsync(_buffer, 0, count, cancellationToken).ConfigureAwait(false); | |||||
| count = _decoder.DecodeFrame(null, 0, 0, _buffer, 0, true); | |||||
| await _next.WriteAsync(_buffer, 0, count, cancelToken).ConfigureAwait(false); | |||||
| } | } | ||||
| } | } | ||||
| @@ -8,20 +8,22 @@ namespace Discord.Audio.Streams | |||||
| public class OpusEncodeStream : AudioOutStream | public class OpusEncodeStream : AudioOutStream | ||||
| { | { | ||||
| public const int SampleRate = 48000; | public const int SampleRate = 48000; | ||||
| private readonly AudioStream _next; | private readonly AudioStream _next; | ||||
| private readonly OpusEncoder _encoder; | private readonly OpusEncoder _encoder; | ||||
| private readonly byte[] _buffer; | private readonly byte[] _buffer; | ||||
| private int _partialFramePos; | private int _partialFramePos; | ||||
| public OpusEncodeStream(AudioStream next, int bitrate, AudioApplication application) | |||||
| private ushort _seq; | |||||
| private uint _timestamp; | |||||
| public OpusEncodeStream(AudioStream next, int bitrate, AudioApplication application, int packetLoss) | |||||
| { | { | ||||
| _next = next; | _next = next; | ||||
| _encoder = new OpusEncoder(bitrate, application); | |||||
| _encoder = new OpusEncoder(bitrate, application, packetLoss); | |||||
| _buffer = new byte[OpusConverter.FrameBytes]; | _buffer = new byte[OpusConverter.FrameBytes]; | ||||
| } | } | ||||
| public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) | |||||
| public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) | |||||
| { | { | ||||
| //Assume threadsafe | //Assume threadsafe | ||||
| while (count > 0) | while (count > 0) | ||||
| @@ -30,10 +32,13 @@ namespace Discord.Audio.Streams | |||||
| { | { | ||||
| //We have enough data and no partial frames. Pass the buffer directly to the encoder | //We have enough data and no partial frames. Pass the buffer directly to the encoder | ||||
| int encFrameSize = _encoder.EncodeFrame(buffer, offset, _buffer, 0); | int encFrameSize = _encoder.EncodeFrame(buffer, offset, _buffer, 0); | ||||
| await _next.WriteAsync(_buffer, 0, encFrameSize, cancellationToken).ConfigureAwait(false); | |||||
| _next.WriteHeader(_seq, _timestamp, false); | |||||
| await _next.WriteAsync(_buffer, 0, encFrameSize, cancelToken).ConfigureAwait(false); | |||||
| offset += OpusConverter.FrameBytes; | offset += OpusConverter.FrameBytes; | ||||
| count -= OpusConverter.FrameBytes; | count -= OpusConverter.FrameBytes; | ||||
| _seq++; | |||||
| _timestamp += OpusConverter.FrameSamplesPerChannel; | |||||
| } | } | ||||
| else if (_partialFramePos + count >= OpusConverter.FrameBytes) | else if (_partialFramePos + count >= OpusConverter.FrameBytes) | ||||
| { | { | ||||
| @@ -41,11 +46,14 @@ namespace Discord.Audio.Streams | |||||
| int partialSize = OpusConverter.FrameBytes - _partialFramePos; | int partialSize = OpusConverter.FrameBytes - _partialFramePos; | ||||
| Buffer.BlockCopy(buffer, offset, _buffer, _partialFramePos, partialSize); | Buffer.BlockCopy(buffer, offset, _buffer, _partialFramePos, partialSize); | ||||
| int encFrameSize = _encoder.EncodeFrame(_buffer, 0, _buffer, 0); | int encFrameSize = _encoder.EncodeFrame(_buffer, 0, _buffer, 0); | ||||
| await _next.WriteAsync(_buffer, 0, encFrameSize, cancellationToken).ConfigureAwait(false); | |||||
| _next.WriteHeader(_seq, _timestamp, false); | |||||
| await _next.WriteAsync(_buffer, 0, encFrameSize, cancelToken).ConfigureAwait(false); | |||||
| offset += partialSize; | offset += partialSize; | ||||
| count -= partialSize; | count -= partialSize; | ||||
| _partialFramePos = 0; | _partialFramePos = 0; | ||||
| _seq++; | |||||
| _timestamp += OpusConverter.FrameSamplesPerChannel; | |||||
| } | } | ||||
| else | else | ||||
| { | { | ||||
| @@ -57,8 +65,8 @@ namespace Discord.Audio.Streams | |||||
| } | } | ||||
| } | } | ||||
| /* | |||||
| public override async Task FlushAsync(CancellationToken cancellationToken) | |||||
| /* //Opus throws memory errors on bad frames | |||||
| public override async Task FlushAsync(CancellationToken cancelToken) | |||||
| { | { | ||||
| try | try | ||||
| { | { | ||||
| @@ -67,7 +75,7 @@ namespace Discord.Audio.Streams | |||||
| } | } | ||||
| catch (Exception) { } //Incomplete frame | catch (Exception) { } //Incomplete frame | ||||
| _partialFramePos = 0; | _partialFramePos = 0; | ||||
| await base.FlushAsync(cancellationToken).ConfigureAwait(false); | |||||
| await base.FlushAsync(cancelToken).ConfigureAwait(false); | |||||
| }*/ | }*/ | ||||
| public override async Task FlushAsync(CancellationToken cancelToken) | public override async Task FlushAsync(CancellationToken cancelToken) | ||||