@@ -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}"; | |||||
} | |||||
} |