Reorganized and renamed ModTools to ModCommands

This commit is contained in:
Noikoio 2018-03-09 22:17:55 -08:00
parent 06583a1472
commit bfb699d62f
8 changed files with 76 additions and 66 deletions

View file

@ -6,30 +6,35 @@ using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Noikoio.RegexBot.Module.ModTools namespace Noikoio.RegexBot.Module.ModCommands
{ {
/// <summary> /// <summary>
/// ModTools module.
/// This class manages reading configuration and creating instances based on it. /// This class manages reading configuration and creating instances based on it.
/// It processes input and looks for messages that intend to invoke commands defined in configuration.
/// </summary> /// </summary>
class ModTools : BotModule /// <remarks>
/// Discord.Net has its own recommended way of implementing commands, but it's not exactly
/// done in a way that would easily allow for flexibility and modifications during runtime.
/// Thus, reinventing the wheel right here.
/// </remarks>
class CommandListener : BotModule
{ {
public override string Name => "ModTools"; public override string Name => "ModCommands";
public ModTools(DiscordSocketClient client) : base(client) public CommandListener(DiscordSocketClient client) : base(client)
{ {
client.MessageReceived += Client_MessageReceived; client.MessageReceived += Client_MessageReceived;
} }
private async Task Client_MessageReceived(SocketMessage arg) private async Task Client_MessageReceived(SocketMessage arg)
{ {
// Always ignore bots // Always ignore these
if (arg.Author.IsBot) return; if (arg.Author.IsBot || arg.Author.IsWebhook) return;
if (arg.Channel is IGuildChannel) await CommandCheckInvoke(arg); if (arg.Channel is IGuildChannel) await CommandCheckInvoke(arg);
} }
[ConfigSection("ModTools")] [ConfigSection("ModCommands")]
public override async Task<object> ProcessConfiguration(JToken configSection) public override async Task<object> ProcessConfiguration(JToken configSection)
{ {
// Constructor throws exception on config errors // Constructor throws exception on config errors
@ -69,12 +74,13 @@ namespace Noikoio.RegexBot.Module.ModTools
{ {
try try
{ {
await Log($"'{c.Label}' invoked by {arg.Author.ToString()} in {g.Name}/#{arg.Channel.Name}");
await c.Invoke(g, arg); await c.Invoke(g, arg);
// TODO Custom invocation log messages? Not by the user, but by the command.
await Log($"{g.Name}/#{arg.Channel.Name}: {arg.Author} invoked {arg.Content}");
} }
catch (Exception ex) catch (Exception ex)
{ {
await Log($"Encountered an error for the command '{c.Label}'. Details follow:"); await Log($"Encountered an unhandled exception while processing '{c.Label}'. Details follow:");
await Log(ex.ToString()); await Log(ex.ToString());
} }
} }

View file

@ -7,9 +7,9 @@ using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Noikoio.RegexBot.Module.ModTools.Commands namespace Noikoio.RegexBot.Module.ModCommands.Commands
{ {
class BanKick : CommandBase class BanKick : Command
{ {
// Ban and kick commands are highly similar in implementation, and thus are handled in a single class. // Ban and kick commands are highly similar in implementation, and thus are handled in a single class.
protected enum CommandMode { Ban, Kick } protected enum CommandMode { Ban, Kick }
@ -28,7 +28,7 @@ namespace Noikoio.RegexBot.Module.ModTools.Commands
// "notifymsg" - Message to send to the target user being acted upon. Default message is used // "notifymsg" - Message to send to the target user being acted upon. Default message is used
// if the value is not specified. If a blank value is given, the feature is disabled. // if the value is not specified. If a blank value is given, the feature is disabled.
// Takes the special values $s for server name and $r for reason text. // Takes the special values $s for server name and $r for reason text.
protected BanKick(ModTools l, string label, JObject conf, CommandMode mode) : base(l, label, conf) protected BanKick(CommandListener l, string label, JObject conf, CommandMode mode) : base(l, label, conf)
{ {
_mode = mode; _mode = mode;
_forceReason = conf["forcereason"]?.Value<bool>() ?? false; _forceReason = conf["forcereason"]?.Value<bool>() ?? false;
@ -190,7 +190,7 @@ namespace Noikoio.RegexBot.Module.ModTools.Commands
private async Task SendUsageMessage(SocketMessage m, string message) private async Task SendUsageMessage(SocketMessage m, string message)
{ {
string desc = $"{this.Command} [user or user ID] " + (_forceReason ? "[reason]" : "*[reason]*") + "\n"; string desc = $"{this.Trigger} [user or user ID] " + (_forceReason ? "[reason]" : "*[reason]*") + "\n";
desc += "Removes the given user from this server and prevents the user from rejoining. "; desc += "Removes the given user from this server and prevents the user from rejoining. ";
desc += (_forceReason ? "L" : "Optionally l") + "ogs the reason for the ban to the Audit Log."; desc += (_forceReason ? "L" : "Optionally l") + "ogs the reason for the ban to the Audit Log.";
if (_purgeDays > 0) if (_purgeDays > 0)
@ -217,13 +217,13 @@ namespace Noikoio.RegexBot.Module.ModTools.Commands
class Ban : BanKick class Ban : BanKick
{ {
public Ban(ModTools l, string label, JObject conf) public Ban(CommandListener l, string label, JObject conf)
: base(l, label, conf, CommandMode.Ban) { } : base(l, label, conf, CommandMode.Ban) { }
} }
class Kick : BanKick class Kick : BanKick
{ {
public Kick(ModTools l, string label, JObject conf) public Kick(CommandListener l, string label, JObject conf)
: base(l, label, conf, CommandMode.Kick) { } : base(l, label, conf, CommandMode.Kick) { }
} }
} }

View file

@ -2,12 +2,12 @@
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Noikoio.RegexBot.Module.ModTools.Commands namespace Noikoio.RegexBot.Module.ModCommands.Commands
{ {
class ConfReload : CommandBase class ConfReload : Command
{ {
// No configuration. // No configuration.
public ConfReload(ModTools l, string label, JObject conf) : base(l, label, conf) { } public ConfReload(CommandListener l, string label, JObject conf) : base(l, label, conf) { }
// Usage: (command) // Usage: (command)
public override async Task Invoke(SocketGuild g, SocketMessage msg) public override async Task Invoke(SocketGuild g, SocketMessage msg)

View file

@ -4,13 +4,13 @@ using Newtonsoft.Json.Linq;
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Noikoio.RegexBot.Module.ModTools.Commands namespace Noikoio.RegexBot.Module.ModCommands.Commands
{ {
class Say : CommandBase class Say : Command
{ {
// No configuration at the moment. // No configuration at the moment.
// TODO: Whitelist/blacklist - to limit which channels it can "say" into // TODO: Whitelist/blacklist - to limit which channels it can "say" into
public Say(ModTools l, string label, JObject conf) : base(l, label, conf) { } public Say(CommandListener l, string label, JObject conf) : base(l, label, conf) { }
public override async Task Invoke(SocketGuild g, SocketMessage msg) public override async Task Invoke(SocketGuild g, SocketMessage msg)
{ {
@ -33,7 +33,7 @@ namespace Noikoio.RegexBot.Module.ModTools.Commands
private async Task SendUsageMessage(SocketMessage m, string message) private async Task SendUsageMessage(SocketMessage m, string message)
{ {
string desc = $"{this.Command} [channel] [message]\n"; string desc = $"{this.Trigger} [channel] [message]\n";
desc += "Displays the given message exactly as specified to the given channel."; desc += "Displays the given message exactly as specified to the given channel.";
var usageEmbed = new EmbedBuilder() var usageEmbed = new EmbedBuilder()

View file

@ -6,14 +6,14 @@ using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Noikoio.RegexBot.Module.ModTools.Commands namespace Noikoio.RegexBot.Module.ModCommands.Commands
{ {
class Unban : CommandBase class Unban : Command
{ {
// No configuration. // No configuration.
// TODO bring in some options from BanKick. Particularly custom success msg. // TODO bring in some options from BanKick. Particularly custom success msg.
// TODO when ModLogs fully implemented, add a reason? // TODO when ModLogs fully implemented, add a reason?
public Unban(ModTools l, string label, JObject conf) : base(l, label, conf) { } public Unban(CommandListener l, string label, JObject conf) : base(l, label, conf) { }
#region Strings #region Strings
const string FailPrefix = ":x: **Unable to unban:** "; const string FailPrefix = ":x: **Unable to unban:** ";
@ -93,7 +93,7 @@ namespace Noikoio.RegexBot.Module.ModTools.Commands
private async Task SendUsageMessage(SocketMessage m, string message) private async Task SendUsageMessage(SocketMessage m, string message)
{ {
string desc = $"{this.Command} [user or user ID]\n"; string desc = $"{this.Trigger} [user or user ID]\n";
desc += "Unbans the given user, allowing them to rejoin the server."; desc += "Unbans the given user, allowing them to rejoin the server.";
var usageEmbed = new EmbedBuilder() var usageEmbed = new EmbedBuilder()

View file

@ -9,24 +9,25 @@ using System.Reflection;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Noikoio.RegexBot.Module.ModTools namespace Noikoio.RegexBot.Module.ModCommands.Commands
{ {
/// <summary> /// <summary>
/// Base class for ModTools command. /// Base class for a command within the module.
/// We are not using Discord.Net's Commands extension, as it does not allow for changes during runtime. /// After implementing, don't forget to add a reference to
/// <see cref="CreateInstance(CommandListener, JProperty)"/>.
/// </summary> /// </summary>
[DebuggerDisplay("{Label}-type command")] [DebuggerDisplay("Command def: {Label}")]
abstract class CommandBase abstract class Command
{ {
private readonly ModTools _modtools; private readonly CommandListener _modtools;
private readonly string _label; private readonly string _label;
private readonly string _command; private readonly string _command;
protected ModTools Mt => _modtools; protected CommandListener Module => _modtools;
public string Label => _label; public string Label => _label;
public string Command => _command; public string Trigger => _command;
protected CommandBase(ModTools l, string label, JObject conf) public Command(CommandListener l, string label, JObject conf)
{ {
_modtools = l; _modtools = l;
_label = label; _label = label;
@ -46,14 +47,14 @@ namespace Noikoio.RegexBot.Module.ModTools
new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase) new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase)
{ {
// Define all command types and their corresponding Types here // Define all command types and their corresponding Types here
{ "ban", typeof(Commands.Ban) }, { "ban", typeof(Ban) },
{ "confreload", typeof(Commands.ConfReload) }, { "confreload", typeof(ConfReload) },
{ "kick", typeof(Commands.Kick) }, { "kick", typeof(Kick) },
{ "say", typeof(Commands.Say) }, { "say", typeof(Say) },
{ "unban", typeof(Commands.Unban) } { "unban", typeof(Unban) }
}); });
public static CommandBase CreateInstance(ModTools root, JProperty def) public static Command CreateInstance(CommandListener root, JProperty def)
{ {
string label = def.Name; string label = def.Name;
if (string.IsNullOrWhiteSpace(label)) throw new RuleImportException("Label cannot be blank."); if (string.IsNullOrWhiteSpace(label)) throw new RuleImportException("Label cannot be blank.");
@ -68,24 +69,27 @@ namespace Noikoio.RegexBot.Module.ModTools
string ctypestr = definition["type"]?.Value<string>(); string ctypestr = definition["type"]?.Value<string>();
if (string.IsNullOrWhiteSpace(ctypestr)) if (string.IsNullOrWhiteSpace(ctypestr))
throw new RuleImportException($"Value 'type' must be specified in definition for '{label}'."); throw new RuleImportException($"Value 'type' must be specified in definition for '{label}'.");
Type ctype; if (_commands.TryGetValue(ctypestr, out Type ctype))
if (!_commands.TryGetValue(ctypestr, out ctype))
throw new RuleImportException($"The given 'type' value is invalid in definition for '{label}'.");
try
{ {
return (CommandBase)Activator.CreateInstance(ctype, root, label, definition); try
{
return (Command)Activator.CreateInstance(ctype, root, label, definition);
}
catch (TargetInvocationException ex)
{
if (ex.InnerException is RuleImportException)
throw new RuleImportException($"Error in configuration for command '{label}': {ex.InnerException.Message}");
else throw;
}
} }
catch (TargetInvocationException ex) else
{ {
if (ex.InnerException is RuleImportException) throw new RuleImportException($"The given 'type' value is invalid in definition for '{label}'.");
throw new RuleImportException($"Error in configuration for command '{label}': {ex.InnerException.Message}");
else throw;
} }
} }
#endregion #endregion
#region Helper methods and values #region Helper methods and common values
protected static readonly Regex UserMention = new Regex(@"<@!?(?<snowflake>\d+)>", RegexOptions.Compiled); protected static readonly Regex UserMention = new Regex(@"<@!?(?<snowflake>\d+)>", RegexOptions.Compiled);
protected static readonly Regex RoleMention = new Regex(@"<@&(?<snowflake>\d+)>", RegexOptions.Compiled); protected static readonly Regex RoleMention = new Regex(@"<@&(?<snowflake>\d+)>", RegexOptions.Compiled);
protected static readonly Regex ChannelMention = new Regex(@"<#(?<snowflake>\d+)>", RegexOptions.Compiled); protected static readonly Regex ChannelMention = new Regex(@"<#(?<snowflake>\d+)>", RegexOptions.Compiled);

View file

@ -1,21 +1,22 @@
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Noikoio.RegexBot.ConfigItem; using Noikoio.RegexBot.ConfigItem;
using Noikoio.RegexBot.Module.ModCommands.Commands;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
namespace Noikoio.RegexBot.Module.ModTools namespace Noikoio.RegexBot.Module.ModCommands
{ {
/// <summary> /// <summary>
/// Represents ModTools configuration within one server. /// Represents ModTools configuration within one server.
/// </summary> /// </summary>
class ConfigItem class ConfigItem
{ {
private readonly ReadOnlyDictionary<string, CommandBase> _cmdInstances; private readonly ReadOnlyDictionary<string, Command> _cmdInstances;
public ReadOnlyDictionary<string, CommandBase> Commands => _cmdInstances; public ReadOnlyDictionary<string, Command> Commands => _cmdInstances;
public ConfigItem(ModTools instance, JToken inconf) public ConfigItem(CommandListener instance, JToken inconf)
{ {
if (inconf.Type != JTokenType.Object) if (inconf.Type != JTokenType.Object)
{ {
@ -23,9 +24,8 @@ namespace Noikoio.RegexBot.Module.ModTools
} }
var config = (JObject)inconf; var config = (JObject)inconf;
// Command instance creation
// Command instances var commands = new Dictionary<string, Command>(StringComparer.OrdinalIgnoreCase);
var commands = new Dictionary<string, CommandBase>(StringComparer.OrdinalIgnoreCase);
var commandconf = config["Commands"]; var commandconf = config["Commands"];
if (commandconf != null) if (commandconf != null)
{ {
@ -37,16 +37,16 @@ namespace Noikoio.RegexBot.Module.ModTools
foreach (var def in commandconf.Children<JProperty>()) foreach (var def in commandconf.Children<JProperty>())
{ {
string label = def.Name; string label = def.Name;
var cmd = CommandBase.CreateInstance(instance, def); var cmd = Command.CreateInstance(instance, def);
if (commands.ContainsKey(cmd.Command)) if (commands.ContainsKey(cmd.Trigger))
throw new RuleImportException( throw new RuleImportException(
$"{label}: 'command' value must not be equal to that of another definition. " + $"{label}: 'command' value must not be equal to that of another definition. " +
$"Given value is being used for {commands[cmd.Command].Label}."); $"Given value is being used for \"{commands[cmd.Trigger].Label}\".");
commands.Add(cmd.Command, cmd); commands.Add(cmd.Trigger, cmd);
} }
} }
_cmdInstances = new ReadOnlyDictionary<string, CommandBase>(commands); _cmdInstances = new ReadOnlyDictionary<string, Command>(commands);
} }
} }
} }

View file

@ -56,7 +56,7 @@ namespace Noikoio.RegexBot
{ {
new Module.DMLogger.DMLogger(_client), new Module.DMLogger.DMLogger(_client),
new Module.AutoMod.AutoMod(_client), new Module.AutoMod.AutoMod(_client),
new Module.ModTools.ModTools(_client), new Module.ModCommands.CommandListener(_client),
new Module.AutoRespond.AutoRespond(_client), new Module.AutoRespond.AutoRespond(_client),
new EntityCache.Module(_client) // EntityCache goes before anything else that uses its data new EntityCache.Module(_client) // EntityCache goes before anything else that uses its data
}; };