From 4ef860790cee1f267746a222ee0b3f0f43dff6b5 Mon Sep 17 00:00:00 2001 From: Noikoio Date: Tue, 5 Dec 2017 14:23:01 -0800 Subject: [PATCH] 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) { } + } } }