From c76ffdc8983a29300d9d762e0153ada58bc65b86 Mon Sep 17 00:00:00 2001 From: FiniteReality Date: Fri, 6 Jan 2017 14:37:34 +0000 Subject: [PATCH] Allow commands to return a Task This allows bot developers to centralize command result logic by using result data whether the command as successful or not. Example usage: ```csharp var _result = await Commands.ExecuteAsync(context, argPos); if (_result is RuntimeResult result) { await message.Channel.SendMessageAsync(result.Reason); } else if (!_result.IsSuccess) { // Previous error handling } ``` The RuntimeResult class can be subclassed too, for example: ```csharp var _result = await Commands.ExecuteAsync(context, argPos); if (_result is MySubclassedResult result) { var builder = new EmbedBuilder(); for (var pair in result.Data) { builder.AddField(pair.Key, pair.Value, true); } await message.Channel.SendMessageAsync("", embed: builder); } else if (_result is RuntimeResult result) { await message.Channel.SendMessageAsync(result.Reason); } else if (!_result.IsSuccess) { // Previous error handling } ``` --- .../Builders/ModuleClassBuilder.cs | 24 ++++++--- src/Discord.Net.Commands/CommandError.cs | 5 +- src/Discord.Net.Commands/CommandMatch.cs | 4 +- src/Discord.Net.Commands/Info/CommandInfo.cs | 52 +++++++++++++------ .../Results/RuntimeResult.cs | 34 ++++++++++++ 5 files changed, 93 insertions(+), 26 deletions(-) create mode 100644 src/Discord.Net.Commands/Results/RuntimeResult.cs diff --git a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs index 401396900..9393ccdb2 100644 --- a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -191,22 +191,34 @@ namespace Discord.Commands var createInstance = ReflectionUtils.CreateBuilder(typeInfo, service); - builder.Callback = async (ctx, args, map, cmd) => + 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(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(cmd); (instance as IDisposable)?.Dispose(); } - }; + } + + builder.Callback = ExecuteCallback; } private static void BuildParameter(ParameterBuilder builder, System.Reflection.ParameterInfo paramInfo, int position, int count, CommandService service) @@ -284,7 +296,7 @@ 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; } 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 d2bd9ef03..d922a2229 100644 --- a/src/Discord.Net.Commands/CommandMatch.cs +++ b/src/Discord.Net.Commands/CommandMatch.cs @@ -20,9 +20,9 @@ namespace Discord.Commands => Command.CheckPreconditionsAsync(context, 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) + 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/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index a97bd4fa5..1dcff46d5 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -35,14 +35,14 @@ namespace Discord.Commands 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) => { @@ -104,7 +104,7 @@ namespace Discord.Commands return PreconditionResult.FromSuccess(); } - + public async Task ParseAsync(ICommandContext context, int startIndex, SearchResult searchResult, PreconditionResult preconditionResult = null, IServiceProvider services = null) { services = services ?? EmptyServiceProvider.Instance; @@ -113,35 +113,35 @@ namespace Discord.Commands return ParseResult.FromError(searchResult); if (preconditionResult != null && !preconditionResult.IsSuccess) return ParseResult.FromError(preconditionResult); - + string input = searchResult.Text.Substring(startIndex); 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; @@ -161,10 +161,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); }); @@ -178,12 +177,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, this).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) { @@ -200,8 +213,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) @@ -238,7 +256,7 @@ 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 diff --git a/src/Discord.Net.Commands/Results/RuntimeResult.cs b/src/Discord.Net.Commands/Results/RuntimeResult.cs new file mode 100644 index 000000000..5e13bb27a --- /dev/null +++ b/src/Discord.Net.Commands/Results/RuntimeResult.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; + +namespace Discord.Commands +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RuntimeResult : IResult + { + private 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 static RuntimeResult FromSuccess(string reason = null) => + new RuntimeResult(null, reason); + public static RuntimeResult FromError(string reason) => + new RuntimeResult(CommandError.Unsuccessful, reason); + public static RuntimeResult FromError(IResult result) => + new RuntimeResult(result.Error, result.ErrorReason); + + public override string ToString() => Reason ?? (IsSuccess ? "Successful" : "Unsuccessful"); + private string DebuggerDisplay => IsSuccess ? $"Success: {Reason ?? "No Reason"}" : $"{Error}: {Reason}"; + } +}