Even more changes

This commit is contained in:
Deukhoofd 2018-03-30 14:51:38 +02:00
parent ba1096be6d
commit adf1766690
No known key found for this signature in database
GPG Key ID: B4C087AC81641654
12 changed files with 235 additions and 97 deletions

View File

@ -0,0 +1,10 @@
using System;
namespace DeukBot4.MessageHandlers
{
[AttributeUsage(AttributeTargets.Method)]
public class RequireParameterMatchAttribute : Attribute
{
}
}

View File

@ -1,49 +1,55 @@
using System.Diagnostics; using System.Reflection;
using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using DeukBot4.MessageHandlers.CommandHandler.RequestStructure; using DeukBot4.MessageHandlers.CommandHandler.RequestStructure;
using DeukBot4.MessageHandlers.Permissions; using DeukBot4.MessageHandlers.Permissions;
using Discord.WebSocket;
namespace DeukBot4.MessageHandlers.CommandHandler namespace DeukBot4.MessageHandlers.CommandHandler
{ {
public class Command public class Command
{ {
public Command(string name, PermissionLevel permission, string shortHelp, string longHelp, bool forbidInPm, public Command(string name, PermissionLevel permission, string shortHelp,
ParameterMatcher.ParameterType[][] parameterTypes, MethodInfo function, CommandContainerBase commandContainer) string longHelp,
ParameterMatcher.ParameterType[][] parameterTypes, bool forbidInPm, bool requireParameterMatch,
MethodInfo function,
CommandContainerBase commandContainer)
{ {
Name = name; Name = name;
Permission = permission; Permission = permission;
ShortHelp = shortHelp; ShortHelp = shortHelp;
LongHelp = longHelp; LongHelp = longHelp;
Function = function; Function = function;
CommandContainer = commandContainer; CommandContainer = commandContainer;
ParameterTypes = parameterTypes; ParameterTypes = parameterTypes;
HasHelp = true; HasHelp = true;
ForbidInPm = forbidInPm; ForbidInPm = forbidInPm;
RequireParameterMatch = requireParameterMatch;
} }
public Command(string name, PermissionLevel permission, ParameterMatcher.ParameterType[][] parameterTypes, bool forbidInPm, public Command(string name, PermissionLevel permission,
MethodInfo function, CommandContainerBase commandContainer) ParameterMatcher.ParameterType[][] parameterTypes,
bool forbidInPm, bool requireParameterMatch,
MethodInfo function, CommandContainerBase commandContainer)
{ {
Name = name; Name = name;
Permission = permission; Permission = permission;
Function = function; Function = function;
CommandContainer = commandContainer; CommandContainer = commandContainer;
ParameterTypes = parameterTypes; ParameterTypes = parameterTypes;
HasHelp = false; HasHelp = false;
ForbidInPm = forbidInPm; ForbidInPm = forbidInPm;
RequireParameterMatch = requireParameterMatch;
} }
public string Name { get; } public string Name { get; }
public PermissionLevel Permission { get; } public PermissionLevel Permission { get; }
public string ShortHelp { get; } public string ShortHelp { get; }
public string LongHelp { get; } public string LongHelp { get; }
public MethodInfo Function { get; } public MethodInfo Function { get; }
public CommandContainerBase CommandContainer { get; } public CommandContainerBase CommandContainer { get; }
public bool HasHelp { get; } public bool HasHelp { get; }
public ParameterMatcher.ParameterType[][] ParameterTypes { get; } public ParameterMatcher.ParameterType[][] ParameterTypes { get; }
public bool ForbidInPm { get; } public bool ForbidInPm { get; }
public bool RequireParameterMatch { get; }
private string[] _parameterMatchers; private string[] _parameterMatchers;

View File

@ -17,37 +17,44 @@ namespace DeukBot4.MessageHandlers.CommandHandler
foreach (var methodInfo in funcs) foreach (var methodInfo in funcs)
{ {
// grab all command attributes, cast them properly
var commandAttributes = methodInfo.GetCustomAttributes(typeof(CommandAttribute), true) var commandAttributes = methodInfo.GetCustomAttributes(typeof(CommandAttribute), true)
.Select(x => x as CommandAttribute); .Select(x => x as CommandAttribute);
// get the help attribute if it exists
CommandHelpAttribute helpAttribute = null; CommandHelpAttribute helpAttribute = null;
var helpAttributes = methodInfo.GetCustomAttributes(typeof(CommandHelpAttribute), true) var helpAttributes = methodInfo.GetCustomAttributes(typeof(CommandHelpAttribute), true)
.Select(x => x as CommandHelpAttribute); .Select(x => x as CommandHelpAttribute).ToArray();
var commandHelpAttributes = helpAttributes as CommandHelpAttribute[] ?? helpAttributes.ToArray(); if (helpAttributes.Any())
if (commandHelpAttributes.Any()) helpAttribute = helpAttributes[0];
helpAttribute = commandHelpAttributes[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 forbidPm = methodInfo.GetCustomAttributes(typeof(BlockUsageInPmAttribute), true).Any();
var matchParametersExactly =
methodInfo.GetCustomAttributes(typeof(RequireParameterMatchAttribute), true).Any();
foreach (var commandAttribute in commandAttributes) foreach (var commandAttribute in commandAttributes)
{ {
if (commandAttribute == null) if (commandAttribute == null)
continue; continue;
if (helpAttribute == null) if (helpAttribute == null)
{ {
commands.Add(new Command(commandAttribute.Command, commandAttribute.Permission, parameters, forbidPm, commands.Add(new Command(commandAttribute.Command, commandAttribute.Permission, parameters,
methodInfo, this)); forbidPm, matchParametersExactly, methodInfo, this));
} }
else else
{ {
commands.Add(new Command(commandAttribute.Command, commandAttribute.Permission, 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));
} }
} }

View File

@ -13,8 +13,8 @@ namespace DeukBot4.MessageHandlers.CommandHandler
{ {
public static class CommandHandler public static class CommandHandler
{ {
public static Dictionary<string, Command> Commands { get; private set; } = new Dictionary<string, Command>(); public static Dictionary<string, Command> Commands { get; } = new Dictionary<string, Command>();
public const char CommandTrigger = '!'; private const char CommandTrigger = '!';
public static void Build() public static void Build()
{ {
@ -31,7 +31,8 @@ namespace DeukBot4.MessageHandlers.CommandHandler
Commands.Add(command.Name.ToLowerInvariant(), command); 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)) if (string.IsNullOrWhiteSpace(message.Content))
return; return;
if (message.Content[0] != CommandTrigger) return; if (message.Content[0] != CommandTrigger)
return;
var req = await CommandRequest.Create(message); var req = await CommandRequest.Create(message);
var resultCode = req.Item2; var resultCode = req.Item2;
if (resultCode == CommandRequest.RequestCode.Invalid) switch (resultCode)
{ {
await Logger.LogError("Invalid content: " + message.Content); case CommandRequest.RequestCode.Invalid:
return; await Logger.LogError("Invalid content: " + message.Content);
}
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}");
return; return;
} case CommandRequest.RequestCode.Forbidden:
await Logger.Log(
await req.Item1.Command.Invoke(req.Item1); $"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<string> 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) public static Command GetCommand(string name)
{ {
return Commands.TryGetValue(name.ToLowerInvariant(), out var com) ? com : null; return Commands.TryGetValue(name.ToLowerInvariant(), out var com) ? com : null;

View File

@ -55,8 +55,9 @@ usage:
} }
else else
{ {
await request.OriginalMessage.Channel.SendMessageAsync( await request.OriginalMessage.Channel.SendMessageAsync("", embed:
HelpCommandGenerator.GenerateSpecificHelp(request.Parameters[0].AsString(), request.RequestPermissions)); HelpCommandGenerator.GenerateSpecificHelp(request.Parameters[0].AsString(),
request.RequestPermissions));
} }
} }
} }

View File

@ -12,29 +12,25 @@ namespace DeukBot4.MessageHandlers.CommandHandler
[Command("kick", PermissionLevel.Moderator)] [Command("kick", PermissionLevel.Moderator)]
[CommandParameters(ParameterMatcher.ParameterType.User, ParameterMatcher.ParameterType.Remainder)] [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) public async Task KickUser(CommandRequest request)
{ {
// get the server channel object out of message. Return if it's somehow not a server channel // get the server channel object out of message. Return if it's somehow not a server channel
if (!(request.OriginalMessage.Channel is IGuildChannel channel)) if (!(request.OriginalMessage.Channel is IGuildChannel channel))
return; return;
// if no parameters are found, stop // get the id of the user, this parses the string to an id
if (request.Parameters.Length == 0) var user = await request.Parameters[0].AsDiscordUser(channel.Guild);
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 permissions of the user we want to kick // get the permissions of the user we want to kick
var userPermissions = var userPermissions =
await PermissionValidator.GetUserPermissionLevel(request.OriginalMessage.Channel, (SocketUser) user); 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) if (userPermissions >= PermissionLevel.Helper || user.Id == Program.Client.CurrentUser.Id)
{ {
await request.OriginalMessage.Channel.SendMessageAsync("You are not allowed to kick that user"); await request.OriginalMessage.Channel.SendMessageAsync("You are not allowed to kick that user");

View File

@ -1,6 +1,8 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Text; using System.Text;
using DeukBot4.MessageHandlers.Permissions; using DeukBot4.MessageHandlers.Permissions;
using Discord;
namespace DeukBot4.MessageHandlers.CommandHandler namespace DeukBot4.MessageHandlers.CommandHandler
{ {
@ -35,16 +37,24 @@ namespace DeukBot4.MessageHandlers.CommandHandler
return sb.ToString(); 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; return null;
if (cmd.Permission > level) if (cmd.Permission > level)
return null; return null;
if (!cmd.HasHelp) if (!cmd.HasHelp)
return null; return null;
return $"**{cmd.Name}** - *{cmd.LongHelp}*"; var eb = new EmbedBuilder
{
Title = cmd.Name,
Description = cmd.LongHelp,
Color = Color.Gold,
};
return eb.Build();
} }
} }
} }

View File

@ -28,22 +28,22 @@ namespace DeukBot4.MessageHandlers.CommandHandler.RequestStructure
public enum RequestCode 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 originalMessage = message;
var content = message.Content; var content = message.Content;
var res = CommandNameMatcher.Match(content); var res = CommandNameMatcher.Match(content);
if (res.Groups.Count <= 2) if (res.Groups.Count <= 2)
return (null, RequestCode.Invalid); return (null, RequestCode.Invalid, null);
var commandName = res.Groups[1].Value; var commandName = res.Groups[1].Value;
var command = CommandHandler.GetCommand(commandName); var command = CommandHandler.GetCommand(commandName);
if (command == null) if (command == null)
{ {
return (null, RequestCode.Invalid); return (null, RequestCode.UnknownCommand, commandName);
} }
PermissionLevel permission; PermissionLevel permission;
@ -54,15 +54,19 @@ namespace DeukBot4.MessageHandlers.CommandHandler.RequestStructure
catch (Exception e) catch (Exception e)
{ {
await Logger.LogError(e.Message); await Logger.LogError(e.Message);
return (null, RequestCode.Forbidden); return (null, RequestCode.Forbidden, null);
} }
if (permission < command.Permission) if (permission < command.Permission)
{ {
return (null, RequestCode.Forbidden); return (null, RequestCode.Forbidden, permission);
} }
var parameterString = res.Groups[2].Value; var parameterString = res.Groups[2].Value;
var parameters = ParameterMatcher.GetParameterValues(command, parameterString); 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);
} }
} }

View File

@ -40,7 +40,7 @@ namespace DeukBot4.MessageHandlers.CommandHandler.RequestStructure
case ParameterType.Remainder: case ParameterType.Remainder:
return " *(.*)"; return " *(.*)";
case ParameterType.User: case ParameterType.User:
return " *(?:<@(?<id>\\d*)>)|(?<id>\\d*)"; return " *<@(?<id>\\d+)>|(?<id>\\d+)";
default: default:
throw new ArgumentOutOfRangeException(nameof(type), type, null); 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 matches.Groups.Skip(1).Select(x => new RequestParameter(x.Value)).ToArray();
} }
} }
return new RequestParameter[0];
return command.RequireParameterMatch ? null : new RequestParameter[0];
} }
} }
} }

