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.

CommandService.cs 11 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. namespace Discord.Commands
  7. {
  8. public partial class CommandService : IService
  9. {
  10. private readonly List<Command> _allCommands;
  11. private readonly Dictionary<string, CommandMap> _categories;
  12. private readonly CommandMap _map; //Command map stores all commands by their input text, used for fast resolving and parsing
  13. public CommandServiceConfig Config { get; }
  14. public CommandGroupBuilder Root { get; }
  15. public DiscordClient Client { get; private set; }
  16. //AllCommands store a flattened collection of all commands
  17. public IEnumerable<Command> AllCommands => _allCommands;
  18. //Groups store all commands by their module, used for more informative help
  19. internal IEnumerable<CommandMap> Categories => _categories.Values;
  20. public event EventHandler<CommandEventArgs> CommandExecuted = delegate { };
  21. public event EventHandler<CommandErrorEventArgs> CommandErrored = delegate { };
  22. private void OnCommand(CommandEventArgs args)
  23. => CommandExecuted(this, args);
  24. private void OnCommandError(CommandErrorType errorType, CommandEventArgs args, Exception ex = null)
  25. => CommandErrored(this, new CommandErrorEventArgs(errorType, args, ex));
  26. public CommandService()
  27. : this(new CommandServiceConfigBuilder())
  28. {
  29. }
  30. public CommandService(CommandServiceConfigBuilder builder)
  31. : this(builder.Build())
  32. {
  33. if (builder.ExecuteHandler != null)
  34. CommandExecuted += builder.ExecuteHandler;
  35. if (builder.ErrorHandler != null)
  36. CommandErrored += builder.ErrorHandler;
  37. }
  38. public CommandService(CommandServiceConfig config)
  39. {
  40. Config = config;
  41. _allCommands = new List<Command>();
  42. _map = new CommandMap();
  43. _categories = new Dictionary<string, CommandMap>();
  44. Root = new CommandGroupBuilder(this);
  45. }
  46. void IService.Install(DiscordClient client)
  47. {
  48. Client = client;
  49. if (Config.HelpMode != HelpMode.Disabled)
  50. {
  51. CreateCommand("help")
  52. .Parameter("command", ParameterType.Multiple)
  53. .Hide()
  54. .Description("Returns information about commands.")
  55. .Do(async e =>
  56. {
  57. ITextChannel replyChannel = Config.HelpMode == HelpMode.Public ? e.Channel : await e.User.CreatePMChannel().ConfigureAwait(false);
  58. if (e.Args.Length > 0) //Show command help
  59. {
  60. var map = _map.GetItem(string.Join(" ", e.Args));
  61. if (map != null)
  62. await ShowCommandHelp(map, e.User, e.Channel, replyChannel).ConfigureAwait(false);
  63. else
  64. await replyChannel.SendMessage("Unable to display help: Unknown command.").ConfigureAwait(false);
  65. }
  66. else //Show general help
  67. await ShowGeneralHelp(e.User, e.Channel, replyChannel).ConfigureAwait(false);
  68. });
  69. }
  70. client.MessageReceived += async (s, e) =>
  71. {
  72. if (_allCommands.Count == 0) return;
  73. if (e.Message.User == null || e.Message.User.Id == Client.CurrentUser.Id) return;
  74. string msg = e.Message.RawText;
  75. if (msg.Length == 0) return;
  76. string cmdMsg = null;
  77. //Check for command char
  78. if (Config.PrefixChar.HasValue)
  79. {
  80. if (msg[0] == Config.PrefixChar.Value)
  81. cmdMsg = msg.Substring(1);
  82. }
  83. //Check for mention
  84. if (cmdMsg == null && Config.AllowMentionPrefix)
  85. {
  86. string mention = client.CurrentUser.Mention;
  87. if (msg.StartsWith(mention) && msg.Length > mention.Length)
  88. cmdMsg = msg.Substring(mention.Length + 1);
  89. else
  90. {
  91. mention = $"@{client.CurrentUser.Name}";
  92. if (msg.StartsWith(mention) && msg.Length > mention.Length)
  93. cmdMsg = msg.Substring(mention.Length + 1);
  94. }
  95. }
  96. //Check using custom activator
  97. if (cmdMsg == null && Config.CustomPrefixHandler != null)
  98. {
  99. int index = Config.CustomPrefixHandler(e.Message);
  100. if (index >= 0)
  101. cmdMsg = msg.Substring(index);
  102. }
  103. if (cmdMsg == null) return;
  104. //Parse command
  105. IEnumerable<Command> commands;
  106. int argPos;
  107. CommandParser.ParseCommand(cmdMsg, _map, out commands, out argPos);
  108. if (commands == null)
  109. {
  110. CommandEventArgs errorArgs = new CommandEventArgs(e.Message, null, null);
  111. OnCommandError(CommandErrorType.UnknownCommand, errorArgs);
  112. return;
  113. }
  114. else
  115. {
  116. foreach (var command in commands)
  117. {
  118. //Parse arguments
  119. string[] args;
  120. var error = CommandParser.ParseArgs(cmdMsg, argPos, command, out args);
  121. if (error != null)
  122. {
  123. if (error == CommandErrorType.BadArgCount)
  124. continue;
  125. else
  126. {
  127. var errorArgs = new CommandEventArgs(e.Message, command, null);
  128. OnCommandError(error.Value, errorArgs);
  129. return;
  130. }
  131. }
  132. var eventArgs = new CommandEventArgs(e.Message, command, args);
  133. // Check permissions
  134. string errorText;
  135. if (!command.CanRun(eventArgs.User, eventArgs.Channel, out errorText))
  136. {
  137. OnCommandError(CommandErrorType.BadPermissions, eventArgs, errorText != null ? new Exception(errorText) : null);
  138. return;
  139. }
  140. // Run the command
  141. try
  142. {
  143. OnCommand(eventArgs);
  144. await command.Run(eventArgs).ConfigureAwait(false);
  145. }
  146. catch (Exception ex)
  147. {
  148. OnCommandError(CommandErrorType.Exception, eventArgs, ex);
  149. }
  150. return;
  151. }
  152. var errorArgs2 = new CommandEventArgs(e.Message, null, null);
  153. OnCommandError(CommandErrorType.BadArgCount, errorArgs2);
  154. }
  155. };
  156. }
  157. public Task ShowGeneralHelp(User user, ITextChannel channel, ITextChannel replyChannel = null)
  158. {
  159. StringBuilder output = new StringBuilder();
  160. bool isFirstCategory = true;
  161. foreach (var category in _categories)
  162. {
  163. bool isFirstItem = true;
  164. foreach (var group in category.Value.SubGroups)
  165. {
  166. string error;
  167. if (group.IsVisible && (group.HasSubGroups || group.HasNonAliases) && group.CanRun(user, channel, out error))
  168. {
  169. if (isFirstItem)
  170. {
  171. isFirstItem = false;
  172. //This is called for the first item in each category. If we never get here, we dont bother writing the header for a category type (since it's empty)
  173. if (isFirstCategory)
  174. {
  175. isFirstCategory = false;
  176. //Called for the first non-empty category
  177. output.AppendLine("These are the commands you can use:");
  178. }
  179. else
  180. output.AppendLine();
  181. if (category.Key != "")
  182. {
  183. output.Append(Format.Bold(category.Key));
  184. output.Append(": ");
  185. }
  186. }
  187. else
  188. output.Append(", ");
  189. output.Append('`');
  190. output.Append(group.Name);
  191. if (group.HasSubGroups)
  192. output.Append("*");
  193. output.Append('`');
  194. }
  195. }
  196. }
  197. if (output.Length == 0)
  198. output.Append("There are no commands you have permission to run.");
  199. else
  200. output.AppendLine("\n\nRun `help <command>` for more information.");
  201. return (replyChannel ?? channel).SendMessage(output.ToString());
  202. }
  203. private Task ShowCommandHelp(CommandMap map, User user, ITextChannel channel, ITextChannel replyChannel = null)
  204. {
  205. StringBuilder output = new StringBuilder();
  206. IEnumerable<Command> cmds = map.Commands;
  207. bool isFirstCmd = true;
  208. string error;
  209. if (cmds.Any())
  210. {
  211. foreach (var cmd in cmds)
  212. {
  213. if (cmd.CanRun(user, channel, out error))
  214. {
  215. if (isFirstCmd)
  216. isFirstCmd = false;
  217. else
  218. output.AppendLine();
  219. ShowCommandHelpInternal(cmd, user, channel, output);
  220. }
  221. }
  222. }
  223. else
  224. {
  225. output.Append('`');
  226. output.Append(map.FullName);
  227. output.Append("`\n");
  228. }
  229. bool isFirstSubCmd = true;
  230. foreach (var subCmd in map.SubGroups.Where(x => x.CanRun(user, channel, out error) && x.IsVisible))
  231. {
  232. if (isFirstSubCmd)
  233. {
  234. isFirstSubCmd = false;
  235. output.AppendLine("Sub Commands: ");
  236. }
  237. else
  238. output.Append(", ");
  239. output.Append('`');
  240. output.Append(subCmd.Name);
  241. if (subCmd.SubGroups.Any())
  242. output.Append("*");
  243. output.Append('`');
  244. }
  245. if (isFirstCmd && isFirstSubCmd) //Had no commands and no subcommands
  246. {
  247. output.Clear();
  248. output.AppendLine("There are no commands you have permission to run.");
  249. }
  250. return (replyChannel ?? channel).SendMessage(output.ToString());
  251. }
  252. public Task ShowCommandHelp(Command command, User user, ITextChannel channel, ITextChannel replyChannel = null)
  253. {
  254. StringBuilder output = new StringBuilder();
  255. string error;
  256. if (!command.CanRun(user, channel, out error))
  257. output.AppendLine(error ?? "You do not have permission to access this command.");
  258. else
  259. ShowCommandHelpInternal(command, user, channel, output);
  260. return (replyChannel ?? channel).SendMessage(output.ToString());
  261. }
  262. private void ShowCommandHelpInternal(Command command, User user, ITextChannel channel, StringBuilder output)
  263. {
  264. output.Append('`');
  265. output.Append(command.Text);
  266. foreach (var param in command.Parameters)
  267. {
  268. switch (param.Type)
  269. {
  270. case ParameterType.Required:
  271. output.Append($" <{param.Name}>");
  272. break;
  273. case ParameterType.Optional:
  274. output.Append($" [{param.Name}]");
  275. break;
  276. case ParameterType.Multiple:
  277. output.Append($" [{param.Name}...]");
  278. break;
  279. case ParameterType.Unparsed:
  280. output.Append($" [-]");
  281. break;
  282. }
  283. }
  284. output.AppendLine("`");
  285. output.AppendLine($"{command.Description ?? "No description."}");
  286. if (command.Aliases.Any())
  287. output.AppendLine($"Aliases: `" + string.Join("`, `", command.Aliases) + '`');
  288. }
  289. public void CreateGroup(string cmd, Action<CommandGroupBuilder> config = null) => Root.CreateGroup(cmd, config);
  290. public CommandBuilder CreateCommand(string cmd) => Root.CreateCommand(cmd);
  291. internal void AddCommand(Command command)
  292. {
  293. _allCommands.Add(command);
  294. //Get category
  295. CommandMap category;
  296. string categoryName = command.Category ?? "";
  297. if (!_categories.TryGetValue(categoryName, out category))
  298. {
  299. category = new CommandMap();
  300. _categories.Add(categoryName, category);
  301. }
  302. //Add main command
  303. category.AddCommand(command.Text, command, false);
  304. _map.AddCommand(command.Text, command, false);
  305. //Add aliases
  306. foreach (var alias in command.Aliases)
  307. {
  308. category.AddCommand(alias, command, true);
  309. _map.AddCommand(alias, command, true);
  310. }
  311. }
  312. }
  313. }