using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Discord.Commands { public partial class CommandService : IService { private readonly List _allCommands; private readonly Dictionary _categories; private readonly CommandMap _map; //Command map stores all commands by their input text, used for fast resolving and parsing public CommandServiceConfig Config { get; } public CommandGroupBuilder Root { get; } public DiscordClient Client { get; private set; } //AllCommands store a flattened collection of all commands public IEnumerable AllCommands => _allCommands; //Groups store all commands by their module, used for more informative help internal IEnumerable Categories => _categories.Values; public event EventHandler CommandExecuted = delegate { }; public event EventHandler CommandErrored = delegate { }; private void OnCommand(CommandEventArgs args) => CommandExecuted(this, args); private void OnCommandError(CommandErrorType errorType, CommandEventArgs args, Exception ex = null) => CommandErrored(this, new CommandErrorEventArgs(errorType, args, ex)); public CommandService() : this(new CommandServiceConfigBuilder()) { } public CommandService(CommandServiceConfigBuilder builder) : this(builder.Build()) { if (builder.ExecuteHandler != null) CommandExecuted += builder.ExecuteHandler; if (builder.ErrorHandler != null) CommandErrored += builder.ErrorHandler; } public CommandService(CommandServiceConfig config) { Config = config; _allCommands = new List(); _map = new CommandMap(); _categories = new Dictionary(); Root = new CommandGroupBuilder(this); } void IService.Install(DiscordClient client) { Client = client; if (Config.HelpMode != HelpMode.Disabled) { CreateCommand("help") .Parameter("command", ParameterType.Multiple) .Hide() .Description("Returns information about commands.") .Do(async e => { ITextChannel replyChannel = Config.HelpMode == HelpMode.Public ? e.Channel : await e.User.CreatePMChannel().ConfigureAwait(false); if (e.Args.Length > 0) //Show command help { var map = _map.GetItem(string.Join(" ", e.Args)); if (map != null) await ShowCommandHelp(map, e.User, e.Channel, replyChannel).ConfigureAwait(false); else await replyChannel.SendMessage("Unable to display help: Unknown command.").ConfigureAwait(false); } else //Show general help await ShowGeneralHelp(e.User, e.Channel, replyChannel).ConfigureAwait(false); }); } client.MessageReceived += async (s, e) => { if (_allCommands.Count == 0) return; if (e.Message.User == null || e.Message.User.Id == Client.CurrentUser.Id) return; string msg = e.Message.RawText; if (msg.Length == 0) return; string cmdMsg = null; //Check for command char if (Config.PrefixChar.HasValue) { if (msg[0] == Config.PrefixChar.Value) cmdMsg = msg.Substring(1); } //Check for mention if (cmdMsg == null && Config.AllowMentionPrefix) { string mention = client.CurrentUser.Mention; if (msg.StartsWith(mention) && msg.Length > mention.Length) cmdMsg = msg.Substring(mention.Length + 1); else { mention = $"@{client.CurrentUser.Name}"; if (msg.StartsWith(mention) && msg.Length > mention.Length) cmdMsg = msg.Substring(mention.Length + 1); } } //Check using custom activator if (cmdMsg == null && Config.CustomPrefixHandler != null) { int index = Config.CustomPrefixHandler(e.Message); if (index >= 0) cmdMsg = msg.Substring(index); } if (cmdMsg == null) return; //Parse command IEnumerable commands; int argPos; CommandParser.ParseCommand(cmdMsg, _map, out commands, out argPos); if (commands == null) { CommandEventArgs errorArgs = new CommandEventArgs(e.Message, null, null); OnCommandError(CommandErrorType.UnknownCommand, errorArgs); return; } else { foreach (var command in commands) { //Parse arguments string[] args; var error = CommandParser.ParseArgs(cmdMsg, argPos, command, out args); if (error != null) { if (error == CommandErrorType.BadArgCount) continue; else { var errorArgs = new CommandEventArgs(e.Message, command, null); OnCommandError(error.Value, errorArgs); return; } } var eventArgs = new CommandEventArgs(e.Message, command, args); // Check permissions string errorText; if (!command.CanRun(eventArgs.User, eventArgs.Channel, out errorText)) { OnCommandError(CommandErrorType.BadPermissions, eventArgs, errorText != null ? new Exception(errorText) : null); return; } // Run the command try { OnCommand(eventArgs); await command.Run(eventArgs).ConfigureAwait(false); } catch (Exception ex) { OnCommandError(CommandErrorType.Exception, eventArgs, ex); } return; } var errorArgs2 = new CommandEventArgs(e.Message, null, null); OnCommandError(CommandErrorType.BadArgCount, errorArgs2); } }; } public Task ShowGeneralHelp(User user, ITextChannel channel, ITextChannel replyChannel = null) { StringBuilder output = new StringBuilder(); bool isFirstCategory = true; foreach (var category in _categories) { bool isFirstItem = true; foreach (var group in category.Value.SubGroups) { string error; if (group.IsVisible && (group.HasSubGroups || group.HasNonAliases) && group.CanRun(user, channel, out error)) { if (isFirstItem) { isFirstItem = false; //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) if (isFirstCategory) { isFirstCategory = false; //Called for the first non-empty category output.AppendLine("These are the commands you can use:"); } else output.AppendLine(); if (category.Key != "") { output.Append(Format.Bold(category.Key)); output.Append(": "); } } else output.Append(", "); output.Append('`'); output.Append(group.Name); if (group.HasSubGroups) output.Append("*"); output.Append('`'); } } } if (output.Length == 0) output.Append("There are no commands you have permission to run."); else output.AppendLine("\n\nRun `help ` for more information."); return (replyChannel ?? channel).SendMessage(output.ToString()); } private Task ShowCommandHelp(CommandMap map, User user, ITextChannel channel, ITextChannel replyChannel = null) { StringBuilder output = new StringBuilder(); IEnumerable cmds = map.Commands; bool isFirstCmd = true; string error; if (cmds.Any()) { foreach (var cmd in cmds) { if (cmd.CanRun(user, channel, out error)) { if (isFirstCmd) isFirstCmd = false; else output.AppendLine(); ShowCommandHelpInternal(cmd, user, channel, output); } } } else { output.Append('`'); output.Append(map.FullName); output.Append("`\n"); } bool isFirstSubCmd = true; foreach (var subCmd in map.SubGroups.Where(x => x.CanRun(user, channel, out error) && x.IsVisible)) { if (isFirstSubCmd) { isFirstSubCmd = false; output.AppendLine("Sub Commands: "); } else output.Append(", "); output.Append('`'); output.Append(subCmd.Name); if (subCmd.SubGroups.Any()) output.Append("*"); output.Append('`'); } if (isFirstCmd && isFirstSubCmd) //Had no commands and no subcommands { output.Clear(); output.AppendLine("There are no commands you have permission to run."); } return (replyChannel ?? channel).SendMessage(output.ToString()); } public Task ShowCommandHelp(Command command, User user, ITextChannel channel, ITextChannel replyChannel = null) { StringBuilder output = new StringBuilder(); string error; if (!command.CanRun(user, channel, out error)) output.AppendLine(error ?? "You do not have permission to access this command."); else ShowCommandHelpInternal(command, user, channel, output); return (replyChannel ?? channel).SendMessage(output.ToString()); } private void ShowCommandHelpInternal(Command command, User user, ITextChannel channel, StringBuilder output) { output.Append('`'); output.Append(command.Text); foreach (var param in command.Parameters) { switch (param.Type) { case ParameterType.Required: output.Append($" <{param.Name}>"); break; case ParameterType.Optional: output.Append($" [{param.Name}]"); break; case ParameterType.Multiple: output.Append($" [{param.Name}...]"); break; case ParameterType.Unparsed: output.Append($" [-]"); break; } } output.AppendLine("`"); output.AppendLine($"{command.Description ?? "No description."}"); if (command.Aliases.Any()) output.AppendLine($"Aliases: `" + string.Join("`, `", command.Aliases) + '`'); } public void CreateGroup(string cmd, Action config = null) => Root.CreateGroup(cmd, config); public CommandBuilder CreateCommand(string cmd) => Root.CreateCommand(cmd); internal void AddCommand(Command command) { _allCommands.Add(command); //Get category CommandMap category; string categoryName = command.Category ?? ""; if (!_categories.TryGetValue(categoryName, out category)) { category = new CommandMap(); _categories.Add(categoryName, category); } //Add main command category.AddCommand(command.Text, command, false); _map.AddCommand(command.Text, command, false); //Add aliases foreach (var alias in command.Aliases) { category.AddCommand(alias, command, true); _map.AddCommand(alias, command, true); } } } }