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 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;

View File

@ -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));
}
}

View File

@ -13,8 +13,8 @@ namespace DeukBot4.MessageHandlers.CommandHandler
{
public static class CommandHandler
{
public static Dictionary<string, Command> Commands { get; private set; } = new Dictionary<string, Command>();
public const char CommandTrigger = '!';
public static Dictionary<string, Command> Commands { get; } = new Dictionary<string, Command>();
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<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)
{
return Commands.TryGetValue(name.ToLowerInvariant(), out var com) ? com : null;

View File

@ -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));
}
}
}

View File

@ -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");

View File

@ -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();
}
}
}

View File

@ -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);
}
}

View File

@ -40,7 +40,7 @@ namespace DeukBot4.MessageHandlers.CommandHandler.RequestStructure
case ParameterType.Remainder:
return " *(.*)";
case ParameterType.User:
return " *(?:<@(?<id>\\d*)>)|(?<id>\\d*)";
return " *<@(?<id>\\d+)>|(?<id>\\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];
}
}
}

View File

@ -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<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
{
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}"));
}
}
}
}

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];
}
}
}