using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Discord.Commands { /// A Discord.Net client with extensions for handling common bot operations like text commands. public sealed partial class CommandService : IService { private const string DefaultPermissionError = "You do not have permission to access this command."; private readonly CommandServiceConfig _config; private readonly CommandGroupBuilder _root; private DiscordClient _client; public DiscordClient Client => _client; public CommandGroupBuilder Root => _root; //AllCommands store a flattened collection of all commands public IEnumerable AllCommands => _allCommands; private readonly List _allCommands; //Command map stores all commands by their input text, used for fast resolving and parsing private readonly CommandMap _map; //Groups store all commands by their module, used for more informative help internal IEnumerable Categories => _categories.Values; private readonly Dictionary _categories; public CommandService(CommandServiceConfig config) { _config = config; _allCommands = new List(); _map = new CommandMap(null, "", ""); _categories = new Dictionary(); _root = new CommandGroupBuilder(this, "", null); } void IService.Install(DiscordClient client) { _client = client; _config.Lock(); if (_config.HelpMode != HelpMode.Disable) { CreateCommand("help") .Parameter("command", ParameterType.Multiple) .Hide() .Description("Returns information about commands.") .Do(async e => { Channel replyChannel = _config.HelpMode == HelpMode.Public ? e.Channel : await client.CreatePMChannel(e.User); 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); else await client.SendMessage(replyChannel, "Unable to display help: Unknown command."); } else //Show general help await ShowGeneralHelp(e.User, e.Channel, replyChannel); }); } client.MessageReceived += async (s, e) => { if (_allCommands.Count == 0) return; if (e.Message.IsAuthor) return; string msg = e.Message.RawText; if (msg.Length == 0) return; //Check for command char if one is provided var chars = _config.CommandChars; if (chars.Length > 0) { if (!chars.Contains(msg[0])) return; msg = msg.Substring(1); } //Parse command IEnumerable commands; int argPos; CommandParser.ParseCommand(msg, _map, out commands, out argPos); if (commands == null) { CommandEventArgs errorArgs = new CommandEventArgs(e.Message, null, null); RaiseCommandError(CommandErrorType.UnknownCommand, errorArgs); return; } else { foreach (var command in commands) { //Parse arguments string[] args; var error = CommandParser.ParseArgs(msg, argPos, command, out args); if (error != null) { if (error == CommandErrorType.BadArgCount) continue; else { var errorArgs = new CommandEventArgs(e.Message, command, null); RaiseCommandError(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)) { RaiseCommandError(CommandErrorType.BadPermissions, eventArgs, new Exception(errorText ?? DefaultPermissionError)); return; } // Run the command try { RaiseRanCommand(eventArgs); await command.Run(eventArgs).ConfigureAwait(false); } catch (Exception ex) { RaiseCommandError(CommandErrorType.Exception, eventArgs, ex); } return; } var errorArgs2 = new CommandEventArgs(e.Message, null, null); RaiseCommandError(CommandErrorType.BadArgCount, errorArgs2); } }; } public Task ShowGeneralHelp(User user, Channel channel, Channel 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.Append("\n\n"); var chars = _config.CommandChars; if (chars.Length > 0) { if (chars.Length == 1) output.AppendLine($"You can use `{chars[0]}` to call a command."); else output.AppendLine($"You can use `{string.Join(" ", chars.Take(chars.Length - 1))}` or `{chars.Last()}` to call a command."); output.AppendLine($"`{chars[0]}help ` can tell you more about how to use a command."); } else output.AppendLine($"`help ` can tell you more about how to use a command."); } return _client.SendMessage(replyChannel ?? channel, output.ToString()); } private Task ShowCommandHelp(CommandMap map, User user, Channel channel, Channel 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)) { } //output.AppendLine(error ?? DefaultPermissionError); else { 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 _client.SendMessage(replyChannel ?? channel, output.ToString()); } public Task ShowCommandHelp(Command command, User user, Channel channel, Channel replyChannel = null) { StringBuilder output = new StringBuilder(); string error; if (!command.CanRun(user, channel, out error)) output.AppendLine(error ?? DefaultPermissionError); else ShowCommandHelpInternal(command, user, channel, output); return _client.SendMessage(replyChannel ?? channel, output.ToString()); } private void ShowCommandHelpInternal(Command command, User user, Channel 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(" [...]"); 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(null, "", ""); _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); } } } }