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.Threading.Tasks;
namespace Noikoio.RegexBot.Module.ModTools
namespace Noikoio.RegexBot.Module.ModCommands
{
/// <summary>
/// ModTools module.
/// 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>
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;
}
private async Task Client_MessageReceived(SocketMessage arg)
{
// Always ignore bots
if (arg.Author.IsBot) return;
// Always ignore these
if (arg.Author.IsBot || arg.Author.IsWebhook) return;
if (arg.Channel is IGuildChannel) await CommandCheckInvoke(arg);
}
[ConfigSection("ModTools")]
[ConfigSection("ModCommands")]
public override async Task<object> ProcessConfiguration(JToken configSection)
{
// Constructor throws exception on config errors
@ -69,12 +74,13 @@ namespace Noikoio.RegexBot.Module.ModTools
{
try
{
await Log($"'{c.Label}' invoked by {arg.Author.ToString()} in {g.Name}/#{arg.Channel.Name}");
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)
{
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());
}
}

View file

@ -7,9 +7,9 @@ using System.Linq;
using System.Text.RegularExpressions;
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.
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
// 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.
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;
_forceReason = conf["forcereason"]?.Value<bool>() ?? false;
@ -190,7 +190,7 @@ namespace Noikoio.RegexBot.Module.ModTools.Commands
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 += (_forceReason ? "L" : "Optionally l") + "ogs the reason for the ban to the Audit Log.";
if (_purgeDays > 0)
@ -217,13 +217,13 @@ namespace Noikoio.RegexBot.Module.ModTools.Commands
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) { }
}
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) { }
}
}

View file

@ -2,12 +2,12 @@
using Newtonsoft.Json.Linq;
using System.Threading.Tasks;
namespace Noikoio.RegexBot.Module.ModTools.Commands
namespace Noikoio.RegexBot.Module.ModCommands.Commands
{
class ConfReload : CommandBase
class ConfReload : Command
{
// 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)
public override async Task Invoke(SocketGuild g, SocketMessage msg)

View file

@ -4,13 +4,13 @@ using Newtonsoft.Json.Linq;
using System;
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.
// 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)
{
@ -33,7 +33,7 @@ namespace Noikoio.RegexBot.Module.ModTools.Commands
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.";
var usageEmbed = new EmbedBuilder()

View file

@ -6,14 +6,14 @@ using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Noikoio.RegexBot.Module.ModTools.Commands
namespace Noikoio.RegexBot.Module.ModCommands.Commands
{
class Unban : CommandBase
class Unban : Command
{
// No configuration.
// TODO bring in some options from BanKick. Particularly custom success msg.
// 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
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)
{
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.";
var usageEmbed = new EmbedBuilder()

View file

@ -9,24 +9,25 @@ using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Noikoio.RegexBot.Module.ModTools
namespace Noikoio.RegexBot.Module.ModCommands.Commands
{
/// <summary>
/// Base class for ModTools command.
/// We are not using Discord.Net's Commands extension, as it does not allow for changes during runtime.
/// Base class for a command within the module.
/// After implementing, don't forget to add a reference to
/// <see cref="CreateInstance(CommandListener, JProperty)"/>.
/// </summary>
[DebuggerDisplay("{Label}-type command")]
abstract class CommandBase
[DebuggerDisplay("Command def: {Label}")]
abstract class Command
{
private readonly ModTools _modtools;
private readonly CommandListener _modtools;
private readonly string _label;
private readonly string _command;
protected ModTools Mt => _modtools;
protected CommandListener Module => _modtools;
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;
_label = label;
@ -46,14 +47,14 @@ namespace Noikoio.RegexBot.Module.ModTools
new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase)
{
// Define all command types and their corresponding Types here
{ "ban", typeof(Commands.Ban) },
{ "confreload", typeof(Commands.ConfReload) },
{ "kick", typeof(Commands.Kick) },
{ "say", typeof(Commands.Say) },
{ "unban", typeof(Commands.Unban) }
{ "ban", typeof(Ban) },
{ "confreload", typeof(ConfReload) },
{ "kick", typeof(Kick) },
{ "say", typeof(Say) },
{ "unban", typeof(Unban) }
});
public static CommandBase CreateInstance(ModTools root, JProperty def)
public static Command CreateInstance(CommandListener root, JProperty def)
{
string label = def.Name;
if (string.IsNullOrWhiteSpace(label)) throw new RuleImportException("Label cannot be blank.");
@ -68,13 +69,11 @@ namespace Noikoio.RegexBot.Module.ModTools
string ctypestr = definition["type"]?.Value<string>();
if (string.IsNullOrWhiteSpace(ctypestr))
throw new RuleImportException($"Value 'type' must be specified in definition for '{label}'.");
Type ctype;
if (!_commands.TryGetValue(ctypestr, out ctype))
throw new RuleImportException($"The given 'type' value is invalid in definition for '{label}'.");
if (_commands.TryGetValue(ctypestr, out Type ctype))
{
try
{
return (CommandBase)Activator.CreateInstance(ctype, root, label, definition);
return (Command)Activator.CreateInstance(ctype, root, label, definition);
}
catch (TargetInvocationException ex)
{
@ -83,9 +82,14 @@ namespace Noikoio.RegexBot.Module.ModTools
else throw;
}
}
else
{
throw new RuleImportException($"The given 'type' value is invalid in definition for '{label}'.");
}
}
#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 RoleMention = 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 Noikoio.RegexBot.ConfigItem;
using Noikoio.RegexBot.Module.ModCommands.Commands;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace Noikoio.RegexBot.Module.ModTools
namespace Noikoio.RegexBot.Module.ModCommands
{
/// <summary>
/// Represents ModTools configuration within one server.
/// </summary>
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)
{
@ -23,9 +24,8 @@ namespace Noikoio.RegexBot.Module.ModTools
}
var config = (JObject)inconf;
// Command instances
var commands = new Dictionary<string, CommandBase>(StringComparer.OrdinalIgnoreCase);
// Command instance creation
var commands = new Dictionary<string, Command>(StringComparer.OrdinalIgnoreCase);
var commandconf = config["Commands"];
if (commandconf != null)
{
@ -37,16 +37,16 @@ namespace Noikoio.RegexBot.Module.ModTools
foreach (var def in commandconf.Children<JProperty>())
{
string label = def.Name;
var cmd = CommandBase.CreateInstance(instance, def);
if (commands.ContainsKey(cmd.Command))
var cmd = Command.CreateInstance(instance, def);
if (commands.ContainsKey(cmd.Trigger))
throw new RuleImportException(
$"{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.AutoMod.AutoMod(_client),
new Module.ModTools.ModTools(_client),
new Module.ModCommands.CommandListener(_client),
new Module.AutoRespond.AutoRespond(_client),
new EntityCache.Module(_client) // EntityCache goes before anything else that uses its data
};