using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Reflection; using System.Runtime.ExceptionServices; using System.Text; using System.Threading.Tasks; namespace Discord.Interactions { /// /// Represents a cached method execution delegate. /// /// Execution context that will be injected into the module class. /// Method arguments array. /// Service collection for initializing the module. /// Command info class of the executed method. /// /// A task representing the execution operation. /// public delegate Task ExecuteCallback (IInteractionContext context, object[] args, IServiceProvider serviceProvider, ICommandInfo commandInfo); /// /// The base information class for commands. /// /// The type of that is used by this command type. public abstract class CommandInfo : ICommandInfo where TParameter : class, IParameterInfo { private readonly ExecuteCallback _action; private readonly ILookup _groupedPreconditions; internal IReadOnlyDictionary _parameterDictionary { get; } /// public ModuleInfo Module { get; } /// public InteractionService CommandService { get; } /// public string Name { get; } /// public string MethodName { get; } /// public virtual bool IgnoreGroupNames { get; } /// public abstract bool SupportsWildCards { get; } /// public bool IsTopLevelCommand { get; } /// public RunMode RunMode { get; } /// public IReadOnlyCollection Attributes { get; } /// public IReadOnlyCollection Preconditions { get; } /// public abstract IReadOnlyList Parameters { get; } public bool TreatNameAsRegex { get; } internal CommandInfo(Builders.ICommandBuilder builder, ModuleInfo module, InteractionService commandService) { CommandService = commandService; Module = module; Name = builder.Name; MethodName = builder.MethodName; IgnoreGroupNames = builder.IgnoreGroupNames; IsTopLevelCommand = IgnoreGroupNames || CheckTopLevel(Module); RunMode = builder.RunMode != RunMode.Default ? builder.RunMode : commandService._runMode; Attributes = builder.Attributes.ToImmutableArray(); Preconditions = builder.Preconditions.ToImmutableArray(); TreatNameAsRegex = builder.TreatNameAsRegex && SupportsWildCards; _action = builder.Callback; _groupedPreconditions = builder.Preconditions.ToLookup(x => x.Group, x => x, StringComparer.Ordinal); _parameterDictionary = Parameters?.ToDictionary(x => x.Name, x => x).ToImmutableDictionary(); } /// public virtual async Task ExecuteAsync(IInteractionContext context, IServiceProvider services) { switch (RunMode) { case RunMode.Sync: return await ExecuteInternalAsync(context, services).ConfigureAwait(false); case RunMode.Async: _ = Task.Run(async () => { await ExecuteInternalAsync(context, services).ConfigureAwait(false); }); break; default: throw new InvalidOperationException($"RunMode {RunMode} is not supported."); } return ExecuteResult.FromSuccess(); } protected abstract Task ParseArgumentsAsync(IInteractionContext context, IServiceProvider services); private async Task ExecuteInternalAsync(IInteractionContext context, IServiceProvider services) { await CommandService._cmdLogger.DebugAsync($"Executing {GetLogString(context)}").ConfigureAwait(false); using var scope = services?.CreateScope(); if (CommandService._autoServiceScopes) services = scope?.ServiceProvider ?? EmptyServiceProvider.Instance; try { var preconditionResult = await CheckPreconditionsAsync(context, services).ConfigureAwait(false); if (!preconditionResult.IsSuccess) return await InvokeEventAndReturn(context, preconditionResult).ConfigureAwait(false); var argsResult = await ParseArgumentsAsync(context, services).ConfigureAwait(false); if (!argsResult.IsSuccess) return await InvokeEventAndReturn(context, argsResult).ConfigureAwait(false); if(argsResult is not ParseResult parseResult) return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Complex command parsing failed for an unknown reason."); var args = parseResult.Args; var index = 0; foreach (var parameter in Parameters) { var result = await parameter.CheckPreconditionsAsync(context, args[index++], services).ConfigureAwait(false); if (!result.IsSuccess) return await InvokeEventAndReturn(context, result).ConfigureAwait(false); } var task = _action(context, args, services, this); if (task is Task resultTask) { var result = await resultTask.ConfigureAwait(false); await InvokeModuleEvent(context, result).ConfigureAwait(false); if (result is RuntimeResult or ExecuteResult) return result; } else { await task.ConfigureAwait(false); return await InvokeEventAndReturn(context, ExecuteResult.FromSuccess()).ConfigureAwait(false); } return await InvokeEventAndReturn(context, ExecuteResult.FromError(InteractionCommandError.Unsuccessful, "Command execution failed for an unknown reason")).ConfigureAwait(false); } catch (Exception ex) { var originalEx = ex; while (ex is TargetInvocationException) ex = ex.InnerException; var interactionException = new InteractionException(this, context, ex); await Module.CommandService._cmdLogger.ErrorAsync(ex).ConfigureAwait(false); var result = ExecuteResult.FromError(ex); await InvokeModuleEvent(context, result).ConfigureAwait(false); if (Module.CommandService._throwOnError) { if (ex == originalEx) throw; else ExceptionDispatchInfo.Capture(ex).Throw(); } return result; } finally { await CommandService._cmdLogger.VerboseAsync($"Executed {GetLogString(context)}").ConfigureAwait(false); } } protected abstract Task InvokeModuleEvent(IInteractionContext context, IResult result); protected abstract string GetLogString(IInteractionContext context); /// public async Task CheckPreconditionsAsync(IInteractionContext context, IServiceProvider services) { async Task CheckGroups(ILookup preconditions, string type) { foreach (IGrouping preconditionGroup in preconditions) { if (preconditionGroup.Key == null) { foreach (PreconditionAttribute precondition in preconditionGroup) { var result = await precondition.CheckRequirementsAsync(context, this, services).ConfigureAwait(false); if (!result.IsSuccess) return result; } } else { var results = new List(); foreach (PreconditionAttribute precondition in preconditionGroup) results.Add(await precondition.CheckRequirementsAsync(context, this, services).ConfigureAwait(false)); if (!results.Any(p => p.IsSuccess)) return PreconditionGroupResult.FromError($"{type} precondition group {preconditionGroup.Key} failed.", results); } } return PreconditionGroupResult.FromSuccess(); } var moduleResult = await CheckGroups(Module.GroupedPreconditions, "Module").ConfigureAwait(false); if (!moduleResult.IsSuccess) return moduleResult; var commandResult = await CheckGroups(_groupedPreconditions, "Command").ConfigureAwait(false); return !commandResult.IsSuccess ? commandResult : PreconditionResult.FromSuccess(); } protected async Task InvokeEventAndReturn(IInteractionContext context, T result) where T : IResult { await InvokeModuleEvent(context, result).ConfigureAwait(false); return result; } private static bool CheckTopLevel(ModuleInfo parent) { var currentParent = parent; while (currentParent != null) { if (currentParent.IsSlashGroup) return false; currentParent = currentParent.Parent; } return true; } // ICommandInfo /// IReadOnlyCollection ICommandInfo.Parameters => Parameters; /// public override string ToString() { List builder = new(); var currentParent = Module; while (currentParent != null) { if (currentParent.IsSlashGroup) builder.Add(currentParent.SlashGroupName); currentParent = currentParent.Parent; } builder.Reverse(); builder.Add(Name); return string.Join(" ", builder); } } }