diff --git a/DeukBot4/MessageHandlers/Attributes/RequireParameterMatchAttribute.cs b/DeukBot4/MessageHandlers/Attributes/RequireParameterMatchAttribute.cs new file mode 100644 index 0000000..c761c6d --- /dev/null +++ b/DeukBot4/MessageHandlers/Attributes/RequireParameterMatchAttribute.cs @@ -0,0 +1,10 @@ +using System; + +namespace DeukBot4.MessageHandlers +{ + [AttributeUsage(AttributeTargets.Method)] + public class RequireParameterMatchAttribute : Attribute + { + + } +} \ No newline at end of file diff --git a/DeukBot4/MessageHandlers/CommandHandler/Command.cs b/DeukBot4/MessageHandlers/CommandHandler/Command.cs index 010673e..8a86539 100644 --- a/DeukBot4/MessageHandlers/CommandHandler/Command.cs +++ b/DeukBot4/MessageHandlers/CommandHandler/Command.cs @@ -1,49 +1,55 @@ -using System.Diagnostics; -using System.Reflection; +using System.Reflection; using System.Threading.Tasks; using DeukBot4.MessageHandlers.CommandHandler.RequestStructure; using DeukBot4.MessageHandlers.Permissions; -using Discord.WebSocket; namespace DeukBot4.MessageHandlers.CommandHandler { public class Command { - public Command(string name, PermissionLevel permission, string shortHelp, string longHelp, bool forbidInPm, - ParameterMatcher.ParameterType[][] parameterTypes, MethodInfo function, CommandContainerBase commandContainer) + public Command(string name, PermissionLevel permission, string shortHelp, + string longHelp, + ParameterMatcher.ParameterType[][] parameterTypes, bool forbidInPm, bool requireParameterMatch, + MethodInfo function, + CommandContainerBase commandContainer) { - Name = name; - Permission = permission; - ShortHelp = shortHelp; - LongHelp = longHelp; - Function = function; - CommandContainer = commandContainer; - ParameterTypes = parameterTypes; - HasHelp = true; - ForbidInPm = forbidInPm; + Name = name; + Permission = permission; + ShortHelp = shortHelp; + LongHelp = longHelp; + Function = function; + CommandContainer = commandContainer; + ParameterTypes = parameterTypes; + HasHelp = true; + ForbidInPm = forbidInPm; + RequireParameterMatch = requireParameterMatch; } - public Command(string name, PermissionLevel permission, ParameterMatcher.ParameterType[][] parameterTypes, bool forbidInPm, - MethodInfo function, CommandContainerBase commandContainer) + public Command(string name, PermissionLevel permission, + ParameterMatcher.ParameterType[][] parameterTypes, + bool forbidInPm, bool requireParameterMatch, + MethodInfo function, CommandContainerBase commandContainer) { - Name = name; - Permission = permission; - Function = function; - CommandContainer = commandContainer; - ParameterTypes = parameterTypes; - HasHelp = false; - ForbidInPm = forbidInPm; + Name = name; + Permission = permission; + Function = function; + CommandContainer = commandContainer; + ParameterTypes = parameterTypes; + HasHelp = false; + ForbidInPm = forbidInPm; + RequireParameterMatch = requireParameterMatch; } - public string Name { get; } - public PermissionLevel Permission { get; } - public string ShortHelp { get; } - public string LongHelp { get; } - public MethodInfo Function { get; } - public CommandContainerBase CommandContainer { get; } - public bool HasHelp { get; } - public ParameterMatcher.ParameterType[][] ParameterTypes { get; } - public bool ForbidInPm { get; } + public string Name { get; } + public PermissionLevel Permission { get; } + public string ShortHelp { get; } + public string LongHelp { get; } + public MethodInfo Function { get; } + public CommandContainerBase CommandContainer { get; } + public bool HasHelp { get; } + public ParameterMatcher.ParameterType[][] ParameterTypes { get; } + public bool ForbidInPm { get; } + public bool RequireParameterMatch { get; } private string[] _parameterMatchers; diff --git a/DeukBot4/MessageHandlers/CommandHandler/CommandContainerBase.cs b/DeukBot4/MessageHandlers/CommandHandler/CommandContainerBase.cs index a9557e7..7ce08cd 100644 --- a/DeukBot4/MessageHandlers/CommandHandler/CommandContainerBase.cs +++ b/DeukBot4/MessageHandlers/CommandHandler/CommandContainerBase.cs @@ -17,37 +17,44 @@ namespace DeukBot4.MessageHandlers.CommandHandler foreach (var methodInfo in funcs) { + // grab all command attributes, cast them properly var commandAttributes = methodInfo.GetCustomAttributes(typeof(CommandAttribute), true) .Select(x => x as CommandAttribute); + // get the help attribute if it exists CommandHelpAttribute helpAttribute = null; var helpAttributes = methodInfo.GetCustomAttributes(typeof(CommandHelpAttribute), true) - .Select(x => x as CommandHelpAttribute); - var commandHelpAttributes = helpAttributes as CommandHelpAttribute[] ?? helpAttributes.ToArray(); - if (commandHelpAttributes.Any()) - helpAttribute = commandHelpAttributes[0]; + .Select(x => x as CommandHelpAttribute).ToArray(); + if (helpAttributes.Any()) + helpAttribute = helpAttributes[0]; - var parametersAttributes = methodInfo.GetCustomAttributes(typeof(CommandParametersAttribute), true) - .Select(x => x as CommandParametersAttribute); - var commandParametersAttributes = parametersAttributes as CommandParametersAttribute[] ?? - parametersAttributes.ToArray(); - var parameters = commandParametersAttributes.Select(x => x.Types).ToArray(); + // grab all of the potential parameter type arrays + var parameters = methodInfo.GetCustomAttributes(typeof(CommandParametersAttribute), true) + .Select(x => (CommandParametersAttribute) x) + .Select(x => x.Types.ToArray()) + .ToArray(); + + // check if the function has the attribute for blocking usage in PMs var forbidPm = methodInfo.GetCustomAttributes(typeof(BlockUsageInPmAttribute), true).Any(); + var matchParametersExactly = + methodInfo.GetCustomAttributes(typeof(RequireParameterMatchAttribute), true).Any(); + foreach (var commandAttribute in commandAttributes) { if (commandAttribute == null) continue; if (helpAttribute == null) { - commands.Add(new Command(commandAttribute.Command, commandAttribute.Permission, parameters, forbidPm, - methodInfo, this)); + commands.Add(new Command(commandAttribute.Command, commandAttribute.Permission, parameters, + forbidPm, matchParametersExactly, methodInfo, this)); } else { commands.Add(new Command(commandAttribute.Command, commandAttribute.Permission, - helpAttribute.ShortHelp, helpAttribute.LongHelp, forbidPm, parameters, methodInfo, this)); + helpAttribute.ShortHelp, helpAttribute.LongHelp, parameters, forbidPm, + matchParametersExactly, methodInfo, this)); } } diff --git a/DeukBot4/MessageHandlers/CommandHandler/CommandHandler.cs b/DeukBot4/MessageHandlers/CommandHandler/CommandHandler.cs index 7b9f586..bfc2f53 100644 --- a/DeukBot4/MessageHandlers/CommandHandler/CommandHandler.cs +++ b/DeukBot4/MessageHandlers/CommandHandler/CommandHandler.cs @@ -13,8 +13,8 @@ namespace DeukBot4.MessageHandlers.CommandHandler { public static class CommandHandler { - public static Dictionary Commands { get; private set; } = new Dictionary(); - public const char CommandTrigger = '!'; + public static Dictionary Commands { get; } = new Dictionary(); + private const char CommandTrigger = '!'; public static void Build() { @@ -31,7 +31,8 @@ namespace DeukBot4.MessageHandlers.CommandHandler Commands.Add(command.Name.ToLowerInvariant(), command); } - Logger.Log($"Loaded following commands for container {obj.Name}: {commands.Select(x => x.Name).Join(", ")}"); + Logger.Log( + $"Loaded following commands for container {obj.Name}: {commands.Select(x => x.Name).Join(", ")}"); } @@ -42,34 +43,54 @@ namespace DeukBot4.MessageHandlers.CommandHandler if (string.IsNullOrWhiteSpace(message.Content)) return; - if (message.Content[0] != CommandTrigger) return; + if (message.Content[0] != CommandTrigger) + return; + var req = await CommandRequest.Create(message); var resultCode = req.Item2; - if (resultCode == CommandRequest.RequestCode.Invalid) + switch (resultCode) { - await Logger.LogError("Invalid content: " + message.Content); - return; - } - else if (resultCode == CommandRequest.RequestCode.Forbidden) - { - await Logger.Log( - $"Unauthorized user tried to run command: {message.Author.Username} -> {message.Content}"); - } - - else if (resultCode == CommandRequest.RequestCode.OK) - { - if (!(message.Channel is IGuildChannel) && req.Item1.Command.ForbidInPm) - { - await Logger.Log( - $"User is trying to use blocked command in PM: {message.Author.Username}"); + case CommandRequest.RequestCode.Invalid: + await Logger.LogError("Invalid content: " + message.Content); return; - } - - await req.Item1.Command.Invoke(req.Item1); - + case CommandRequest.RequestCode.Forbidden: + await Logger.Log( + $"Unauthorized user tried to run command: {message.Author.Username} -> {message.Content}"); + break; + case CommandRequest.RequestCode.OK: + if (!(message.Channel is IGuildChannel) && req.Item1.Command.ForbidInPm) + { + await Logger.Log( + $"User is trying to use blocked command in PM: {message.Author.Username}"); + return; + } + await req.Item1.Command.Invoke(req.Item1); + break; + case CommandRequest.RequestCode.UnknownCommand: + var similar = await GetSimilarCommand(req.Item3.ToString()); + await message.Channel.SendMessageAsync( + $"Unknown command: ``{req.Item3.ToString()}``. Did you mean: ``{similar}``?"); + break; + default: + throw new ArgumentOutOfRangeException(); } } + private static async Task GetSimilarCommand(string command) + { + var closestString = ""; + var similarity = int.MaxValue; + foreach (var cmd in Commands) + { + var distance = Lehvenstein.LevenshteinDistance(command, cmd.Key); + if (distance >= similarity) + continue; + similarity = distance; + closestString = cmd.Key; + } + return closestString; + } + public static Command GetCommand(string name) { return Commands.TryGetValue(name.ToLowerInvariant(), out var com) ? com : null; diff --git a/DeukBot4/MessageHandlers/CommandHandler/Commands/GeneralCommands.cs b/DeukBot4/MessageHandlers/CommandHandler/Commands/GeneralCommands.cs index 1e61794..cdc2421 100644 --- a/DeukBot4/MessageHandlers/CommandHandler/Commands/GeneralCommands.cs +++ b/DeukBot4/MessageHandlers/CommandHandler/Commands/GeneralCommands.cs @@ -55,8 +55,9 @@ usage: } else { - await request.OriginalMessage.Channel.SendMessageAsync( - HelpCommandGenerator.GenerateSpecificHelp(request.Parameters[0].AsString(), request.RequestPermissions)); + await request.OriginalMessage.Channel.SendMessageAsync("", embed: + HelpCommandGenerator.GenerateSpecificHelp(request.Parameters[0].AsString(), + request.RequestPermissions)); } } } diff --git a/DeukBot4/MessageHandlers/CommandHandler/Commands/ModeratorCommands.cs b/DeukBot4/MessageHandlers/CommandHandler/Commands/ModeratorCommands.cs index f63152b..6830cd8 100644 --- a/DeukBot4/MessageHandlers/CommandHandler/Commands/ModeratorCommands.cs +++ b/DeukBot4/MessageHandlers/CommandHandler/Commands/ModeratorCommands.cs @@ -12,29 +12,25 @@ namespace DeukBot4.MessageHandlers.CommandHandler [Command("kick", PermissionLevel.Moderator)] [CommandParameters(ParameterMatcher.ParameterType.User, ParameterMatcher.ParameterType.Remainder)] - [BlockUsageInPm] + [CommandHelp("Kicks a user from the server", + "Kicks a user from the server. Will not work on people with a helper role, or higher.\n" + + "Usage: \n" + + "``!kick {User Mention} {optional: Reason}``\n" + + "``!kick {User ID} {optional: Reason}``")] + [BlockUsageInPm, RequireParameterMatch] public async Task KickUser(CommandRequest request) { // get the server channel object out of message. Return if it's somehow not a server channel if (!(request.OriginalMessage.Channel is IGuildChannel channel)) return; - // if no parameters are found, stop - if (request.Parameters.Length == 0) - return; - // if the first parameter is empty, stop, this means it's not a valid user id - if (string.IsNullOrWhiteSpace(request.Parameters[0].AsString())) - return; - - // get the id, this parses the string to an id - var id = request.Parameters[0].AsUlong(); - // get the user using this id and the channel object. - var user = await channel.Guild.GetUserAsync(id); + // get the id of the user, this parses the string to an id + var user = await request.Parameters[0].AsDiscordUser(channel.Guild); // get the permissions of the user we want to kick var userPermissions = await PermissionValidator.GetUserPermissionLevel(request.OriginalMessage.Channel, (SocketUser) user); - // if the user has sufficient permissions, or is this bot, warn the user that he's not allowed to do that, and stop + // if the user has sufficient permissions, or is deukbot, warn the user that he's not allowed to do that, and stop if (userPermissions >= PermissionLevel.Helper || user.Id == Program.Client.CurrentUser.Id) { await request.OriginalMessage.Channel.SendMessageAsync("You are not allowed to kick that user"); diff --git a/DeukBot4/MessageHandlers/CommandHandler/HelpCommandGenerator.cs b/DeukBot4/MessageHandlers/CommandHandler/HelpCommandGenerator.cs index 429a6f9..e32042b 100644 --- a/DeukBot4/MessageHandlers/CommandHandler/HelpCommandGenerator.cs +++ b/DeukBot4/MessageHandlers/CommandHandler/HelpCommandGenerator.cs @@ -1,6 +1,8 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Text; using DeukBot4.MessageHandlers.Permissions; +using Discord; namespace DeukBot4.MessageHandlers.CommandHandler { @@ -35,16 +37,24 @@ namespace DeukBot4.MessageHandlers.CommandHandler return sb.ToString(); } - public static string GenerateSpecificHelp(string command, PermissionLevel level) + public static Embed GenerateSpecificHelp(string command, PermissionLevel level) { - if (!CommandHandler.Commands.TryGetValue(command, out var cmd)) + if (!CommandHandler.Commands.TryGetValue(command.ToLowerInvariant(), out var cmd)) return null; if (cmd.Permission > level) return null; if (!cmd.HasHelp) return null; - return $"**{cmd.Name}** - *{cmd.LongHelp}*"; + var eb = new EmbedBuilder + { + Title = cmd.Name, + Description = cmd.LongHelp, + Color = Color.Gold, + }; + + + return eb.Build(); } } } \ No newline at end of file diff --git a/DeukBot4/MessageHandlers/CommandHandler/RequestStructure/CommandRequest.cs b/DeukBot4/MessageHandlers/CommandHandler/RequestStructure/CommandRequest.cs index a7e6e43..1a87245 100644 --- a/DeukBot4/MessageHandlers/CommandHandler/RequestStructure/CommandRequest.cs +++ b/DeukBot4/MessageHandlers/CommandHandler/RequestStructure/CommandRequest.cs @@ -28,22 +28,22 @@ namespace DeukBot4.MessageHandlers.CommandHandler.RequestStructure public enum RequestCode { - OK, Invalid, Forbidden + OK, UnknownCommand, Invalid, Forbidden } - public static async Task<(CommandRequest, RequestCode)> Create(SocketMessage message) + public static async Task<(CommandRequest, RequestCode, object)> Create(SocketMessage message) { var originalMessage = message; var content = message.Content; var res = CommandNameMatcher.Match(content); if (res.Groups.Count <= 2) - return (null, RequestCode.Invalid); + return (null, RequestCode.Invalid, null); var commandName = res.Groups[1].Value; var command = CommandHandler.GetCommand(commandName); if (command == null) { - return (null, RequestCode.Invalid); + return (null, RequestCode.UnknownCommand, commandName); } PermissionLevel permission; @@ -54,15 +54,19 @@ namespace DeukBot4.MessageHandlers.CommandHandler.RequestStructure catch (Exception e) { await Logger.LogError(e.Message); - return (null, RequestCode.Forbidden); + return (null, RequestCode.Forbidden, null); } if (permission < command.Permission) { - return (null, RequestCode.Forbidden); + return (null, RequestCode.Forbidden, permission); } var parameterString = res.Groups[2].Value; var parameters = ParameterMatcher.GetParameterValues(command, parameterString); - return (new CommandRequest(originalMessage, command, permission, parameters), RequestCode.OK); + if (parameters == null) + { + return (null, RequestCode.Invalid, parameterString); + } + return (new CommandRequest(originalMessage, command, permission, parameters), RequestCode.OK, null); } } diff --git a/DeukBot4/MessageHandlers/CommandHandler/RequestStructure/ParameterMatcher.cs b/DeukBot4/MessageHandlers/CommandHandler/RequestStructure/ParameterMatcher.cs index 6121d3e..cce29a7 100644 --- a/DeukBot4/MessageHandlers/CommandHandler/RequestStructure/ParameterMatcher.cs +++ b/DeukBot4/MessageHandlers/CommandHandler/RequestStructure/ParameterMatcher.cs @@ -40,7 +40,7 @@ namespace DeukBot4.MessageHandlers.CommandHandler.RequestStructure case ParameterType.Remainder: return " *(.*)"; case ParameterType.User: - return " *(?:<@(?\\d*)>)|(?\\d*)"; + return " *<@(?\\d+)>|(?\\d+)"; default: throw new ArgumentOutOfRangeException(nameof(type), type, null); } @@ -56,7 +56,8 @@ namespace DeukBot4.MessageHandlers.CommandHandler.RequestStructure return matches.Groups.Skip(1).Select(x => new RequestParameter(x.Value)).ToArray(); } } - return new RequestParameter[0]; + + return command.RequireParameterMatch ? null : new RequestParameter[0]; } } } \ No newline at end of file diff --git a/DeukBot4/MessageHandlers/CommandHandler/RequestStructure/RequestParameter.cs b/DeukBot4/MessageHandlers/CommandHandler/RequestStructure/RequestParameter.cs index 48b8667..7dea536 100644 --- a/DeukBot4/MessageHandlers/CommandHandler/RequestStructure/RequestParameter.cs +++ b/DeukBot4/MessageHandlers/CommandHandler/RequestStructure/RequestParameter.cs @@ -1,4 +1,7 @@ using System; +using System.Linq; +using System.Threading.Tasks; +using Discord; namespace DeukBot4.MessageHandlers.CommandHandler.RequestStructure { @@ -33,5 +36,14 @@ namespace DeukBot4.MessageHandlers.CommandHandler.RequestStructure } throw new ArgumentException(); } + + public async Task AsDiscordUser(IGuild guild) + { + if (ulong.TryParse(_value, out var i)) + { + return await guild.GetUserAsync(i); + } + return null; + } } } \ No newline at end of file diff --git a/DeukBot4/MessageHandlers/MainHandler.cs b/DeukBot4/MessageHandlers/MainHandler.cs index 93d35cc..ff31f09 100644 --- a/DeukBot4/MessageHandlers/MainHandler.cs +++ b/DeukBot4/MessageHandlers/MainHandler.cs @@ -14,12 +14,23 @@ namespace DeukBot4.MessageHandlers } try { - await CommandHandler.CommandHandler.HandleMessage(message); +#pragma warning disable 4014 + CommandHandler.CommandHandler.HandleMessage(message); + HandlePrivateMessage(message); +#pragma warning restore 4014 } catch (Exception e) { await Logger.LogError(e.ToString()); } } + + private static async Task HandlePrivateMessage(SocketMessage message) + { + if (message.Channel is ISocketPrivateChannel) + { + await Logger.Log(($"Private Message: {message.Author.Username}- {message.Content}")); + } + } } } \ No newline at end of file diff --git a/DeukBot4/Utilities/Lehvenstein.cs b/DeukBot4/Utilities/Lehvenstein.cs new file mode 100644 index 0000000..f8c6bbd --- /dev/null +++ b/DeukBot4/Utilities/Lehvenstein.cs @@ -0,0 +1,59 @@ +using System; + +namespace DeukBot4.Utilities +{ + public class Lehvenstein + { + /// + /// Calculates the Levenshtein distance between two strings--the number of changes that need to be made for the first string to become the second. + /// + /// The first string, used as a source. + /// The second string, used as a target. + /// The number of changes that need to be made to convert the first string to the second. + /// + /// From http://www.merriampark.com/ldcsharp.htm + /// + public static int LevenshteinDistance(string first, string second) + { + if (first == null) + { + throw new ArgumentNullException(nameof(first)); + } + if (second == null) + { + throw new ArgumentNullException(nameof(second)); + } + + int n = first.Length; + int m = second.Length; + var d = new int[n + 1, m + 1]; // matrix + + if (n == 0) return m; + if (m == 0) return n; + + for (int i = 0; i <= n; d[i, 0] = i++) + { + } + + for (int j = 0; j <= m; d[0, j] = j++) + { + } + + for (int i = 1; i <= n; i++) + { + + for (int j = 1; j <= m; j++) + { + int cost = (second.Substring(j - 1, 1) == first.Substring(i - 1, 1) ? 0 : 1); // cost + d[i, j] = Math.Min( + Math.Min( + d[i - 1, j] + 1, + d[i, j - 1] + 1), + d[i - 1, j - 1] + cost); + } + } + + return d[n, m]; + } + } +} \ No newline at end of file