| @@ -200,3 +200,4 @@ project.lock.json | |||||
| /test/Discord.Net.Tests/config.json | /test/Discord.Net.Tests/config.json | ||||
| /docs/_build | /docs/_build | ||||
| *.pyc | *.pyc | ||||
| /.editorconfig | |||||
| @@ -7,6 +7,6 @@ namespace Discord.Commands | |||||
| { | { | ||||
| public abstract class PreconditionAttribute : Attribute | public abstract class PreconditionAttribute : Attribute | ||||
| { | { | ||||
| public abstract void CheckPermissions(PreconditionContext context); | |||||
| public abstract Task<PreconditionResult> CheckPermissions(IMessage context); | |||||
| } | } | ||||
| } | } | ||||
| @@ -7,10 +7,12 @@ namespace Discord.Commands | |||||
| { | { | ||||
| public class RequireDMAttribute : PreconditionAttribute | public class RequireDMAttribute : PreconditionAttribute | ||||
| { | { | ||||
| public override void CheckPermissions(PreconditionContext context) | |||||
| public override Task<PreconditionResult> 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()); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -7,10 +7,12 @@ namespace Discord.Commands | |||||
| { | { | ||||
| public class RequireGuildAttribute : PreconditionAttribute | public class RequireGuildAttribute : PreconditionAttribute | ||||
| { | { | ||||
| public override void CheckPermissions(PreconditionContext context) | |||||
| public override Task<PreconditionResult> 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()); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -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<PreconditionResult> 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(); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -19,7 +19,7 @@ namespace Discord.Commands | |||||
| public string Text { get; } | public string Text { get; } | ||||
| public Module Module { get; } | public Module Module { get; } | ||||
| public IReadOnlyList<CommandParameter> Parameters { get; } | public IReadOnlyList<CommandParameter> Parameters { get; } | ||||
| public IReadOnlyList<PreconditionAttribute> Permissions { get; } | |||||
| public IReadOnlyList<PreconditionAttribute> Preconditions { get; } | |||||
| internal Command(Module module, object instance, CommandAttribute attribute, MethodInfo methodInfo, string groupPrefix) | internal Command(Module module, object instance, CommandAttribute attribute, MethodInfo methodInfo, string groupPrefix) | ||||
| { | { | ||||
| @@ -38,22 +38,20 @@ namespace Discord.Commands | |||||
| Synopsis = synopsis.Text; | Synopsis = synopsis.Text; | ||||
| Parameters = BuildParameters(methodInfo); | Parameters = BuildParameters(methodInfo); | ||||
| Permissions = BuildPermissions(methodInfo); | |||||
| Preconditions = BuildPreconditions(methodInfo); | |||||
| _action = BuildAction(methodInfo); | _action = BuildAction(methodInfo); | ||||
| } | } | ||||
| public bool MeetsPreconditions(IMessage message) | |||||
| public async Task<PreconditionResult> 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<ParseResult> Parse(IMessage msg, SearchResult searchResult) | public async Task<ParseResult> Parse(IMessage msg, SearchResult searchResult) | ||||
| @@ -68,8 +66,9 @@ namespace Discord.Commands | |||||
| if (!parseResult.IsSuccess) | if (!parseResult.IsSuccess) | ||||
| return ExecuteResult.FromError(parseResult); | 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 | try | ||||
| { | { | ||||
| @@ -82,7 +81,7 @@ namespace Discord.Commands | |||||
| } | } | ||||
| } | } | ||||
| private IReadOnlyList<PreconditionAttribute> BuildPermissions(MethodInfo methodInfo) | |||||
| private IReadOnlyList<PreconditionAttribute> BuildPreconditions(MethodInfo methodInfo) | |||||
| { | { | ||||
| return methodInfo.GetCustomAttributes<PreconditionAttribute>().ToImmutableArray(); | return methodInfo.GetCustomAttributes<PreconditionAttribute>().ToImmutableArray(); | ||||
| } | } | ||||
| @@ -208,16 +208,19 @@ namespace Discord.Commands | |||||
| if (!searchResult.IsSuccess) | if (!searchResult.IsSuccess) | ||||
| return searchResult; | return searchResult; | ||||
| // TODO: this logic is for users who don't manually search/execute: should we keep it? | |||||
| IReadOnlyList<Command> 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--) | 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); | var parseResult = await commands[i].Parse(message, searchResult); | ||||
| if (!parseResult.IsSuccess) | if (!parseResult.IsSuccess) | ||||
| { | { | ||||
| @@ -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; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -28,6 +28,8 @@ namespace Discord.Commands | |||||
| => new ExecuteResult(ex, CommandError.Exception, ex.Message); | => new ExecuteResult(ex, CommandError.Exception, ex.Message); | ||||
| internal static ExecuteResult FromError(ParseResult result) | internal static ExecuteResult FromError(ParseResult result) | ||||
| => new ExecuteResult(null, result.Error, result.ErrorReason); | => 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}"; | public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; | ||||
| private string DebuggerDisplay => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; | private string DebuggerDisplay => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; | ||||
| @@ -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}"; | |||||
| } | |||||
| } | |||||