using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Threading.Tasks; namespace Discord.Commands { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class Command { private static readonly MethodInfo _convertParamsMethod = typeof(Command).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList)); private static readonly ConcurrentDictionary, object>> _arrayConverters = new ConcurrentDictionary, object>>(); private readonly object _instance; private readonly Func, Task> _action; public MethodInfo Source { get; } public Module Module { get; } public string Name { get; } public string Summary { get; } public string Remarks { get; } public string Text { get; } public bool HasVarArgs { get; } public IReadOnlyList Aliases { get; } public IReadOnlyList Parameters { get; } public IReadOnlyList Preconditions { get; } internal Command(MethodInfo source, Module module, object instance, CommandAttribute attribute, string groupPrefix) { try { Source = source; Module = module; _instance = instance; Name = source.Name; if (attribute.Text == null) Text = groupPrefix; if (groupPrefix != "") groupPrefix += " "; if (attribute.Text != null) Text = groupPrefix + attribute.Text; var aliasesBuilder = ImmutableArray.CreateBuilder(); aliasesBuilder.Add(Text); var aliasesAttr = source.GetCustomAttribute(); if (aliasesAttr != null) aliasesBuilder.AddRange(aliasesAttr.Aliases.Select(x => groupPrefix + x)); Aliases = aliasesBuilder.ToImmutable(); var nameAttr = source.GetCustomAttribute(); if (nameAttr != null) Name = nameAttr.Text; var summary = source.GetCustomAttribute(); if (summary != null) Summary = summary.Text; var remarksAttr = source.GetCustomAttribute(); if (remarksAttr != null) Remarks = remarksAttr.Text; Parameters = BuildParameters(source); HasVarArgs = Parameters.Count > 0 ? Parameters[Parameters.Count - 1].IsMultiple : false; Preconditions = BuildPreconditions(source); _action = BuildAction(source); } catch (Exception ex) { throw new Exception($"Failed to build command {source.DeclaringType.FullName}.{source.Name}", ex); } } public async Task CheckPreconditions(IUserMessage context) { foreach (PreconditionAttribute precondition in Module.Preconditions) { var result = await precondition.CheckPermissions(context, this, Module.Instance).ConfigureAwait(false); if (!result.IsSuccess) return result; } foreach (PreconditionAttribute precondition in Preconditions) { var result = await precondition.CheckPermissions(context, this, Module.Instance).ConfigureAwait(false); if (!result.IsSuccess) return result; } return PreconditionResult.FromSuccess(); } public async Task Parse(IUserMessage context, SearchResult searchResult, PreconditionResult? preconditionResult = null) { if (!searchResult.IsSuccess) return ParseResult.FromError(searchResult); if (preconditionResult != null && !preconditionResult.Value.IsSuccess) return ParseResult.FromError(preconditionResult.Value); string input = searchResult.Text; var matchingAliases = Aliases.Where(alias => input.StartsWith(alias)); string matchingAlias = ""; foreach (string alias in matchingAliases) { if (alias.Length > matchingAlias.Length) matchingAlias = alias; } input = input.Substring(matchingAlias.Length); return await CommandParser.ParseArgs(this, context, input, 0).ConfigureAwait(false); } public Task Execute(IUserMessage context, ParseResult parseResult) { if (!parseResult.IsSuccess) return Task.FromResult(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])); 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])); paramList[i] = parseResult.ParamValues[i].Values.First().Value; } return Execute(context, argList, paramList); } public async Task Execute(IUserMessage context, IEnumerable argList, IEnumerable paramList) { try { await _action.Invoke(context, GenerateArgs(argList, paramList)).ConfigureAwait(false);//Note: This code may need context return ExecuteResult.FromSuccess(); } catch (Exception ex) { return ExecuteResult.FromError(ex); } } private IReadOnlyList BuildPreconditions(MethodInfo methodInfo) { return methodInfo.GetCustomAttributes().ToImmutableArray(); } private IReadOnlyList BuildParameters(MethodInfo methodInfo) { var parameters = methodInfo.GetParameters(); if (parameters.Length == 0 || parameters[0].ParameterType != typeof(IUserMessage)) throw new InvalidOperationException($"The first parameter of a command must be {nameof(IUserMessage)}."); var paramBuilder = ImmutableArray.CreateBuilder(parameters.Length - 1); for (int i = 1; i < parameters.Length; i++) { var parameter = parameters[i]; var type = parameter.ParameterType; //Detect 'params' bool isMultiple = parameter.GetCustomAttribute() != null; if (isMultiple) type = type.GetElementType(); var reader = Module.Service.GetTypeReader(type); var typeInfo = type.GetTypeInfo(); //Detect enums if (reader == null && typeInfo.IsEnum) { reader = EnumTypeReader.GetReader(type); Module.Service.AddTypeReader(type, reader); } if (reader == null) throw new InvalidOperationException($"{type.FullName} is not supported as a command parameter, are you missing a TypeReader?"); bool isRemainder = parameter.GetCustomAttribute() != null; if (isRemainder && i != parameters.Length - 1) throw new InvalidOperationException("Remainder parameters must be the last parameter in a command."); string name = parameter.Name; string summary = parameter.GetCustomAttribute()?.Text; bool isOptional = parameter.IsOptional; object defaultValue = parameter.HasDefaultValue ? parameter.DefaultValue : null; paramBuilder.Add(new CommandParameter(parameters[i], name, summary, type, reader, isOptional, isRemainder, isMultiple, defaultValue)); } return paramBuilder.ToImmutable(); } private Func, Task> BuildAction(MethodInfo methodInfo) { if (methodInfo.ReturnType != typeof(Task)) throw new InvalidOperationException("Commands must return a non-generic Task."); return (msg, args) => { object[] newArgs = new object[args.Count + 1]; newArgs[0] = msg; for (int i = 0; i < args.Count; i++) newArgs[i + 1] = args[i]; var result = methodInfo.Invoke(_instance, newArgs); return result as Task ?? Task.CompletedTask; }; } private object[] GenerateArgs(IEnumerable argList, IEnumerable paramsList) { int argCount = Parameters.Count; var array = new object[Parameters.Count]; if (HasVarArgs) argCount--; int i = 0; foreach (var arg in argList) { if (i == argCount) throw new InvalidOperationException("Command was invoked with too many parameters"); array[i++] = arg; } if (i < argCount) throw new InvalidOperationException("Command was invoked with too few parameters"); if (HasVarArgs) { var func = _arrayConverters.GetOrAdd(Parameters[Parameters.Count - 1].ElementType, t => { var method = _convertParamsMethod.MakeGenericMethod(t); return (Func, object>)method.CreateDelegate(typeof(Func, object>)); }); array[i] = func(paramsList); } return array; } private static T[] ConvertParamsList(IEnumerable paramsList) => paramsList.Cast().ToArray(); public override string ToString() => Name; private string DebuggerDisplay => $"{Module.Name}.{Name} ({Text})"; } }