diff --git a/.gitignore b/.gitignore index d6c4cf780..d7bf0ef19 100644 --- a/.gitignore +++ b/.gitignore @@ -200,3 +200,4 @@ project.lock.json /test/Discord.Net.Tests/config.json /docs/_build *.pyc +/.editorconfig diff --git a/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs b/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs index 38130aa76..834cf8162 100644 --- a/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs @@ -7,6 +7,6 @@ namespace Discord.Commands { public abstract class PreconditionAttribute : Attribute { - public abstract void CheckPermissions(PreconditionContext context); + public abstract Task CheckPermissions(IMessage context); } } diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireDMAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireDMAttribute.cs index 32d863a0e..ac621a602 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireDMAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireDMAttribute.cs @@ -7,10 +7,12 @@ namespace Discord.Commands { public class RequireDMAttribute : PreconditionAttribute { - public override void CheckPermissions(PreconditionContext context) + public override Task CheckPermissions(IMessage context) { - if (context.Message.Channel is IGuildChannel) - context.Handled = true; + if (context.Channel is IGuildChannel) + return Task.FromResult(PreconditionResult.FromError("Command must be used in a DM")); + + return Task.FromResult(PreconditionResult.FromSuccess()); } } } diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireGuildAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireGuildAttribute.cs index d1615f299..58de17222 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireGuildAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireGuildAttribute.cs @@ -7,10 +7,12 @@ namespace Discord.Commands { public class RequireGuildAttribute : PreconditionAttribute { - public override void CheckPermissions(PreconditionContext context) + public override Task CheckPermissions(IMessage context) { - if (!(context.Message.Channel is IGuildChannel)) - context.Handled = true; + if (!(context.Channel is IGuildChannel)) + return Task.FromResult(PreconditionResult.FromError("Command must be used in a guild")); + + return Task.FromResult(PreconditionResult.FromSuccess()); } } } diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireRoleAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireRoleAttribute.cs new file mode 100644 index 000000000..7412315f3 --- /dev/null +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireRoleAttribute.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + public class RequireRoleAttribute : RequireGuildAttribute + { + public string Role { get; set; } + public StringComparer Comparer { get; set; } + + public RequireRoleAttribute(string roleName) + { + Role = roleName; + Comparer = StringComparer.Ordinal; + } + + public RequireRoleAttribute(string roleName, StringComparer comparer) + { + Role = roleName; + Comparer = comparer; + } + + public override async Task CheckPermissions(IMessage context) + { + var result = await base.CheckPermissions(context).ConfigureAwait(false); + + if (!result.IsSuccess) + return result; + + var author = (context.Author as IGuildUser); + + if (author != null) + { + var hasRole = author.Roles.Any(x => Comparer.Compare(x.Name, Role) == 0); + + if (!hasRole) + return PreconditionResult.FromError($"User does not have the '{Role}' role."); + } + + return PreconditionResult.FromSuccess(); + } + } +} diff --git a/src/Discord.Net.Commands/Command.cs b/src/Discord.Net.Commands/Command.cs index 4c7e6e7cd..482622d6a 100644 --- a/src/Discord.Net.Commands/Command.cs +++ b/src/Discord.Net.Commands/Command.cs @@ -19,7 +19,7 @@ namespace Discord.Commands public string Text { get; } public Module Module { get; } public IReadOnlyList Parameters { get; } - public IReadOnlyList Permissions { get; } + public IReadOnlyList Preconditions { get; } internal Command(Module module, object instance, CommandAttribute attribute, MethodInfo methodInfo, string groupPrefix) { @@ -38,22 +38,20 @@ namespace Discord.Commands Synopsis = synopsis.Text; Parameters = BuildParameters(methodInfo); - Permissions = BuildPermissions(methodInfo); + Preconditions = BuildPreconditions(methodInfo); _action = BuildAction(methodInfo); } - public bool MeetsPreconditions(IMessage message) + public async Task CheckPreconditions(IMessage context) { - var context = new PreconditionContext(this, message); - - foreach (PreconditionAttribute permission in Permissions) + foreach (PreconditionAttribute permission in Preconditions) { - permission.CheckPermissions(context); - if (context.Handled) - return false; + var result = await permission.CheckPermissions(context).ConfigureAwait(false); + if (!result.IsSuccess) + return result; } - return true; + return PreconditionResult.FromSuccess(); } public async Task Parse(IMessage msg, SearchResult searchResult) @@ -68,8 +66,9 @@ namespace Discord.Commands if (!parseResult.IsSuccess) return ExecuteResult.FromError(parseResult); - if (!MeetsPreconditions(msg)) // TODO: should we have to check this here, or leave it entirely to the bot dev? - return ExecuteResult.FromError(CommandError.UnmetPrecondition, "Permissions check failed"); + var precondition = await CheckPreconditions(msg).ConfigureAwait(false); + if (!precondition.IsSuccess) // TODO: should we have to check this here, or leave it entirely to the bot dev? + return ExecuteResult.FromError(precondition); try { @@ -82,7 +81,7 @@ namespace Discord.Commands } } - private IReadOnlyList BuildPermissions(MethodInfo methodInfo) + private IReadOnlyList BuildPreconditions(MethodInfo methodInfo) { return methodInfo.GetCustomAttributes().ToImmutableArray(); } diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index dbf385a5c..9446d5700 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -208,16 +208,19 @@ namespace Discord.Commands if (!searchResult.IsSuccess) return searchResult; - // TODO: this logic is for users who don't manually search/execute: should we keep it? - - IReadOnlyList commands = searchResult.Commands - .Where(x => x.MeetsPreconditions(message)).ToImmutableArray(); - - if (commands.Count == 0 && searchResult.Commands.Count > 0) - return ParseResult.FromError(CommandError.UnmetPrecondition, "Unmet precondition"); + var commands = searchResult.Commands; for (int i = commands.Count - 1; i >= 0; i--) { + var preconditionResult = await commands[i].CheckPreconditions(message); + if (!preconditionResult.IsSuccess) + { + if (commands.Count == 1) + return preconditionResult; + else + continue; + } + var parseResult = await commands[i].Parse(message, searchResult); if (!parseResult.IsSuccess) { diff --git a/src/Discord.Net.Commands/Context/PreconditionContext.cs b/src/Discord.Net.Commands/Context/PreconditionContext.cs deleted file mode 100644 index 7a1ef118e..000000000 --- a/src/Discord.Net.Commands/Context/PreconditionContext.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Discord.Commands -{ - public class PreconditionContext - { - public Command Command { get; internal set; } - public IMessage Message { get; internal set; } - - public bool Handled { get; set; } - - internal PreconditionContext(Command command, IMessage message) - { - Command = command; - Message = message; - - Handled = false; - } - } -} diff --git a/src/Discord.Net.Commands/Results/ExecuteResult.cs b/src/Discord.Net.Commands/Results/ExecuteResult.cs index a06e8dd99..60d47c7cb 100644 --- a/src/Discord.Net.Commands/Results/ExecuteResult.cs +++ b/src/Discord.Net.Commands/Results/ExecuteResult.cs @@ -28,6 +28,8 @@ namespace Discord.Commands => new ExecuteResult(ex, CommandError.Exception, ex.Message); internal static ExecuteResult FromError(ParseResult result) => new ExecuteResult(null, result.Error, result.ErrorReason); + internal static ExecuteResult FromError(PreconditionResult result) + => new ExecuteResult(null, result.Error, result.ErrorReason); 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 new file mode 100644 index 000000000..9d36ba23f --- /dev/null +++ b/src/Discord.Net.Commands/Results/PreconditionResult.cs @@ -0,0 +1,27 @@ +using System.Diagnostics; + +namespace Discord.Commands +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public struct PreconditionResult : IResult + { + public CommandError? Error { get; } + public string ErrorReason { get; } + + public bool IsSuccess => !Error.HasValue; + + private PreconditionResult(CommandError? error, string errorReason) + { + Error = error; + ErrorReason = errorReason; + } + + internal static PreconditionResult FromSuccess() + => new PreconditionResult(null, null); + internal static PreconditionResult FromError(string reason) + => new PreconditionResult(CommandError.UnmetPrecondition, reason); + + public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; + private string DebuggerDisplay => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; + } +}