You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

Command.cs 11 kB

9 years ago
9 years ago
9 years ago
9 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. using System;
  2. using System.Collections.Concurrent;
  3. using System.Collections.Generic;
  4. using System.Collections.Immutable;
  5. using System.Diagnostics;
  6. using System.Linq;
  7. using System.Reflection;
  8. using System.Threading.Tasks;
  9. namespace Discord.Commands
  10. {
  11. [DebuggerDisplay(@"{DebuggerDisplay,nq}")]
  12. public class Command
  13. {
  14. private static readonly MethodInfo _convertParamsMethod = typeof(Command).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList));
  15. private static readonly ConcurrentDictionary<Type, Func<IEnumerable<object>, object>> _arrayConverters = new ConcurrentDictionary<Type, Func<IEnumerable<object>, object>>();
  16. private readonly object _instance;
  17. private readonly Func<IUserMessage, IReadOnlyList<object>, Task> _action;
  18. public MethodInfo Source { get; }
  19. public Module Module { get; }
  20. public string Name { get; }
  21. public string Summary { get; }
  22. public string Remarks { get; }
  23. public string Text { get; }
  24. public bool HasVarArgs { get; }
  25. public IReadOnlyList<string> Aliases { get; }
  26. public IReadOnlyList<CommandParameter> Parameters { get; }
  27. public IReadOnlyList<PreconditionAttribute> Preconditions { get; }
  28. internal Command(MethodInfo source, Module module, object instance, CommandAttribute attribute, string groupPrefix)
  29. {
  30. try
  31. {
  32. Source = source;
  33. Module = module;
  34. _instance = instance;
  35. Name = source.Name;
  36. if (attribute.Text == null)
  37. Text = groupPrefix;
  38. if (groupPrefix != "")
  39. groupPrefix += " ";
  40. if (attribute.Text != null)
  41. Text = groupPrefix + attribute.Text;
  42. var aliasesBuilder = ImmutableArray.CreateBuilder<string>();
  43. aliasesBuilder.Add(Text);
  44. var aliasesAttr = source.GetCustomAttribute<AliasAttribute>();
  45. if (aliasesAttr != null)
  46. aliasesBuilder.AddRange(aliasesAttr.Aliases.Select(x => groupPrefix + x));
  47. Aliases = aliasesBuilder.ToImmutable();
  48. var nameAttr = source.GetCustomAttribute<NameAttribute>();
  49. if (nameAttr != null)
  50. Name = nameAttr.Text;
  51. var summary = source.GetCustomAttribute<SummaryAttribute>();
  52. if (summary != null)
  53. Summary = summary.Text;
  54. var remarksAttr = source.GetCustomAttribute<RemarksAttribute>();
  55. if (remarksAttr != null)
  56. Remarks = remarksAttr.Text;
  57. Parameters = BuildParameters(source);
  58. HasVarArgs = Parameters.Count > 0 ? Parameters[Parameters.Count - 1].IsMultiple : false;
  59. Preconditions = BuildPreconditions(source);
  60. _action = BuildAction(source);
  61. }
  62. catch (Exception ex)
  63. {
  64. throw new Exception($"Failed to build command {source.DeclaringType.FullName}.{source.Name}", ex);
  65. }
  66. }
  67. public async Task<PreconditionResult> CheckPreconditions(IUserMessage context)
  68. {
  69. foreach (PreconditionAttribute precondition in Module.Preconditions)
  70. {
  71. var result = await precondition.CheckPermissions(context, this, Module.Instance).ConfigureAwait(false);
  72. if (!result.IsSuccess)
  73. return result;
  74. }
  75. foreach (PreconditionAttribute precondition in Preconditions)
  76. {
  77. var result = await precondition.CheckPermissions(context, this, Module.Instance).ConfigureAwait(false);
  78. if (!result.IsSuccess)
  79. return result;
  80. }
  81. return PreconditionResult.FromSuccess();
  82. }
  83. public async Task<ParseResult> Parse(IUserMessage context, SearchResult searchResult, PreconditionResult? preconditionResult = null)
  84. {
  85. if (!searchResult.IsSuccess)
  86. return ParseResult.FromError(searchResult);
  87. if (preconditionResult != null && !preconditionResult.Value.IsSuccess)
  88. return ParseResult.FromError(preconditionResult.Value);
  89. string input = searchResult.Text;
  90. var matchingAliases = Aliases.Where(alias => input.StartsWith(alias));
  91. string matchingAlias = "";
  92. foreach (string alias in matchingAliases)
  93. {
  94. if (alias.Length > matchingAlias.Length)
  95. matchingAlias = alias;
  96. }
  97. input = input.Substring(matchingAlias.Length);
  98. return await CommandParser.ParseArgs(this, context, input, 0).ConfigureAwait(false);
  99. }
  100. public Task<ExecuteResult> Execute(IUserMessage context, ParseResult parseResult)
  101. {
  102. if (!parseResult.IsSuccess)
  103. return Task.FromResult(ExecuteResult.FromError(parseResult));
  104. var argList = new object[parseResult.ArgValues.Count];
  105. for (int i = 0; i < parseResult.ArgValues.Count; i++)
  106. {
  107. if (!parseResult.ArgValues[i].IsSuccess)
  108. return Task.FromResult(ExecuteResult.FromError(parseResult.ArgValues[i]));
  109. argList[i] = parseResult.ArgValues[i].Values.First().Value;
  110. }
  111. var paramList = new object[parseResult.ParamValues.Count];
  112. for (int i = 0; i < parseResult.ParamValues.Count; i++)
  113. {
  114. if (!parseResult.ParamValues[i].IsSuccess)
  115. return Task.FromResult(ExecuteResult.FromError(parseResult.ParamValues[i]));
  116. paramList[i] = parseResult.ParamValues[i].Values.First().Value;
  117. }
  118. return Execute(context, argList, paramList);
  119. }
  120. public async Task<ExecuteResult> Execute(IUserMessage context, IEnumerable<object> argList, IEnumerable<object> paramList)
  121. {
  122. try
  123. {
  124. await _action.Invoke(context, GenerateArgs(argList, paramList)).ConfigureAwait(false);//Note: This code may need context
  125. return ExecuteResult.FromSuccess();
  126. }
  127. catch (Exception ex)
  128. {
  129. return ExecuteResult.FromError(ex);
  130. }
  131. }
  132. private IReadOnlyList<PreconditionAttribute> BuildPreconditions(MethodInfo methodInfo)
  133. {
  134. return methodInfo.GetCustomAttributes<PreconditionAttribute>().ToImmutableArray();
  135. }
  136. private IReadOnlyList<CommandParameter> BuildParameters(MethodInfo methodInfo)
  137. {
  138. var parameters = methodInfo.GetParameters();
  139. if (parameters.Length == 0 || parameters[0].ParameterType != typeof(IUserMessage))
  140. throw new InvalidOperationException($"The first parameter of a command must be {nameof(IUserMessage)}.");
  141. var paramBuilder = ImmutableArray.CreateBuilder<CommandParameter>(parameters.Length - 1);
  142. for (int i = 1; i < parameters.Length; i++)
  143. {
  144. var parameter = parameters[i];
  145. var type = parameter.ParameterType;
  146. //Detect 'params'
  147. bool isMultiple = parameter.GetCustomAttribute<ParamArrayAttribute>() != null;
  148. if (isMultiple)
  149. type = type.GetElementType();
  150. var reader = Module.Service.GetTypeReader(type);
  151. var typeInfo = type.GetTypeInfo();
  152. //Detect enums
  153. if (reader == null && typeInfo.IsEnum)
  154. {
  155. reader = EnumTypeReader.GetReader(type);
  156. Module.Service.AddTypeReader(type, reader);
  157. }
  158. if (reader == null)
  159. throw new InvalidOperationException($"{type.FullName} is not supported as a command parameter, are you missing a TypeReader?");
  160. bool isRemainder = parameter.GetCustomAttribute<RemainderAttribute>() != null;
  161. if (isRemainder && i != parameters.Length - 1)
  162. throw new InvalidOperationException("Remainder parameters must be the last parameter in a command.");
  163. string name = parameter.Name;
  164. string summary = parameter.GetCustomAttribute<SummaryAttribute>()?.Text;
  165. bool isOptional = parameter.IsOptional;
  166. object defaultValue = parameter.HasDefaultValue ? parameter.DefaultValue : null;
  167. paramBuilder.Add(new CommandParameter(parameters[i], name, summary, type, reader, isOptional, isRemainder, isMultiple, defaultValue));
  168. }
  169. return paramBuilder.ToImmutable();
  170. }
  171. private Func<IUserMessage, IReadOnlyList<object>, Task> BuildAction(MethodInfo methodInfo)
  172. {
  173. if (methodInfo.ReturnType != typeof(Task))
  174. throw new InvalidOperationException("Commands must return a non-generic Task.");
  175. return (msg, args) =>
  176. {
  177. object[] newArgs = new object[args.Count + 1];
  178. newArgs[0] = msg;
  179. for (int i = 0; i < args.Count; i++)
  180. newArgs[i + 1] = args[i];
  181. var result = methodInfo.Invoke(_instance, newArgs);
  182. return result as Task ?? Task.CompletedTask;
  183. };
  184. }
  185. private object[] GenerateArgs(IEnumerable<object> argList, IEnumerable<object> paramsList)
  186. {
  187. int argCount = Parameters.Count;
  188. var array = new object[Parameters.Count];
  189. if (HasVarArgs)
  190. argCount--;
  191. int i = 0;
  192. foreach (var arg in argList)
  193. {
  194. if (i == argCount)
  195. throw new InvalidOperationException("Command was invoked with too many parameters");
  196. array[i++] = arg;
  197. }
  198. if (i < argCount)
  199. throw new InvalidOperationException("Command was invoked with too few parameters");
  200. if (HasVarArgs)
  201. {
  202. var func = _arrayConverters.GetOrAdd(Parameters[Parameters.Count - 1].ElementType, t =>
  203. {
  204. var method = _convertParamsMethod.MakeGenericMethod(t);
  205. return (Func<IEnumerable<object>, object>)method.CreateDelegate(typeof(Func<IEnumerable<object>, object>));
  206. });
  207. array[i] = func(paramsList);
  208. }
  209. return array;
  210. }
  211. private static T[] ConvertParamsList<T>(IEnumerable<object> paramsList)
  212. => paramsList.Cast<T>().ToArray();
  213. public override string ToString() => Name;
  214. private string DebuggerDisplay => $"{Module.Name}.{Name} ({Text})";
  215. }
  216. }