From d7b1486615a2addb6c9a00484102b7cfc3105aca Mon Sep 17 00:00:00 2001 From: Noikoio Date: Sun, 4 Mar 2018 17:59:35 -0800 Subject: [PATCH 01/17] Added comment for consistency --- Module/ModTools/Commands/Say.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Module/ModTools/Commands/Say.cs b/Module/ModTools/Commands/Say.cs index baf2424..ff6b3c3 100644 --- a/Module/ModTools/Commands/Say.cs +++ b/Module/ModTools/Commands/Say.cs @@ -8,10 +8,10 @@ namespace Noikoio.RegexBot.Module.ModTools.Commands { class Say : CommandBase { - public Say(ModTools l, string label, JObject conf) : base(l, label, conf) { } - + // 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 override async Task Invoke(SocketGuild g, SocketMessage msg) { string[] line = msg.Content.Split(new char[] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries); From c5229a22b29029939622933733a42fdd5d263c15 Mon Sep 17 00:00:00 2001 From: Noikoio Date: Sun, 4 Mar 2018 18:04:10 -0800 Subject: [PATCH 02/17] Added basic cache error handling --- Module/ModTools/Commands/BanKick.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Module/ModTools/Commands/BanKick.cs b/Module/ModTools/Commands/BanKick.cs index 2009926..5302bf6 100644 --- a/Module/ModTools/Commands/BanKick.cs +++ b/Module/ModTools/Commands/BanKick.cs @@ -99,7 +99,17 @@ namespace Noikoio.RegexBot.Module.ModTools.Commands Match m = UserMention.Match(targetstr); if (m.Success) targetstr = m.Groups["snowflake"].Value; - var qres = (await EntityCache.EntityCache.QueryAsync(g.Id, targetstr)).FirstOrDefault(); + EntityCache.CacheUser qres; + try + { + qres = (await EntityCache.EntityCache.QueryAsync(g.Id, targetstr)).FirstOrDefault(); + } + catch (Npgsql.NpgsqlException ex) + { + await Log("A database error occurred during user lookup: " + ex.Message); + await msg.Channel.SendMessageAsync(FailPrefix + FailDefault); + return; + } if (qres == null) { await SendUsageMessage(msg, TargetNotFound); From dc7725c7b08afb9a5756dbeec765fb56faf6755a Mon Sep 17 00:00:00 2001 From: Noikoio Date: Sun, 4 Mar 2018 18:36:32 -0800 Subject: [PATCH 03/17] Added 'Unban' command type --- Module/ModTools/CommandBase.cs | 3 +- Module/ModTools/Commands/Unban.cs | 107 ++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 Module/ModTools/Commands/Unban.cs diff --git a/Module/ModTools/CommandBase.cs b/Module/ModTools/CommandBase.cs index 9542ea4..5f98a75 100644 --- a/Module/ModTools/CommandBase.cs +++ b/Module/ModTools/CommandBase.cs @@ -48,7 +48,8 @@ namespace Noikoio.RegexBot.Module.ModTools // Define all command types and their corresponding Types here { "ban", typeof(Commands.Ban) }, { "kick", typeof(Commands.Kick) }, - { "say", typeof(Commands.Say) } + { "say", typeof(Commands.Say) }, + { "unban", typeof(Commands.Unban) } }); public static CommandBase CreateInstance(ModTools root, JProperty def) diff --git a/Module/ModTools/Commands/Unban.cs b/Module/ModTools/Commands/Unban.cs new file mode 100644 index 0000000..aa14484 --- /dev/null +++ b/Module/ModTools/Commands/Unban.cs @@ -0,0 +1,107 @@ +using Discord; +using Discord.WebSocket; +using Newtonsoft.Json.Linq; +using System; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Noikoio.RegexBot.Module.ModTools.Commands +{ + class Unban : CommandBase + { + // 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) { } + + #region Strings + const string FailPrefix = ":x: **Unable to unban:** "; + const string Fail403 = "I do not have the required permissions to perform that action."; + const string Fail404 = "The target user is no longer available."; + const string FailDefault = "An unknown error occurred. Notify the bot operator."; + const string TargetNotFound = ":x: **Unable to determine the target user.**"; + const string Success = ":white_check_mark: Unbanned user **{0}**."; + #endregion + + // Usage: (command) (user query) + public override async Task Invoke(SocketGuild g, SocketMessage msg) + { + // TODO oh god there's so much boilerplate copypasted from BanKick make it stop + + string[] line = msg.Content.Split(new char[] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries); + string targetstr; + if (line.Length < 2) + { + await SendUsageMessage(msg, null); + return; + } + targetstr = line[1]; + + // Getting SocketGuildUser target + SocketGuildUser targetobj = null; + + // Extract snowflake value from mention (if a mention was given) + Match m = UserMention.Match(targetstr); + if (m.Success) targetstr = m.Groups["snowflake"].Value; + + EntityCache.CacheUser qres; + try + { + qres = (await EntityCache.EntityCache.QueryAsync(g.Id, targetstr)).FirstOrDefault(); + } + catch (Npgsql.NpgsqlException ex) + { + await Log("A database error occurred during user lookup: " + ex.Message); + await msg.Channel.SendMessageAsync(FailPrefix + FailDefault); + return; + } + + if (qres == null) + { + await SendUsageMessage(msg, TargetNotFound); + return; + } + + ulong targetuid = qres.UserId; + targetobj = g.GetUser(targetuid); + string targetdisp = targetobj?.ToString() ?? $"ID {targetuid}"; + + // Do the action + try + { + await g.RemoveBanAsync(targetuid); + await msg.Channel.SendMessageAsync(string.Format(Success, targetdisp)); + } + catch (Discord.Net.HttpException ex) + { + if (ex.HttpCode == System.Net.HttpStatusCode.Forbidden) + { + await msg.Channel.SendMessageAsync(FailPrefix + Fail403); + } + else if (ex.HttpCode == System.Net.HttpStatusCode.NotFound) + { + await msg.Channel.SendMessageAsync(FailPrefix + Fail404); + } + else + { + await msg.Channel.SendMessageAsync(FailPrefix + FailDefault); + await Log(ex.ToString()); + } + } + } + + private async Task SendUsageMessage(SocketMessage m, string message) + { + string desc = $"{this.Command} [user or user ID]\n"; + desc += "Unbans the given user, allowing them to rejoin the server."; + + var usageEmbed = new EmbedBuilder() + { + Title = "Usage", + Description = desc + }; + await m.Channel.SendMessageAsync(message ?? "", embed: usageEmbed); + } + } +} From 7a8a82102f9f7dcab039b880a491b64c525760bd Mon Sep 17 00:00:00 2001 From: Noikoio Date: Sun, 4 Mar 2018 20:14:54 -0800 Subject: [PATCH 04/17] Added 'ConfReload' command type --- Module/ModTools/CommandBase.cs | 1 + Module/ModTools/Commands/ConfReload.cs | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 Module/ModTools/Commands/ConfReload.cs diff --git a/Module/ModTools/CommandBase.cs b/Module/ModTools/CommandBase.cs index 5f98a75..7cf247c 100644 --- a/Module/ModTools/CommandBase.cs +++ b/Module/ModTools/CommandBase.cs @@ -47,6 +47,7 @@ namespace Noikoio.RegexBot.Module.ModTools { // 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) } diff --git a/Module/ModTools/Commands/ConfReload.cs b/Module/ModTools/Commands/ConfReload.cs new file mode 100644 index 0000000..d8da69e --- /dev/null +++ b/Module/ModTools/Commands/ConfReload.cs @@ -0,0 +1,25 @@ +using Discord.WebSocket; +using Newtonsoft.Json.Linq; +using System.Threading.Tasks; + +namespace Noikoio.RegexBot.Module.ModTools.Commands +{ + class ConfReload : CommandBase + { + // No configuration. + protected ConfReload(ModTools l, string label, JObject conf) : base(l, label, conf) { } + + // Usage: (command) + public override async Task Invoke(SocketGuild g, SocketMessage msg) + { + bool status = await RegexBot.Config.ReloadServerConfig(); + string res; + if (status) res = ":white_check_mark: Configuration reloaded with no issues. Check the console to verify."; + else res = ":x: Reload failed. Check the console."; + await msg.Channel.SendMessageAsync(res); + } + + // Crazy idea: somehow redirect all logging messages created from invoking config reloading + // and pass them onto the invoking channel. + } +} From fe1f466b97dbd3ae52292c93784288d17bc3dd5f Mon Sep 17 00:00:00 2001 From: Noikoio Date: Sun, 4 Mar 2018 20:45:12 -0800 Subject: [PATCH 05/17] Fixed bugs relating to config load --- Module/ModTools/CommandBase.cs | 2 +- Module/ModTools/Commands/ConfReload.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Module/ModTools/CommandBase.cs b/Module/ModTools/CommandBase.cs index 7cf247c..c4e1699 100644 --- a/Module/ModTools/CommandBase.cs +++ b/Module/ModTools/CommandBase.cs @@ -59,7 +59,7 @@ namespace Noikoio.RegexBot.Module.ModTools if (string.IsNullOrWhiteSpace(label)) throw new RuleImportException("Label cannot be blank."); var definition = (JObject)def.Value; - string cmdinvoke = definition["command"].Value(); + string cmdinvoke = definition["command"]?.Value(); if (string.IsNullOrWhiteSpace(cmdinvoke)) throw new RuleImportException($"{label}: 'command' value was not specified."); if (cmdinvoke.Contains(" ")) diff --git a/Module/ModTools/Commands/ConfReload.cs b/Module/ModTools/Commands/ConfReload.cs index d8da69e..093f00b 100644 --- a/Module/ModTools/Commands/ConfReload.cs +++ b/Module/ModTools/Commands/ConfReload.cs @@ -7,7 +7,7 @@ namespace Noikoio.RegexBot.Module.ModTools.Commands class ConfReload : CommandBase { // No configuration. - protected ConfReload(ModTools l, string label, JObject conf) : base(l, label, conf) { } + public ConfReload(ModTools l, string label, JObject conf) : base(l, label, conf) { } // Usage: (command) public override async Task Invoke(SocketGuild g, SocketMessage msg) From 06583a1472946946e9e2c0187e2775ad16c200e1 Mon Sep 17 00:00:00 2001 From: Noikoio Date: Sun, 4 Mar 2018 21:51:42 -0800 Subject: [PATCH 06/17] Updated documentation --- docs/modtools.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/modtools.md b/docs/modtools.md index e7210ef..fee33ad 100644 --- a/docs/modtools.md +++ b/docs/modtools.md @@ -47,7 +47,7 @@ Additional behavior can be specified in its configuration: #### Kick * `"type": "kick"` * Usage: (*command*) (*user name or ID*) [*reason*] -Removes the given user from the server the command was invoked in, and sends the reason, if any, to the server's audit log. +Removes the given user from the server the command was invoked in and sends the reason, if any, to the server's audit log. Additional behavior can be specified in its configuration: * forcereason (*boolean*) - Forces the reason to be specified if set to true. If none is specified, the action is not taken. Defaults to *false*. @@ -61,4 +61,9 @@ Additional behavior can be specified in its configuration: #### Say * `"type": "say"` * Usage: (*command*) (*channel name or ID*) (*message*) -Causes the bot to send the given message exactly as specified to the given channel. \ No newline at end of file +Causes the bot to send the given message exactly as specified to the given channel. + +#### Configuration Reload +* `"type": "confreload"` +* Usage: (*command*) +Reloads server configuration. The bot will reply indicating if the reload was successful. \ No newline at end of file From bfb699d62f4817a3bfe2e94c76fbb3bed0b8b5f3 Mon Sep 17 00:00:00 2001 From: Noikoio Date: Fri, 9 Mar 2018 22:17:55 -0800 Subject: [PATCH 07/17] Reorganized and renamed ModTools to ModCommands --- .../CommandListener.cs} | 26 +++++---- .../Commands/BanKick.cs | 12 ++-- .../Commands/ConfReload.cs | 6 +- .../{ModTools => ModCommands}/Commands/Say.cs | 8 +-- .../Commands/Unban.cs | 8 +-- .../Commands/_CommandBase.cs} | 56 ++++++++++--------- .../{ModTools => ModCommands}/ConfigItem.cs | 24 ++++---- RegexBot.cs | 2 +- 8 files changed, 76 insertions(+), 66 deletions(-) rename Module/{ModTools/ModTools.cs => ModCommands/CommandListener.cs} (67%) rename Module/{ModTools => ModCommands}/Commands/BanKick.cs (95%) rename Module/{ModTools => ModCommands}/Commands/ConfReload.cs (79%) rename Module/{ModTools => ModCommands}/Commands/Say.cs (91%) rename Module/{ModTools => ModCommands}/Commands/Unban.cs (93%) rename Module/{ModTools/CommandBase.cs => ModCommands/Commands/_CommandBase.cs} (65%) rename Module/{ModTools => ModCommands}/ConfigItem.cs (62%) diff --git a/Module/ModTools/ModTools.cs b/Module/ModCommands/CommandListener.cs similarity index 67% rename from Module/ModTools/ModTools.cs rename to Module/ModCommands/CommandListener.cs index 231de46..884e31c 100644 --- a/Module/ModTools/ModTools.cs +++ b/Module/ModCommands/CommandListener.cs @@ -6,30 +6,35 @@ using System; using System.Linq; using System.Threading.Tasks; -namespace Noikoio.RegexBot.Module.ModTools +namespace Noikoio.RegexBot.Module.ModCommands { /// - /// 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. /// - class ModTools : BotModule + /// + /// 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. + /// + 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 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()); } } diff --git a/Module/ModTools/Commands/BanKick.cs b/Module/ModCommands/Commands/BanKick.cs similarity index 95% rename from Module/ModTools/Commands/BanKick.cs rename to Module/ModCommands/Commands/BanKick.cs index 5302bf6..1b9de33 100644 --- a/Module/ModTools/Commands/BanKick.cs +++ b/Module/ModCommands/Commands/BanKick.cs @@ -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() ?? 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) { } } } diff --git a/Module/ModTools/Commands/ConfReload.cs b/Module/ModCommands/Commands/ConfReload.cs similarity index 79% rename from Module/ModTools/Commands/ConfReload.cs rename to Module/ModCommands/Commands/ConfReload.cs index 093f00b..22d66df 100644 --- a/Module/ModTools/Commands/ConfReload.cs +++ b/Module/ModCommands/Commands/ConfReload.cs @@ -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) diff --git a/Module/ModTools/Commands/Say.cs b/Module/ModCommands/Commands/Say.cs similarity index 91% rename from Module/ModTools/Commands/Say.cs rename to Module/ModCommands/Commands/Say.cs index ff6b3c3..c2a103e 100644 --- a/Module/ModTools/Commands/Say.cs +++ b/Module/ModCommands/Commands/Say.cs @@ -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() diff --git a/Module/ModTools/Commands/Unban.cs b/Module/ModCommands/Commands/Unban.cs similarity index 93% rename from Module/ModTools/Commands/Unban.cs rename to Module/ModCommands/Commands/Unban.cs index aa14484..e38cc86 100644 --- a/Module/ModTools/Commands/Unban.cs +++ b/Module/ModCommands/Commands/Unban.cs @@ -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() diff --git a/Module/ModTools/CommandBase.cs b/Module/ModCommands/Commands/_CommandBase.cs similarity index 65% rename from Module/ModTools/CommandBase.cs rename to Module/ModCommands/Commands/_CommandBase.cs index c4e1699..3896365 100644 --- a/Module/ModTools/CommandBase.cs +++ b/Module/ModCommands/Commands/_CommandBase.cs @@ -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 { /// - /// 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 + /// . /// - [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(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,24 +69,27 @@ namespace Noikoio.RegexBot.Module.ModTools string ctypestr = definition["type"]?.Value(); 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}'."); - - try + if (_commands.TryGetValue(ctypestr, out Type ctype)) { - 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($"Error in configuration for command '{label}': {ex.InnerException.Message}"); - else throw; + 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(@"<@!?(?\d+)>", RegexOptions.Compiled); protected static readonly Regex RoleMention = new Regex(@"<@&(?\d+)>", RegexOptions.Compiled); protected static readonly Regex ChannelMention = new Regex(@"<#(?\d+)>", RegexOptions.Compiled); diff --git a/Module/ModTools/ConfigItem.cs b/Module/ModCommands/ConfigItem.cs similarity index 62% rename from Module/ModTools/ConfigItem.cs rename to Module/ModCommands/ConfigItem.cs index 5a29ac6..87c59c2 100644 --- a/Module/ModTools/ConfigItem.cs +++ b/Module/ModCommands/ConfigItem.cs @@ -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 { /// /// Represents ModTools configuration within one server. /// class ConfigItem { - private readonly ReadOnlyDictionary _cmdInstances; + private readonly ReadOnlyDictionary _cmdInstances; - public ReadOnlyDictionary Commands => _cmdInstances; + public ReadOnlyDictionary 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(StringComparer.OrdinalIgnoreCase); + // Command instance creation + var commands = new Dictionary(StringComparer.OrdinalIgnoreCase); var commandconf = config["Commands"]; if (commandconf != null) { @@ -37,16 +37,16 @@ namespace Noikoio.RegexBot.Module.ModTools foreach (var def in commandconf.Children()) { 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(commands); + _cmdInstances = new ReadOnlyDictionary(commands); } } } diff --git a/RegexBot.cs b/RegexBot.cs index e4e8669..277ad5b 100644 --- a/RegexBot.cs +++ b/RegexBot.cs @@ -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 }; From 99fe2967b687574a4b58e4223121a41cd87e4a44 Mon Sep 17 00:00:00 2001 From: Noikoio Date: Fri, 9 Mar 2018 23:16:30 -0800 Subject: [PATCH 08/17] Moved (created) SendUsageMessageAsync to base class --- Module/ModCommands/Commands/BanKick.cs | 36 ++++++++------------- Module/ModCommands/Commands/Say.cs | 32 ++++++++---------- Module/ModCommands/Commands/Unban.cs | 25 ++++---------- Module/ModCommands/Commands/_CommandBase.cs | 25 +++++++++++++- 4 files changed, 58 insertions(+), 60 deletions(-) diff --git a/Module/ModCommands/Commands/BanKick.cs b/Module/ModCommands/Commands/BanKick.cs index 1b9de33..73b69c7 100644 --- a/Module/ModCommands/Commands/BanKick.cs +++ b/Module/ModCommands/Commands/BanKick.cs @@ -1,5 +1,4 @@ -using Discord; -using Discord.WebSocket; +using Discord.WebSocket; using Newtonsoft.Json.Linq; using Noikoio.RegexBot.ConfigItem; using System; @@ -9,9 +8,9 @@ using System.Threading.Tasks; namespace Noikoio.RegexBot.Module.ModCommands.Commands { + // Ban and kick commands are highly similar in implementation, and thus are handled in a single class. 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 } private readonly CommandMode _mode; @@ -49,6 +48,13 @@ namespace Noikoio.RegexBot.Module.ModCommands.Commands if (string.IsNullOrWhiteSpace(val)) _notifyMsg = null; // empty value - disable message else _notifyMsg = val; } + + // Building usage message here + DefaultUsageMsg = $"{this.Trigger} [user or user ID] " + (_forceReason ? "[reason]" : "*[reason]*") + "\n" + + "Removes the given user from this server and prevents the user from rejoining. " + + (_forceReason ? "L" : "Optionally l") + "ogs the reason for the ban to the Audit Log."; + if (_purgeDays > 0) + DefaultUsageMsg += $"\nAdditionally removes the user's post history for the last {_purgeDays} day(s)."; } #region Strings @@ -71,7 +77,7 @@ namespace Noikoio.RegexBot.Module.ModCommands.Commands string reason; if (line.Length < 2) { - await SendUsageMessage(msg, null); + await SendUsageMessageAsync(msg.Channel, null); return; } targetstr = line[1]; @@ -86,7 +92,7 @@ namespace Noikoio.RegexBot.Module.ModCommands.Commands // No reason given if (_forceReason) { - await SendUsageMessage(msg, ReasonRequired); + await SendUsageMessageAsync(msg.Channel, ReasonRequired); return; } reason = null; @@ -112,7 +118,7 @@ namespace Noikoio.RegexBot.Module.ModCommands.Commands } if (qres == null) { - await SendUsageMessage(msg, TargetNotFound); + await SendUsageMessageAsync(msg.Channel, TargetNotFound); return; } ulong targetuid = qres.UserId; @@ -122,7 +128,7 @@ namespace Noikoio.RegexBot.Module.ModCommands.Commands if (_mode == CommandMode.Kick && targetobj == null) { // Can't kick without obtaining the user object - await SendUsageMessage(msg, TargetNotFound); + await SendUsageMessageAsync(msg.Channel, TargetNotFound); return; } @@ -188,22 +194,6 @@ namespace Noikoio.RegexBot.Module.ModCommands.Commands return true; } - private async Task SendUsageMessage(SocketMessage m, string message) - { - 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) - desc += $"\nAdditionally removes the user's post history for the last {_purgeDays} day(s)."; - - var usageEmbed = new EmbedBuilder() - { - Title = "Usage", - Description = desc - }; - await m.Channel.SendMessageAsync(message ?? "", embed: usageEmbed); - } - private string BuildSuccessMessage(string targetstr) { const string defaultmsgBan = ":white_check_mark: Banned user **$target**."; diff --git a/Module/ModCommands/Commands/Say.cs b/Module/ModCommands/Commands/Say.cs index c2a103e..a5895df 100644 --- a/Module/ModCommands/Commands/Say.cs +++ b/Module/ModCommands/Commands/Say.cs @@ -10,40 +10,36 @@ namespace Noikoio.RegexBot.Module.ModCommands.Commands { // No configuration at the moment. // TODO: Whitelist/blacklist - to limit which channels it can "say" into - public Say(CommandListener l, string label, JObject conf) : base(l, label, conf) { } - + public Say(CommandListener l, string label, JObject conf) : base(l, label, conf) { + DefaultUsageMsg = $"{this.Trigger} [channel] [message]\n" + + "Displays the given message exactly as specified to the given channel."; + } + + #region Strings + const string ChannelRequired = ":x: You must specify a channel."; + const string MessageRequired = ":x: You must specify a message."; + const string TargetNotFound = ":x: Unable to find given channel."; + #endregion + public override async Task Invoke(SocketGuild g, SocketMessage msg) { string[] line = msg.Content.Split(new char[] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries); if (line.Length <= 1) { - await SendUsageMessage(msg, ":x: You must specify a channel."); + await SendUsageMessageAsync(msg.Channel, ChannelRequired); return; } if (line.Length <= 2 || string.IsNullOrWhiteSpace(line[2])) { - await SendUsageMessage(msg, ":x: You must specify a message."); + await SendUsageMessageAsync(msg.Channel, MessageRequired); return; } var ch = GetTextChannelFromString(g, line[1]); - if (ch == null) await SendUsageMessage(msg, ":x: Unable to find given channel."); + if (ch == null) await SendUsageMessageAsync(msg.Channel, TargetNotFound); await ch.SendMessageAsync(line[2]); } - private async Task SendUsageMessage(SocketMessage m, string message) - { - string desc = $"{this.Trigger} [channel] [message]\n"; - desc += "Displays the given message exactly as specified to the given channel."; - - var usageEmbed = new EmbedBuilder() - { - Title = "Usage", - Description = desc - }; - await m.Channel.SendMessageAsync(message ?? "", embed: usageEmbed); - } - private SocketTextChannel GetTextChannelFromString(SocketGuild g, string input) { // Method 1: Check for channel mention diff --git a/Module/ModCommands/Commands/Unban.cs b/Module/ModCommands/Commands/Unban.cs index e38cc86..8ebd5be 100644 --- a/Module/ModCommands/Commands/Unban.cs +++ b/Module/ModCommands/Commands/Unban.cs @@ -1,5 +1,4 @@ -using Discord; -using Discord.WebSocket; +using Discord.WebSocket; using Newtonsoft.Json.Linq; using System; using System.Linq; @@ -13,7 +12,10 @@ namespace Noikoio.RegexBot.Module.ModCommands.Commands // No configuration. // TODO bring in some options from BanKick. Particularly custom success msg. // TODO when ModLogs fully implemented, add a reason? - public Unban(CommandListener l, string label, JObject conf) : base(l, label, conf) { } + public Unban(CommandListener l, string label, JObject conf) : base(l, label, conf) { + DefaultUsageMsg = $"{this.Trigger} [user or user ID]\n" + + "Unbans the given user, allowing them to rejoin the server."; + } #region Strings const string FailPrefix = ":x: **Unable to unban:** "; @@ -33,7 +35,7 @@ namespace Noikoio.RegexBot.Module.ModCommands.Commands string targetstr; if (line.Length < 2) { - await SendUsageMessage(msg, null); + await SendUsageMessageAsync(msg.Channel, null); return; } targetstr = line[1]; @@ -59,7 +61,7 @@ namespace Noikoio.RegexBot.Module.ModCommands.Commands if (qres == null) { - await SendUsageMessage(msg, TargetNotFound); + await SendUsageMessageAsync(msg.Channel, TargetNotFound); return; } @@ -90,18 +92,5 @@ namespace Noikoio.RegexBot.Module.ModCommands.Commands } } } - - private async Task SendUsageMessage(SocketMessage m, string message) - { - string desc = $"{this.Trigger} [user or user ID]\n"; - desc += "Unbans the given user, allowing them to rejoin the server."; - - var usageEmbed = new EmbedBuilder() - { - Title = "Usage", - Description = desc - }; - await m.Channel.SendMessageAsync(message ?? "", embed: usageEmbed); - } } } diff --git a/Module/ModCommands/Commands/_CommandBase.cs b/Module/ModCommands/Commands/_CommandBase.cs index 3896365..1be47f0 100644 --- a/Module/ModCommands/Commands/_CommandBase.cs +++ b/Module/ModCommands/Commands/_CommandBase.cs @@ -1,4 +1,5 @@ -using Discord.WebSocket; +using Discord; +using Discord.WebSocket; using Newtonsoft.Json.Linq; using Noikoio.RegexBot.ConfigItem; using System; @@ -95,5 +96,27 @@ namespace Noikoio.RegexBot.Module.ModCommands.Commands protected static readonly Regex ChannelMention = new Regex(@"<#(?\d+)>", RegexOptions.Compiled); protected static readonly Regex EmojiMatch = new Regex(@"<:(?[A-Za-z0-9_]{2,}):(?\d+)>", RegexOptions.Compiled); #endregion + + #region Usage message + protected string DefaultUsageMsg { get; set; } + /// + /// Sends out the default usage message () within an embed. + /// An optional message can be included, for uses such as notifying users of incorrect usage. + /// + /// Target channel for sending the message. + /// The message to send alongside the default usage message. + protected async Task SendUsageMessageAsync(ISocketMessageChannel target, string message = null) + { + if (DefaultUsageMsg == null) + throw new InvalidOperationException("DefaultUsage was not defined."); + + var usageEmbed = new EmbedBuilder() + { + Title = "Usage", + Description = DefaultUsageMsg + }; + await target.SendMessageAsync(message ?? "", embed: usageEmbed); + } + #endregion } } From 800956e2aa8eab7a8da5de1cf002f8e46603c788 Mon Sep 17 00:00:00 2001 From: Noikoio Date: Sat, 10 Mar 2018 10:38:50 -0800 Subject: [PATCH 09/17] Created GetUserDataFromString in base class --- Module/ModCommands/Commands/BanKick.cs | 35 +++++++----------- Module/ModCommands/Commands/Unban.cs | 36 ++++++------------- Module/ModCommands/Commands/_CommandBase.cs | 39 +++++++++++++++++++-- 3 files changed, 61 insertions(+), 49 deletions(-) diff --git a/Module/ModCommands/Commands/BanKick.cs b/Module/ModCommands/Commands/BanKick.cs index 73b69c7..6630dad 100644 --- a/Module/ModCommands/Commands/BanKick.cs +++ b/Module/ModCommands/Commands/BanKick.cs @@ -59,9 +59,7 @@ namespace Noikoio.RegexBot.Module.ModCommands.Commands #region Strings const string FailPrefix = ":x: **Failed to {0} user:** "; - const string Fail403 = "I do not have the required permissions to perform that action."; - const string Fail404 = "The target user is no longer available."; - const string FailDefault = "An unknown error occurred. Notify the bot operator."; + const string Fail404 = "The specified user is no longer in the server."; const string NotifyDefault = "You have been {0} from $s for the following reason:\n$r"; const string NotifyReasonNone = "No reason specified."; const string NotifyFailed = "\n(User was unable to receive notification message.)"; @@ -98,32 +96,25 @@ namespace Noikoio.RegexBot.Module.ModCommands.Commands reason = null; } - // Getting SocketGuildUser target - SocketGuildUser targetobj = null; - - // Extract snowflake value from mention (if a mention was given) - Match m = UserMention.Match(targetstr); - if (m.Success) targetstr = m.Groups["snowflake"].Value; - - EntityCache.CacheUser qres; - try + // Retrieve target user + var (targetId, targetData) = await GetUserDataFromString(g.Id, targetstr); + if (targetId == 1) { - qres = (await EntityCache.EntityCache.QueryAsync(g.Id, targetstr)).FirstOrDefault(); - } - catch (Npgsql.NpgsqlException ex) - { - await Log("A database error occurred during user lookup: " + ex.Message); await msg.Channel.SendMessageAsync(FailPrefix + FailDefault); return; } - if (qres == null) + if (targetId == 0) { await SendUsageMessageAsync(msg.Channel, TargetNotFound); return; } - ulong targetuid = qres.UserId; - targetobj = g.GetUser(targetuid); - string targetdisp = targetobj?.ToString() ?? $"ID {targetuid}"; + + SocketGuildUser targetobj = g.GetUser(targetId); + string targetdisp; + if (targetData != null) + targetdisp = $"{targetData.Username}#{targetData.Discriminator}"; + else + targetdisp = $"ID {targetId}"; if (_mode == CommandMode.Kick && targetobj == null) { @@ -143,7 +134,7 @@ namespace Noikoio.RegexBot.Module.ModCommands.Commands reasonlog = Uri.EscapeDataString(reasonlog); #warning Remove EscapeDataString call on next Discord.Net update #if !DEBUG - if (_mode == CommandMode.Ban) await g.AddBanAsync(targetuid, _purgeDays, reasonlog); + if (_mode == CommandMode.Ban) await g.AddBanAsync(targetId, _purgeDays, reasonlog); else await targetobj.KickAsync(reason); #else #warning "Actual kick/ban action is DISABLED during debug." diff --git a/Module/ModCommands/Commands/Unban.cs b/Module/ModCommands/Commands/Unban.cs index 8ebd5be..e5343da 100644 --- a/Module/ModCommands/Commands/Unban.cs +++ b/Module/ModCommands/Commands/Unban.cs @@ -19,9 +19,7 @@ namespace Noikoio.RegexBot.Module.ModCommands.Commands #region Strings const string FailPrefix = ":x: **Unable to unban:** "; - const string Fail403 = "I do not have the required permissions to perform that action."; - const string Fail404 = "The target user is no longer available."; - const string FailDefault = "An unknown error occurred. Notify the bot operator."; + protected const string Fail404 = "The specified user does not exist or is not in the ban list."; const string TargetNotFound = ":x: **Unable to determine the target user.**"; const string Success = ":white_check_mark: Unbanned user **{0}**."; #endregion @@ -29,8 +27,6 @@ namespace Noikoio.RegexBot.Module.ModCommands.Commands // Usage: (command) (user query) public override async Task Invoke(SocketGuild g, SocketMessage msg) { - // TODO oh god there's so much boilerplate copypasted from BanKick make it stop - string[] line = msg.Content.Split(new char[] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries); string targetstr; if (line.Length < 2) @@ -40,39 +36,29 @@ namespace Noikoio.RegexBot.Module.ModCommands.Commands } targetstr = line[1]; - // Getting SocketGuildUser target - SocketGuildUser targetobj = null; - - // Extract snowflake value from mention (if a mention was given) - Match m = UserMention.Match(targetstr); - if (m.Success) targetstr = m.Groups["snowflake"].Value; - - EntityCache.CacheUser qres; - try + // Retrieve target user + var (targetId, targetData) = await GetUserDataFromString(g.Id, targetstr); + if (targetId == 1) { - qres = (await EntityCache.EntityCache.QueryAsync(g.Id, targetstr)).FirstOrDefault(); - } - catch (Npgsql.NpgsqlException ex) - { - await Log("A database error occurred during user lookup: " + ex.Message); await msg.Channel.SendMessageAsync(FailPrefix + FailDefault); return; } - - if (qres == null) + if (targetId == 0) { await SendUsageMessageAsync(msg.Channel, TargetNotFound); return; } - ulong targetuid = qres.UserId; - targetobj = g.GetUser(targetuid); - string targetdisp = targetobj?.ToString() ?? $"ID {targetuid}"; + string targetdisp; + if (targetData != null) + targetdisp = $"{targetData.Username}#{targetData.Discriminator}"; + else + targetdisp = $"ID {targetId}"; // Do the action try { - await g.RemoveBanAsync(targetuid); + await g.RemoveBanAsync(targetId); await msg.Channel.SendMessageAsync(string.Format(Success, targetdisp)); } catch (Discord.Net.HttpException ex) diff --git a/Module/ModCommands/Commands/_CommandBase.cs b/Module/ModCommands/Commands/_CommandBase.cs index 1be47f0..0509124 100644 --- a/Module/ModCommands/Commands/_CommandBase.cs +++ b/Module/ModCommands/Commands/_CommandBase.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; +using System.Linq; using System.Reflection; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -95,9 +96,9 @@ namespace Noikoio.RegexBot.Module.ModCommands.Commands protected static readonly Regex RoleMention = new Regex(@"<@&(?\d+)>", RegexOptions.Compiled); protected static readonly Regex ChannelMention = new Regex(@"<#(?\d+)>", RegexOptions.Compiled); protected static readonly Regex EmojiMatch = new Regex(@"<:(?[A-Za-z0-9_]{2,}):(?\d+)>", RegexOptions.Compiled); - #endregion + protected const string Fail403 = "I do not have the required permissions to perform that action."; + protected const string FailDefault = "An unknown error occurred. Notify the bot operator."; - #region Usage message protected string DefaultUsageMsg { get; set; } /// /// Sends out the default usage message () within an embed. @@ -117,6 +118,40 @@ namespace Noikoio.RegexBot.Module.ModCommands.Commands }; await target.SendMessageAsync(message ?? "", embed: usageEmbed); } + + /// + /// Helper method for turning input into user data. Only returns the first cache result. + /// + /// + /// First value: 0 for no data, 1 for no data + exception. + /// May return a partial result: a valid ulong value but no CacheUser. + /// + protected async Task<(ulong, EntityCache.CacheUser)> GetUserDataFromString(ulong guild, string input) + { + ulong uid = 0; + EntityCache.CacheUser cdata = null; + + Match m = UserMention.Match(input); + if (m.Success) + { + input = m.Groups["snowflake"].Value; + uid = ulong.Parse(input); + } + + try + { + cdata = (await EntityCache.EntityCache.QueryAsync(guild, input)) + .FirstOrDefault(); + if (cdata != null) uid = cdata.UserId; + } + catch (Npgsql.NpgsqlException ex) + { + await Log("A databasae error occurred during user lookup: " + ex.Message); + if (uid == 0) uid = 1; + } + + return (uid, cdata); + } #endregion } } From 154b549abb34e96b744f375fa473c6a3304017ba Mon Sep 17 00:00:00 2001 From: Noikoio Date: Sat, 10 Mar 2018 18:10:50 -0800 Subject: [PATCH 10/17] 'Remove and Sort Usings' --- Module/ModCommands/Commands/BanKick.cs | 2 -- Module/ModCommands/Commands/Say.cs | 3 +-- Module/ModCommands/Commands/Unban.cs | 2 -- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/Module/ModCommands/Commands/BanKick.cs b/Module/ModCommands/Commands/BanKick.cs index 6630dad..9fdbc0b 100644 --- a/Module/ModCommands/Commands/BanKick.cs +++ b/Module/ModCommands/Commands/BanKick.cs @@ -2,8 +2,6 @@ using Newtonsoft.Json.Linq; using Noikoio.RegexBot.ConfigItem; using System; -using System.Linq; -using System.Text.RegularExpressions; using System.Threading.Tasks; namespace Noikoio.RegexBot.Module.ModCommands.Commands diff --git a/Module/ModCommands/Commands/Say.cs b/Module/ModCommands/Commands/Say.cs index a5895df..ea8518a 100644 --- a/Module/ModCommands/Commands/Say.cs +++ b/Module/ModCommands/Commands/Say.cs @@ -1,5 +1,4 @@ -using Discord; -using Discord.WebSocket; +using Discord.WebSocket; using Newtonsoft.Json.Linq; using System; using System.Threading.Tasks; diff --git a/Module/ModCommands/Commands/Unban.cs b/Module/ModCommands/Commands/Unban.cs index e5343da..a2e74bf 100644 --- a/Module/ModCommands/Commands/Unban.cs +++ b/Module/ModCommands/Commands/Unban.cs @@ -1,8 +1,6 @@ using Discord.WebSocket; using Newtonsoft.Json.Linq; using System; -using System.Linq; -using System.Text.RegularExpressions; using System.Threading.Tasks; namespace Noikoio.RegexBot.Module.ModCommands.Commands From 18cd562993afb6b18f8d445e57126335f14b7cdb Mon Sep 17 00:00:00 2001 From: Noikoio Date: Wed, 14 Mar 2018 20:12:19 -0700 Subject: [PATCH 11/17] Fixed incorrect usage message with kick command --- Module/ModCommands/Commands/BanKick.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Module/ModCommands/Commands/BanKick.cs b/Module/ModCommands/Commands/BanKick.cs index 9fdbc0b..28c94e1 100644 --- a/Module/ModCommands/Commands/BanKick.cs +++ b/Module/ModCommands/Commands/BanKick.cs @@ -49,8 +49,10 @@ namespace Noikoio.RegexBot.Module.ModCommands.Commands // Building usage message here DefaultUsageMsg = $"{this.Trigger} [user or user ID] " + (_forceReason ? "[reason]" : "*[reason]*") + "\n" - + "Removes the given user from this server and prevents the user from rejoining. " - + (_forceReason ? "L" : "Optionally l") + "ogs the reason for the ban to the Audit Log."; + + "Removes the given user from this server" + + (_mode == CommandMode.Ban ? " and prevents the user from rejoining" : "") + ". " + + (_forceReason ? "L" : "Optionally l") + "ogs the reason for the " + + (_mode == CommandMode.Ban ? "ban" : "kick") + " to the Audit Log."; if (_purgeDays > 0) DefaultUsageMsg += $"\nAdditionally removes the user's post history for the last {_purgeDays} day(s)."; } From 6127bb2ee7823008e5bca648ad196df135c9da48 Mon Sep 17 00:00:00 2001 From: Noikoio Date: Wed, 14 Mar 2018 23:59:24 -0700 Subject: [PATCH 12/17] Fix race condition between sending notification and removing from server --- Module/ModCommands/Commands/BanKick.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Module/ModCommands/Commands/BanKick.cs b/Module/ModCommands/Commands/BanKick.cs index 28c94e1..0231d31 100644 --- a/Module/ModCommands/Commands/BanKick.cs +++ b/Module/ModCommands/Commands/BanKick.cs @@ -132,6 +132,7 @@ namespace Noikoio.RegexBot.Module.ModCommands.Commands string reasonlog = $"Invoked by {msg.Author.ToString()}."; if (reason != null) reasonlog += $" Reason: {reason}"; reasonlog = Uri.EscapeDataString(reasonlog); + await notifyTask; #warning Remove EscapeDataString call on next Discord.Net update #if !DEBUG if (_mode == CommandMode.Ban) await g.AddBanAsync(targetId, _purgeDays, reasonlog); @@ -140,7 +141,7 @@ namespace Noikoio.RegexBot.Module.ModCommands.Commands #warning "Actual kick/ban action is DISABLED during debug." #endif string resultmsg = BuildSuccessMessage(targetdisp); - if (await notifyTask == false) resultmsg += NotifyFailed; + if (notifyTask.Result == false) resultmsg += NotifyFailed; await msg.Channel.SendMessageAsync(resultmsg); } catch (Discord.Net.HttpException ex) From 7708a81949860056c327691e98bfdf1e90aaf852 Mon Sep 17 00:00:00 2001 From: Noikoio Date: Thu, 15 Mar 2018 00:23:46 -0700 Subject: [PATCH 13/17] Fixed incorrect message being sent to audit log --- Module/ModCommands/Commands/BanKick.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Module/ModCommands/Commands/BanKick.cs b/Module/ModCommands/Commands/BanKick.cs index 0231d31..ec644cd 100644 --- a/Module/ModCommands/Commands/BanKick.cs +++ b/Module/ModCommands/Commands/BanKick.cs @@ -136,7 +136,7 @@ namespace Noikoio.RegexBot.Module.ModCommands.Commands #warning Remove EscapeDataString call on next Discord.Net update #if !DEBUG if (_mode == CommandMode.Ban) await g.AddBanAsync(targetId, _purgeDays, reasonlog); - else await targetobj.KickAsync(reason); + else await targetobj.KickAsync(reasonlog); #else #warning "Actual kick/ban action is DISABLED during debug." #endif From eaf147b4179927cec6e28c72e82924f0c176a48f Mon Sep 17 00:00:00 2001 From: Noikoio Date: Thu, 15 Mar 2018 21:26:59 -0700 Subject: [PATCH 14/17] Added role manipulation commands --- .../ModCommands/Commands/RoleManipulation.cs | 133 ++++++++++++++++++ Module/ModCommands/Commands/_CommandBase.cs | 8 +- 2 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 Module/ModCommands/Commands/RoleManipulation.cs diff --git a/Module/ModCommands/Commands/RoleManipulation.cs b/Module/ModCommands/Commands/RoleManipulation.cs new file mode 100644 index 0000000..e6a0769 --- /dev/null +++ b/Module/ModCommands/Commands/RoleManipulation.cs @@ -0,0 +1,133 @@ +using Discord.WebSocket; +using Newtonsoft.Json.Linq; +using Noikoio.RegexBot.ConfigItem; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace Noikoio.RegexBot.Module.ModCommands.Commands +{ + // Role adding and removing is largely the same, and thus are handled in a single class. + class RoleManipulation : Command + { + protected enum CommandMode { Add, Del } + private readonly CommandMode _mode; + + private readonly EntityName _role; + private readonly string _successMsg; + // Configuration: + // "role" - string; The given role that applies to this command. + // "successmsg" - string; Messages to display on command success. Overrides default. + + protected RoleManipulation(CommandListener l, string label, JObject conf, CommandMode mode) : base(l, label, conf) + { + _mode = mode; + var rolestr = conf["role"]?.Value(); + if (string.IsNullOrWhiteSpace(rolestr)) throw new RuleImportException("Role must be provided."); + _role = new EntityName(rolestr, EntityType.Role); + _successMsg = conf["successmsg"]?.Value(); + + DefaultUsageMsg = $"{this.Trigger} [user or user ID]\n" + + (_mode == CommandMode.Add ? "Adds" : "Removes") + " the specified role " + + (_mode == CommandMode.Add ? "to" : "from") + " the given user."; + } + + #region Strings + const string FailPrefix = ":x: **Failed to apply role change:** "; + const string TargetNotFound = ":x: **Unable to determine the target user.**"; + const string RoleNotFound = ":x: **Failed to determine the specified role for this command.**"; + const string Success = ":white_check_mark: Successfully {0} role for **{1}**."; + #endregion + + public override async Task Invoke(SocketGuild g, SocketMessage msg) + { + string[] line = msg.Content.Split(new char[] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries); + string targetstr; + if (line.Length < 2) + { + await SendUsageMessageAsync(msg.Channel, null); + return; + } + targetstr = line[1]; + + // Retrieve target user + var (targetId, targetData) = await GetUserDataFromString(g.Id, targetstr); + if (targetId == 1) + { + await msg.Channel.SendMessageAsync(FailPrefix + FailDefault); + return; + } + if (targetId == 0) + { + await SendUsageMessageAsync(msg.Channel, TargetNotFound); + return; + } + + string targetdisp; + if (targetData != null) + targetdisp = $"{targetData.Username}#{targetData.Discriminator}"; + else + targetdisp = $"ID {targetId}"; + + // Determine role + SocketRole cmdRole; + if (_role.Id.HasValue) + { + cmdRole = g.GetRole(_role.Id.Value); + } + else + { + var res = g.Roles.Where(rn => + string.Equals(rn.Name, _role.Name, StringComparison.InvariantCultureIgnoreCase)) + .FirstOrDefault(); + if (res == null) + { + await msg.Channel.SendMessageAsync(RoleNotFound); + await Log(RoleNotFound); + return; + } + cmdRole = res; + } + + // Do the action + try + { + var u = g.GetUser(targetId); + if (_mode == CommandMode.Add) + await u.AddRoleAsync(cmdRole); + else + await u.RemoveRoleAsync(cmdRole); + await msg.Channel.SendMessageAsync(BuildSuccessMessage(targetdisp)); + } + catch (Discord.Net.HttpException ex) + { + if (ex.HttpCode == System.Net.HttpStatusCode.Forbidden) + { + await msg.Channel.SendMessageAsync(FailPrefix + Fail403); + } + else + { + await msg.Channel.SendMessageAsync(FailPrefix + FailDefault); + await Log(ex.ToString()); + } + } + } + + private string BuildSuccessMessage(string targetstr) + { + const string defaultmsg = ":white_check_mark: Successfully {0} role for **$target**."; + string msg = _successMsg ?? string.Format(defaultmsg, _mode == CommandMode.Add ? "set" : "unset"); + return msg.Replace("$target", targetstr); + } + } + + class RoleAdd : RoleManipulation + { + public RoleAdd(CommandListener l, string label, JObject conf) : base(l, label, conf, CommandMode.Add) { } + } + + class RoleDel : RoleManipulation + { + public RoleDel(CommandListener l, string label, JObject conf) : base(l, label, conf, CommandMode.Del) { } + } +} diff --git a/Module/ModCommands/Commands/_CommandBase.cs b/Module/ModCommands/Commands/_CommandBase.cs index 0509124..703b9b0 100644 --- a/Module/ModCommands/Commands/_CommandBase.cs +++ b/Module/ModCommands/Commands/_CommandBase.cs @@ -53,7 +53,13 @@ namespace Noikoio.RegexBot.Module.ModCommands.Commands { "confreload", typeof(ConfReload) }, { "kick", typeof(Kick) }, { "say", typeof(Say) }, - { "unban", typeof(Unban) } + { "unban", typeof(Unban) }, + { "addrole", typeof(RoleAdd) }, + { "roleadd", typeof(RoleAdd) }, + { "grantrole", typeof(RoleAdd) }, + { "delrole", typeof(RoleDel) }, + { "roledel", typeof(RoleDel) }, + { "revokerole", typeof(RoleDel) } }); public static Command CreateInstance(CommandListener root, JProperty def) From 6860bf98de93bfd75aa8fdd3fcd30bc502e999e2 Mon Sep 17 00:00:00 2001 From: Noikoio Date: Thu, 15 Mar 2018 21:46:18 -0700 Subject: [PATCH 15/17] Updated documentation; removed some aliases --- Module/ModCommands/Commands/_CommandBase.cs | 6 +--- docs/{modtools.md => modcommands.md} | 34 +++++++++++++-------- docs/serverdef.md | 2 +- 3 files changed, 24 insertions(+), 18 deletions(-) rename docs/{modtools.md => modcommands.md} (74%) diff --git a/Module/ModCommands/Commands/_CommandBase.cs b/Module/ModCommands/Commands/_CommandBase.cs index 703b9b0..64e5a02 100644 --- a/Module/ModCommands/Commands/_CommandBase.cs +++ b/Module/ModCommands/Commands/_CommandBase.cs @@ -55,11 +55,7 @@ namespace Noikoio.RegexBot.Module.ModCommands.Commands { "say", typeof(Say) }, { "unban", typeof(Unban) }, { "addrole", typeof(RoleAdd) }, - { "roleadd", typeof(RoleAdd) }, - { "grantrole", typeof(RoleAdd) }, - { "delrole", typeof(RoleDel) }, - { "roledel", typeof(RoleDel) }, - { "revokerole", typeof(RoleDel) } + { "delrole", typeof(RoleDel) } }); public static Command CreateInstance(CommandListener root, JProperty def) diff --git a/docs/modtools.md b/docs/modcommands.md similarity index 74% rename from docs/modtools.md rename to docs/modcommands.md index fee33ad..bfbc307 100644 --- a/docs/modtools.md +++ b/docs/modcommands.md @@ -1,10 +1,10 @@ -## ModTools +## ModCommands -ModTools is the current name for the component that provides commands for use by moderators. Commands are defined based on a number of available template-like *type*s, which can then be customized with further configuration. +ModCommands is the name of the component that provides the ability for one to create useful commands for moderation. These commands are defined based on a number of available template-like *type*s, which can then be customized with further configuration. Sample within a [server definition](serverdef.html): ``` -"ModTools": { +"ModCommands": { "Commands": { "Kick": { // a plain and simple kick command "type": "kick", @@ -20,14 +20,14 @@ Sample within a [server definition](serverdef.html): ``` ### Definition structure -Commands are defined within a JSON object named `Commands` within another object named `ModTools`. They are defined by means of name/value pairs, with the name serving as its label. +Commands are defined within a JSON object named `Commands` within another object named `ModCommands`. They are defined by means of name/value pairs, with the name serving as its label. The following values are **required** in a definition: -* type (*string*) - Specifies the behavior that the command should take. +* type (*string*) - Specifies the type of behavior that the command should take. * command (*string*) - The text trigger for the command being defined. Must not contain spaces, and it is recommended to start it with an uncommon symbol, such as `!`. ### Command types -Each command type specifies the action taken by the bot when the command is invoked. Certain types offer additional configuration options as well. +Each command type specifies the action taken by the bot when the command is invoked. Certain types offer additional configuration options. #### Ban * `"type": "ban"` @@ -44,6 +44,11 @@ Additional behavior can be specified in its configuration: * Uses a default message if this configuration value is not specified. * To disable, specify a blank value. +#### Configuration Reload +* `"type": "confreload"` +* Usage: (*command*) +Reloads server configuration. The bot will reply indicating if the reload was successful. + #### Kick * `"type": "kick"` * Usage: (*command*) (*user name or ID*) [*reason*] @@ -58,12 +63,17 @@ Additional behavior can be specified in its configuration: * Uses a default message if this configuration value is not specified. * To disable, specify a blank value. +#### Role manipulation +* `"type": "addrole"` or `"type": "delrole" +* Usage: (*command*) (*user name or ID*) +Sets or unsets a predefined role upon the given user. + +Additional configuration: +* role (*string*) - The role that applies to this command. May be defined in the same type of format accepted within [entity lists](entitylist.html). +* successmsg (*string*) - Custom message to display on success. If not specified, a default message is used. + * The string *$target* can be used in the value to represent the command target. + #### Say * `"type": "say"` * Usage: (*command*) (*channel name or ID*) (*message*) -Causes the bot to send the given message exactly as specified to the given channel. - -#### Configuration Reload -* `"type": "confreload"` -* Usage: (*command*) -Reloads server configuration. The bot will reply indicating if the reload was successful. \ No newline at end of file +Causes the bot to send the given message exactly as specified to the given channel. \ No newline at end of file diff --git a/docs/serverdef.md b/docs/serverdef.md index 2d6de3a..88ce186 100644 --- a/docs/serverdef.md +++ b/docs/serverdef.md @@ -28,4 +28,4 @@ The following is a list of accepted members within a server definition. * moderators (*[entity list](entitylist.html)*) - A list of entities to consider as moderators. Actions done by members of this list are able to execute *ModTools* commands and are exempt from certain *AutoMod* rules. See their respective pages for more details. * [automod](automod.html) (*name/value pairs*) - See respective page. * [autoresponses](autorespond.html) (*name/value pairs*) - See respective page. -* [ModTools](modtools.html) (*name/value pairs*) - See respective page. \ No newline at end of file +* [ModCommands](modcommands.html) (*name/value pairs*) - See respective page. \ No newline at end of file From bdc0d223cd4e9516871a49a687fcbd549ed4e5ef Mon Sep 17 00:00:00 2001 From: Noikoio Date: Thu, 15 Mar 2018 22:00:34 -0700 Subject: [PATCH 16/17] Removed redundant JSON object in configuration --- Module/ModCommands/ConfigItem.cs | 26 ++++++++------------------ docs/modcommands.md | 20 +++++++++----------- 2 files changed, 17 insertions(+), 29 deletions(-) diff --git a/Module/ModCommands/ConfigItem.cs b/Module/ModCommands/ConfigItem.cs index 87c59c2..0f7c8e5 100644 --- a/Module/ModCommands/ConfigItem.cs +++ b/Module/ModCommands/ConfigItem.cs @@ -22,29 +22,19 @@ namespace Noikoio.RegexBot.Module.ModCommands { throw new RuleImportException("Configuration for this section is invalid."); } - var config = (JObject)inconf; // Command instance creation var commands = new Dictionary(StringComparer.OrdinalIgnoreCase); - var commandconf = config["Commands"]; - if (commandconf != null) + foreach (var def in inconf.Children()) { - if (commandconf.Type != JTokenType.Object) - { - throw new RuleImportException("CommandDefs is not properly defined."); - } + string label = def.Name; + 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.Trigger].Label}\"."); - foreach (var def in commandconf.Children()) - { - string label = def.Name; - 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.Trigger].Label}\"."); - - commands.Add(cmd.Trigger, cmd); - } + commands.Add(cmd.Trigger, cmd); } _cmdInstances = new ReadOnlyDictionary(commands); } diff --git a/docs/modcommands.md b/docs/modcommands.md index bfbc307..685099a 100644 --- a/docs/modcommands.md +++ b/docs/modcommands.md @@ -5,22 +5,20 @@ ModCommands is the name of the component that provides the ability for one to cr Sample within a [server definition](serverdef.html): ``` "ModCommands": { - "Commands": { - "Kick": { // a plain and simple kick command - "type": "kick", - "command": "!!kick" - }, - "Party Ban": { // self-explanatory - "type": "ban", - "command": "!!party", - "successmsg": "Yay! $target got banned!\nhttps://i.imgur.com/i4CIBtT.jpg" - } + "Kick": { // a plain and simple kick command + "type": "kick", + "command": "!!kick" + }, + "Party Ban": { // self-explanatory + "type": "ban", + "command": "!!party", + "successmsg": "Yay! $target got banned!\nhttps://i.imgur.com/i4CIBtT.jpg" } } ``` ### Definition structure -Commands are defined within a JSON object named `Commands` within another object named `ModCommands`. They are defined by means of name/value pairs, with the name serving as its label. +Commands are defined within a `ModCommands` object, itself within a [server definition](serverdef.html). They are defined by means of name/value pairs, with the name serving as its label. The following values are **required** in a definition: * type (*string*) - Specifies the type of behavior that the command should take. From 3298efc65c368b592287508d3e00845bbb1680e8 Mon Sep 17 00:00:00 2001 From: Noikoio Date: Sat, 17 Mar 2018 13:51:09 -0700 Subject: [PATCH 17/17] Removed leftover ModTools references --- Module/ModCommands/Commands/_CommandBase.cs | 8 ++++---- Module/ModCommands/ConfigItem.cs | 2 +- docs/serverdef.md | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Module/ModCommands/Commands/_CommandBase.cs b/Module/ModCommands/Commands/_CommandBase.cs index 64e5a02..fa58f9b 100644 --- a/Module/ModCommands/Commands/_CommandBase.cs +++ b/Module/ModCommands/Commands/_CommandBase.cs @@ -21,17 +21,17 @@ namespace Noikoio.RegexBot.Module.ModCommands.Commands [DebuggerDisplay("Command def: {Label}")] abstract class Command { - private readonly CommandListener _modtools; + private readonly CommandListener _mod; private readonly string _label; private readonly string _command; - protected CommandListener Module => _modtools; + protected CommandListener Module => _mod; public string Label => _label; public string Trigger => _command; public Command(CommandListener l, string label, JObject conf) { - _modtools = l; + _mod = l; _label = label; _command = conf["command"].Value(); } @@ -40,7 +40,7 @@ namespace Noikoio.RegexBot.Module.ModCommands.Commands protected Task Log(string text) { - return _modtools.Log($"{Label}: {text}"); + return _mod.Log($"{Label}: {text}"); } #region Config loading diff --git a/Module/ModCommands/ConfigItem.cs b/Module/ModCommands/ConfigItem.cs index 0f7c8e5..7657dff 100644 --- a/Module/ModCommands/ConfigItem.cs +++ b/Module/ModCommands/ConfigItem.cs @@ -8,7 +8,7 @@ using System.Collections.ObjectModel; namespace Noikoio.RegexBot.Module.ModCommands { /// - /// Represents ModTools configuration within one server. + /// Contains a server's ModCommands configuration. /// class ConfigItem { diff --git a/docs/serverdef.md b/docs/serverdef.md index 88ce186..00d9980 100644 --- a/docs/serverdef.md +++ b/docs/serverdef.md @@ -25,7 +25,7 @@ servers: [ The following is a list of accepted members within a server definition. * id (*integer*) - **Required.** A value containing the server's [unique ID](https://support.discordapp.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-). * name (*string*) - Preferably a readable version of the server's name. Not used for anything other than internal logging. -* moderators (*[entity list](entitylist.html)*) - A list of entities to consider as moderators. Actions done by members of this list are able to execute *ModTools* commands and are exempt from certain *AutoMod* rules. See their respective pages for more details. +* moderators (*[entity list](entitylist.html)*) - A list of entities to consider as moderators. Actions done by members of this list are able to execute *ModCommands* commands and are exempt from certain *AutoMod* rules. See their respective pages for more details. * [automod](automod.html) (*name/value pairs*) - See respective page. * [autoresponses](autorespond.html) (*name/value pairs*) - See respective page. * [ModCommands](modcommands.html) (*name/value pairs*) - See respective page. \ No newline at end of file