From 1d24e2d839cb1c9d6ff25a8a0b07faed3cbc414a Mon Sep 17 00:00:00 2001 From: Noikoio Date: Sat, 2 Dec 2017 22:14:00 -0800 Subject: [PATCH 1/6] Preparing to add additional features --- Module/ModTools/CommandBase.cs | 4 ++ Module/ModTools/ModTools.cs | 84 +++++++++++++++++----------------- 2 files changed, 47 insertions(+), 41 deletions(-) diff --git a/Module/ModTools/CommandBase.cs b/Module/ModTools/CommandBase.cs index d63cdc1..5b8a533 100644 --- a/Module/ModTools/CommandBase.cs +++ b/Module/ModTools/CommandBase.cs @@ -11,6 +11,10 @@ using System.Threading.Tasks; namespace Noikoio.RegexBot.Module.ModTools { + /// + /// Base class for ModTools command. + /// We are not using Discord.Net's Commands extension, as it does not allow for changes during runtime. + /// [DebuggerDisplay("{Label}-type command")] abstract class CommandBase { diff --git a/Module/ModTools/ModTools.cs b/Module/ModTools/ModTools.cs index 0d2cc78..a5fddde 100644 --- a/Module/ModTools/ModTools.cs +++ b/Module/ModTools/ModTools.cs @@ -1,20 +1,19 @@ -using Discord.WebSocket; +using Discord; +using Discord.WebSocket; using Newtonsoft.Json.Linq; using Noikoio.RegexBot.ConfigItem; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using System.Reflection; using System.Threading.Tasks; namespace Noikoio.RegexBot.Module.ModTools { /// - /// ModTools module object. - /// Implements moderation commands that are individually defined and enabled in configuration. + /// ModTools module. + /// This class manages reading configuration and creating instances based on it. /// - // We are not using Discord.Net's Commands extension, as it does not allow for changes during runtime. class ModTools : BotModule { public override string Name => "ModTools"; @@ -26,47 +25,14 @@ namespace Noikoio.RegexBot.Module.ModTools private async Task Client_MessageReceived(SocketMessage arg) { - // Disregard if not in a guild - SocketGuild g = (arg.Author as SocketGuildUser)?.Guild; - if (g == null) return; - - // Get guild config - ServerConfig sc = RegexBot.Config.Servers.FirstOrDefault(s => s.Id == g.Id); - if (sc == null) return; - - // Disregard if not a bot moderator - if (!sc.Moderators.ExistsInList(arg)) return; - - // Disregard if the message contains a newline character - if (arg.Content.Contains("\n")) return; - - // Check for and invoke command... - string cmdchk; - int spc = arg.Content.IndexOf(' '); - if (spc != -1) cmdchk = arg.Content.Substring(0, spc); - else cmdchk = arg.Content; - if (((IDictionary)GetConfig(g.Id)).TryGetValue(cmdchk, out var c)) - { - // ...on the thread pool. - await Task.Run(async () => - { - try - { - await Log($"'{c.Label}' invoked by {arg.Author.ToString()} in {g.Name}/#{arg.Channel.Name}"); - await c.Invoke(g, arg); - } - catch (Exception ex) - { - await Log($"Encountered an error for the command '{c.Label}'. Details follow:"); - await Log(ex.ToString()); - } - }); - } + await CommandCheckInvoke(arg); } [ConfigSection("modtools")] public override async Task ProcessConfiguration(JToken configSection) { + // TODO: put command definitions elsewhere, not in root of this config + var commands = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var def in configSection.Children()) @@ -85,5 +51,41 @@ namespace Noikoio.RegexBot.Module.ModTools } public new Task Log(string text) => base.Log(text); + + private async Task CommandCheckInvoke(SocketMessage arg) + { + // Disregard if not in a guild + SocketGuild g = (arg.Author as SocketGuildUser)?.Guild; + if (g == null) return; + + // Get guild config + ServerConfig sc = RegexBot.Config.Servers.FirstOrDefault(s => s.Id == g.Id); + if (sc == null) return; + + // Disregard if not a bot moderator + if (!sc.Moderators.ExistsInList(arg)) return; + + // Disregard if the message contains a newline character + if (arg.Content.Contains("\n")) return; + + // Check for and invoke command + string cmdchk; + int spc = arg.Content.IndexOf(' '); + if (spc != -1) cmdchk = arg.Content.Substring(0, spc); + else cmdchk = arg.Content; + if (((IDictionary)GetConfig(g.Id)).TryGetValue(cmdchk, out var c)) + { + try + { + await Log($"'{c.Label}' invoked by {arg.Author.ToString()} in {g.Name}/#{arg.Channel.Name}"); + await c.Invoke(g, arg); + } + catch (Exception ex) + { + await Log($"Encountered an error for the command '{c.Label}'. Details follow:"); + await Log(ex.ToString()); + } + } + } } } From 4ef860790cee1f267746a222ee0b3f0f43dff6b5 Mon Sep 17 00:00:00 2001 From: Noikoio Date: Tue, 5 Dec 2017 14:23:01 -0800 Subject: [PATCH 2/6] Committing in-progress implementation of ban petitions --- Module/ModTools/Commands/BanKick.cs | 10 +- Module/ModTools/ModTools.cs | 138 ++++++++++++++++++++++++++-- 2 files changed, 139 insertions(+), 9 deletions(-) diff --git a/Module/ModTools/Commands/BanKick.cs b/Module/ModTools/Commands/BanKick.cs index 6e4538c..ea382c1 100644 --- a/Module/ModTools/Commands/BanKick.cs +++ b/Module/ModTools/Commands/BanKick.cs @@ -8,7 +8,6 @@ using System.Threading.Tasks; namespace Noikoio.RegexBot.Module.ModTools.Commands { - class BanKick : CommandBase { // Ban and kick commands are highly similar in implementation, and thus are handled in a single class. @@ -20,6 +19,11 @@ namespace Noikoio.RegexBot.Module.ModTools.Commands private readonly string _successMsg; private readonly string _notifyMsg; + const string DefaultMsg = "You have been {0} from $s for the following reason:\n$r"; + const string DefaultMsgBanAppend = "\n\nIf the moderators have allowed it, you may petition your ban by" + + " submitting **one** message to the moderation team. To do so, reply to this message with" + + " `!petition [Your message here]`."; + // Configuration: // "forcereason" - boolean; Force a reason to be given. Defaults to false. // "purgedays" - integer; Number of days of target's post history to delete, if banning. @@ -41,8 +45,8 @@ namespace Noikoio.RegexBot.Module.ModTools.Commands if (conf["notifymsg"] == null) { // Message not specified - use default - string act = _mode == CommandMode.Ban ? "banned" : "kicked"; - _notifyMsg = $"You have been {act} from $s for the following reason:\n$r"; + _notifyMsg = string.Format(DefaultMsg, mode == CommandMode.Ban ? "banned" : "kicked"); + if (_mode == CommandMode.Ban) _notifyMsg += DefaultMsgBanAppend; } else { diff --git a/Module/ModTools/ModTools.cs b/Module/ModTools/ModTools.cs index a5fddde..afb05f0 100644 --- a/Module/ModTools/ModTools.cs +++ b/Module/ModTools/ModTools.cs @@ -25,13 +25,26 @@ namespace Noikoio.RegexBot.Module.ModTools private async Task Client_MessageReceived(SocketMessage arg) { - await CommandCheckInvoke(arg); + if (arg.Channel is IDMChannel) await PetitionRelayCheck(arg); + else if (arg.Channel is IGuildChannel) await CommandCheckInvoke(arg); } + #region Config [ConfigSection("modtools")] public override async Task ProcessConfiguration(JToken configSection) { - // TODO: put command definitions elsewhere, not in root of this config + if (configSection.Type != JTokenType.Object) + { + throw new RuleImportException("Configuration for this section is invalid."); + } + + // BIG TO DO LIST: + /* + * 1. Have commands go into their own space within modtools. Candidate name: "banappeal" + * 2. Add a property for where to put the petition channel + * 3. Within ban cmd load, have it check for the existence of a petition channel... if possible? + * I guess otherwise silently discard, if the info isn't readily available. I don't know. + */ var commands = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -50,13 +63,24 @@ namespace Noikoio.RegexBot.Module.ModTools return new ReadOnlyDictionary(commands); } + /* + * Config is stored in a tuple. I admit, not the best choice... + * Consider a different approach if more data needs to be stored in the future. + * Item 1: Command config (Dictionary) + * Item 2: Ban petition channel (EntityName) + */ + + private new Tuple, EntityName> GetConfig(ulong guildId) + => (Tuple, EntityName>)base.GetConfig(guildId); + private Dictionary GetCommandConfig(ulong guild) => GetConfig(guild).Item1; + private EntityName GetPetitionConfig(ulong guild) => GetConfig(guild).Item2; + #endregion + public new Task Log(string text) => base.Log(text); private async Task CommandCheckInvoke(SocketMessage arg) { - // Disregard if not in a guild - SocketGuild g = (arg.Author as SocketGuildUser)?.Guild; - if (g == null) return; + SocketGuild g = ((SocketGuildUser)arg.Author).Guild; // Get guild config ServerConfig sc = RegexBot.Config.Servers.FirstOrDefault(s => s.Id == g.Id); @@ -73,7 +97,7 @@ namespace Noikoio.RegexBot.Module.ModTools int spc = arg.Content.IndexOf(' '); if (spc != -1) cmdchk = arg.Content.Substring(0, spc); else cmdchk = arg.Content; - if (((IDictionary)GetConfig(g.Id)).TryGetValue(cmdchk, out var c)) + if ((GetCommandConfig(g.Id)).TryGetValue(cmdchk, out var c)) { try { @@ -87,5 +111,107 @@ namespace Noikoio.RegexBot.Module.ModTools } } } + + /// + /// List of available appeals. Key is user (for quick lookup). Value is guild (for quick config resolution). + /// TODO expiration? + /// + private Dictionary _openPetitions; // Key: user, Value: guild + public void AddPetition(ulong guild, ulong user) + { + lock (_openPetitions) _openPetitions[user] = guild; + } + private async Task PetitionRelayCheck(SocketMessage msg) + { + const string PetitionAccepted = "Your petition has been forwarded to the moderators for review."; + const string PetitionDenied = "You may not submit a ban petition."; + if (!msg.Content.StartsWith("!petition ", StringComparison.InvariantCultureIgnoreCase)) return; + + // Input validation + string ptext = msg.Content.Substring(10); + if (string.IsNullOrWhiteSpace(ptext)) + { + // Just ignore. + return; + } + + ulong targetGuild = 0; + lock (_openPetitions) + { + if (_openPetitions.TryGetValue(msg.Author.Id, out targetGuild)) + { + _openPetitions.Remove(msg.Author.Id); + } + } + + // It's possible the sender may still block messages sent to them, + // hence the empty catch blocks you'll see up ahead. + + if (targetGuild == 0) + { + try { await msg.Author.SendMessageAsync(PetitionDenied); } + catch (Discord.Net.HttpException) { } + return; + } + var gObj = Client.GetGuild(targetGuild); + if (gObj == null) + { + // Guild is missing. No longer in guild? + try { await msg.Author.SendMessageAsync(PetitionDenied); } + catch (Discord.Net.HttpException) { } + return; + } + + // Get petition reporting target if not already known + var rch = GetPetitionConfig(targetGuild); + ISocketMessageChannel rchObj; + if (!rch.Id.HasValue) + { + rchObj = gObj.TextChannels + .Where(c => c.Name.Equals(rch.Name, StringComparison.InvariantCultureIgnoreCase)) + .FirstOrDefault(); + } + else + { + rchObj = (ISocketMessageChannel)gObj.GetChannel(rch.Id.Value); + } + if (rchObj == null) + { + // Channel not found. + await Log("Petition reporting channel could not be resolved."); + try { await msg.Author.SendMessageAsync(PetitionDenied); } + catch (Discord.Net.HttpException) { } + return; + } + + // Ready to relay as embed + try + { + await rchObj.SendMessageAsync("", embed: new EmbedBuilder() + { + Color = new Color(0x00FFD9), + + Author = new EmbedAuthorBuilder() + { + Name = $"{msg.Author.ToString()} - Ban petition:", + IconUrl = msg.Author.GetAvatarUrl() + }, + Description = ptext, + Timestamp = msg.Timestamp + }); + } + catch (Discord.Net.HttpException ex) + { + await Log("Failed to relay petition message by " + msg.Author.ToString()); + await Log(ex.Message); + // For the user's point of view, fail silently. + try { await msg.Author.SendMessageAsync(PetitionDenied); } + catch (Discord.Net.HttpException) { } + } + + // Success. Notify user. + try { await msg.Author.SendMessageAsync(PetitionAccepted); } + catch (Discord.Net.HttpException) { } + } } } From 1e681478f59c44115195779beb497d4e3834b9f8 Mon Sep 17 00:00:00 2001 From: Noikoio Date: Fri, 8 Dec 2017 10:29:32 -0800 Subject: [PATCH 3/6] Disable actual ban/kick when DEBUG is set --- Module/ModTools/Commands/BanKick.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Module/ModTools/Commands/BanKick.cs b/Module/ModTools/Commands/BanKick.cs index ea382c1..f782e21 100644 --- a/Module/ModTools/Commands/BanKick.cs +++ b/Module/ModTools/Commands/BanKick.cs @@ -131,6 +131,9 @@ namespace Noikoio.RegexBot.Module.ModTools.Commands } else notifyfail = true; + // Give target user ability to petition + Mt.AddPetition(g.Id, targetuid); + // Do the action try { @@ -138,8 +141,12 @@ namespace Noikoio.RegexBot.Module.ModTools.Commands if (reason != null) reasonlog += $" Reason: {reason}"; 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); else await targetobj.KickAsync(reason); +#else +#warning "Actual kick/ban action is DISABLED during debug." +#endif string resultmsg = BuildSuccessMessage(targetdisp); if (notifyfail) { From 1656e4fa64b5cdb76f3c489a56ebdd463d93396f Mon Sep 17 00:00:00 2001 From: Noikoio Date: Fri, 8 Dec 2017 10:30:41 -0800 Subject: [PATCH 4/6] Fully implemented ban petitions --- Module/ModTools/CommandBase.cs | 3 +- Module/ModTools/ModTools.cs | 83 ++++++++++++++++++++++------------ 2 files changed, 57 insertions(+), 29 deletions(-) diff --git a/Module/ModTools/CommandBase.cs b/Module/ModTools/CommandBase.cs index 5b8a533..9542ea4 100644 --- a/Module/ModTools/CommandBase.cs +++ b/Module/ModTools/CommandBase.cs @@ -21,7 +21,8 @@ namespace Noikoio.RegexBot.Module.ModTools private readonly ModTools _modtools; private readonly string _label; private readonly string _command; - + + protected ModTools Mt => _modtools; public string Label => _label; public string Command => _command; diff --git a/Module/ModTools/ModTools.cs b/Module/ModTools/ModTools.cs index afb05f0..a9d4632 100644 --- a/Module/ModTools/ModTools.cs +++ b/Module/ModTools/ModTools.cs @@ -37,30 +37,48 @@ namespace Noikoio.RegexBot.Module.ModTools { throw new RuleImportException("Configuration for this section is invalid."); } - - // BIG TO DO LIST: - /* - * 1. Have commands go into their own space within modtools. Candidate name: "banappeal" - * 2. Add a property for where to put the petition channel - * 3. Within ban cmd load, have it check for the existence of a petition channel... if possible? - * I guess otherwise silently discard, if the info isn't readily available. I don't know. - */ + var config = (JObject)configSection; - var commands = new Dictionary(StringComparer.OrdinalIgnoreCase); - - foreach (var def in configSection.Children()) + // Ban petition reporting channel + EntityName? petitionrpt; + var petitionstr = config["PetitionRelay"]?.Value(); + if (string.IsNullOrEmpty(petitionstr)) petitionrpt = null; + else if (petitionstr.Length > 1 && petitionstr[0] != '#') { - string label = def.Name; - var cmd = CommandBase.CreateInstance(this, def); - if (commands.ContainsKey(cmd.Command)) - 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}."); - - commands.Add(cmd.Command, cmd); + // Not a channel. + throw new RuleImportException("PetitionRelay value must be set to a channel."); } - await Log($"Loaded {commands.Count} command definition(s)."); - return new ReadOnlyDictionary(commands); + else + { + petitionrpt = new EntityName(petitionstr.Substring(1), EntityType.Channel); + } + + // And then the commands + var commands = new Dictionary(StringComparer.OrdinalIgnoreCase); + var commandconf = config["Commands"]; + if (commandconf != null) + { + if (commandconf.Type != JTokenType.Object) + { + throw new RuleImportException("CommandDefs is not properly defined."); + } + + foreach (var def in commandconf.Children()) + { + string label = def.Name; + var cmd = CommandBase.CreateInstance(this, def); + if (commands.ContainsKey(cmd.Command)) + 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}."); + + commands.Add(cmd.Command, cmd); + } + await Log($"Loaded {commands.Count} command definition(s)."); + } + + return new Tuple, EntityName?>( + new ReadOnlyDictionary(commands), petitionrpt); } /* @@ -70,10 +88,10 @@ namespace Noikoio.RegexBot.Module.ModTools * Item 2: Ban petition channel (EntityName) */ - private new Tuple, EntityName> GetConfig(ulong guildId) - => (Tuple, EntityName>)base.GetConfig(guildId); - private Dictionary GetCommandConfig(ulong guild) => GetConfig(guild).Item1; - private EntityName GetPetitionConfig(ulong guild) => GetConfig(guild).Item2; + private new Tuple, EntityName?> GetConfig(ulong guildId) + => (Tuple, EntityName?>)base.GetConfig(guildId); + private ReadOnlyDictionary GetCommandConfig(ulong guild) => GetConfig(guild).Item1; + private EntityName? GetPetitionConfig(ulong guild) => GetConfig(guild).Item2; #endregion public new Task Log(string text) => base.Log(text); @@ -116,9 +134,11 @@ namespace Noikoio.RegexBot.Module.ModTools /// List of available appeals. Key is user (for quick lookup). Value is guild (for quick config resolution). /// TODO expiration? /// - private Dictionary _openPetitions; // Key: user, Value: guild + private Dictionary _openPetitions = new Dictionary(); public void AddPetition(ulong guild, ulong user) { + // Do nothing if disabled + if (GetPetitionConfig(guild) == null) return; lock (_openPetitions) _openPetitions[user] = guild; } private async Task PetitionRelayCheck(SocketMessage msg) @@ -163,7 +183,9 @@ namespace Noikoio.RegexBot.Module.ModTools } // Get petition reporting target if not already known - var rch = GetPetitionConfig(targetGuild); + var pcv = GetPetitionConfig(targetGuild); + if (!pcv.HasValue) return; // No target. How'd we get here, anyway? + var rch = pcv.Value; ISocketMessageChannel rchObj; if (!rch.Id.HasValue) { @@ -197,7 +219,12 @@ namespace Noikoio.RegexBot.Module.ModTools IconUrl = msg.Author.GetAvatarUrl() }, Description = ptext, - Timestamp = msg.Timestamp + Timestamp = msg.Timestamp, + + Footer = new EmbedFooterBuilder() + { + Text = "User ID: " + msg.Author.Id + } }); } catch (Discord.Net.HttpException ex) From d49299e063799af45b99fa2ca93fd67f9b257d5d Mon Sep 17 00:00:00 2001 From: Noikoio Date: Fri, 8 Dec 2017 13:57:35 -0800 Subject: [PATCH 5/6] Added Config class to replace that previous mess --- Module/ModTools/ConfigItem.cs | 75 +++++++++++++++++++++++ Module/ModTools/ModTools.cs | 108 ++++++++++++---------------------- 2 files changed, 113 insertions(+), 70 deletions(-) create mode 100644 Module/ModTools/ConfigItem.cs diff --git a/Module/ModTools/ConfigItem.cs b/Module/ModTools/ConfigItem.cs new file mode 100644 index 0000000..a135a3f --- /dev/null +++ b/Module/ModTools/ConfigItem.cs @@ -0,0 +1,75 @@ +using Newtonsoft.Json.Linq; +using Noikoio.RegexBot.ConfigItem; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace Noikoio.RegexBot.Module.ModTools +{ + /// + /// Represents ModTools configuration within one server. + /// + class ConfigItem + { + private EntityName? _petitionReportCh; + private readonly ReadOnlyDictionary _cmdInstances; + + public EntityName? PetitionReportingChannel => _petitionReportCh; + public ReadOnlyDictionary Commands => _cmdInstances; + + public ConfigItem(ModTools instance, JToken inconf) + { + if (inconf.Type != JTokenType.Object) + { + throw new RuleImportException("Configuration for this section is invalid."); + } + var config = (JObject)inconf; + + // Ban petition reporting channel + var petitionstr = config["PetitionRelay"]?.Value(); + if (string.IsNullOrEmpty(petitionstr)) _petitionReportCh = null; + else if (petitionstr.Length > 1 && petitionstr[0] != '#') + { + // Not a channel. + throw new RuleImportException("PetitionRelay value must be set to a channel."); + } + else + { + _petitionReportCh = new EntityName(petitionstr.Substring(1), EntityType.Channel); + } + + // Command instances + var commands = new Dictionary(StringComparer.OrdinalIgnoreCase); + var commandconf = config["Commands"]; + if (commandconf != null) + { + if (commandconf.Type != JTokenType.Object) + { + throw new RuleImportException("CommandDefs is not properly defined."); + } + + foreach (var def in commandconf.Children()) + { + string label = def.Name; + var cmd = CommandBase.CreateInstance(instance, def); + if (commands.ContainsKey(cmd.Command)) + 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}."); + + commands.Add(cmd.Command, cmd); + } + } + _cmdInstances = new ReadOnlyDictionary(commands); + } + + public void UpdatePetitionChannel(ulong id) + { + if (!PetitionReportingChannel.HasValue) return; + if (PetitionReportingChannel.Value.Id.HasValue) return; // nothing to update + + // For lack of a better option - create a new EntityName with ID already provided + _petitionReportCh = new EntityName($"{id}::{PetitionReportingChannel.Value.Name}", EntityType.Channel); + } + } +} diff --git a/Module/ModTools/ModTools.cs b/Module/ModTools/ModTools.cs index a9d4632..0c505b8 100644 --- a/Module/ModTools/ModTools.cs +++ b/Module/ModTools/ModTools.cs @@ -4,7 +4,6 @@ using Newtonsoft.Json.Linq; using Noikoio.RegexBot.ConfigItem; using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; @@ -28,71 +27,23 @@ namespace Noikoio.RegexBot.Module.ModTools if (arg.Channel is IDMChannel) await PetitionRelayCheck(arg); else if (arg.Channel is IGuildChannel) await CommandCheckInvoke(arg); } - - #region Config + [ConfigSection("modtools")] public override async Task ProcessConfiguration(JToken configSection) { - if (configSection.Type != JTokenType.Object) - { - throw new RuleImportException("Configuration for this section is invalid."); - } - var config = (JObject)configSection; + // Constructor throws exception on config errors + var conf = new ConfigItem(this, configSection); - // Ban petition reporting channel - EntityName? petitionrpt; - var petitionstr = config["PetitionRelay"]?.Value(); - if (string.IsNullOrEmpty(petitionstr)) petitionrpt = null; - else if (petitionstr.Length > 1 && petitionstr[0] != '#') - { - // Not a channel. - throw new RuleImportException("PetitionRelay value must be set to a channel."); - } - else - { - petitionrpt = new EntityName(petitionstr.Substring(1), EntityType.Channel); - } + // Log results + if (conf.Commands.Count > 0) + await Log(conf.Commands.Count + " command definition(s) loaded."); + if (conf.PetitionReportingChannel.HasValue) + await Log("Ban petitioning has been enabled."); - // And then the commands - var commands = new Dictionary(StringComparer.OrdinalIgnoreCase); - var commandconf = config["Commands"]; - if (commandconf != null) - { - if (commandconf.Type != JTokenType.Object) - { - throw new RuleImportException("CommandDefs is not properly defined."); - } - - foreach (var def in commandconf.Children()) - { - string label = def.Name; - var cmd = CommandBase.CreateInstance(this, def); - if (commands.ContainsKey(cmd.Command)) - 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}."); - - commands.Add(cmd.Command, cmd); - } - await Log($"Loaded {commands.Count} command definition(s)."); - } - - return new Tuple, EntityName?>( - new ReadOnlyDictionary(commands), petitionrpt); + return conf; } - /* - * Config is stored in a tuple. I admit, not the best choice... - * Consider a different approach if more data needs to be stored in the future. - * Item 1: Command config (Dictionary) - * Item 2: Ban petition channel (EntityName) - */ - - private new Tuple, EntityName?> GetConfig(ulong guildId) - => (Tuple, EntityName?>)base.GetConfig(guildId); - private ReadOnlyDictionary GetCommandConfig(ulong guild) => GetConfig(guild).Item1; - private EntityName? GetPetitionConfig(ulong guild) => GetConfig(guild).Item2; - #endregion + private new ConfigItem GetConfig(ulong guildId) => (ConfigItem)base.GetConfig(guildId); public new Task Log(string text) => base.Log(text); @@ -115,7 +66,7 @@ namespace Noikoio.RegexBot.Module.ModTools int spc = arg.Content.IndexOf(' '); if (spc != -1) cmdchk = arg.Content.Substring(0, spc); else cmdchk = arg.Content; - if ((GetCommandConfig(g.Id)).TryGetValue(cmdchk, out var c)) + if (GetConfig(g.Id).Commands.TryGetValue(cmdchk, out var c)) { try { @@ -130,6 +81,7 @@ namespace Noikoio.RegexBot.Module.ModTools } } + #region Ban petitions /// /// List of available appeals. Key is user (for quick lookup). Value is guild (for quick config resolution). /// TODO expiration? @@ -138,13 +90,17 @@ namespace Noikoio.RegexBot.Module.ModTools public void AddPetition(ulong guild, ulong user) { // Do nothing if disabled - if (GetPetitionConfig(guild) == null) return; + if (!GetConfig(guild).PetitionReportingChannel.HasValue) return; lock (_openPetitions) _openPetitions[user] = guild; } private async Task PetitionRelayCheck(SocketMessage msg) { const string PetitionAccepted = "Your petition has been forwarded to the moderators for review."; const string PetitionDenied = "You may not submit a ban petition."; + + // It's possible the sender may still block messages sent to them, + // hence the empty catch blocks you'll see up ahead. + if (!msg.Content.StartsWith("!petition ", StringComparison.InvariantCultureIgnoreCase)) return; // Input validation @@ -154,6 +110,13 @@ namespace Noikoio.RegexBot.Module.ModTools // Just ignore. return; } + if (ptext.Length > 1000) + { + // Enforce petition length limit. + try { await msg.Author.SendMessageAsync("Your petition message is too long. Try again with a shorter message."); } + catch (Discord.Net.HttpException) { } + return; + } ulong targetGuild = 0; lock (_openPetitions) @@ -163,12 +126,10 @@ namespace Noikoio.RegexBot.Module.ModTools _openPetitions.Remove(msg.Author.Id); } } - - // It's possible the sender may still block messages sent to them, - // hence the empty catch blocks you'll see up ahead. - + if (targetGuild == 0) { + // Not in the list. Nothing to do. try { await msg.Author.SendMessageAsync(PetitionDenied); } catch (Discord.Net.HttpException) { } return; @@ -182,9 +143,9 @@ namespace Noikoio.RegexBot.Module.ModTools return; } - // Get petition reporting target if not already known - var pcv = GetPetitionConfig(targetGuild); - if (!pcv.HasValue) return; // No target. How'd we get here, anyway? + // Get petition reporting target + var pcv = GetConfig(targetGuild).PetitionReportingChannel; + if (!pcv.HasValue) return; // No target. This should be logically impossible, but... just in case. var rch = pcv.Value; ISocketMessageChannel rchObj; if (!rch.Id.HasValue) @@ -192,11 +153,17 @@ namespace Noikoio.RegexBot.Module.ModTools rchObj = gObj.TextChannels .Where(c => c.Name.Equals(rch.Name, StringComparison.InvariantCultureIgnoreCase)) .FirstOrDefault(); + // Update value if found + if (rchObj != null) + { + GetConfig(targetGuild).UpdatePetitionChannel(rchObj.Id); + } } else { - rchObj = (ISocketMessageChannel)gObj.GetChannel(rch.Id.Value); + rchObj = gObj.GetChannel(rch.Id.Value) as ISocketMessageChannel; } + if (rchObj == null) { // Channel not found. @@ -206,7 +173,7 @@ namespace Noikoio.RegexBot.Module.ModTools return; } - // Ready to relay as embed + // Ready to relay try { await rchObj.SendMessageAsync("", embed: new EmbedBuilder() @@ -240,5 +207,6 @@ namespace Noikoio.RegexBot.Module.ModTools try { await msg.Author.SendMessageAsync(PetitionAccepted); } catch (Discord.Net.HttpException) { } } + #endregion } } From 1d5b0beaf42ca6127eddb15c3cb1d3aadb88a527 Mon Sep 17 00:00:00 2001 From: Noikoio Date: Sun, 10 Dec 2017 11:03:42 -0800 Subject: [PATCH 6/6] Fix petition becoming available on kick --- Module/ModTools/Commands/BanKick.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Module/ModTools/Commands/BanKick.cs b/Module/ModTools/Commands/BanKick.cs index f782e21..872b57b 100644 --- a/Module/ModTools/Commands/BanKick.cs +++ b/Module/ModTools/Commands/BanKick.cs @@ -132,7 +132,7 @@ namespace Noikoio.RegexBot.Module.ModTools.Commands else notifyfail = true; // Give target user ability to petition - Mt.AddPetition(g.Id, targetuid); + if (_mode == CommandMode.Ban) Mt.AddPetition(g.Id, targetuid); // Do the action try