diff --git a/Discord.Net.targets b/Discord.Net.targets index 947819898..6dc4bb140 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,7 +1,7 @@ - 1.0.0 - rc3 + 1.0.1 + RogueException discord;discordapp https://github.com/RogueException/Discord.Net diff --git a/README.md b/README.md index c5ed907ee..2b58d4579 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Discord.Net v1.0.0-rc +# Discord.Net [![NuGet](https://img.shields.io/nuget/vpre/Discord.Net.svg?maxAge=2592000?style=plastic)](https://www.nuget.org/packages/Discord.Net) [![MyGet](https://img.shields.io/myget/discord-net/vpre/Discord.Net.svg)](https://www.myget.org/feed/Packages/discord-net) [![Build status](https://ci.appveyor.com/api/projects/status/5sb7n8a09w9clute/branch/dev?svg=true)](https://ci.appveyor.com/project/RogueException/discord-net/branch/dev) diff --git a/appveyor.yml b/appveyor.yml index 3bf70c09c..d94e2ad68 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -34,7 +34,7 @@ after_build: if ($Env:APPVEYOR_REPO_TAG -eq "true") { nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="" } else { - nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="-$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 } diff --git a/docs/guides/commands/commands.md b/docs/guides/commands/commands.md index 6dd595861..e021b1eb3 100644 --- a/docs/guides/commands/commands.md +++ b/docs/guides/commands/commands.md @@ -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 diff --git a/docs/guides/commands/samples/command_handler.cs b/docs/guides/commands/samples/command_handler.cs index 71869415b..6b5d4ad2b 100644 --- a/docs/guides/commands/samples/command_handler.cs +++ b/docs/guides/commands/samples/command_handler.cs @@ -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); - } - + } } diff --git a/docs/guides/commands/samples/dependency_map_setup.cs b/docs/guides/commands/samples/dependency_map_setup.cs index aa39150e7..e205d891d 100644 --- a/docs/guides/commands/samples/dependency_map_setup.cs +++ b/docs/guides/commands/samples/dependency_map_setup.cs @@ -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()); } diff --git a/docs/guides/commands/samples/require_owner.cs b/docs/guides/commands/samples/require_owner.cs index 137446553..3611afab8 100644 --- a/docs/guides/commands/samples/require_owner.cs +++ b/docs/guides/commands/samples/require_owner.cs @@ -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 CheckPermissions(ICommandContext context, CommandInfo command, IDependencyMap map) + public async override Task CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services) { // Get the ID of the bot's owner - var ownerId = (await map.Get().GetApplicationInfoAsync()).Owner.Id; + var ownerId = (await services.GetService().GetApplicationInfoAsync()).Owner.Id; // If this command was executed by that user, return a success if (context.User.Id == ownerId) return PreconditionResult.FromSuccess(); diff --git a/docs/guides/concepts/samples/events.cs b/docs/guides/concepts/samples/events.cs index c662b51a9..cf0492cb5 100644 --- a/docs/guides/concepts/samples/events.cs +++ b/docs/guides/concepts/samples/events.cs @@ -8,7 +8,11 @@ public class Program public async Task MainAsync() { - _client = new DiscordSocketClient(); + // When working with events that have Cacheable 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 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}"); } -} \ No newline at end of file +} diff --git a/docs/guides/getting_started/intro.md b/docs/guides/getting_started/intro.md index 8bcfa9086..837814511 100644 --- a/docs/guides/getting_started/intro.md +++ b/docs/guides/getting_started/intro.md @@ -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 \ No newline at end of file +**todo:** diagram of bot structure diff --git a/docs/guides/getting_started/samples/intro/structure.cs b/docs/guides/getting_started/samples/intro/structure.cs index 9e783bb9b..706d0a38d 100644 --- a/docs/guides/getting_started/samples/intro/structure.cs +++ b/docs/guides/getting_started/samples/intro/structure.cs @@ -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. 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(); - // 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(); + // 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, diff --git a/docs/guides/migrating/migrating.md b/docs/guides/migrating/migrating.md index 8f96dff98..bc628a5f8 100644 --- a/docs/guides/migrating/migrating.md +++ b/docs/guides/migrating/migrating.md @@ -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 diff --git a/docs/guides/voice/samples/audio_ffmpeg.cs b/docs/guides/voice/samples/audio_ffmpeg.cs index 716ec3d6c..b9430ac11 100644 --- a/docs/guides/voice/samples/audio_ffmpeg.cs +++ b/docs/guides/voice/samples/audio_ffmpeg.cs @@ -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(); } diff --git a/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs b/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs index e099380f6..3727510d9 100644 --- a/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs @@ -6,6 +6,13 @@ namespace Discord.Commands [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = true)] public abstract class PreconditionAttribute : Attribute { + /// + /// 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 = + /// or not at all will require *all* preconditions to pass, just like normal (A && B). + /// + public string Group { get; set; } = null; + public abstract Task CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services); } } diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs index 82975a2f6..0f865e864 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs @@ -44,14 +44,16 @@ namespace Discord.Commands public override async Task 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(); diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs index 44c69d76a..b7729b0c8 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs @@ -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()); diff --git a/src/Discord.Net.Commands/Builders/CommandBuilder.cs b/src/Discord.Net.Commands/Builders/CommandBuilder.cs index ff89b7559..b6d002c70 100644 --- a/src/Discord.Net.Commands/Builders/CommandBuilder.cs +++ b/src/Discord.Net.Commands/Builders/CommandBuilder.cs @@ -10,10 +10,11 @@ namespace Discord.Commands.Builders { private readonly List _preconditions; private readonly List _parameters; + private readonly List _attributes; private readonly List _aliases; public ModuleBuilder Module { get; } - internal Func Callback { get; set; } + internal Func Callback { get; set; } public string Name { get; set; } public string Summary { get; set; } @@ -24,6 +25,7 @@ namespace Discord.Commands.Builders public IReadOnlyList Preconditions => _preconditions; public IReadOnlyList Parameters => _parameters; + public IReadOnlyList Attributes => _attributes; public IReadOnlyList Aliases => _aliases; //Automatic @@ -33,10 +35,11 @@ namespace Discord.Commands.Builders _preconditions = new List(); _parameters = new List(); + _attributes = new List(); _aliases = new List(); } //User-defined - internal CommandBuilder(ModuleBuilder module, string primaryAlias, Func callback) + internal CommandBuilder(ModuleBuilder module, string primaryAlias, Func callback) : this(module) { Discord.Preconditions.NotNull(primaryAlias, nameof(primaryAlias)); @@ -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); diff --git a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs index d79239057..0a33c9e26 100644 --- a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs @@ -10,6 +10,7 @@ namespace Discord.Commands.Builders private readonly List _commands; private readonly List _submodules; private readonly List _preconditions; + private readonly List _attributes; private readonly List _aliases; public CommandService Service { get; } @@ -21,6 +22,7 @@ namespace Discord.Commands.Builders public IReadOnlyList Commands => _commands; public IReadOnlyList Modules => _submodules; public IReadOnlyList Preconditions => _preconditions; + public IReadOnlyList Attributes => _attributes; public IReadOnlyList Aliases => _aliases; //Automatic @@ -32,6 +34,7 @@ namespace Discord.Commands.Builders _commands = new List(); _submodules = new List(); _preconditions = new List(); + _attributes = new List(); _aliases = new List(); } //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 callback, Action createFunc) + public ModuleBuilder AddCommand(string primaryAlias, Func callback, Action createFunc) { var builder = new CommandBuilder(this, primaryAlias, callback); createFunc(builder); diff --git a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs index d8464ea72..6fae719ee 100644 --- a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -12,25 +12,42 @@ namespace Discord.Commands { private static readonly TypeInfo _moduleTypeInfo = typeof(IModuleBase).GetTypeInfo(); - public static IEnumerable Search(Assembly assembly) + public static async Task> 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() != null) && + info.GetCustomAttribute() == null; + } + + var result = new List(); + + 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 Build(CommandService service, params TypeInfo[] validTypes) => Build(validTypes, service); - public static Dictionary Build(IEnumerable validTypes, CommandService service) + + public static Task> BuildAsync(CommandService service, params TypeInfo[] validTypes) => BuildAsync(validTypes, service); + public static async Task> BuildAsync(IEnumerable 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(typeInfo, service); - builder.Callback = async (ctx, args, map) => + async Task 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 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)) && !methodInfo.IsStatic && !methodInfo.IsGenericMethod; } } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs index 6761033b0..d1782d7ea 100644 --- a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs @@ -8,7 +8,8 @@ namespace Discord.Commands.Builders { public class ParameterBuilder { - private readonly List _preconditions; + private readonly List _preconditions; + private readonly List _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 Preconditions => _preconditions; + public IReadOnlyList Attributes => _attributes; //Automatic internal ParameterBuilder(CommandBuilder command) { _preconditions = new List(); + _attributes = new List(); 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); diff --git a/src/Discord.Net.Commands/CommandError.cs b/src/Discord.Net.Commands/CommandError.cs index 41b4822ad..abfc14e1d 100644 --- a/src/Discord.Net.Commands/CommandError.cs +++ b/src/Discord.Net.Commands/CommandError.cs @@ -18,6 +18,9 @@ UnmetPrecondition, //Execute - Exception + Exception, + + //Runtime + Unsuccessful } } diff --git a/src/Discord.Net.Commands/CommandMatch.cs b/src/Discord.Net.Commands/CommandMatch.cs index 04a2d040f..d922a2229 100644 --- a/src/Discord.Net.Commands/CommandMatch.cs +++ b/src/Discord.Net.Commands/CommandMatch.cs @@ -18,11 +18,11 @@ namespace Discord.Commands public Task CheckPreconditionsAsync(ICommandContext context, IServiceProvider services = null) => Command.CheckPreconditionsAsync(context, services); - public Task ParseAsync(ICommandContext context, SearchResult searchResult, PreconditionResult? preconditionResult = null) - => Command.ParseAsync(context, Alias.Length, searchResult, preconditionResult); - public Task ExecuteAsync(ICommandContext context, IEnumerable argList, IEnumerable paramList, IServiceProvider services) + public Task ParseAsync(ICommandContext context, SearchResult searchResult, PreconditionResult preconditionResult = null, IServiceProvider services = null) + => Command.ParseAsync(context, Alias.Length, searchResult, preconditionResult, services); + public Task ExecuteAsync(ICommandContext context, IEnumerable argList, IEnumerable paramList, IServiceProvider services) => Command.ExecuteAsync(context, argList, paramList, services); - public Task ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) + public Task ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) => Command.ExecuteAsync(context, parseResult, services); } } diff --git a/src/Discord.Net.Commands/CommandParser.cs b/src/Discord.Net.Commands/CommandParser.cs index 5b4ba2480..394f8589d 100644 --- a/src/Discord.Net.Commands/CommandParser.cs +++ b/src/Discord.Net.Commands/CommandParser.cs @@ -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 ParseArgs(CommandInfo command, ICommandContext context, string input, int startPos) + public static async Task 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); diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index f526e8f3b..6ea2abcf3 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -33,7 +33,7 @@ namespace Discord.Commands public IEnumerable Modules => _moduleDefs.Select(x => x); public IEnumerable Commands => _moduleDefs.SelectMany(x => x.Commands); - public ILookup TypeReaders => _typeReaders.SelectMany(x => x.Value.Select(y => new {y.Key, y.Value})).ToLookup(x => x.Key, x => x.Value); + public ILookup TypeReaders => _typeReaders.SelectMany(x => x.Value.Select(y => new { y.Key, y.Value })).ToLookup(x => x.Key, x => x.Value); public CommandService() : this(new CommandServiceConfig()) { } public CommandService(CommandServiceConfig config) @@ -59,6 +59,9 @@ namespace Discord.Commands foreach (var type in PrimitiveParsers.SupportedTypes) _defaultTypeReaders[type] = PrimitiveTypeReader.Create(type); + _defaultTypeReaders[typeof(string)] = + new PrimitiveTypeReader((string x, out string y) => { y = x; return true; }, 0); + var entityTypeReaders = ImmutableList.CreateBuilder>(); entityTypeReaders.Add(new Tuple(typeof(IMessage), typeof(MessageTypeReader<>))); entityTypeReaders.Add(new Tuple(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()); + var readers = _typeReaders.GetOrAdd(type, x => new ConcurrentDictionary()); readers[reader.GetType()] = reader; } internal IDictionary GetTypeReaders(Type type) { - ConcurrentDictionary 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(); + + 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(); + 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 argList, paramList; + switch (multiMatchHandling) { - IReadOnlyList 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); } } } diff --git a/src/Discord.Net.Commands/IModuleBase.cs b/src/Discord.Net.Commands/IModuleBase.cs index fda768b53..479724ae3 100644 --- a/src/Discord.Net.Commands/IModuleBase.cs +++ b/src/Discord.Net.Commands/IModuleBase.cs @@ -4,8 +4,8 @@ { void SetContext(ICommandContext context); - void BeforeExecute(); + void BeforeExecute(CommandInfo command); - void AfterExecute(); + void AfterExecute(CommandInfo command); } } diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index 5acd1f648..ebef80baf 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -18,7 +18,7 @@ namespace Discord.Commands private static readonly System.Reflection.MethodInfo _convertParamsMethod = typeof(CommandInfo).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList)); private static readonly ConcurrentDictionary, object>> _arrayConverters = new ConcurrentDictionary, object>>(); - private readonly Func _action; + private readonly Func _action; public ModuleInfo Module { get; } public string Name { get; } @@ -31,18 +31,19 @@ namespace Discord.Commands public IReadOnlyList Aliases { get; } public IReadOnlyList Parameters { get; } public IReadOnlyList Preconditions { get; } + public IReadOnlyList 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 CheckGroups(IEnumerable preconditions, string type) { - var result = await precondition.CheckPermissions(context, this, services).ConfigureAwait(false); - if (!result.IsSuccess) - return result; - } + foreach (IGrouping 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(); + 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 ParseAsync(ICommandContext context, int startIndex, SearchResult searchResult, PreconditionResult? preconditionResult = null) + + public async Task 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 ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) + public Task 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 ExecuteAsync(ICommandContext context, IEnumerable argList, IEnumerable paramList, IServiceProvider services) + public async Task ExecuteAsync(ICommandContext context, IEnumerable argList, IEnumerable 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 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 resultTask) + { + var result = await resultTask.ConfigureAwait(false); + if (result is RuntimeResult execResult) + return execResult; + } + else if (task is Task 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 argList, IEnumerable 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().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}"; } } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Commands/Info/ModuleInfo.cs b/src/Discord.Net.Commands/Info/ModuleInfo.cs index a2094df65..97b90bf4e 100644 --- a/src/Discord.Net.Commands/Info/ModuleInfo.cs +++ b/src/Discord.Net.Commands/Info/ModuleInfo.cs @@ -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 Aliases { get; } public IReadOnlyList Commands { get; } public IReadOnlyList Preconditions { get; } + public IReadOnlyList Attributes { get; } public IReadOnlyList 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 BuildAttributes(ModuleBuilder builder) + { + var result = new List(); + + ModuleBuilder parent = builder; + while (parent != null) + { + result.AddRange(parent.Attributes); + parent = parent.Parent; + } + + return result; + } } } diff --git a/src/Discord.Net.Commands/Info/ParameterInfo.cs b/src/Discord.Net.Commands/Info/ParameterInfo.cs index 2ecf26a9f..e417b1ab6 100644 --- a/src/Discord.Net.Commands/Info/ParameterInfo.cs +++ b/src/Discord.Net.Commands/Info/ParameterInfo.cs @@ -21,6 +21,7 @@ namespace Discord.Commands public object DefaultValue { get; } public IReadOnlyList Preconditions { get; } + public IReadOnlyList 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 Parse(ICommandContext context, string input) + public async Task 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; diff --git a/src/Discord.Net.Commands/ModuleBase.cs b/src/Discord.Net.Commands/ModuleBase.cs index ed0b49006..f51656e40 100644 --- a/src/Discord.Net.Commands/ModuleBase.cs +++ b/src/Discord.Net.Commands/ModuleBase.cs @@ -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); } } diff --git a/src/Discord.Net.Commands/PrimitiveParsers.cs b/src/Discord.Net.Commands/PrimitiveParsers.cs index 623ddafa7..6a54ba402 100644 --- a/src/Discord.Net.Commands/PrimitiveParsers.cs +++ b/src/Discord.Net.Commands/PrimitiveParsers.cs @@ -31,11 +31,6 @@ namespace Discord.Commands parserBuilder[typeof(DateTimeOffset)] = (TryParseDelegate)DateTimeOffset.TryParse; parserBuilder[typeof(TimeSpan)] = (TryParseDelegate)TimeSpan.TryParse; parserBuilder[typeof(char)] = (TryParseDelegate)char.TryParse; - parserBuilder[typeof(string)] = (TryParseDelegate)delegate (string str, out string value) - { - value = str; - return true; - }; return parserBuilder.ToImmutable(); } diff --git a/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs b/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs index d2e34b436..72c62282e 100644 --- a/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs @@ -9,7 +9,7 @@ namespace Discord.Commands internal class ChannelTypeReader : TypeReader where T : class, IChannel { - public override async Task Read(ICommandContext context, string input) + public override async Task Read(ICommandContext context, string input, IServiceProvider services) { if (context.Guild != null) { diff --git a/src/Discord.Net.Commands/Readers/EnumTypeReader.cs b/src/Discord.Net.Commands/Readers/EnumTypeReader.cs index 7b2ff505a..383b8e63c 100644 --- a/src/Discord.Net.Commands/Readers/EnumTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/EnumTypeReader.cs @@ -44,12 +44,11 @@ namespace Discord.Commands _enumsByValue = byValueBuilder.ToImmutable(); } - public override Task Read(ICommandContext context, string input) + public override Task 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)); diff --git a/src/Discord.Net.Commands/Readers/MessageTypeReader.cs b/src/Discord.Net.Commands/Readers/MessageTypeReader.cs index 9baa1901a..895713e4f 100644 --- a/src/Discord.Net.Commands/Readers/MessageTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/MessageTypeReader.cs @@ -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 : TypeReader where T : class, IMessage { - public override async Task Read(ICommandContext context, string input) + public override async Task 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); } diff --git a/src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs b/src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs index aa4c7c7a4..2656741f0 100644 --- a/src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs @@ -15,17 +15,25 @@ namespace Discord.Commands internal class PrimitiveTypeReader : TypeReader { private readonly TryParseDelegate _tryParse; + private readonly float _score; public PrimitiveTypeReader() + : this(PrimitiveParsers.Get(), 1) + { } + + public PrimitiveTypeReader(TryParseDelegate tryParse, float score) { - _tryParse = PrimitiveParsers.Get(); + 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 Read(ICommandContext context, string input) + public override Task 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}")); } } diff --git a/src/Discord.Net.Commands/Readers/RoleTypeReader.cs b/src/Discord.Net.Commands/Readers/RoleTypeReader.cs index a90432782..17786e6f0 100644 --- a/src/Discord.Net.Commands/Readers/RoleTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/RoleTypeReader.cs @@ -9,7 +9,7 @@ namespace Discord.Commands internal class RoleTypeReader : TypeReader where T : class, IRole { - public override Task Read(ICommandContext context, string input) + public override Task Read(ICommandContext context, string input, IServiceProvider services) { ulong id; diff --git a/src/Discord.Net.Commands/Readers/TypeReader.cs b/src/Discord.Net.Commands/Readers/TypeReader.cs index d53491e92..2c4644376 100644 --- a/src/Discord.Net.Commands/Readers/TypeReader.cs +++ b/src/Discord.Net.Commands/Readers/TypeReader.cs @@ -1,9 +1,10 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; namespace Discord.Commands { public abstract class TypeReader { - public abstract Task Read(ICommandContext context, string input); + public abstract Task Read(ICommandContext context, string input, IServiceProvider services); } } diff --git a/src/Discord.Net.Commands/Readers/UserTypeReader.cs b/src/Discord.Net.Commands/Readers/UserTypeReader.cs index 84d772612..8f24677f2 100644 --- a/src/Discord.Net.Commands/Readers/UserTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/UserTypeReader.cs @@ -10,7 +10,7 @@ namespace Discord.Commands internal class UserTypeReader : TypeReader where T : class, IUser { - public override async Task Read(ICommandContext context, string input) + public override async Task Read(ICommandContext context, string input, IServiceProvider services) { var results = new Dictionary(); IReadOnlyCollection 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)); diff --git a/src/Discord.Net.Commands/Results/PreconditionGroupResult.cs b/src/Discord.Net.Commands/Results/PreconditionGroupResult.cs new file mode 100644 index 000000000..1d7f29122 --- /dev/null +++ b/src/Discord.Net.Commands/Results/PreconditionGroupResult.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Diagnostics; + +namespace Discord.Commands +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class PreconditionGroupResult : PreconditionResult + { + public IReadOnlyCollection PreconditionResults { get; } + + protected PreconditionGroupResult(CommandError? error, string errorReason, ICollection preconditions) + : base(error, errorReason) + { + PreconditionResults = (preconditions ?? new List(0)).ToReadOnlyCollection(); + } + + public static new PreconditionGroupResult FromSuccess() + => new PreconditionGroupResult(null, null, null); + public static PreconditionGroupResult FromError(string reason, ICollection 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}"; + } +} diff --git a/src/Discord.Net.Commands/Results/PreconditionResult.cs b/src/Discord.Net.Commands/Results/PreconditionResult.cs index 77ba1b5b9..ca65a373e 100644 --- a/src/Discord.Net.Commands/Results/PreconditionResult.cs +++ b/src/Discord.Net.Commands/Results/PreconditionResult.cs @@ -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; diff --git a/src/Discord.Net.Commands/Results/RuntimeResult.cs b/src/Discord.Net.Commands/Results/RuntimeResult.cs new file mode 100644 index 000000000..2a326a7a3 --- /dev/null +++ b/src/Discord.Net.Commands/Results/RuntimeResult.cs @@ -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}"; + } +} diff --git a/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs index 4cca0e864..ab88f66ae 100644 --- a/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs +++ b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs @@ -58,7 +58,7 @@ namespace Discord.Commands { foreach (var prop in ownerType.DeclaredProperties) { - if (prop.SetMethod?.IsPublic == true && prop.GetCustomAttribute() == null) + if (prop.SetMethod?.IsStatic == false && prop.SetMethod?.IsPublic == true && prop.GetCustomAttribute() == null) result.Add(prop); } ownerType = ownerType.BaseType.GetTypeInfo(); diff --git a/src/Discord.Net.Core/Audio/AudioStream.cs b/src/Discord.Net.Core/Audio/AudioStream.cs index d39bcc48a..97820ea73 100644 --- a/src/Discord.Net.Core/Audio/AudioStream.cs +++ b/src/Discord.Net.Core/Audio/AudioStream.cs @@ -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(); diff --git a/src/Discord.Net.Core/Audio/IAudioClient.cs b/src/Discord.Net.Core/Audio/IAudioClient.cs index 7373a8e4d..9be8ceef5 100644 --- a/src/Discord.Net.Core/Audio/IAudioClient.cs +++ b/src/Discord.Net.Core/Audio/IAudioClient.cs @@ -28,8 +28,8 @@ namespace Discord.Audio /// Creates a new outgoing stream accepting Opus-encoded data. This is a direct stream with no internal timer. AudioOutStream CreateDirectOpusStream(); /// Creates a new outgoing stream accepting PCM (raw) data. - AudioOutStream CreatePCMStream(AudioApplication application, int? bitrate = null, int bufferMillis = 1000); + AudioOutStream CreatePCMStream(AudioApplication application, int? bitrate = null, int bufferMillis = 1000, int packetLoss = 30); /// Creates a new direct outgoing stream accepting PCM (raw) data. This is a direct stream with no internal timer. - AudioOutStream CreateDirectPCMStream(AudioApplication application, int? bitrate = null); + AudioOutStream CreateDirectPCMStream(AudioApplication application, int? bitrate = null, int packetLoss = 30); } } diff --git a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs index a6a1d9a74..3b9512eaa 100644 --- a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs @@ -30,7 +30,9 @@ namespace Discord /// Gets a collection of pinned messages in this channel. Task> GetPinnedMessagesAsync(RequestOptions options = null); /// Bulk deletes multiple messages. - Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null); + Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null); + /// Bulk deletes multiple messages. + Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null); /// Broadcasts the "user is typing" message to all users in this channel, lasting 10 seconds. Task TriggerTypingAsync(RequestOptions options = null); diff --git a/src/Discord.Net.Core/Entities/Emotes/Emoji.cs b/src/Discord.Net.Core/Entities/Emotes/Emoji.cs index 96226c715..c2dfc31ad 100644 --- a/src/Discord.Net.Core/Entities/Emotes/Emoji.cs +++ b/src/Discord.Net.Core/Entities/Emotes/Emoji.cs @@ -6,8 +6,16 @@ public class Emoji : IEmote { // TODO: need to constrain this to unicode-only emojis somehow + + /// + /// The unicode representation of this emote. + /// + public string Name { get; } + + public override string ToString() => Name; + /// - /// Creates a unciode emoji. + /// Creates a unicode emoji. /// /// The pure UTF-8 encoding of an emoji public Emoji(string unicode) @@ -15,9 +23,17 @@ Name = unicode; } - /// - /// The unicode representation of this emote. - /// - 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(); } } diff --git a/src/Discord.Net.Core/Entities/Emotes/Emote.cs b/src/Discord.Net.Core/Entities/Emotes/Emote.cs index b1ca272eb..f498c818e 100644 --- a/src/Discord.Net.Core/Entities/Emotes/Emote.cs +++ b/src/Discord.Net.Core/Entities/Emotes/Emote.cs @@ -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(); + } + } + /// /// Parse an Emote from its raw format /// @@ -58,6 +77,6 @@ namespace Discord } private string DebuggerDisplay => $"{Name} ({Id})"; - public override string ToString() => Name; + public override string ToString() => $"<:{Name}:{Id}>"; } } diff --git a/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs b/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs index e883c707e..8d776a4cd 100644 --- a/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs +++ b/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs @@ -20,7 +20,7 @@ namespace Discord RoleIds = roleIds; } - public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id})"; + public override string ToString() => $"<:{Name}:{Id}>"; } } diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 506cbd3e4..7874f5fd1 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -66,10 +66,10 @@ namespace Discord Task> GetBansAsync(RequestOptions options = null); /// Bans the provided user from this guild and optionally prunes their recent messages. /// The number of days to remove messages from this user for - must be between [0, 7] - Task AddBanAsync(IUser user, int pruneDays = 0, RequestOptions options = null); + Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null); /// Bans the provided user id from this guild and optionally prunes their recent messages. /// The number of days to remove messages from this user for - must be between [0, 7] - Task AddBanAsync(ulong userId, int pruneDays = 0, RequestOptions options = null); + Task AddBanAsync(ulong userId, int pruneDays = 0, string reason = null, RequestOptions options = null); /// Unbans the provided user if it is currently banned. Task RemoveBanAsync(IUser user, RequestOptions options = null); /// Unbans the provided user id if it is currently banned. diff --git a/src/Discord.Net.Core/Entities/Guilds/VerificationLevel.cs b/src/Discord.Net.Core/Entities/Guilds/VerificationLevel.cs index d6828b5c9..ac51fe927 100644 --- a/src/Discord.Net.Core/Entities/Guilds/VerificationLevel.cs +++ b/src/Discord.Net.Core/Entities/Guilds/VerificationLevel.cs @@ -9,6 +9,8 @@ /// Users must fulfill the requirements of Low, and be registered on Discord for at least 5 minutes. Medium = 2, /// Users must fulfill the requirements of Medium, and be a member of this guild for at least 10 minutes. - High = 3 + High = 3, + /// Users must fulfill the requirements of High, and must have a verified phone on their Discord account. + Extreme = 4 } } diff --git a/src/Discord.Net.Core/Entities/Messages/Embed.cs b/src/Discord.Net.Core/Entities/Messages/Embed.cs index ebde05d4c..5fae7acde 100644 --- a/src/Discord.Net.Core/Entities/Messages/Embed.cs +++ b/src/Discord.Net.Core/Entities/Messages/Embed.cs @@ -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 Fields { get; internal set; } - internal Embed(string type) + internal Embed(EmbedType type) { Type = type; Fields = ImmutableArray.Create(); } - 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})"; } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs b/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs index 142e36832..c59473704 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; namespace Discord { diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs b/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs index 33582070a..29d85cd90 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; namespace Discord { diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs b/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs index fa4847721..f21d42c0c 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs @@ -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(); } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs b/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs index 943ac5b52..24722b158 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; namespace Discord { diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs b/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs index 4e125bf2a..209a93e37 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs @@ -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(); } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedType.cs b/src/Discord.Net.Core/Entities/Messages/EmbedType.cs new file mode 100644 index 000000000..469e968a5 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/EmbedType.cs @@ -0,0 +1,13 @@ +namespace Discord +{ + public enum EmbedType + { + Rich, + Link, + Video, + Image, + Gifv, + Article, + Tweet + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs b/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs index eaf6f4a4c..f00681d89 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs @@ -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(); } } diff --git a/src/Discord.Net.Core/Entities/Messages/IEmbed.cs b/src/Discord.Net.Core/Entities/Messages/IEmbed.cs index 5eef5ec9b..f390c4c28 100644 --- a/src/Discord.Net.Core/Entities/Messages/IEmbed.cs +++ b/src/Discord.Net.Core/Entities/Messages/IEmbed.cs @@ -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; } diff --git a/src/Discord.Net.Core/Entities/Roles/Color.cs b/src/Discord.Net.Core/Entities/Roles/Color.cs index 3250acb2d..89e76df6d 100644 --- a/src/Discord.Net.Core/Entities/Roles/Color.cs +++ b/src/Discord.Net.Core/Entities/Roles/Color.cs @@ -8,6 +8,46 @@ namespace Discord { /// Gets the default user color value. public static readonly Color Default = new Color(0); + /// Gets the teal color value + public static readonly Color Teal = new Color(0x1ABC9C); + /// Gets the dark teal color value + public static readonly Color DarkTeal = new Color(0x11806A); + /// Gets the green color value + public static readonly Color Green = new Color(0x2ECC71); + /// Gets the dark green color value + public static readonly Color DarkGreen = new Color(0x1F8B4C); + /// Gets the blue color value + public static readonly Color Blue = new Color(0x3498DB); + /// Gets the dark blue color value + public static readonly Color DarkBlue = new Color(0x206694); + /// Gets the purple color value + public static readonly Color Purple = new Color(0x9B59B6); + /// Gets the dark purple color value + public static readonly Color DarkPurple = new Color(0x71368A); + /// Gets the magenta color value + public static readonly Color Magenta = new Color(0xE91E63); + /// Gets the dark magenta color value + public static readonly Color DarkMagenta = new Color(0xAD1457); + /// Gets the gold color value + public static readonly Color Gold = new Color(0xF1C40F); + /// Gets the light orange color value + public static readonly Color LightOrange = new Color(0xC27C0E); + /// Gets the orange color value + public static readonly Color Orange = new Color(0xE67E22); + /// Gets the dark orange color value + public static readonly Color DarkOrange = new Color(0xA84300); + /// Gets the red color value + public static readonly Color Red = new Color(0xE74C3C); + /// Gets the dark red color value + public static readonly Color DarkRed = new Color(0x992D22); + /// Gets the light grey color value + public static readonly Color LightGrey = new Color(0x979C9F); + /// Gets the lighter grey color value + public static readonly Color LighterGrey = new Color(0x95A5A6); + /// Gets the dark grey color value + public static readonly Color DarkGrey = new Color(0x607D8B); + /// Gets the darker grey color value + public static readonly Color DarkerGrey = new Color(0x546E7A); /// Gets the encoded value for this color. public uint RawValue { get; } diff --git a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs index cd9516395..57cad1333 100644 --- a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs @@ -25,7 +25,7 @@ namespace Discord ChannelPermissions GetPermissions(IGuildChannel channel); /// Kicks this user from this guild. - Task KickAsync(RequestOptions options = null); + Task KickAsync(string reason = null, RequestOptions options = null); /// Modifies this user's properties in this guild. Task ModifyAsync(Action func, RequestOptions options = null); diff --git a/src/Discord.Net.Core/Entities/Users/IUser.cs b/src/Discord.Net.Core/Entities/Users/IUser.cs index 45d8862f1..e3f270f6f 100644 --- a/src/Discord.Net.Core/Entities/Users/IUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IUser.cs @@ -20,8 +20,6 @@ namespace Discord string Username { get; } /// Returns a private message channel to this user, creating one if it does not already exist. - Task GetDMChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - /// Returns a private message channel to this user, creating one if it does not already exist. - Task CreateDMChannelAsync(RequestOptions options = null); + Task GetOrCreateDMChannelAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Extensions/StringExtensions.cs b/src/Discord.Net.Core/Extensions/StringExtensions.cs new file mode 100644 index 000000000..c0ebb2626 --- /dev/null +++ b/src/Discord.Net.Core/Extensions/StringExtensions.cs @@ -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); + } +} diff --git a/src/Discord.Net.Core/Extensions/UserExtensions.cs b/src/Discord.Net.Core/Extensions/UserExtensions.cs new file mode 100644 index 000000000..0861ed33e --- /dev/null +++ b/src/Discord.Net.Core/Extensions/UserExtensions.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; + +namespace Discord +{ + public static class UserExtensions + { + public static async Task 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); + } + } +} diff --git a/src/Discord.Net.Core/Net/Rest/IRestClient.cs b/src/Discord.Net.Core/Net/Rest/IRestClient.cs index b5f136cb0..addfa9061 100644 --- a/src/Discord.Net.Core/Net/Rest/IRestClient.cs +++ b/src/Discord.Net.Core/Net/Rest/IRestClient.cs @@ -9,8 +9,8 @@ namespace Discord.Net.Rest void SetHeader(string key, string value); void SetCancelToken(CancellationToken cancelToken); - Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly = false); - Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly = false); - Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly = false); + Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly = false, string reason = null); + Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly = false, string reason = null); + Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly = false, string reason = null); } } diff --git a/src/Discord.Net.Core/RequestOptions.cs b/src/Discord.Net.Core/RequestOptions.cs index 4f5910c53..5f3a8814b 100644 --- a/src/Discord.Net.Core/RequestOptions.cs +++ b/src/Discord.Net.Core/RequestOptions.cs @@ -14,6 +14,10 @@ namespace Discord public CancellationToken CancelToken { get; set; } = CancellationToken.None; public RetryMode? RetryMode { get; set; } public bool HeaderOnly { get; internal set; } + /// + /// The reason for this action in the guild's audit log + /// + public string AuditLogReason { get; set; } internal bool IgnoreState { get; set; } internal string BucketId { get; set; } diff --git a/src/Discord.Net.Rest/API/Common/Embed.cs b/src/Discord.Net.Rest/API/Common/Embed.cs index f6325efbb..1c9fa34e2 100644 --- a/src/Discord.Net.Rest/API/Common/Embed.cs +++ b/src/Discord.Net.Rest/API/Common/Embed.cs @@ -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")] diff --git a/src/Discord.Net.Rest/API/Common/EmbedAuthor.cs b/src/Discord.Net.Rest/API/Common/EmbedAuthor.cs index e69fee6eb..4381a9da3 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedAuthor.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedAuthor.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +using System; +using Newtonsoft.Json; namespace Discord.API { diff --git a/src/Discord.Net.Rest/API/Common/EmbedFooter.cs b/src/Discord.Net.Rest/API/Common/EmbedFooter.cs index 27048972e..3dd7020d9 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedFooter.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedFooter.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +using System; +using Newtonsoft.Json; namespace Discord.API { diff --git a/src/Discord.Net.Rest/API/Common/EmbedImage.cs b/src/Discord.Net.Rest/API/Common/EmbedImage.cs index a5ef748f8..c6b3562a3 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedImage.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedImage.cs @@ -1,4 +1,5 @@ #pragma warning disable CS1591 +using System; using Newtonsoft.Json; namespace Discord.API diff --git a/src/Discord.Net.Rest/API/Common/EmbedProvider.cs b/src/Discord.Net.Rest/API/Common/EmbedProvider.cs index 8c46b10dc..1658eda1a 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedProvider.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedProvider.cs @@ -1,4 +1,5 @@ #pragma warning disable CS1591 +using System; using Newtonsoft.Json; namespace Discord.API diff --git a/src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs b/src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs index f22953a25..993beb72b 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs @@ -1,4 +1,5 @@ #pragma warning disable CS1591 +using System; using Newtonsoft.Json; namespace Discord.API diff --git a/src/Discord.Net.Rest/API/Common/EmbedVideo.cs b/src/Discord.Net.Rest/API/Common/EmbedVideo.cs index 09e933784..610cf58a8 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedVideo.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedVideo.cs @@ -1,4 +1,5 @@ #pragma warning disable CS1591 +using System; using Newtonsoft.Json; namespace Discord.API diff --git a/src/Discord.Net.Rest/API/Rest/CreateGuildBanParams.cs b/src/Discord.Net.Rest/API/Rest/CreateGuildBanParams.cs index 0c148fe70..f0432e517 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateGuildBanParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateGuildBanParams.cs @@ -4,5 +4,6 @@ namespace Discord.API.Rest internal class CreateGuildBanParams { public Optional DeleteMessageDays { get; set; } + public string Reason { get; set; } } } diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index a632e5d42..1fac66ec5 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -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(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> endpoint = () => $"guilds/{guildId}/members?limit={limit}&after={afterUserId}"; return await SendAsync>("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) { diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index 96d60b54e..f06899608 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -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 messages, RequestOptions options) + public static async Task DeleteMessagesAsync(IMessageChannel channel, BaseDiscordClient client, + IEnumerable messageIds, RequestOptions options) { - var msgs = messages.Select(x => x.Id).ToArray(); + var msgs = messageIds.ToArray(); if (msgs.Length < 100) { var args = new DeleteMessagesParams(msgs); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs index 33dedafad..ab1a6602c 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs @@ -73,7 +73,9 @@ namespace Discord.Rest => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs index 02fc5786a..859c7234c 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs @@ -86,7 +86,9 @@ namespace Discord.Rest => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index 2a2a7bf6c..54db3062e 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -64,7 +64,9 @@ namespace Discord.Rest => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); diff --git a/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs index cb2cec9a4..bb0f31166 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs @@ -43,7 +43,9 @@ namespace Discord.Rest => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index c4aa2172f..3b5788d93 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -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, diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 803951a37..0bce2f5c0 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -137,10 +137,10 @@ namespace Discord.Rest public Task> 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); diff --git a/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs index 98a191379..7b0285891 100644 --- a/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs +++ b/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs @@ -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(); } - 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() { diff --git a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs index f6db057f2..2fce5f619 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs @@ -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); /// public Task AddRoleAsync(IRole role, RequestOptions options = null) => AddRolesAsync(new[] { role }, options); diff --git a/src/Discord.Net.Rest/Entities/Users/RestUser.cs b/src/Discord.Net.Rest/Entities/Users/RestUser.cs index cded876c8..d8ade3a6b 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -54,7 +54,7 @@ namespace Discord.Rest Update(model); } - public Task CreateDMChannelAsync(RequestOptions options = null) + public Task 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 IUser.GetDMChannelAsync(CacheMode mode, RequestOptions options) - => Task.FromResult(null); - async Task IUser.CreateDMChannelAsync(RequestOptions options) - => await CreateDMChannelAsync(options).ConfigureAwait(false); + async Task IUser.GetOrCreateDMChannelAsync(RequestOptions options) + => await GetOrCreateDMChannelAsync(options); } } diff --git a/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs b/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs index ae794becc..bb44f2777 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs @@ -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."); } diff --git a/src/Discord.Net.Rest/Entities/Users/UserHelper.cs b/src/Discord.Net.Rest/Entities/Users/UserHelper.cs index 82e59227d..562cfaae8 100644 --- a/src/Discord.Net.Rest/Entities/Users/UserHelper.cs +++ b/src/Discord.Net.Rest/Entities/Users/UserHelper.cs @@ -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 CreateDMChannelAsync(IUser user, BaseDiscordClient client, diff --git a/src/Discord.Net.Rest/Extensions/EmbedBuilderExtensions.cs b/src/Discord.Net.Rest/Extensions/EmbedBuilderExtensions.cs new file mode 100644 index 000000000..cee9a136e --- /dev/null +++ b/src/Discord.Net.Rest/Extensions/EmbedBuilderExtensions.cs @@ -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()); + } +} diff --git a/src/Discord.Net.Rest/Net/DefaultRestClient.cs b/src/Discord.Net.Rest/Net/DefaultRestClient.cs index 20fbe2278..a54107829 100644 --- a/src/Discord.Net.Rest/Net/DefaultRestClient.cs +++ b/src/Discord.Net.Rest/Net/DefaultRestClient.cs @@ -62,26 +62,31 @@ namespace Discord.Net.Rest _cancelToken = cancelToken; } - public async Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly) + public async Task 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 SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly) + public async Task 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 SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly) + public async Task SendAsync(string method, string endpoint, IReadOnlyDictionary 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) { diff --git a/src/Discord.Net.Rest/Net/Queue/Requests/JsonRestRequest.cs b/src/Discord.Net.Rest/Net/Queue/Requests/JsonRestRequest.cs index 83c5e0eb5..2949bab3c 100644 --- a/src/Discord.Net.Rest/Net/Queue/Requests/JsonRestRequest.cs +++ b/src/Discord.Net.Rest/Net/Queue/Requests/JsonRestRequest.cs @@ -15,7 +15,7 @@ namespace Discord.Net.Queue public override async Task SendAsync() { - return await Client.SendAsync(Method, Endpoint, Json, Options.CancelToken, Options.HeaderOnly).ConfigureAwait(false); + return await Client.SendAsync(Method, Endpoint, Json, Options.CancelToken, Options.HeaderOnly, Options.AuditLogReason).ConfigureAwait(false); } } } diff --git a/src/Discord.Net.Rest/Net/Queue/Requests/MultipartRestRequest.cs b/src/Discord.Net.Rest/Net/Queue/Requests/MultipartRestRequest.cs index 424a5325e..c8d97bbdf 100644 --- a/src/Discord.Net.Rest/Net/Queue/Requests/MultipartRestRequest.cs +++ b/src/Discord.Net.Rest/Net/Queue/Requests/MultipartRestRequest.cs @@ -16,7 +16,7 @@ namespace Discord.Net.Queue public override async Task SendAsync() { - return await Client.SendAsync(Method, Endpoint, MultipartParams, Options.CancelToken, Options.HeaderOnly).ConfigureAwait(false); + return await Client.SendAsync(Method, Endpoint, MultipartParams, Options.CancelToken, Options.HeaderOnly, Options.AuditLogReason).ConfigureAwait(false); } } } diff --git a/src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs b/src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs index 7f358e786..8f160273a 100644 --- a/src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs +++ b/src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs @@ -28,7 +28,7 @@ namespace Discord.Net.Queue public virtual async Task 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); } } } diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs index d8b9204dc..a404b7d66 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs @@ -54,7 +54,9 @@ namespace Discord.Rpc => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs index 4508fd2c1..316b1f122 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs @@ -57,7 +57,9 @@ namespace Discord.Rpc => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs index 8c572678f..2caa988dd 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs @@ -58,7 +58,9 @@ namespace Discord.Rpc => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs b/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs index 7ed11e57d..c6b0b2fd8 100644 --- a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs +++ b/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs @@ -49,7 +49,7 @@ namespace Discord.Rpc Username = model.Username.Value; } - public Task CreateDMChannelAsync(RequestOptions options = null) + public Task 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 IUser.GetDMChannelAsync(CacheMode mode, RequestOptions options) - => Task.FromResult(null); - async Task IUser.CreateDMChannelAsync(RequestOptions options) - => await CreateDMChannelAsync(options).ConfigureAwait(false); + async Task IUser.GetOrCreateDMChannelAsync(RequestOptions options) + => await GetOrCreateDMChannelAsync(options); } } diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index 19639a418..1f33b3cc5 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -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); } diff --git a/src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs b/src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs index a12854d69..1ff5a5d9a 100644 --- a/src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs +++ b/src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs @@ -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)); } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs index 29586389c..fb302f132 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs @@ -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) diff --git a/src/Discord.Net.WebSocket/Audio/Streams/JitterBuffer.cs b/src/Discord.Net.WebSocket/Audio/Streams/JitterBuffer.cs index a5ecdea6f..10f842a9d 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/JitterBuffer.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/JitterBuffer.cs @@ -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); } } -} \ No newline at end of file +}*/ \ No newline at end of file diff --git a/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs index 43289c60e..58c4f4c70 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs @@ -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); } } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs index 2a3c03a47..f5883ad4b 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs @@ -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) diff --git a/src/Discord.Net.WebSocket/Audio/Streams/OutputStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/OutputStream.cs index 6238e93b4..cba4e3cb6 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/OutputStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/OutputStream.cs @@ -13,7 +13,8 @@ namespace Discord.Audio.Streams { _client = client; } - + + public override void WriteHeader(ushort seq, uint timestamp, bool missed) { } //Ignore public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) { cancelToken.ThrowIfCancellationRequested(); diff --git a/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs index 78f895381..ce407eada 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs @@ -33,14 +33,14 @@ namespace Discord.Audio.Streams { if (_hasHeader) throw new InvalidOperationException("Header received with no payload"); + _hasHeader = true; _nextSeq = seq; _nextTimestamp = timestamp; } - 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) { - cancellationToken.ThrowIfCancellationRequested(); - + cancelToken.ThrowIfCancellationRequested(); if (!_hasHeader) throw new InvalidOperationException("Received payload without an RTP header"); _hasHeader = false; @@ -57,6 +57,7 @@ namespace Discord.Audio.Streams Buffer.BlockCopy(_header, 0, _buffer, 0, 12); //Copy RTP header from to the buffer Buffer.BlockCopy(buffer, offset, _buffer, 12, count); + _next.WriteHeader(_nextSeq, _nextTimestamp, false); await _next.WriteAsync(_buffer, 0, count + 12).ConfigureAwait(false); } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs index b00a7f403..bacc9be47 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs @@ -10,6 +10,9 @@ namespace Discord.Audio.Streams private readonly AudioClient _client; private readonly AudioStream _next; private readonly byte[] _nonce; + private bool _hasHeader; + private ushort _nextSeq; + private uint _nextTimestamp; public SodiumEncryptStream(AudioStream next, IAudioClient client) { @@ -17,16 +20,29 @@ namespace Discord.Audio.Streams _client = (AudioClient)client; _nonce = new byte[24]; } + + public override void WriteHeader(ushort seq, uint timestamp, bool missed) + { + if (_hasHeader) + throw new InvalidOperationException("Header received with no payload"); + _nextSeq = seq; + _nextTimestamp = timestamp; + _hasHeader = true; + } public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) { cancelToken.ThrowIfCancellationRequested(); + if (!_hasHeader) + throw new InvalidOperationException("Received payload without an RTP header"); + _hasHeader = false; if (_client.SecretKey == null) return; Buffer.BlockCopy(buffer, offset, _nonce, 0, 12); //Copy nonce from RTP header count = SecretBox.Encrypt(buffer, offset + 12, count - 12, buffer, 12, _nonce, _client.SecretKey); + _next.WriteHeader(_nextSeq, _nextTimestamp, false); await _next.WriteAsync(buffer, 0, count + 12, cancelToken).ConfigureAwait(false); } diff --git a/src/Discord.Net.WebSocket/Commands/ShardedCommandContext.cs b/src/Discord.Net.WebSocket/Commands/ShardedCommandContext.cs index 627b9b390..a29c9bb70 100644 --- a/src/Discord.Net.WebSocket/Commands/ShardedCommandContext.cs +++ b/src/Discord.Net.WebSocket/Commands/ShardedCommandContext.cs @@ -2,30 +2,20 @@ namespace Discord.Commands { - public class ShardedCommandContext : ICommandContext + public class ShardedCommandContext : SocketCommandContext, ICommandContext { - public DiscordShardedClient Client { get; } - public SocketGuild Guild { get; } - public ISocketMessageChannel Channel { get; } - public SocketUser User { get; } - public SocketUserMessage Message { get; } - - public bool IsPrivate => Channel is IPrivateChannel; + public new DiscordShardedClient Client { get; } public ShardedCommandContext(DiscordShardedClient client, SocketUserMessage msg) + : base(client.GetShard(GetShardId(client, (msg.Channel as SocketGuildChannel)?.Guild)), msg) { Client = client; - Guild = (msg.Channel as SocketGuildChannel)?.Guild; - Channel = msg.Channel; - User = msg.Author; - Message = msg; } + private static int GetShardId(DiscordShardedClient client, IGuild guild) + => guild == null ? 0 : client.GetShardIdFor(guild); + //ICommandContext IDiscordClient ICommandContext.Client => Client; - IGuild ICommandContext.Guild => Guild; - IMessageChannel ICommandContext.Channel => Channel; - IUser ICommandContext.User => User; - IUserMessage ICommandContext.Message => Message; } } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 4476b78c4..b13ceca1d 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -34,7 +34,7 @@ namespace Discord.WebSocket private int _lastSeq; private ImmutableDictionary _voiceRegions; private Task _heartbeatTask, _guildDownloadTask; - private int _unavailableGuilds; + private int _unavailableGuildCount; private long _lastGuildAvailableTime, _lastMessageTime; private int _nextAudioId; private DateTimeOffset? _statusSince; @@ -60,7 +60,7 @@ namespace Discord.WebSocket internal int? HandlerTimeout { get; private set; } internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; - public new SocketSelfUser CurrentUser { get { return base.CurrentUser as SocketSelfUser; } private set { base.CurrentUser = value; } } + public new SocketSelfUser CurrentUser { get => base.CurrentUser as SocketSelfUser; private set => base.CurrentUser = value; } public IReadOnlyCollection Guilds => State.Guilds; public IReadOnlyCollection PrivateChannels => State.PrivateChannels; public IReadOnlyCollection DMChannels @@ -360,7 +360,7 @@ namespace Discord.WebSocket private async Task SendStatusAsync() { if (CurrentUser == null) - throw new InvalidOperationException("Presence data cannot be sent before the client has logged in."); + return; var game = Game; var status = Status; var statusSince = _statusSince; @@ -474,7 +474,7 @@ namespace Discord.WebSocket AddPrivateChannel(data.PrivateChannels[i], state); _sessionId = data.SessionId; - _unavailableGuilds = unavailableGuilds; + _unavailableGuildCount = unavailableGuilds; CurrentUser = currentUser; State = state; } @@ -537,10 +537,9 @@ namespace Discord.WebSocket if (guild != null) { guild.Update(State, data); - - var unavailableGuilds = _unavailableGuilds; - if (unavailableGuilds != 0) - _unavailableGuilds = unavailableGuilds - 1; + + if (_unavailableGuildCount != 0) + _unavailableGuildCount--; await GuildAvailableAsync(guild).ConfigureAwait(false); if (guild.DownloadedMemberCount >= guild.MemberCount && !guild.DownloaderPromise.IsCompleted) @@ -622,7 +621,7 @@ namespace Discord.WebSocket var before = guild.Clone(); guild.Update(State, data); //This is treated as an extension of GUILD_AVAILABLE - _unavailableGuilds--; + _unavailableGuildCount--; _lastGuildAvailableTime = Environment.TickCount; await GuildAvailableAsync(guild).ConfigureAwait(false); await TimedInvokeAsync(_guildUpdatedEvent, nameof(GuildUpdated), before, guild).ConfigureAwait(false); @@ -646,7 +645,7 @@ namespace Discord.WebSocket if (guild != null) { await GuildUnavailableAsync(guild).ConfigureAwait(false); - _unavailableGuilds++; + _unavailableGuildCount++; } else { @@ -1212,10 +1211,10 @@ namespace Discord.WebSocket var data = (payload as JToken).ToObject(_serializer); if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) { - SocketUserMessage cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; + var cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; bool isCached = cachedMsg != null; var user = await channel.GetUserAsync(data.UserId, CacheMode.CacheOnly); - SocketReaction reaction = SocketReaction.Create(data, channel, cachedMsg, Optional.Create(user)); + var reaction = SocketReaction.Create(data, channel, cachedMsg, Optional.Create(user)); var cacheable = new Cacheable(cachedMsg, data.MessageId, isCached, async () => await channel.GetMessageAsync(data.MessageId) as IUserMessage); cachedMsg?.AddReaction(reaction); @@ -1236,10 +1235,10 @@ namespace Discord.WebSocket var data = (payload as JToken).ToObject(_serializer); if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) { - SocketUserMessage cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; + var cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; bool isCached = cachedMsg != null; var user = await channel.GetUserAsync(data.UserId, CacheMode.CacheOnly); - SocketReaction reaction = SocketReaction.Create(data, channel, cachedMsg, Optional.Create(user)); + var reaction = SocketReaction.Create(data, channel, cachedMsg, Optional.Create(user)); var cacheable = new Cacheable(cachedMsg, data.MessageId, isCached, async () => await channel.GetMessageAsync(data.MessageId) as IUserMessage); cachedMsg?.RemoveReaction(reaction); @@ -1260,7 +1259,7 @@ namespace Discord.WebSocket var data = (payload as JToken).ToObject(_serializer); if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) { - SocketUserMessage cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; + var cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; bool isCached = cachedMsg != null; var cacheable = new Cacheable(cachedMsg, data.MessageId, isCached, async () => await channel.GetMessageAsync(data.MessageId) as IUserMessage); @@ -1289,7 +1288,7 @@ namespace Discord.WebSocket return; } - foreach (var id in data.Ids) + foreach (ulong id in data.Ids) { var msg = SocketChannelHelper.RemoveMessage(channel, this, id); bool isCached = msg != null; @@ -1542,7 +1541,7 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Heartbeat Started").ConfigureAwait(false); while (!cancelToken.IsCancellationRequested) { - var now = Environment.TickCount; + int now = Environment.TickCount; //Did server respond to our last heartbeat, or are we still receiving messages (long load?) if (_heartbeatTimes.Count != 0 && (now - _lastMessageTime) > intervalMillis) @@ -1589,7 +1588,7 @@ namespace Discord.WebSocket try { await logger.DebugAsync("GuildDownloader Started").ConfigureAwait(false); - while ((_unavailableGuilds != 0) && (Environment.TickCount - _lastGuildAvailableTime < 2000)) + while ((_unavailableGuildCount != 0) && (Environment.TickCount - _lastGuildAvailableTime < 2000)) await Task.Delay(500, cancelToken).ConfigureAwait(false); await logger.DebugAsync("GuildDownloader Stopped").ConfigureAwait(false); } @@ -1634,6 +1633,9 @@ namespace Discord.WebSocket { var channel = SocketChannel.CreatePrivate(this, state, model); state.AddChannel(channel as SocketChannel); + if (channel is SocketDMChannel dm) + dm.Recipient.GlobalUser.DMChannel = dm; + return channel; } internal ISocketPrivateChannel RemovePrivateChannel(ulong id) @@ -1641,6 +1643,9 @@ namespace Discord.WebSocket var channel = State.RemoveChannel(id) as ISocketPrivateChannel; if (channel != null) { + if (channel is SocketDMChannel dmChannel) + dmChannel.Recipient.GlobalUser.DMChannel = null; + foreach (var recipient in channel.Recipients) recipient.GlobalUser.RemoveRef(this); } @@ -1744,27 +1749,27 @@ namespace Discord.WebSocket private async Task UnknownGlobalUserAsync(string evnt, ulong userId) { - var details = $"{evnt} User={userId}"; + string details = $"{evnt} User={userId}"; await _gatewayLogger.WarningAsync($"Unknown User ({details}).").ConfigureAwait(false); } private async Task UnknownChannelUserAsync(string evnt, ulong userId, ulong channelId) { - var details = $"{evnt} User={userId} Channel={channelId}"; + string details = $"{evnt} User={userId} Channel={channelId}"; await _gatewayLogger.WarningAsync($"Unknown User ({details}).").ConfigureAwait(false); } private async Task UnknownGuildUserAsync(string evnt, ulong userId, ulong guildId) { - var details = $"{evnt} User={userId} Guild={guildId}"; + string details = $"{evnt} User={userId} Guild={guildId}"; await _gatewayLogger.WarningAsync($"Unknown User ({details}).").ConfigureAwait(false); } private async Task IncompleteGuildUserAsync(string evnt, ulong userId, ulong guildId) { - var details = $"{evnt} User={userId} Guild={guildId}"; + string details = $"{evnt} User={userId} Guild={guildId}"; await _gatewayLogger.DebugAsync($"User has not been downloaded ({details}).").ConfigureAwait(false); } private async Task UnknownChannelAsync(string evnt, ulong channelId) { - var details = $"{evnt} Channel={channelId}"; + string details = $"{evnt} Channel={channelId}"; await _gatewayLogger.WarningAsync($"Unknown Channel ({details}).").ConfigureAwait(false); } private async Task UnknownChannelAsync(string evnt, ulong channelId, ulong guildId) @@ -1774,22 +1779,22 @@ namespace Discord.WebSocket await UnknownChannelAsync(evnt, channelId).ConfigureAwait(false); return; } - var details = $"{evnt} Channel={channelId} Guild={guildId}"; + string details = $"{evnt} Channel={channelId} Guild={guildId}"; await _gatewayLogger.WarningAsync($"Unknown Channel ({details}).").ConfigureAwait(false); } private async Task UnknownRoleAsync(string evnt, ulong roleId, ulong guildId) { - var details = $"{evnt} Role={roleId} Guild={guildId}"; + string details = $"{evnt} Role={roleId} Guild={guildId}"; await _gatewayLogger.WarningAsync($"Unknown Role ({details}).").ConfigureAwait(false); } private async Task UnknownGuildAsync(string evnt, ulong guildId) { - var details = $"{evnt} Guild={guildId}"; + string details = $"{evnt} Guild={guildId}"; await _gatewayLogger.WarningAsync($"Unknown Guild ({details}).").ConfigureAwait(false); } private async Task UnsyncedGuildAsync(string evnt, ulong guildId) { - var details = $"{evnt} Guild={guildId}"; + string details = $"{evnt} Guild={guildId}"; await _gatewayLogger.DebugAsync($"Unsynced Guild ({details}).").ConfigureAwait(false); } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs index efc08657a..36d8e8e66 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs @@ -77,7 +77,9 @@ namespace Discord.WebSocket => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index ca5567922..19ea0d647 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -105,7 +105,9 @@ namespace Discord.WebSocket => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index 0f6d1d0a2..2d4f65253 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -81,7 +81,9 @@ namespace Discord.WebSocket => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 5358605c8..aae18be36 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -281,10 +281,10 @@ namespace Discord.WebSocket public Task> 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); diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs index 9f58f1cf6..35bee9e68 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs @@ -29,5 +29,27 @@ namespace Discord.WebSocket emote = new Emoji(model.Emoji.Name); return new SocketReaction(channel, model.MessageId, message, model.UserId, user, emote); } + + public override bool Equals(object other) + { + if (other == null) return false; + if (other == this) return true; + + var otherReaction = other as SocketReaction; + if (otherReaction == null) return false; + + return UserId == otherReaction.UserId && MessageId == otherReaction.MessageId && Emote.Equals(otherReaction.Emote); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = UserId.GetHashCode(); + hashCode = (hashCode * 397) ^ MessageId.GetHashCode(); + hashCode = (hashCode * 397) ^ Emote.GetHashCode(); + return hashCode; + } + } } } diff --git a/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs b/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs index 57d913317..7d24d8e1c 100644 --- a/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs +++ b/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs @@ -1,6 +1,8 @@ using Discord.Rest; using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Threading.Tasks; using Model = Discord.API.Role; @@ -22,6 +24,8 @@ namespace Discord.WebSocket public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public bool IsEveryone => Id == Guild.Id; public string Mention => MentionUtils.MentionRole(Id); + public IEnumerable Members + => Guild.Users.Where(x => x.Roles.Any(r => r.Id == Id)); internal SocketRole(SocketGuild guild, ulong id) : base(guild.Discord, id) diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs index 0cd5f749e..3117eb14c 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Linq; using Model = Discord.API.User; using PresenceModel = Discord.API.Presence; @@ -51,6 +52,7 @@ namespace Discord.WebSocket internal void Update(ClientState state, PresenceModel model) { Presence = SocketPresence.Create(model); + DMChannel = state.DMChannels.FirstOrDefault(x => x.Recipient.Id == Id); } internal new SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser; diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index b92559a40..844b0c7f4 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -122,8 +122,8 @@ namespace Discord.WebSocket public Task ModifyAsync(Action func, RequestOptions options = null) => UserHelper.ModifyAsync(this, Discord, func, options); - public Task KickAsync(RequestOptions options = null) - => UserHelper.KickAsync(this, Discord, options); + public Task KickAsync(string reason = null, RequestOptions options = null) + => UserHelper.KickAsync(this, Discord, reason, options); /// public Task AddRoleAsync(IRole role, RequestOptions options = null) => AddRolesAsync(new[] { role }, options); @@ -146,11 +146,7 @@ namespace Discord.WebSocket IGuild IGuildUser.Guild => Guild; ulong IGuildUser.GuildId => Guild.Id; IReadOnlyCollection IGuildUser.RoleIds => _roleIds; - - //IUser - Task IUser.GetDMChannelAsync(CacheMode mode, RequestOptions options) - => Task.FromResult(GlobalUser.DMChannel); - + //IVoiceState IVoiceChannel IVoiceState.VoiceChannel => VoiceChannel; } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index 1b599bf7e..a0c78b93f 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -53,10 +53,10 @@ namespace Discord.WebSocket hasChanges = true; } return hasChanges; - } + } - public Task CreateDMChannelAsync(RequestOptions options = null) - => UserHelper.CreateDMChannelAsync(this, Discord, options); + public async Task GetOrCreateDMChannelAsync(RequestOptions options = null) + => GlobalUser.DMChannel ?? await UserHelper.CreateDMChannelAsync(this, Discord, options) as IDMChannel; public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); @@ -64,11 +64,5 @@ namespace Discord.WebSocket public override string ToString() => $"{Username}#{Discriminator}"; private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; internal SocketUser Clone() => MemberwiseClone() as SocketUser; - - //IUser - Task IUser.GetDMChannelAsync(CacheMode mode, RequestOptions options) - => Task.FromResult(GlobalUser.DMChannel); - async Task IUser.CreateDMChannelAsync(RequestOptions options) - => await CreateDMChannelAsync(options).ConfigureAwait(false); } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs index c34f866cb..78a29639b 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs @@ -47,7 +47,7 @@ namespace Discord.WebSocket 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."); } diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index 2a637dbfb..864083599 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,39 +2,39 @@ Discord.Net - 1.0.0-rc3$suffix$ + 1.0.1$suffix$ Discord.Net RogueException RogueException - An aynchronous API wrapper for Discord. This metapackage includes all of the optional Discord.Net components. + An asynchronous API wrapper for Discord. This metapackage includes all of the optional Discord.Net components. discord;discordapp https://github.com/RogueException/Discord.Net http://opensource.org/licenses/MIT false - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + diff --git a/test/Discord.Net.Tests/Net/CachedRestClient.cs b/test/Discord.Net.Tests/Net/CachedRestClient.cs index f4b3bb279..4bc8a386a 100644 --- a/test/Discord.Net.Tests/Net/CachedRestClient.cs +++ b/test/Discord.Net.Tests/Net/CachedRestClient.cs @@ -66,7 +66,7 @@ namespace Discord.Net _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; } - public async Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly) + public async Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly, string reason = null) { if (method != "GET") throw new InvalidOperationException("This RestClient only supports GET requests."); @@ -75,11 +75,11 @@ namespace Discord.Net var bytes = await _blobCache.DownloadUrl(uri, _headers); return new RestResponse(HttpStatusCode.OK, _headers, new MemoryStream(bytes)); } - public Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly) + public Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly, string reason = null) { throw new InvalidOperationException("This RestClient does not support payloads."); } - public Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly) + public Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly, string reason = null) { throw new InvalidOperationException("This RestClient does not support multipart requests."); }