From c729eaae8b51dfdaaf140c959b05eb66a0e170be Mon Sep 17 00:00:00 2001 From: Googie2149 Date: Wed, 28 Oct 2015 23:11:15 -0400 Subject: [PATCH] Revamped CommandsPlugin This uses a dictionary for the commands list, if a command has a max args set it'll only get that amount, will call the UnkownCommand event, and now has a built in help command that can be optionally enabled. CommandChar is now a list, but a single character can still be used. Externally, not much should have changed, but commands can be hidden from the help command and a description can be set. There's probably more that I've forgotten about. --- Discord.Net.sln | 9 + src/Discord.Net.Commands/Command.cs | 6 +- src/Discord.Net.Commands/CommandBuilder.cs | 12 + .../CommandsPlugin.Events.cs | 7 +- src/Discord.Net.Commands/CommandsPlugin.cs | 321 ++++++++++++------ 5 files changed, 243 insertions(+), 112 deletions(-) diff --git a/Discord.Net.sln b/Discord.Net.sln index 1c32308ff..3aefab91d 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -30,6 +30,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net.Modules", "src\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Modules", "src\Discord.Net.Modules.Net45\Discord.Net.Modules.csproj", "{3091164F-66AE-4543-A63D-167C1116241D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestingProject", "TestingProject\TestingProject.csproj", "{6CD8116D-6749-4174-81EB-C4EB4B1F185B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -79,6 +81,12 @@ Global {3091164F-66AE-4543-A63D-167C1116241D}.FullDebug|Any CPU.Build.0 = Debug|Any CPU {3091164F-66AE-4543-A63D-167C1116241D}.Release|Any CPU.ActiveCfg = Release|Any CPU {3091164F-66AE-4543-A63D-167C1116241D}.Release|Any CPU.Build.0 = Release|Any CPU + {6CD8116D-6749-4174-81EB-C4EB4B1F185B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6CD8116D-6749-4174-81EB-C4EB4B1F185B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6CD8116D-6749-4174-81EB-C4EB4B1F185B}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU + {6CD8116D-6749-4174-81EB-C4EB4B1F185B}.FullDebug|Any CPU.Build.0 = Debug|Any CPU + {6CD8116D-6749-4174-81EB-C4EB4B1F185B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6CD8116D-6749-4174-81EB-C4EB4B1F185B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -93,5 +101,6 @@ Global {1B5603B4-6F8F-4289-B945-7BAAE523D740} = {DF03D4E8-38F6-4FE1-BC52-E38124BE8AFD} {01584E8A-78DA-486F-9EF9-A894E435841B} = {EA68EBE2-51C8-4440-9EF7-D633C90A5D35} {3091164F-66AE-4543-A63D-167C1116241D} = {DF03D4E8-38F6-4FE1-BC52-E38124BE8AFD} + {6CD8116D-6749-4174-81EB-C4EB4B1F185B} = {6317A2E6-8E36-4C3E-949B-3F10EC888AB9} EndGlobalSection EndGlobal diff --git a/src/Discord.Net.Commands/Command.cs b/src/Discord.Net.Commands/Command.cs index f8728f4c6..2f4978566 100644 --- a/src/Discord.Net.Commands/Command.cs +++ b/src/Discord.Net.Commands/Command.cs @@ -9,13 +9,15 @@ namespace Discord.Commands public int? MinArgs { get; internal set; } public int? MaxArgs { get; internal set; } public int MinPerms { get; internal set; } - internal readonly string[] Parts; + public bool IsHidden { get; internal set; } + public string Description { get; internal set; } internal Func Handler; internal Command(string text) { Text = text; - Parts = text.ToLowerInvariant().Split(' '); + IsHidden = false; // Set false by default to avoid null error + Description = "No description set for this command."; } } } diff --git a/src/Discord.Net.Commands/CommandBuilder.cs b/src/Discord.Net.Commands/CommandBuilder.cs index 0db5a388f..13e3ff89f 100644 --- a/src/Discord.Net.Commands/CommandBuilder.cs +++ b/src/Discord.Net.Commands/CommandBuilder.cs @@ -54,6 +54,18 @@ namespace Discord.Commands return this; } + public CommandBuilder Desc(string desc) + { + _command.Description = desc; + return this; + } + + public CommandBuilder IsHidden() + { + _command.IsHidden = true; + return this; + } + public CommandBuilder Do(Func func) { _command.Handler = func; diff --git a/src/Discord.Net.Commands/CommandsPlugin.Events.cs b/src/Discord.Net.Commands/CommandsPlugin.Events.cs index 5901aa2a7..482855f4d 100644 --- a/src/Discord.Net.Commands/CommandsPlugin.Events.cs +++ b/src/Discord.Net.Commands/CommandsPlugin.Events.cs @@ -3,10 +3,12 @@ namespace Discord.Commands { public class PermissionException : Exception { public PermissionException() : base("User does not have permission to run this command.") { } } + public class ArgumentException : Exception { public ArgumentException() : base("This command requires more arguments.") { } } public class CommandEventArgs { public Message Message { get; } public Command Command { get; } + public string MessageText { get; } public string CommandText { get; } public string ArgText { get; } public int? Permissions { get; } @@ -16,10 +18,11 @@ namespace Discord.Commands public Channel Channel => Message.Channel; public Server Server => Message.Channel.Server; - public CommandEventArgs(Message message, Command command, string commandText, string argText, int? permissions, string[] args) + public CommandEventArgs(Message message, Command command, string messageText, string commandText, string argText, int? permissions, string[] args) { Message = message; Command = command; + MessageText = messageText; CommandText = commandText; ArgText = argText; Permissions = permissions; @@ -31,7 +34,7 @@ namespace Discord.Commands public Exception Exception { get; } public CommandErrorEventArgs(CommandEventArgs baseArgs, Exception ex) - : base(baseArgs.Message, baseArgs.Command, baseArgs.CommandText, baseArgs.ArgText, baseArgs.Permissions, baseArgs.Args) + : base(baseArgs.Message, baseArgs.Command, baseArgs.MessageText, baseArgs.CommandText, baseArgs.ArgText, baseArgs.Permissions, baseArgs.Args) { Exception = ex; } diff --git a/src/Discord.Net.Commands/CommandsPlugin.cs b/src/Discord.Net.Commands/CommandsPlugin.cs index 44abce8f4..17199f3d5 100644 --- a/src/Discord.Net.Commands/CommandsPlugin.cs +++ b/src/Discord.Net.Commands/CommandsPlugin.cs @@ -1,5 +1,8 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; namespace Discord.Commands { @@ -7,132 +10,234 @@ namespace Discord.Commands public partial class CommandsPlugin { private readonly DiscordClient _client; - private List _commands; private Func _getPermissions; - public IEnumerable Commands => _commands; + private Dictionary _commands; + + public Dictionary Commands => _commands; - public char CommandChar { get; set; } + public char CommandChar { get { return CommandChars[0]; } set { CommandChars = new List { value }; } } // This could possibly be removed entirely. Not sure. + public List CommandChars { get; set; } public bool UseCommandChar { get; set; } public bool RequireCommandCharInPublic { get; set; } public bool RequireCommandCharInPrivate { get; set; } - - public CommandsPlugin(DiscordClient client, Func getPermissions = null) - { - _client = client; - _getPermissions = getPermissions; - _commands = new List(); - - CommandChar = '/'; - UseCommandChar = false; - RequireCommandCharInPublic = true; - RequireCommandCharInPrivate = true; - - client.MessageReceived += async (s, e) => - { - //If commands aren't being used, don't bother processing them - if (_commands.Count == 0) - return; - - //Ignore messages from ourselves - if (e.Message.User == client.CurrentUser) - return; - - //Check for the command character - string msg = e.Message.Text; - if (UseCommandChar) - { - if (msg.Length == 0) - return; - bool isPrivate = e.Message.Channel.IsPrivate; - bool hasCommandChar = msg[0] == CommandChar; - if (hasCommandChar) - msg = msg.Substring(1); - if (!isPrivate && RequireCommandCharInPublic && !hasCommandChar) - return; - if (isPrivate && RequireCommandCharInPrivate && !hasCommandChar) - return; - } - - CommandPart[] args; - if (!CommandParser.ParseArgs(msg, out args)) - return; - - for (int i = 0; i < _commands.Count; i++) - { - Command cmd = _commands[i]; - - //Check Command Parts - if (args.Length < cmd.Parts.Length) - continue; - - bool isValid = true; - for (int j = 0; j < cmd.Parts.Length; j++) - { - if (!string.Equals(args[j].Value, cmd.Parts[j], StringComparison.OrdinalIgnoreCase)) - { - isValid = false; - break; - } - } - if (!isValid) - continue; - - //Check Arg Count - int argCount = args.Length - cmd.Parts.Length; - if (argCount < cmd.MinArgs || argCount > cmd.MaxArgs) - continue; - - //Clean Args - string[] newArgs = new string[argCount]; - for (int j = 0; j < newArgs.Length; j++) - newArgs[j] = args[j + cmd.Parts.Length].Value; - - //Get ArgText - string argText; - if (argCount == 0) - argText = ""; - else - argText = msg.Substring(args[cmd.Parts.Length].Index); - - //Check Permissions - int permissions = _getPermissions != null ? _getPermissions(e.Message.User) : 0; - var eventArgs = new CommandEventArgs(e.Message, cmd, msg, argText, permissions, newArgs); - if (permissions < cmd.MinPerms) - { - RaiseCommandError(eventArgs, new PermissionException()); - return; - } - - //Run Command + public bool HelpInPublic { get; set; } + + public CommandsPlugin(DiscordClient client, Func getPermissions = null, bool builtInHelp = false) + { + _client = client; // Wait why is this even set + _getPermissions = getPermissions; + + _commands = new Dictionary(); + + CommandChar = '!'; // Kept around to keep from possibly throwing an error. Might not be necessary. + CommandChars = new List { '!', '?', '/' }; + UseCommandChar = true; + RequireCommandCharInPublic = true; + RequireCommandCharInPrivate = true; + HelpInPublic = true; + + if (builtInHelp) + { + CreateCommand("help") + .ArgsBetween(0, 1) + .IsHidden() + .Desc("Returns information about commands.") + .Do(async e => + { + if (e.Command.Text != "help") + { + await Reply(e, CommandDetails(e.Command)); + } + else + { + if (e.Args == null) + { + StringBuilder output = new StringBuilder(); + bool first = true; + output.AppendLine("These are the commands you can use:"); + output.Append("`"); + int permissions = getPermissions(e.User); + foreach (KeyValuePair k in _commands) + { + if (permissions >= k.Value.MinPerms && !k.Value.IsHidden) + if (first) + { + output.Append(k.Key); + first = false; + } + else + output.Append($", {k.Key}"); + } + output.Append("`"); + + if (CommandChars.Count == 1) + output.AppendLine($"{Environment.NewLine}You can use `{CommandChars[0]}` to call a command."); + else + output.AppendLine($"{Environment.NewLine}You can use `{String.Join(" ", CommandChars.Take(CommandChars.Count - 1))}` and `{CommandChars.Last()}` to call a command."); + + output.AppendLine("`help ` can tell you more about how to use a command."); + + await Reply(e, output.ToString()); + } + else + { + if (_commands.ContainsKey(e.Args[0])) + await Reply(e, CommandDetails(_commands[e.Args[0]])); + else + await Reply(e, $"`{e.Args[0]}` is not a valid command."); + } + } + }); + + } + + client.MessageReceived += async (s, e) => + { + // This will need to be changed once a built in help command is made + if (_commands.Count == 0) + return; + + if (e.Message.IsAuthor) + return; + + string msg = e.Message.Text; + + if (msg.Length == 0) + return; + + if (UseCommandChar) + { + bool isPrivate = e.Message.Channel.IsPrivate; + bool hasCommandChar = CommandChars.Contains(msg[0]); + if (hasCommandChar) + msg = msg.Substring(1); + + if (isPrivate && RequireCommandCharInPrivate && !hasCommandChar) + return; // If private, and command char is required, and it doesn't have it, ignore it. + if (!isPrivate && RequireCommandCharInPublic && !hasCommandChar) + return; // Same, but public. + } + + string cmd; + CommandPart[] args; + if (!CommandParser.Parse(msg, out cmd, out args)) + return; + + if (_commands.ContainsKey(cmd)) + { + Command comm = _commands[cmd]; + + //Get ArgText + int argCount = args.Length; + string argText; + if (argCount == 0) + argText = ""; + else + argText = msg.Substring(args[0].Index); + + //Clean Args + string[] newArgs = null; + + if (comm.MaxArgs != null && argCount > 0) + { + newArgs = new string[(int)comm.MaxArgs]; + for (int j = 0; j < newArgs.Length; j++) + newArgs[j] = args[j].Value; + } + + // Check permissions here + int permissions = _getPermissions != null ? _getPermissions(e.Message.User) : 0; + var eventArgs = new CommandEventArgs(e.Message, comm, msg, cmd, argText, permissions, newArgs); + if (permissions < comm.MinPerms) + { + RaiseCommandError(eventArgs, new PermissionException()); + return; + } + + //Check Arg Count + if (argCount < comm.MinArgs) + { + RaiseCommandError(eventArgs, new ArgumentException()); + if (builtInHelp) + await _commands["help"].Handler(eventArgs); + return; + } + + // Actually run the command here RaiseRanCommand(eventArgs); - try - { - var task = cmd.Handler(eventArgs); - if (task != null) - await task.ConfigureAwait(false); - } - catch (Exception ex) - { - RaiseCommandError(eventArgs, ex); - } - break; - } - }; - } + try + { + var task = comm.Handler(eventArgs); + if (task != null) + await task.ConfigureAwait(false); + } + catch (Exception ex) + { + RaiseCommandError(eventArgs, ex); + } + } + else + { + CommandEventArgs eventArgs = new CommandEventArgs(e.Message, null, msg, cmd, null, null, null); + RaiseUnknownCommand(eventArgs); + if (builtInHelp) + await Reply(eventArgs, $"Command `cmd` does not exist."); + return; + } + }; + } + + internal string CommandDetails(Command comm) + { + StringBuilder output = new StringBuilder(); + + output.Append($"`{comm.Text}`"); + + if (comm.MinArgs != null && comm.MaxArgs != null) + { + if (comm.MinArgs == comm.MaxArgs) + { + if (comm.MaxArgs != 0) + output.Append($" {comm.MinArgs.ToString()} Args"); + } + else + output.Append($" {comm.MinArgs.ToString()} - {comm.MaxArgs.ToString()} Args"); + } + else if (comm.MinArgs != null && comm.MaxArgs == null) + { + output.Append($" ≥{comm.MinArgs.ToString()} Args"); + } + else if (comm.MinArgs == null && comm.MaxArgs != null) + { + output.Append($" ≤{comm.MaxArgs.ToString()} Args"); + } + + output.Append($": {comm.Description}"); + + return output.ToString(); + } + + internal async Task Reply(CommandEventArgs e, string message) + { + if (HelpInPublic) + await _client.SendMessage(e.Channel, message); + else + await _client.SendPrivateMessage(e.User, message); + } public void CreateCommandGroup(string cmd, Action config = null) => config(new CommandGroupBuilder(this, cmd, 0)); public CommandBuilder CreateCommand(string cmd) { var command = new Command(cmd); - _commands.Add(command); + _commands.Add(cmd, command); return new CommandBuilder(command); } internal void AddCommand(Command command) { - _commands.Add(command); + _commands.Add(command.Text, command); } } }