View File

@ -1,4 +1,7 @@
using System; using System;
using System.Linq;
using System.Threading.Tasks;
using Discord;
namespace DeukBot4.MessageHandlers.CommandHandler.RequestStructure namespace DeukBot4.MessageHandlers.CommandHandler.RequestStructure
{ {
@ -33,5 +36,14 @@ namespace DeukBot4.MessageHandlers.CommandHandler.RequestStructure
} }
throw new ArgumentException(); throw new ArgumentException();
} }
public async Task<IGuildUser> AsDiscordUser(IGuild guild)
{
if (ulong.TryParse(_value, out var i))
{
return await guild.GetUserAsync(i);
}
return null;
}
} }
} }

View File

@ -14,12 +14,23 @@ namespace DeukBot4.MessageHandlers
} }
try try
{ {
await CommandHandler.CommandHandler.HandleMessage(message); #pragma warning disable 4014
CommandHandler.CommandHandler.HandleMessage(message);
HandlePrivateMessage(message);
#pragma warning restore 4014
} }
catch (Exception e) catch (Exception e)
{ {
await Logger.LogError(e.ToString()); 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}"));
}
}
} }
} }

View File

@ -0,0 +1,59 @@
using System;
namespace DeukBot4.Utilities
{
public class Lehvenstein
{
/// <summary>
/// Calculates the Levenshtein distance between two strings--the number of changes that need to be made for the first string to become the second.
/// </summary>
/// <param name="first">The first string, used as a source.</param>
/// <param name="second">The second string, used as a target.</param>
/// <returns>The number of changes that need to be made to convert the first string to the second.</returns>
/// <remarks>
/// From http://www.merriampark.com/ldcsharp.htm
/// </remarks>
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];
}
}
}