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);
}
}
}