| @@ -1,7 +1,7 @@ | |||
| <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | |||
| <PropertyGroup> | |||
| <VersionPrefix>1.0.0</VersionPrefix> | |||
| <VersionSuffix>rc3</VersionSuffix> | |||
| <VersionPrefix>1.0.1</VersionPrefix> | |||
| <VersionSuffix></VersionSuffix> | |||
| <Authors>RogueException</Authors> | |||
| <PackageTags>discord;discordapp</PackageTags> | |||
| <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.myget.org/feed/Packages/discord-net) | |||
| [](https://ci.appveyor.com/project/RogueException/discord-net/branch/dev) | |||
| @@ -34,7 +34,7 @@ after_build: | |||
| if ($Env:APPVEYOR_REPO_TAG -eq "true") { | |||
| nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="" | |||
| } else { | |||
| nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="-$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 } | |||
| @@ -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 | |||
| module instance is only as long as the command being invoked. | |||
| **Avoid using long-running code** in your modules whereever possible. | |||
| **Avoid using long-running code** in your modules wherever possible. | |||
| You should **not** be implementing very much logic into your modules; | |||
| outsource to a service for that. | |||
| @@ -167,8 +167,8 @@ a dependency map. | |||
| Modules are constructed using Dependency Injection. Any parameters | |||
| that are placed in the constructor must be injected into an | |||
| @Discord.Commands.IDependencyMap. Alternatively, you may accept an | |||
| IDependencyMap as an argument and extract services yourself. | |||
| @System.IServiceProvider. Alternatively, you may accept an | |||
| IServiceProvider as an argument and extract services yourself. | |||
| ### Module Properties | |||
| @@ -205,21 +205,20 @@ you use DI when writing your modules. | |||
| ### Setup | |||
| First, you need to create an @Discord.Commands.IDependencyMap. | |||
| The library includes @Discord.Commands.DependencyMap to help with | |||
| this, however you may create your own IDependencyMap if you wish. | |||
| First, you need to create an @System.IServiceProvider | |||
| You may create your own IServiceProvider if you wish. | |||
| Next, add the dependencies your modules will use to the map. | |||
| Finally, pass the map into the `LoadAssembly` method. | |||
| Your modules will automatically be loaded with this dependency map. | |||
| [!code-csharp[DependencyMap Setup](samples/dependency_map_setup.cs)] | |||
| [!code-csharp[IServiceProvider Setup](samples/dependency_map_setup.cs)] | |||
| ### Usage in Modules | |||
| In the constructor of your module, any parameters will be filled in by | |||
| the @Discord.Commands.IDependencyMap you pass into `LoadAssembly`. | |||
| the @System.IServiceProvider you pass into `LoadAssembly`. | |||
| Any publicly settable properties will also be filled in the same manner. | |||
| @@ -228,12 +227,12 @@ Any publicly settable properties will also be filled in the same manner. | |||
| being injected. | |||
| >[!NOTE] | |||
| >If you accept `CommandService` or `IDependencyMap` as a parameter in | |||
| >If you accept `CommandService` or `IServiceProvider` as a parameter in | |||
| your constructor or as an injectable property, these entries will be filled | |||
| by the CommandService the module was loaded from, and the DependencyMap passed | |||
| by the CommandService the module was loaded from, and the ServiceProvider passed | |||
| into it, respectively. | |||
| [!code-csharp[DependencyMap in Modules](samples/dependency_module.cs)] | |||
| [!code-csharp[ServiceProvider in Modules](samples/dependency_module.cs)] | |||
| # Preconditions | |||
| @@ -1,14 +1,16 @@ | |||
| using System; | |||
| using System.Threading.Tasks; | |||
| using System.Reflection; | |||
| using Discord; | |||
| using Discord.WebSocket; | |||
| using Discord.Commands; | |||
| using Microsoft.Extensions.DependencyInjection; | |||
| public class Program | |||
| { | |||
| private CommandService commands; | |||
| private DiscordSocketClient client; | |||
| private DependencyMap map; | |||
| private IServiceProvider services; | |||
| static void Main(string[] args) => new Program().Start().GetAwaiter().GetResult(); | |||
| @@ -19,38 +21,40 @@ public class Program | |||
| string token = "bot token here"; | |||
| map = new DependencyMap(); | |||
| services = new ServiceCollection() | |||
| .BuildServiceProvider(); | |||
| await InstallCommands(); | |||
| await client.LoginAsync(TokenType.Bot, token); | |||
| await client.ConnectAsync(); | |||
| await client.StartAsync(); | |||
| await Task.Delay(-1); | |||
| } | |||
| public async Task InstallCommands() | |||
| { | |||
| // Hook the MessageReceived Event into our Command Handler | |||
| client.MessageReceived += HandleCommand; | |||
| // Discover all of the commands in this assembly and load them. | |||
| // Discover all of the commands in this assembly and load them. | |||
| await commands.AddModulesAsync(Assembly.GetEntryAssembly()); | |||
| } | |||
| public async Task HandleCommand(SocketMessage messageParam) | |||
| { | |||
| { | |||
| // Don't process the command if it was a System Message | |||
| var message = messageParam as SocketUserMessage; | |||
| if (message == null) return; | |||
| // Create a number to track where the prefix ends and the command begins | |||
| int argPos = 0; | |||
| // Determine if the message is a command, based on if it starts with '!' or a mention prefix | |||
| if (!(message.HasCharPrefix('!', ref argPos) || message.HasMentionPrefix(client.CurrentUser, ref argPos))) return; | |||
| // Create a number to track where the prefix ends and the command begins | |||
| int argPos = 0; | |||
| // Determine if the message is a command, based on if it starts with '!' or a mention prefix | |||
| if (!(message.HasCharPrefix('!', ref argPos) || message.HasMentionPrefix(client.CurrentUser, ref argPos))) return; | |||
| // Create a Command Context | |||
| var context = new CommandContext(client, message); | |||
| // Execute the command. (result does not indicate a return value, | |||
| // rather an object stating if the command executed succesfully) | |||
| var result = await commands.ExecuteAsync(context, argPos, map); | |||
| // rather an object stating if the command executed successfully) | |||
| var result = await commands.ExecuteAsync(context, argPos, service); | |||
| if (!result.IsSuccess) | |||
| await context.Channel.SendMessageAsync(result.ErrorReason); | |||
| } | |||
| } | |||
| } | |||
| @@ -7,12 +7,11 @@ public class Commands | |||
| { | |||
| public async Task Install(DiscordSocketClient client) | |||
| { | |||
| // Here, we will inject the Dependency Map with | |||
| // Here, we will inject the ServiceProvider with | |||
| // all of the services our client will use. | |||
| _map.Add(client); | |||
| _map.Add(commands); | |||
| _map.Add(new NotificationService(_map)); | |||
| _map.Add(new DatabaseService(_map)); | |||
| _serviceCollection.AddSingleton(client) | |||
| _serviceCollection.AddSingleton(new NotificationService()) | |||
| _serviceCollection.AddSingleton(new DatabaseService()) | |||
| // ... | |||
| await _commands.AddModulesAsync(Assembly.GetEntryAssembly()); | |||
| } | |||
| @@ -2,16 +2,18 @@ | |||
| using Discord.Commands; | |||
| using Discord.WebSocket; | |||
| using Microsoft.Extensions.DependencyInjection; | |||
| using System; | |||
| using System.Threading.Tasks; | |||
| // Inherit from PreconditionAttribute | |||
| public class RequireOwnerAttribute : PreconditionAttribute | |||
| { | |||
| // Override the CheckPermissions method | |||
| public async override Task<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 | |||
| 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 (context.User.Id == ownerId) | |||
| return PreconditionResult.FromSuccess(); | |||
| @@ -8,7 +8,11 @@ public class Program | |||
| 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.StartAsync(); | |||
| @@ -25,7 +29,8 @@ public class Program | |||
| 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(); | |||
| Console.WriteLine($"{message} -> {after}"); | |||
| } | |||
| } | |||
| } | |||
| @@ -211,7 +211,7 @@ For your reference, you may view the [completed program]. | |||
| # Building a bot with commands | |||
| 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 | |||
| structure. | |||
| @@ -224,4 +224,4 @@ should be to separate the program (initialization and command handler), | |||
| the modules (handle commands), and the services (persistent storage, | |||
| pure functions, data manipulation). | |||
| **todo:** diagram of bot structure | |||
| **todo:** diagram of bot structure | |||
| @@ -30,8 +30,8 @@ class Program | |||
| LogLevel = LogSeverity.Info, | |||
| // If you or another service needs to do anything with messages | |||
| // (eg. checking Reactions), you should probably | |||
| // set the MessageCacheSize here. | |||
| // (eg. checking Reactions, checking the content of edited/deleted messages), | |||
| // you must set the MessageCacheSize. You may adjust the number as needed. | |||
| //MessageCacheSize = 50, | |||
| // If your platform doesn't have native websockets, | |||
| @@ -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>. | |||
| 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.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; | |||
| } | |||
| @@ -92,16 +99,17 @@ class Program | |||
| // and other dependencies that your commands might need. | |||
| _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. | |||
| // Tip: There's an overload taking in a 'validateScopes' bool to make sure | |||
| // you haven't made any mistakes in your dependency graph. | |||
| _services = _map.BuildServiceProvider(); | |||
| // Either search the program and add all Module classes that can be found. | |||
| // 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. | |||
| _client.MessageReceived += HandleCommandAsync; | |||
| } | |||
| @@ -120,7 +128,7 @@ class Program | |||
| // commands to be invoked by mentioning the bot instead. | |||
| 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); | |||
| // 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) | |||
| To hook an event into MessageReceived, we now use the following code: | |||
| [!code-csharp[Event Registration](guides/samples/migrating/event.cs)] | |||
| [!code-csharp[Event Registration](samples/event.cs)] | |||
| > **All Event Handlers in 1.0 MUST return Task!** | |||
| @@ -50,7 +50,7 @@ If your event handler is marked as `async`, it will automatically return `Task`. | |||
| if you do not need to execute asynchronus code, do _not_ mark your handler as `async`, and instead, | |||
| stick a `return Task.CompletedTask` at the bottom. | |||
| [!code-csharp[Sync Event Registration](guides/samples/migrating/sync_event.cs)] | |||
| [!code-csharp[Sync Event Registration](samples/sync_event.cs)] | |||
| **Event handlers no longer require a sender.** The only arguments your event handler needs to accept | |||
| are the parameters used by the event. It is recommended to look at the event in IntelliSense or on the | |||
| @@ -3,7 +3,7 @@ private async Task SendAsync(IAudioClient client, string path) | |||
| // Create FFmpeg using the previous example | |||
| var ffmpeg = CreateStream(path); | |||
| var output = ffmpeg.StandardOutput.BaseStream; | |||
| var discord = client.CreatePCMStream(AudioApplication.Mixed, 1920); | |||
| var discord = client.CreatePCMStream(AudioApplication.Mixed); | |||
| await output.CopyToAsync(discord); | |||
| await discord.FlushAsync(); | |||
| } | |||
| @@ -6,6 +6,13 @@ namespace Discord.Commands | |||
| [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = true)] | |||
| 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); | |||
| } | |||
| } | |||
| @@ -44,14 +44,16 @@ namespace Discord.Commands | |||
| 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 (guildUser == null) | |||
| return PreconditionResult.FromError("Command must be used in a guild channel"); | |||
| if (!guildUser.GuildPermissions.Has(GuildPermission.Value)) | |||
| return PreconditionResult.FromError($"Command requires guild permission {GuildPermission.Value}"); | |||
| return PreconditionResult.FromError($"Bot requires guild permission {GuildPermission.Value}"); | |||
| } | |||
| if (ChannelPermission.HasValue) | |||
| @@ -65,7 +67,7 @@ namespace Discord.Commands | |||
| perms = ChannelPermissions.All(guildChannel); | |||
| if (!perms.Has(ChannelPermission.Value)) | |||
| return PreconditionResult.FromError($"Command requires channel permission {ChannelPermission.Value}"); | |||
| return PreconditionResult.FromError($"Bot requires channel permission {ChannelPermission.Value}"); | |||
| } | |||
| return PreconditionResult.FromSuccess(); | |||
| @@ -52,7 +52,7 @@ namespace Discord.Commands | |||
| if (guildUser == null) | |||
| return Task.FromResult(PreconditionResult.FromError("Command must be used in a guild channel")); | |||
| if (!guildUser.GuildPermissions.Has(GuildPermission.Value)) | |||
| return Task.FromResult(PreconditionResult.FromError($"Command requires guild permission {GuildPermission.Value}")); | |||
| return Task.FromResult(PreconditionResult.FromError($"User requires guild permission {GuildPermission.Value}")); | |||
| } | |||
| if (ChannelPermission.HasValue) | |||
| @@ -66,7 +66,7 @@ namespace Discord.Commands | |||
| perms = ChannelPermissions.All(guildChannel); | |||
| if (!perms.Has(ChannelPermission.Value)) | |||
| return Task.FromResult(PreconditionResult.FromError($"Command requires channel permission {ChannelPermission.Value}")); | |||
| return Task.FromResult(PreconditionResult.FromError($"User requires channel permission {ChannelPermission.Value}")); | |||
| } | |||
| return Task.FromResult(PreconditionResult.FromSuccess()); | |||
| @@ -10,10 +10,11 @@ namespace Discord.Commands.Builders | |||
| { | |||
| private readonly List<PreconditionAttribute> _preconditions; | |||
| private readonly List<ParameterBuilder> _parameters; | |||
| private readonly List<Attribute> _attributes; | |||
| private readonly List<string> _aliases; | |||
| 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 Summary { get; set; } | |||
| @@ -24,6 +25,7 @@ namespace Discord.Commands.Builders | |||
| public IReadOnlyList<PreconditionAttribute> Preconditions => _preconditions; | |||
| public IReadOnlyList<ParameterBuilder> Parameters => _parameters; | |||
| public IReadOnlyList<Attribute> Attributes => _attributes; | |||
| public IReadOnlyList<string> Aliases => _aliases; | |||
| //Automatic | |||
| @@ -33,10 +35,11 @@ namespace Discord.Commands.Builders | |||
| _preconditions = new List<PreconditionAttribute>(); | |||
| _parameters = new List<ParameterBuilder>(); | |||
| _attributes = new List<Attribute>(); | |||
| _aliases = new List<string>(); | |||
| } | |||
| //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) | |||
| { | |||
| Discord.Preconditions.NotNull(primaryAlias, nameof(primaryAlias)); | |||
| @@ -77,12 +80,17 @@ namespace Discord.Commands.Builders | |||
| { | |||
| for (int i = 0; i < aliases.Length; i++) | |||
| { | |||
| var alias = aliases[i] ?? ""; | |||
| string alias = aliases[i] ?? ""; | |||
| if (!_aliases.Contains(alias)) | |||
| _aliases.Add(alias); | |||
| } | |||
| return this; | |||
| } | |||
| public CommandBuilder AddAttributes(params Attribute[] attributes) | |||
| { | |||
| _attributes.AddRange(attributes); | |||
| return this; | |||
| } | |||
| public CommandBuilder AddPrecondition(PreconditionAttribute precondition) | |||
| { | |||
| _preconditions.Add(precondition); | |||
| @@ -122,11 +130,11 @@ namespace Discord.Commands.Builders | |||
| var firstMultipleParam = _parameters.FirstOrDefault(x => x.IsMultiple); | |||
| if ((firstMultipleParam != null) && (firstMultipleParam != lastParam)) | |||
| throw new InvalidOperationException("Only the last parameter in a command may have the Multiple flag."); | |||
| throw new InvalidOperationException($"Only the last parameter in a command may have the Multiple flag. Parameter: {firstMultipleParam.Name} in {PrimaryAlias}"); | |||
| var firstRemainderParam = _parameters.FirstOrDefault(x => x.IsRemainder); | |||
| if ((firstRemainderParam != null) && (firstRemainderParam != lastParam)) | |||
| throw new InvalidOperationException("Only the last parameter in a command may have the Remainder flag."); | |||
| throw new InvalidOperationException($"Only the last parameter in a command may have the Remainder flag. Parameter: {firstRemainderParam.Name} in {PrimaryAlias}"); | |||
| } | |||
| return new CommandInfo(this, info, service); | |||
| @@ -10,6 +10,7 @@ namespace Discord.Commands.Builders | |||
| private readonly List<CommandBuilder> _commands; | |||
| private readonly List<ModuleBuilder> _submodules; | |||
| private readonly List<PreconditionAttribute> _preconditions; | |||
| private readonly List<Attribute> _attributes; | |||
| private readonly List<string> _aliases; | |||
| public CommandService Service { get; } | |||
| @@ -21,6 +22,7 @@ namespace Discord.Commands.Builders | |||
| public IReadOnlyList<CommandBuilder> Commands => _commands; | |||
| public IReadOnlyList<ModuleBuilder> Modules => _submodules; | |||
| public IReadOnlyList<PreconditionAttribute> Preconditions => _preconditions; | |||
| public IReadOnlyList<Attribute> Attributes => _attributes; | |||
| public IReadOnlyList<string> Aliases => _aliases; | |||
| //Automatic | |||
| @@ -32,6 +34,7 @@ namespace Discord.Commands.Builders | |||
| _commands = new List<CommandBuilder>(); | |||
| _submodules = new List<ModuleBuilder>(); | |||
| _preconditions = new List<PreconditionAttribute>(); | |||
| _attributes = new List<Attribute>(); | |||
| _aliases = new List<string>(); | |||
| } | |||
| //User-defined | |||
| @@ -63,18 +66,23 @@ namespace Discord.Commands.Builders | |||
| { | |||
| for (int i = 0; i < aliases.Length; i++) | |||
| { | |||
| var alias = aliases[i] ?? ""; | |||
| string alias = aliases[i] ?? ""; | |||
| if (!_aliases.Contains(alias)) | |||
| _aliases.Add(alias); | |||
| } | |||
| return this; | |||
| } | |||
| public ModuleBuilder AddAttributes(params Attribute[] attributes) | |||
| { | |||
| _attributes.AddRange(attributes); | |||
| return this; | |||
| } | |||
| public ModuleBuilder AddPrecondition(PreconditionAttribute precondition) | |||
| { | |||
| _preconditions.Add(precondition); | |||
| return this; | |||
| } | |||
| public ModuleBuilder AddCommand(string primaryAlias, Func<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); | |||
| createFunc(builder); | |||
| @@ -12,25 +12,42 @@ namespace Discord.Commands | |||
| { | |||
| 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()) | |||
| throw new InvalidOperationException("Could not find any valid modules from the given selection");*/ | |||
| var topLevelGroups = validTypes.Where(x => x.DeclaringType == null); | |||
| var subGroups = validTypes.Intersect(topLevelGroups); | |||
| @@ -48,10 +65,13 @@ namespace Discord.Commands | |||
| BuildModule(module, typeInfo, service); | |||
| BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service); | |||
| builtTypes.Add(typeInfo); | |||
| result[typeInfo.AsType()] = module.Build(service); | |||
| } | |||
| await service._cmdLogger.DebugAsync($"Successfully built {builtTypes.Count} modules.").ConfigureAwait(false); | |||
| return result; | |||
| } | |||
| @@ -102,6 +122,9 @@ namespace Discord.Commands | |||
| case PreconditionAttribute precondition: | |||
| builder.AddPrecondition(precondition); | |||
| break; | |||
| default: | |||
| builder.AddAttributes(attribute); | |||
| break; | |||
| } | |||
| } | |||
| @@ -128,26 +151,35 @@ namespace Discord.Commands | |||
| foreach (var attribute in attributes) | |||
| { | |||
| // TODO: C#7 type switch | |||
| if (attribute is CommandAttribute) | |||
| switch (attribute) | |||
| { | |||
| var cmdAttr = attribute as CommandAttribute; | |||
| builder.AddAliases(cmdAttr.Text); | |||
| builder.RunMode = cmdAttr.RunMode; | |||
| builder.Name = builder.Name ?? cmdAttr.Text; | |||
| case CommandAttribute command: | |||
| builder.AddAliases(command.Text); | |||
| builder.RunMode = command.RunMode; | |||
| builder.Name = builder.Name ?? command.Text; | |||
| break; | |||
| case NameAttribute name: | |||
| builder.Name = name.Text; | |||
| break; | |||
| case PriorityAttribute priority: | |||
| builder.Priority = priority.Priority; | |||
| break; | |||
| case SummaryAttribute summary: | |||
| builder.Summary = summary.Text; | |||
| break; | |||
| case RemarksAttribute remarks: | |||
| builder.Remarks = remarks.Text; | |||
| break; | |||
| case AliasAttribute alias: | |||
| builder.AddAliases(alias.Aliases); | |||
| break; | |||
| case PreconditionAttribute precondition: | |||
| builder.AddPrecondition(precondition); | |||
| break; | |||
| default: | |||
| builder.AddAttributes(attribute); | |||
| break; | |||
| } | |||
| else if (attribute is NameAttribute) | |||
| builder.Name = (attribute as NameAttribute).Text; | |||
| else if (attribute is PriorityAttribute) | |||
| builder.Priority = (attribute as PriorityAttribute).Priority; | |||
| else if (attribute is SummaryAttribute) | |||
| builder.Summary = (attribute as SummaryAttribute).Text; | |||
| else if (attribute is RemarksAttribute) | |||
| builder.Remarks = (attribute as RemarksAttribute).Text; | |||
| else if (attribute is AliasAttribute) | |||
| builder.AddAliases((attribute as AliasAttribute).Aliases); | |||
| else if (attribute is PreconditionAttribute) | |||
| builder.AddPrecondition(attribute as PreconditionAttribute); | |||
| } | |||
| if (builder.Name == null) | |||
| @@ -165,22 +197,34 @@ namespace Discord.Commands | |||
| 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 | |||
| { | |||
| instance.BeforeExecute(); | |||
| instance.BeforeExecute(cmd); | |||
| 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 | |||
| { | |||
| instance.AfterExecute(); | |||
| instance.AfterExecute(cmd); | |||
| (instance as IDisposable)?.Dispose(); | |||
| } | |||
| }; | |||
| } | |||
| builder.Callback = ExecuteCallback; | |||
| } | |||
| private static void BuildParameter(ParameterBuilder builder, System.Reflection.ParameterInfo paramInfo, int position, int count, CommandService service) | |||
| @@ -195,24 +239,30 @@ namespace Discord.Commands | |||
| foreach (var attribute in attributes) | |||
| { | |||
| // TODO: C#7 type switch | |||
| if (attribute is SummaryAttribute) | |||
| builder.Summary = (attribute as SummaryAttribute).Text; | |||
| else if (attribute is OverrideTypeReaderAttribute) | |||
| builder.TypeReader = GetTypeReader(service, paramType, (attribute as OverrideTypeReaderAttribute).TypeReader); | |||
| else if (attribute is ParameterPreconditionAttribute) | |||
| builder.AddPrecondition(attribute as ParameterPreconditionAttribute); | |||
| else if (attribute is ParamArrayAttribute) | |||
| { | |||
| builder.IsMultiple = true; | |||
| paramType = paramType.GetElementType(); | |||
| } | |||
| else if (attribute is RemainderAttribute) | |||
| switch (attribute) | |||
| { | |||
| if (position != count-1) | |||
| throw new InvalidOperationException("Remainder parameters must be the last parameter in a command."); | |||
| builder.IsRemainder = true; | |||
| case SummaryAttribute summary: | |||
| builder.Summary = summary.Text; | |||
| break; | |||
| case OverrideTypeReaderAttribute typeReader: | |||
| builder.TypeReader = GetTypeReader(service, paramType, typeReader.TypeReader); | |||
| break; | |||
| case ParamArrayAttribute _: | |||
| builder.IsMultiple = true; | |||
| paramType = paramType.GetElementType(); | |||
| break; | |||
| case ParameterPreconditionAttribute precon: | |||
| builder.AddPrecondition(precon); | |||
| break; | |||
| case RemainderAttribute _: | |||
| if (position != count - 1) | |||
| throw new InvalidOperationException($"Remainder parameters must be the last parameter in a command. Parameter: {paramInfo.Name} in {paramInfo.Member.DeclaringType.Name}.{paramInfo.Member.Name}"); | |||
| builder.IsRemainder = true; | |||
| break; | |||
| default: | |||
| builder.AddAttributes(attribute); | |||
| break; | |||
| } | |||
| } | |||
| @@ -258,9 +308,9 @@ namespace Discord.Commands | |||
| private static bool IsValidCommandDefinition(MethodInfo methodInfo) | |||
| { | |||
| 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.IsGenericMethod; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -8,7 +8,8 @@ namespace Discord.Commands.Builders | |||
| { | |||
| public class ParameterBuilder | |||
| { | |||
| private readonly List<ParameterPreconditionAttribute> _preconditions; | |||
| private readonly List<ParameterPreconditionAttribute> _preconditions; | |||
| private readonly List<Attribute> _attributes; | |||
| public CommandBuilder Command { get; } | |||
| public string Name { get; internal set; } | |||
| @@ -22,11 +23,13 @@ namespace Discord.Commands.Builders | |||
| public string Summary { get; set; } | |||
| public IReadOnlyList<ParameterPreconditionAttribute> Preconditions => _preconditions; | |||
| public IReadOnlyList<Attribute> Attributes => _attributes; | |||
| //Automatic | |||
| internal ParameterBuilder(CommandBuilder command) | |||
| { | |||
| _preconditions = new List<ParameterPreconditionAttribute>(); | |||
| _attributes = new List<Attribute>(); | |||
| Command = command; | |||
| } | |||
| @@ -49,7 +52,7 @@ namespace Discord.Commands.Builders | |||
| TypeReader = Command.Module.Service.GetDefaultTypeReader(type); | |||
| if (TypeReader == null) | |||
| throw new InvalidOperationException($"{type} does not have a TypeReader registered for it"); | |||
| throw new InvalidOperationException($"{type} does not have a TypeReader registered for it. Parameter: {Name} in {Command.PrimaryAlias}"); | |||
| if (type.GetTypeInfo().IsValueType) | |||
| DefaultValue = Activator.CreateInstance(type); | |||
| @@ -84,6 +87,11 @@ namespace Discord.Commands.Builders | |||
| return this; | |||
| } | |||
| public ParameterBuilder AddAttributes(params Attribute[] attributes) | |||
| { | |||
| _attributes.AddRange(attributes); | |||
| return this; | |||
| } | |||
| public ParameterBuilder AddPrecondition(ParameterPreconditionAttribute precondition) | |||
| { | |||
| _preconditions.Add(precondition); | |||
| @@ -18,6 +18,9 @@ | |||
| UnmetPrecondition, | |||
| //Execute | |||
| Exception | |||
| Exception, | |||
| //Runtime | |||
| Unsuccessful | |||
| } | |||
| } | |||
| @@ -18,11 +18,11 @@ namespace Discord.Commands | |||
| public Task<PreconditionResult> CheckPreconditionsAsync(ICommandContext context, IServiceProvider services = null) | |||
| => 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); | |||
| 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); | |||
| } | |||
| } | |||
| @@ -1,4 +1,5 @@ | |||
| using System.Collections.Immutable; | |||
| using System; | |||
| using System.Collections.Immutable; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| @@ -13,7 +14,7 @@ namespace Discord.Commands | |||
| QuotedParameter | |||
| } | |||
| public static async Task<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; | |||
| StringBuilder argBuilder = new StringBuilder(input.Length); | |||
| @@ -110,7 +111,7 @@ namespace Discord.Commands | |||
| if (curParam == null) | |||
| return ParseResult.FromError(CommandError.BadArgCount, "The input text has too many parameters."); | |||
| var typeReaderResult = await curParam.Parse(context, argString).ConfigureAwait(false); | |||
| var typeReaderResult = await curParam.Parse(context, argString, services).ConfigureAwait(false); | |||
| if (!typeReaderResult.IsSuccess && typeReaderResult.Error != CommandError.MultipleMatches) | |||
| return ParseResult.FromError(typeReaderResult); | |||
| @@ -133,7 +134,7 @@ namespace Discord.Commands | |||
| if (curParam != null && curParam.IsRemainder) | |||
| { | |||
| var typeReaderResult = await curParam.Parse(context, argBuilder.ToString()).ConfigureAwait(false); | |||
| var typeReaderResult = await curParam.Parse(context, argBuilder.ToString(), services).ConfigureAwait(false); | |||
| if (!typeReaderResult.IsSuccess) | |||
| return ParseResult.FromError(typeReaderResult); | |||
| argList.Add(typeReaderResult); | |||
| @@ -33,7 +33,7 @@ namespace Discord.Commands | |||
| public IEnumerable<ModuleInfo> Modules => _moduleDefs.Select(x => x); | |||
| 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(CommandServiceConfig config) | |||
| @@ -59,6 +59,9 @@ namespace Discord.Commands | |||
| foreach (var type in PrimitiveParsers.SupportedTypes) | |||
| _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>>(); | |||
| entityTypeReaders.Add(new Tuple<Type, Type>(typeof(IMessage), typeof(MessageTypeReader<>))); | |||
| entityTypeReaders.Add(new Tuple<Type, Type>(typeof(IChannel), typeof(ChannelTypeReader<>))); | |||
| @@ -95,7 +98,7 @@ namespace Discord.Commands | |||
| if (_typedModuleDefs.ContainsKey(type)) | |||
| throw new ArgumentException($"This module has already been added."); | |||
| var module = ModuleClassBuilder.Build(this, typeInfo).FirstOrDefault(); | |||
| var module = (await ModuleClassBuilder.BuildAsync(this, typeInfo).ConfigureAwait(false)).FirstOrDefault(); | |||
| if (module.Value == default(ModuleInfo)) | |||
| throw new InvalidOperationException($"Could not build the module {type.FullName}, did you pass an invalid type?"); | |||
| @@ -114,8 +117,8 @@ namespace Discord.Commands | |||
| await _moduleLock.WaitAsync().ConfigureAwait(false); | |||
| try | |||
| { | |||
| var types = ModuleClassBuilder.Search(assembly).ToArray(); | |||
| var moduleDefs = ModuleClassBuilder.Build(types, this); | |||
| var types = await ModuleClassBuilder.SearchAsync(assembly, this).ConfigureAwait(false); | |||
| var moduleDefs = await ModuleClassBuilder.BuildAsync(types, this).ConfigureAwait(false); | |||
| foreach (var info in moduleDefs) | |||
| { | |||
| @@ -161,8 +164,7 @@ namespace Discord.Commands | |||
| await _moduleLock.WaitAsync().ConfigureAwait(false); | |||
| try | |||
| { | |||
| ModuleInfo module; | |||
| if (!_typedModuleDefs.TryRemove(type, out module)) | |||
| if (!_typedModuleDefs.TryRemove(type, out var module)) | |||
| return false; | |||
| return RemoveModuleInternal(module); | |||
| @@ -196,20 +198,18 @@ namespace Discord.Commands | |||
| } | |||
| 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; | |||
| } | |||
| 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 null; | |||
| } | |||
| internal TypeReader GetDefaultTypeReader(Type type) | |||
| { | |||
| TypeReader reader; | |||
| if (_defaultTypeReaders.TryGetValue(type, out reader)) | |||
| if (_defaultTypeReaders.TryGetValue(type, out var reader)) | |||
| return reader; | |||
| var typeInfo = type.GetTypeInfo(); | |||
| @@ -235,13 +235,13 @@ namespace Discord.Commands | |||
| } | |||
| //Execution | |||
| public SearchResult Search(ICommandContext context, int argPos) | |||
| public SearchResult Search(ICommandContext context, int argPos) | |||
| => Search(context, context.Message.Content.Substring(argPos)); | |||
| public SearchResult Search(ICommandContext context, string input) | |||
| { | |||
| string searchInput = _caseSensitive ? input : input.ToLowerInvariant(); | |||
| var matches = _map.GetCommands(searchInput).OrderByDescending(x => x.Command.Priority).ToImmutableArray(); | |||
| if (matches.Length > 0) | |||
| return SearchResult.FromSuccess(input, matches); | |||
| else | |||
| @@ -259,46 +259,86 @@ namespace Discord.Commands | |||
| return searchResult; | |||
| 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 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 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 string Name { get; } | |||
| @@ -31,18 +31,19 @@ namespace Discord.Commands | |||
| public IReadOnlyList<string> Aliases { get; } | |||
| public IReadOnlyList<ParameterInfo> Parameters { get; } | |||
| public IReadOnlyList<PreconditionAttribute> Preconditions { get; } | |||
| public IReadOnlyList<Attribute> Attributes { get; } | |||
| internal CommandInfo(CommandBuilder builder, ModuleInfo module, CommandService service) | |||
| { | |||
| Module = module; | |||
| Name = builder.Name; | |||
| Summary = builder.Summary; | |||
| Remarks = builder.Remarks; | |||
| RunMode = (builder.RunMode == RunMode.Default ? service._defaultRunMode : builder.RunMode); | |||
| Priority = builder.Priority; | |||
| Aliases = module.Aliases | |||
| .Permutate(builder.Aliases, (first, second) => | |||
| { | |||
| @@ -57,6 +58,7 @@ namespace Discord.Commands | |||
| .ToImmutableArray(); | |||
| Preconditions = builder.Preconditions.ToImmutableArray(); | |||
| Attributes = builder.Attributes.ToImmutableArray(); | |||
| Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray(); | |||
| HasVarArgs = builder.Parameters.Count > 0 ? builder.Parameters[builder.Parameters.Count - 1].IsMultiple : false; | |||
| @@ -68,58 +70,80 @@ namespace Discord.Commands | |||
| { | |||
| 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(); | |||
| } | |||
| 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) | |||
| return ParseResult.FromError(searchResult); | |||
| if (preconditionResult != null && !preconditionResult.Value.IsSuccess) | |||
| return ParseResult.FromError(preconditionResult.Value); | |||
| if (preconditionResult != null && !preconditionResult.IsSuccess) | |||
| return ParseResult.FromError(preconditionResult); | |||
| string input = searchResult.Text.Substring(startIndex); | |||
| return await CommandParser.ParseArgs(this, context, input, 0).ConfigureAwait(false); | |||
| return await CommandParser.ParseArgs(this, context, services, input, 0).ConfigureAwait(false); | |||
| } | |||
| public Task<ExecuteResult> ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) | |||
| public Task<IResult> ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) | |||
| { | |||
| if (!parseResult.IsSuccess) | |||
| return Task.FromResult(ExecuteResult.FromError(parseResult)); | |||
| return Task.FromResult((IResult)ExecuteResult.FromError(parseResult)); | |||
| var argList = new object[parseResult.ArgValues.Count]; | |||
| for (int i = 0; i < parseResult.ArgValues.Count; i++) | |||
| { | |||
| if (!parseResult.ArgValues[i].IsSuccess) | |||
| return Task.FromResult(ExecuteResult.FromError(parseResult.ArgValues[i])); | |||
| return Task.FromResult((IResult)ExecuteResult.FromError(parseResult.ArgValues[i])); | |||
| argList[i] = parseResult.ArgValues[i].Values.First().Value; | |||
| } | |||
| var paramList = new object[parseResult.ParamValues.Count]; | |||
| for (int i = 0; i < parseResult.ParamValues.Count; i++) | |||
| { | |||
| if (!parseResult.ParamValues[i].IsSuccess) | |||
| return Task.FromResult(ExecuteResult.FromError(parseResult.ParamValues[i])); | |||
| return Task.FromResult((IResult)ExecuteResult.FromError(parseResult.ParamValues[i])); | |||
| paramList[i] = parseResult.ParamValues[i].Values.First().Value; | |||
| } | |||
| return ExecuteAsync(context, argList, paramList, 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; | |||
| @@ -130,7 +154,7 @@ namespace Discord.Commands | |||
| for (int position = 0; position < Parameters.Count; position++) | |||
| { | |||
| var parameter = Parameters[position]; | |||
| var argument = args[position]; | |||
| object argument = args[position]; | |||
| var result = await parameter.CheckPreconditionsAsync(context, argument, services).ConfigureAwait(false); | |||
| if (!result.IsSuccess) | |||
| return ExecuteResult.FromError(result); | |||
| @@ -139,10 +163,9 @@ namespace Discord.Commands | |||
| switch (RunMode) | |||
| { | |||
| 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 | |||
| var t2 = Task.Run(async () => | |||
| var t2 = Task.Run(async () => | |||
| { | |||
| 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); | |||
| 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) | |||
| { | |||
| @@ -178,8 +215,13 @@ namespace Discord.Commands | |||
| else | |||
| 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) | |||
| @@ -190,7 +232,7 @@ namespace Discord.Commands | |||
| argCount--; | |||
| int i = 0; | |||
| foreach (var arg in argList) | |||
| foreach (object arg in argList) | |||
| { | |||
| if (i == argCount) | |||
| throw new InvalidOperationException("Command was invoked with too many parameters"); | |||
| @@ -216,11 +258,11 @@ namespace Discord.Commands | |||
| => paramsList.Cast<T>().ToArray(); | |||
| internal string GetLogText(ICommandContext context) | |||
| { | |||
| { | |||
| if (context.Guild != null) | |||
| return $"\"{Name}\" for {context.User} in {context.Guild}/{context.Channel}"; | |||
| else | |||
| return $"\"{Name}\" for {context.User} in {context.Channel}"; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -1,3 +1,4 @@ | |||
| using System; | |||
| using System.Linq; | |||
| using System.Collections.Generic; | |||
| using System.Collections.Immutable; | |||
| @@ -16,6 +17,7 @@ namespace Discord.Commands | |||
| public IReadOnlyList<string> Aliases { get; } | |||
| public IReadOnlyList<CommandInfo> Commands { get; } | |||
| public IReadOnlyList<PreconditionAttribute> Preconditions { get; } | |||
| public IReadOnlyList<Attribute> Attributes { get; } | |||
| public IReadOnlyList<ModuleInfo> Submodules { get; } | |||
| public ModuleInfo Parent { get; } | |||
| public bool IsSubmodule => Parent != null; | |||
| @@ -32,6 +34,7 @@ namespace Discord.Commands | |||
| Aliases = BuildAliases(builder, service).ToImmutableArray(); | |||
| Commands = builder.Commands.Select(x => x.Build(this, service)).ToImmutableArray(); | |||
| Preconditions = BuildPreconditions(builder).ToImmutableArray(); | |||
| Attributes = BuildAttributes(builder).ToImmutableArray(); | |||
| Submodules = BuildSubmodules(builder, service).ToImmutableArray(); | |||
| } | |||
| @@ -86,5 +89,19 @@ namespace Discord.Commands | |||
| return result; | |||
| } | |||
| private static List<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 IReadOnlyList<ParameterPreconditionAttribute> Preconditions { get; } | |||
| public IReadOnlyList<Attribute> Attributes { get; } | |||
| internal ParameterInfo(ParameterBuilder builder, CommandInfo command, CommandService service) | |||
| { | |||
| @@ -36,6 +37,7 @@ namespace Discord.Commands | |||
| DefaultValue = builder.DefaultValue; | |||
| Preconditions = builder.Preconditions.ToImmutableArray(); | |||
| Attributes = builder.Attributes.ToImmutableArray(); | |||
| _reader = builder.TypeReader; | |||
| } | |||
| @@ -54,9 +56,10 @@ namespace Discord.Commands | |||
| 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; | |||
| @@ -15,11 +15,11 @@ namespace Discord.Commands | |||
| return await Context.Channel.SendMessageAsync(message, isTTS, embed, options).ConfigureAwait(false); | |||
| } | |||
| protected virtual void BeforeExecute() | |||
| protected virtual void BeforeExecute(CommandInfo command) | |||
| { | |||
| } | |||
| protected virtual void AfterExecute() | |||
| protected virtual void AfterExecute(CommandInfo command) | |||
| { | |||
| } | |||
| @@ -27,13 +27,11 @@ namespace Discord.Commands | |||
| void IModuleBase.SetContext(ICommandContext context) | |||
| { | |||
| var newValue = context as T; | |||
| if (newValue == null) | |||
| throw new InvalidOperationException($"Invalid context type. Expected {typeof(T).Name}, got {context.GetType().Name}"); | |||
| Context = newValue; | |||
| Context = newValue ?? throw new InvalidOperationException($"Invalid context type. Expected {typeof(T).Name}, got {context.GetType().Name}"); | |||
| } | |||
| void IModuleBase.BeforeExecute() => BeforeExecute(); | |||
| void IModuleBase.BeforeExecute(CommandInfo command) => BeforeExecute(command); | |||
| void IModuleBase.AfterExecute() => AfterExecute(); | |||
| void IModuleBase.AfterExecute(CommandInfo command) => AfterExecute(command); | |||
| } | |||
| } | |||
| @@ -31,11 +31,6 @@ namespace Discord.Commands | |||
| parserBuilder[typeof(DateTimeOffset)] = (TryParseDelegate<DateTimeOffset>)DateTimeOffset.TryParse; | |||
| parserBuilder[typeof(TimeSpan)] = (TryParseDelegate<TimeSpan>)TimeSpan.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(); | |||
| } | |||
| @@ -9,7 +9,7 @@ namespace Discord.Commands | |||
| internal class ChannelTypeReader<T> : TypeReader | |||
| 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) | |||
| { | |||
| @@ -44,12 +44,11 @@ namespace Discord.Commands | |||
| _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; | |||
| if (_tryParse(input, out baseValue)) | |||
| if (_tryParse(input, out T baseValue)) | |||
| { | |||
| if (_enumsByValue.TryGetValue(baseValue, out enumValue)) | |||
| return Task.FromResult(TypeReaderResult.FromSuccess(enumValue)); | |||
| @@ -1,4 +1,5 @@ | |||
| using System.Globalization; | |||
| using System; | |||
| using System.Globalization; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.Commands | |||
| @@ -6,15 +7,14 @@ namespace Discord.Commands | |||
| internal class MessageTypeReader<T> : TypeReader | |||
| 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; | |||
| //By Id (1.0) | |||
| if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id)) | |||
| { | |||
| var msg = await context.Channel.GetMessageAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T; | |||
| if (msg != null) | |||
| if (await context.Channel.GetMessageAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) is T msg) | |||
| return TypeReaderResult.FromSuccess(msg); | |||
| } | |||
| @@ -15,17 +15,25 @@ namespace Discord.Commands | |||
| internal class PrimitiveTypeReader<T> : TypeReader | |||
| { | |||
| private readonly TryParseDelegate<T> _tryParse; | |||
| private readonly float _score; | |||
| 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}")); | |||
| } | |||
| } | |||
| @@ -9,7 +9,7 @@ namespace Discord.Commands | |||
| internal class RoleTypeReader<T> : TypeReader | |||
| 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; | |||
| @@ -1,9 +1,10 @@ | |||
| using System.Threading.Tasks; | |||
| using System; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.Commands | |||
| { | |||
| 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 | |||
| 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>(); | |||
| 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) | |||
| { | |||
| string username = input.Substring(0, index); | |||
| ushort discriminator; | |||
| if (ushort.TryParse(input.Substring(index + 1), out discriminator)) | |||
| if (ushort.TryParse(input.Substring(index + 1), out ushort discriminator)) | |||
| { | |||
| var channelUser = channelUsers.FirstOrDefault(x => x.DiscriminatorValue == discriminator && | |||
| string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase)); | |||
| @@ -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 | |||
| { | |||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
| public struct PreconditionResult : IResult | |||
| public class PreconditionResult : IResult | |||
| { | |||
| public CommandError? Error { get; } | |||
| public string ErrorReason { get; } | |||
| public bool IsSuccess => !Error.HasValue; | |||
| private PreconditionResult(CommandError? error, string errorReason) | |||
| protected PreconditionResult(CommandError? error, string errorReason) | |||
| { | |||
| Error = error; | |||
| ErrorReason = errorReason; | |||
| @@ -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) | |||
| { | |||
| 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); | |||
| } | |||
| ownerType = ownerType.BaseType.GetTypeInfo(); | |||
| @@ -11,7 +11,10 @@ namespace Discord.Audio | |||
| public override bool CanSeek => 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) | |||
| { | |||
| 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> | |||
| AudioOutStream CreateDirectOpusStream(); | |||
| /// <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> | |||
| 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> | |||
| Task<IReadOnlyCollection<IMessage>> GetPinnedMessagesAsync(RequestOptions options = null); | |||
| /// <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> | |||
| Task TriggerTypingAsync(RequestOptions options = null); | |||
| @@ -6,8 +6,16 @@ | |||
| public class Emoji : IEmote | |||
| { | |||
| // 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> | |||
| /// Creates a unciode emoji. | |||
| /// Creates a unicode emoji. | |||
| /// </summary> | |||
| /// <param name="unicode">The pure UTF-8 encoding of an emoji</param> | |||
| public Emoji(string unicode) | |||
| @@ -15,9 +23,17 @@ | |||
| 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; | |||
| } | |||
| 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> | |||
| /// Parse an Emote from its raw format | |||
| /// </summary> | |||
| @@ -58,6 +77,6 @@ namespace Discord | |||
| } | |||
| private string DebuggerDisplay => $"{Name} ({Id})"; | |||
| public override string ToString() => Name; | |||
| public override string ToString() => $"<:{Name}:{Id}>"; | |||
| } | |||
| } | |||
| @@ -20,7 +20,7 @@ namespace Discord | |||
| RoleIds = roleIds; | |||
| } | |||
| public override string ToString() => Name; | |||
| private string DebuggerDisplay => $"{Name} ({Id})"; | |||
| public override string ToString() => $"<:{Name}:{Id}>"; | |||
| } | |||
| } | |||
| @@ -66,10 +66,10 @@ namespace Discord | |||
| Task<IReadOnlyCollection<IBan>> GetBansAsync(RequestOptions options = null); | |||
| /// <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> | |||
| 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> | |||
| /// <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> | |||
| Task RemoveBanAsync(IUser user, RequestOptions options = null); | |||
| /// <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> | |||
| Medium = 2, | |||
| /// <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.Collections.Immutable; | |||
| using System.Diagnostics; | |||
| using System.Linq; | |||
| namespace Discord | |||
| { | |||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
| public class Embed : IEmbed | |||
| { | |||
| public string Type { get; } | |||
| public EmbedType Type { get; } | |||
| public string Description { get; internal set; } | |||
| public string Url { get; internal set; } | |||
| @@ -22,12 +23,12 @@ namespace Discord | |||
| public EmbedThumbnail? Thumbnail { get; internal set; } | |||
| public ImmutableArray<EmbedField> Fields { get; internal set; } | |||
| internal Embed(string type) | |||
| internal Embed(EmbedType type) | |||
| { | |||
| Type = type; | |||
| Fields = ImmutableArray.Create<EmbedField>(); | |||
| } | |||
| internal Embed(string type, | |||
| internal Embed(EmbedType type, | |||
| string title, | |||
| string description, | |||
| string url, | |||
| @@ -56,6 +57,8 @@ namespace Discord | |||
| Fields = fields; | |||
| } | |||
| public int Length => Title?.Length + Author?.Name?.Length + Description?.Length + Footer?.Text?.Length + Fields.Sum(f => f.Name.Length + f.Value.ToString().Length) ?? 0; | |||
| public override string ToString() => Title; | |||
| private string DebuggerDisplay => $"{Title} ({Type})"; | |||
| } | |||
| @@ -1,4 +1,5 @@ | |||
| using System.Diagnostics; | |||
| using System; | |||
| using System.Diagnostics; | |||
| namespace Discord | |||
| { | |||
| @@ -1,4 +1,5 @@ | |||
| using System.Diagnostics; | |||
| using System; | |||
| using System.Diagnostics; | |||
| namespace Discord | |||
| { | |||
| @@ -1,4 +1,5 @@ | |||
| using System.Diagnostics; | |||
| using System; | |||
| using System.Diagnostics; | |||
| namespace Discord | |||
| { | |||
| @@ -19,6 +20,6 @@ namespace Discord | |||
| } | |||
| private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})"; | |||
| public override string ToString() => Url; | |||
| public override string ToString() => Url.ToString(); | |||
| } | |||
| } | |||
| @@ -1,4 +1,5 @@ | |||
| using System.Diagnostics; | |||
| using System; | |||
| using System.Diagnostics; | |||
| namespace Discord | |||
| { | |||
| @@ -1,4 +1,5 @@ | |||
| using System.Diagnostics; | |||
| using System; | |||
| using System.Diagnostics; | |||
| namespace Discord | |||
| { | |||
| @@ -19,6 +20,6 @@ namespace Discord | |||
| } | |||
| private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})"; | |||
| public override string ToString() => Url; | |||
| public override string ToString() => Url.ToString(); | |||
| } | |||
| } | |||
| @@ -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 | |||
| { | |||
| @@ -17,6 +18,6 @@ namespace Discord | |||
| } | |||
| private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})"; | |||
| public override string ToString() => Url; | |||
| public override string ToString() => Url.ToString(); | |||
| } | |||
| } | |||
| @@ -6,9 +6,9 @@ namespace Discord | |||
| public interface IEmbed | |||
| { | |||
| string Url { get; } | |||
| string Type { get; } | |||
| string Title { get; } | |||
| string Description { get; } | |||
| EmbedType Type { get; } | |||
| DateTimeOffset? Timestamp { get; } | |||
| Color? Color { get; } | |||
| EmbedImage? Image { get; } | |||
| @@ -8,6 +8,46 @@ namespace Discord | |||
| { | |||
| /// <summary> Gets the default user color value. </summary> | |||
| 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> | |||
| public uint RawValue { get; } | |||
| @@ -25,7 +25,7 @@ namespace Discord | |||
| ChannelPermissions GetPermissions(IGuildChannel channel); | |||
| /// <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> | |||
| Task ModifyAsync(Action<GuildUserProperties> func, RequestOptions options = null); | |||
| @@ -20,8 +20,6 @@ namespace Discord | |||
| string Username { get; } | |||
| /// <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 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 RetryMode? RetryMode { get; 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 string BucketId { get; set; } | |||
| @@ -1,6 +1,7 @@ | |||
| #pragma warning disable CS1591 | |||
| using System; | |||
| using Newtonsoft.Json; | |||
| using Newtonsoft.Json.Converters; | |||
| namespace Discord.API | |||
| { | |||
| @@ -8,14 +9,14 @@ namespace Discord.API | |||
| { | |||
| [JsonProperty("title")] | |||
| public string Title { get; set; } | |||
| [JsonProperty("type")] | |||
| public string Type { get; set; } | |||
| [JsonProperty("description")] | |||
| public string Description { get; set; } | |||
| [JsonProperty("url")] | |||
| public string Url { get; set; } | |||
| [JsonProperty("color")] | |||
| public uint? Color { get; set; } | |||
| [JsonProperty("type"), JsonConverter(typeof(StringEnumConverter))] | |||
| public EmbedType Type { get; set; } | |||
| [JsonProperty("timestamp")] | |||
| public DateTimeOffset? Timestamp { get; set; } | |||
| [JsonProperty("author")] | |||
| @@ -1,4 +1,5 @@ | |||
| using Newtonsoft.Json; | |||
| using System; | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API | |||
| { | |||
| @@ -1,4 +1,5 @@ | |||
| using Newtonsoft.Json; | |||
| using System; | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API | |||
| { | |||
| @@ -1,4 +1,5 @@ | |||
| #pragma warning disable CS1591 | |||
| using System; | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API | |||
| @@ -1,4 +1,5 @@ | |||
| #pragma warning disable CS1591 | |||
| using System; | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API | |||
| @@ -1,4 +1,5 @@ | |||
| #pragma warning disable CS1591 | |||
| using System; | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API | |||
| @@ -1,4 +1,5 @@ | |||
| #pragma warning disable CS1591 | |||
| using System; | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API | |||
| @@ -4,5 +4,6 @@ namespace Discord.API.Rest | |||
| internal class CreateGuildBanParams | |||
| { | |||
| public Optional<int> DeleteMessageDays { get; set; } | |||
| public string Reason { get; set; } | |||
| } | |||
| } | |||
| @@ -30,7 +30,7 @@ namespace Discord.API | |||
| protected readonly JsonSerializer _serializer; | |||
| protected readonly SemaphoreSlim _stateLock; | |||
| private readonly RestClientProvider RestClientProvider; | |||
| private readonly RestClientProvider _restClientProvider; | |||
| protected bool _isDisposed; | |||
| private CancellationTokenSource _loginCancelToken; | |||
| @@ -48,7 +48,7 @@ namespace Discord.API | |||
| public DiscordRestApiClient(RestClientProvider restClientProvider, string userAgent, RetryMode defaultRetryMode = RetryMode.AlwaysRetry, | |||
| JsonSerializer serializer = null) | |||
| { | |||
| RestClientProvider = restClientProvider; | |||
| _restClientProvider = restClientProvider; | |||
| UserAgent = userAgent; | |||
| DefaultRetryMode = defaultRetryMode; | |||
| _serializer = serializer ?? new JsonSerializer { DateFormatString = "yyyy-MM-ddTHH:mm:ssZ", ContractResolver = new DiscordContractResolver() }; | |||
| @@ -60,7 +60,7 @@ namespace Discord.API | |||
| } | |||
| internal void SetBaseUrl(string baseUrl) | |||
| { | |||
| RestClient = RestClientProvider(baseUrl); | |||
| RestClient = _restClientProvider(baseUrl); | |||
| RestClient.SetHeader("accept", "*/*"); | |||
| RestClient.SetHeader("user-agent", UserAgent); | |||
| RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken)); | |||
| @@ -189,7 +189,7 @@ namespace Discord.API | |||
| options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; | |||
| options.IsClientBucket = AuthTokenType == TokenType.User; | |||
| var json = payload != null ? SerializeJson(payload) : null; | |||
| string json = payload != null ? SerializeJson(payload) : null; | |||
| var request = new JsonRestRequest(RestClient, method, endpoint, json, options); | |||
| await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); | |||
| } | |||
| @@ -233,7 +233,7 @@ namespace Discord.API | |||
| options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; | |||
| options.IsClientBucket = AuthTokenType == TokenType.User; | |||
| var json = payload != null ? SerializeJson(payload) : null; | |||
| string json = payload != null ? SerializeJson(payload) : null; | |||
| var request = new JsonRestRequest(RestClient, method, endpoint, json, options); | |||
| return DeserializeJson<TResponse>(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false)); | |||
| } | |||
| @@ -803,7 +803,8 @@ namespace Discord.API | |||
| options = RequestOptions.CreateOrClone(options); | |||
| var ids = new BucketIds(guildId: guildId); | |||
| await SendAsync("PUT", () => $"guilds/{guildId}/bans/{userId}?delete-message-days={args.DeleteMessageDays}", ids, options: options).ConfigureAwait(false); | |||
| string reason = string.IsNullOrWhiteSpace(args.Reason) ? "" : $"&reason={args.Reason}"; | |||
| await SendAsync("PUT", () => $"guilds/{guildId}/bans/{userId}?delete-message-days={args.DeleteMessageDays}{reason}", ids, options: options).ConfigureAwait(false); | |||
| } | |||
| public async Task RemoveGuildBanAsync(ulong guildId, ulong userId, RequestOptions options = null) | |||
| { | |||
| @@ -980,14 +981,15 @@ namespace Discord.API | |||
| Expression<Func<string>> endpoint = () => $"guilds/{guildId}/members?limit={limit}&after={afterUserId}"; | |||
| 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(userId, 0, nameof(userId)); | |||
| options = RequestOptions.CreateOrClone(options); | |||
| var ids = new BucketIds(guildId: guildId); | |||
| await SendAsync("DELETE", () => $"guilds/{guildId}/members/{userId}", ids, options: options).ConfigureAwait(false); | |||
| reason = string.IsNullOrWhiteSpace(reason) ? "" : $"?reason={reason}"; | |||
| await SendAsync("DELETE", () => $"guilds/{guildId}/members/{userId}{reason}", ids, options: options).ConfigureAwait(false); | |||
| } | |||
| public async Task ModifyGuildMemberAsync(ulong guildId, ulong userId, Rest.ModifyGuildMemberParams args, RequestOptions options = null) | |||
| { | |||
| @@ -178,12 +178,12 @@ namespace Discord.Rest | |||
| var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS }; | |||
| var model = await client.ApiClient.UploadFileAsync(channel.Id, args, options).ConfigureAwait(false); | |||
| return RestUserMessage.Create(client, channel, client.CurrentUser, model); | |||
| } | |||
| } | |||
| public static async Task DeleteMessagesAsync(IMessageChannel channel, BaseDiscordClient client, | |||
| IEnumerable<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) | |||
| { | |||
| var args = new DeleteMessagesParams(msgs); | |||
| @@ -73,7 +73,9 @@ namespace Discord.Rest | |||
| => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); | |||
| 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) | |||
| => ChannelHelper.TriggerTypingAsync(this, Discord, options); | |||
| @@ -86,7 +86,9 @@ namespace Discord.Rest | |||
| => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); | |||
| 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) | |||
| => ChannelHelper.TriggerTypingAsync(this, Discord, options); | |||
| @@ -64,7 +64,9 @@ namespace Discord.Rest | |||
| => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); | |||
| 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) | |||
| => ChannelHelper.TriggerTypingAsync(this, Discord, options); | |||
| @@ -43,7 +43,9 @@ namespace Discord.Rest | |||
| => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); | |||
| 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) | |||
| => ChannelHelper.TriggerTypingAsync(this, Discord, options); | |||
| @@ -107,9 +107,9 @@ namespace Discord.Rest | |||
| } | |||
| public static async Task AddBanAsync(IGuild guild, BaseDiscordClient client, | |||
| ulong userId, int pruneDays, RequestOptions options) | |||
| ulong userId, int pruneDays, string reason, RequestOptions options) | |||
| { | |||
| var args = new CreateGuildBanParams { DeleteMessageDays = pruneDays }; | |||
| var args = new CreateGuildBanParams { DeleteMessageDays = pruneDays, Reason = reason }; | |||
| await client.ApiClient.CreateGuildBanAsync(guild.Id, userId, args, options).ConfigureAwait(false); | |||
| } | |||
| public static async Task RemoveBanAsync(IGuild guild, BaseDiscordClient client, | |||
| @@ -137,10 +137,10 @@ namespace Discord.Rest | |||
| public Task<IReadOnlyCollection<RestBan>> GetBansAsync(RequestOptions options = null) | |||
| => GuildHelper.GetBansAsync(this, Discord, options); | |||
| public Task AddBanAsync(IUser user, int pruneDays = 0, RequestOptions options = null) | |||
| => GuildHelper.AddBanAsync(this, Discord, user.Id, pruneDays, options); | |||
| public Task AddBanAsync(ulong userId, int pruneDays = 0, RequestOptions options = null) | |||
| => GuildHelper.AddBanAsync(this, Discord, userId, pruneDays, options); | |||
| public Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null) | |||
| => GuildHelper.AddBanAsync(this, Discord, user.Id, pruneDays, reason, options); | |||
| public Task AddBanAsync(ulong userId, int pruneDays = 0, string reason = null, RequestOptions options = null) | |||
| => GuildHelper.AddBanAsync(this, Discord, userId, pruneDays, reason, options); | |||
| public Task RemoveBanAsync(IUser user, RequestOptions options = null) | |||
| => GuildHelper.RemoveBanAsync(this, Discord, user.Id, options); | |||
| @@ -8,19 +8,66 @@ namespace Discord | |||
| { | |||
| 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() | |||
| { | |||
| _embed = new Embed("rich"); | |||
| _embed = new Embed(EmbedType.Rich); | |||
| 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 EmbedFooterBuilder Footer { get; set; } | |||
| @@ -30,8 +77,10 @@ namespace Discord | |||
| get => _fields; | |||
| 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; | |||
| return this; | |||
| } | |||
| public EmbedBuilder WithAuthor(string name, string iconUrl = null, string url = null) | |||
| { | |||
| var author = new EmbedAuthorBuilder | |||
| { | |||
| Name = name, | |||
| IconUrl = iconUrl, | |||
| Url = url | |||
| }; | |||
| Author = author; | |||
| return this; | |||
| } | |||
| public EmbedBuilder WithFooter(EmbedFooterBuilder footer) | |||
| { | |||
| Footer = footer; | |||
| @@ -100,6 +160,16 @@ namespace Discord | |||
| Footer = footer; | |||
| return this; | |||
| } | |||
| public EmbedBuilder WithFooter(string text, string iconUrl = null) | |||
| { | |||
| var footer = new EmbedFooterBuilder | |||
| { | |||
| Text = text, | |||
| IconUrl = iconUrl | |||
| }; | |||
| Footer = footer; | |||
| return this; | |||
| } | |||
| public EmbedBuilder AddField(string name, object value) | |||
| { | |||
| @@ -107,7 +177,7 @@ namespace Discord | |||
| .WithIsInline(false) | |||
| .WithName(name) | |||
| .WithValue(value); | |||
| Fields.Add(field); | |||
| AddField(field); | |||
| return this; | |||
| } | |||
| public EmbedBuilder AddInlineField(string name, object value) | |||
| @@ -116,11 +186,16 @@ namespace Discord | |||
| .WithIsInline(true) | |||
| .WithName(name) | |||
| .WithValue(value); | |||
| Fields.Add(field); | |||
| AddField(field); | |||
| return this; | |||
| } | |||
| 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); | |||
| return this; | |||
| } | |||
| @@ -128,7 +203,18 @@ namespace Discord | |||
| { | |||
| var field = new EmbedFieldBuilder(); | |||
| 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; | |||
| } | |||
| @@ -140,6 +226,12 @@ namespace Discord | |||
| for (int i = 0; i < Fields.Count; i++) | |||
| fields.Add(Fields[i].Build()); | |||
| _embed.Fields = fields.ToImmutable(); | |||
| if (_embed.Length > MaxEmbedLength) | |||
| { | |||
| throw new InvalidOperationException($"Total embed length must be less than or equal to {MaxEmbedLength}"); | |||
| } | |||
| return _embed; | |||
| } | |||
| public static implicit operator Embed(EmbedBuilder builder) => builder?.Build(); | |||
| @@ -149,9 +241,32 @@ namespace Discord | |||
| { | |||
| private EmbedField _field; | |||
| public string Name { get { return _field.Name; } set { _field.Name = value; } } | |||
| public object Value { get { return _field.Value; } set { _field.Value = value.ToString(); } } | |||
| public bool IsInline { get { return _field.Inline; } set { _field.Inline = value; } } | |||
| public const int MaxFieldNameLength = 256; | |||
| public const int MaxFieldValueLength = 1024; | |||
| public string Name | |||
| { | |||
| get => _field.Name; | |||
| set | |||
| { | |||
| if (string.IsNullOrEmpty(value)) throw new ArgumentException($"Field name must not be null or empty.", nameof(Name)); | |||
| if (value.Length > MaxFieldNameLength) throw new ArgumentException($"Field name length must be less than or equal to {MaxFieldNameLength}.", nameof(Name)); | |||
| _field.Name = value; | |||
| } | |||
| } | |||
| public object Value | |||
| { | |||
| get => _field.Value; | |||
| set | |||
| { | |||
| var stringValue = value?.ToString(); | |||
| if (string.IsNullOrEmpty(stringValue)) throw new ArgumentException($"Field value must not be null or empty.", nameof(Value)); | |||
| if (stringValue.Length > MaxFieldValueLength) throw new ArgumentException($"Field value length must be less than or equal to {MaxFieldValueLength}.", nameof(Value)); | |||
| _field.Value = stringValue; | |||
| } | |||
| } | |||
| public bool IsInline { get => _field.Inline; set { _field.Inline = value; } } | |||
| public EmbedFieldBuilder() | |||
| { | |||
| @@ -182,9 +297,35 @@ namespace Discord | |||
| { | |||
| private EmbedAuthor _author; | |||
| public string Name { get { return _author.Name; } set { _author.Name = value; } } | |||
| public string Url { get { return _author.Url; } set { _author.Url = value; } } | |||
| public string IconUrl { get { return _author.IconUrl; } set { _author.IconUrl = value; } } | |||
| public const int MaxAuthorNameLength = 256; | |||
| public string Name | |||
| { | |||
| get => _author.Name; | |||
| set | |||
| { | |||
| if (value?.Length > MaxAuthorNameLength) throw new ArgumentException($"Author name length must be less than or equal to {MaxAuthorNameLength}.", nameof(Name)); | |||
| _author.Name = value; | |||
| } | |||
| } | |||
| public string Url | |||
| { | |||
| get => _author.Url; | |||
| set | |||
| { | |||
| if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(Url)); | |||
| _author.Url = value; | |||
| } | |||
| } | |||
| public string IconUrl | |||
| { | |||
| get => _author.IconUrl; | |||
| set | |||
| { | |||
| if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(IconUrl)); | |||
| _author.IconUrl = value; | |||
| } | |||
| } | |||
| public EmbedAuthorBuilder() | |||
| { | |||
| @@ -215,8 +356,26 @@ namespace Discord | |||
| { | |||
| private EmbedFooter _footer; | |||
| public string Text { get { return _footer.Text; } set { _footer.Text = value; } } | |||
| public string IconUrl { get { return _footer.IconUrl; } set { _footer.IconUrl = value; } } | |||
| public const int MaxFooterTextLength = 2048; | |||
| public string Text | |||
| { | |||
| get => _footer.Text; | |||
| set | |||
| { | |||
| if (value?.Length > MaxFooterTextLength) throw new ArgumentException($"Footer text length must be less than or equal to {MaxFooterTextLength}.", nameof(Text)); | |||
| _footer.Text = value; | |||
| } | |||
| } | |||
| public string IconUrl | |||
| { | |||
| get => _footer.IconUrl; | |||
| set | |||
| { | |||
| if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(IconUrl)); | |||
| _footer.IconUrl = value; | |||
| } | |||
| } | |||
| public EmbedFooterBuilder() | |||
| { | |||
| @@ -85,8 +85,8 @@ namespace Discord.Rest | |||
| else if (args.RoleIds.IsSpecified) | |||
| UpdateRoles(args.RoleIds.Value.ToArray()); | |||
| } | |||
| public Task KickAsync(RequestOptions options = null) | |||
| => UserHelper.KickAsync(this, Discord, options); | |||
| public Task KickAsync(string reason = null, RequestOptions options = null) | |||
| => UserHelper.KickAsync(this, Discord, reason, options); | |||
| /// <inheritdoc /> | |||
| public Task AddRoleAsync(IRole role, RequestOptions options = null) | |||
| => AddRolesAsync(new[] { role }, options); | |||
| @@ -54,7 +54,7 @@ namespace Discord.Rest | |||
| Update(model); | |||
| } | |||
| public Task<RestDMChannel> CreateDMChannelAsync(RequestOptions options = null) | |||
| public Task<RestDMChannel> GetOrCreateDMChannelAsync(RequestOptions options = null) | |||
| => UserHelper.CreateDMChannelAsync(this, Discord, options); | |||
| 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" : "")})"; | |||
| //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; | |||
| 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."); | |||
| } | |||
| @@ -53,9 +53,9 @@ namespace Discord.Rest | |||
| } | |||
| public static async Task KickAsync(IGuildUser user, BaseDiscordClient client, | |||
| RequestOptions options) | |||
| string reason, RequestOptions options) | |||
| { | |||
| await client.ApiClient.RemoveGuildMemberAsync(user.GuildId, user.Id, options).ConfigureAwait(false); | |||
| await client.ApiClient.RemoveGuildMemberAsync(user.GuildId, user.Id, reason, options).ConfigureAwait(false); | |||
| } | |||
| public static async Task<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; | |||
| } | |||
| 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); | |||
| using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) | |||
| { | |||
| if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); | |||
| return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); | |||
| } | |||
| } | |||
| public async Task<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); | |||
| using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) | |||
| { | |||
| if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); | |||
| restRequest.Content = new StringContent(json, Encoding.UTF8, "application/json"); | |||
| return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); | |||
| } | |||
| } | |||
| public async Task<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); | |||
| using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) | |||
| { | |||
| if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); | |||
| var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture)); | |||
| if (multipartParams != null) | |||
| { | |||
| @@ -15,7 +15,7 @@ namespace Discord.Net.Queue | |||
| 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() | |||
| { | |||
| 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() | |||
| { | |||
| 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); | |||
| 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) | |||
| => ChannelHelper.TriggerTypingAsync(this, Discord, options); | |||
| @@ -57,7 +57,9 @@ namespace Discord.Rpc | |||
| => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); | |||
| 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) | |||
| => ChannelHelper.TriggerTypingAsync(this, Discord, options); | |||
| @@ -58,7 +58,9 @@ namespace Discord.Rpc | |||
| => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); | |||
| 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) | |||
| => ChannelHelper.TriggerTypingAsync(this, Discord, options); | |||
| @@ -49,7 +49,7 @@ namespace Discord.Rpc | |||
| Username = model.Username.Value; | |||
| } | |||
| public Task<RestDMChannel> CreateDMChannelAsync(RequestOptions options = null) | |||
| public Task<RestDMChannel> GetOrCreateDMChannelAsync(RequestOptions options = null) | |||
| => UserHelper.CreateDMChannelAsync(this, Discord, options); | |||
| 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" : "")})"; | |||
| //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) | |||
| { | |||
| 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() | |||
| { | |||
| 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) | |||
| @@ -174,11 +174,11 @@ namespace Discord.Audio | |||
| //Assume Thread-safe | |||
| 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 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)); | |||
| await _streamCreatedEvent.InvokeAsync(userId, readerStream); | |||
| } | |||
| @@ -17,7 +17,7 @@ namespace Discord.Audio | |||
| public AudioApplication Application { 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) | |||
| throw new ArgumentOutOfRangeException(nameof(bitrate)); | |||
| @@ -48,7 +48,7 @@ namespace Discord.Audio | |||
| _ptr = CreateEncoder(SamplingRate, Channels, (int)opusApplication, out var error); | |||
| CheckError(error); | |||
| 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.SetBitrate, bitrate)); | |||
| } | |||
| @@ -88,11 +88,12 @@ namespace Discord.Audio.Streams | |||
| if (_queuedFrames.TryDequeue(out Frame frame)) | |||
| { | |||
| 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); | |||
| _bufferPool.Enqueue(frame.Buffer); | |||
| _queueLock.Release(); | |||
| nextTick += _ticksPerFrame; | |||
| seq++; | |||
| timestamp += OpusEncoder.FrameSamplesPerChannel; | |||
| _silenceFrames = 0; | |||
| #if DEBUG | |||
| @@ -105,12 +106,13 @@ namespace Discord.Audio.Streams | |||
| { | |||
| if (_silenceFrames++ < MaxSilenceFrames) | |||
| { | |||
| _next.WriteHeader(seq++, timestamp, false); | |||
| _next.WriteHeader(seq, timestamp, false); | |||
| await _next.WriteAsync(_silenceFrame, 0, _silenceFrame.Length).ConfigureAwait(false); | |||
| } | |||
| else | |||
| await _client.SetSpeakingAsync(false).ConfigureAwait(false); | |||
| nextTick += _ticksPerFrame; | |||
| seq++; | |||
| timestamp += OpusEncoder.FrameSamplesPerChannel; | |||
| } | |||
| #if DEBUG | |||
| @@ -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) | |||
| { | |||
| if (cancelToken.CanBeCanceled) | |||
| @@ -1,4 +1,4 @@ | |||
| using Discord.Logging; | |||
| /*using Discord.Logging; | |||
| using System; | |||
| using System.Collections.Concurrent; | |||
| using System.Threading; | |||
| @@ -243,4 +243,4 @@ namespace Discord.Audio.Streams | |||
| return Task.Delay(0); | |||
| } | |||
| } | |||
| } | |||
| }*/ | |||
| @@ -25,12 +25,13 @@ namespace Discord.Audio.Streams | |||
| public override void WriteHeader(ushort seq, uint timestamp, bool missed) | |||
| { | |||
| if (_hasHeader) | |||
| throw new InvalidOperationException("Header received with no payload"); | |||
| _nextMissed = missed; | |||
| throw new InvalidOperationException("Header received with no payload"); | |||
| _hasHeader = true; | |||
| _nextMissed = missed; | |||
| _next.WriteHeader(seq, timestamp, missed); | |||
| } | |||
| public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) | |||
| public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) | |||
| { | |||
| if (!_hasHeader) | |||
| throw new InvalidOperationException("Received payload without an RTP header"); | |||
| @@ -39,17 +40,17 @@ namespace Discord.Audio.Streams | |||
| if (!_nextMissed) | |||
| { | |||
| 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) | |||
| { | |||
| 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 | |||
| { | |||
| 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 const int SampleRate = 48000; | |||
| private readonly AudioStream _next; | |||
| private readonly OpusEncoder _encoder; | |||
| private readonly byte[] _buffer; | |||
| 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; | |||
| _encoder = new OpusEncoder(bitrate, application); | |||
| _encoder = new OpusEncoder(bitrate, application, packetLoss); | |||
| _buffer = new byte[OpusConverter.FrameBytes]; | |||
| } | |||
| public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) | |||
| public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) | |||
| { | |||
| //Assume threadsafe | |||
| while (count > 0) | |||
| @@ -30,10 +32,13 @@ namespace Discord.Audio.Streams | |||
| { | |||
| //We have enough data and no partial frames. Pass the buffer directly to the encoder | |||
| 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; | |||
| count -= OpusConverter.FrameBytes; | |||
| _seq++; | |||
| _timestamp += OpusConverter.FrameSamplesPerChannel; | |||
| } | |||
| else if (_partialFramePos + count >= OpusConverter.FrameBytes) | |||
| { | |||
| @@ -41,11 +46,14 @@ namespace Discord.Audio.Streams | |||
| int partialSize = OpusConverter.FrameBytes - _partialFramePos; | |||
| Buffer.BlockCopy(buffer, offset, _buffer, _partialFramePos, partialSize); | |||
| 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; | |||
| count -= partialSize; | |||
| _partialFramePos = 0; | |||
| _seq++; | |||
| _timestamp += OpusConverter.FrameSamplesPerChannel; | |||
| } | |||
| 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 | |||
| { | |||
| @@ -67,7 +75,7 @@ namespace Discord.Audio.Streams | |||
| } | |||
| catch (Exception) { } //Incomplete frame | |||
| _partialFramePos = 0; | |||
| await base.FlushAsync(cancellationToken).ConfigureAwait(false); | |||
| await base.FlushAsync(cancelToken).ConfigureAwait(false); | |||
| }*/ | |||
| public override async Task FlushAsync(CancellationToken cancelToken) | |||