diff --git a/Module/ModLogs/EventType.cs b/Module/ModLogs/EventType.cs new file mode 100644 index 0000000..c364bed --- /dev/null +++ b/Module/ModLogs/EventType.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Noikoio.RegexBot.Module.ModLogs +{ + // Types of non-custom events that can be referenced by ModLogs in configuration. + // Enum value names will show themselves to the user in the form of strings valid in configuration, + // so try not to change those without good reason. + [Flags] + enum EventType + { + None = 0x0, + Note = 0x1, + Warn = 0x2, + Kick = 0x4, + Ban = 0x8, + JoinGuild = 0x10, + LeaveGuild = 0x20, + NameChange = 0x40, + MsgEdit = 0x80, + MsgDelete = 0x100 + } +} diff --git a/Module/ModLogs/GuildConfig.cs b/Module/ModLogs/GuildConfig.cs new file mode 100644 index 0000000..f150c5a --- /dev/null +++ b/Module/ModLogs/GuildConfig.cs @@ -0,0 +1,127 @@ +using Newtonsoft.Json.Linq; +using Noikoio.RegexBot.ConfigItem; +using System; + +namespace Noikoio.RegexBot.Module.ModLogs +{ + /// + /// ModLogs guild-specific configuration values. + /// + class GuildConfig + { + // Event reporting + private readonly EntityName _rptTarget; + private EventType _rptTypes; + /// + /// Target reporting channel. + /// + public EntityName? RptTarget => _rptTarget; + /// + /// Event types to send to the reporting channel. + /// + public EventType RptTypes => _rptTypes; + + // Query command + private readonly string _qCmd; // command name + private readonly EntityList _qAccess; // list of those able to issue the command + private readonly EventType _qDefaultAnswer; // default entry types to display + /// + /// Query command. The first word in an incoming message, including prefix, that triggers a query. + /// + public string QrCommand => _qCmd; + /// + /// List of users permitted to invoke the query command. + /// If null, refer to the guild's Moderators list. + /// + public EntityList QrPermittedUsers => _qAccess; + /// + /// Event types to display in a query. + /// + public EventType QrTypes => _qDefaultAnswer; + + public GuildConfig(JObject cfgRoot) + { + // AutoReporting settings + var arcfg = cfgRoot["AutoReporting"]; + if (arcfg == null) + { + _rptTarget = default(EntityName); // NOTE: Change this if EntityName becomes a class later + _rptTypes = EventType.None; + } + else if (arcfg.Type == JTokenType.Object) + { + string chval = arcfg["Channel"]?.Value(); + if (chval == null) throw new RuleImportException("Reporting channel is not defined."); + if (!string.IsNullOrWhiteSpace(chval) && chval[0] == '#') + _rptTarget = new EntityName(chval.Substring(1, chval.Length-1), EntityType.Channel); + else + throw new RuleImportException("Reporting channel is not properly defined."); + // Require the channel's ID for now. + if (!_rptTarget.Id.HasValue) throw new RuleImportException("Reporting channel's ID must be specified."); + + // TODO make optional + string rpval = arcfg["Events"]?.Value(); + _rptTypes = GetTypesFromString(rpval); + } + else + { + throw new RuleImportException("Section for AutoReporting is not correctly defined."); + } + + // QueryCommand settings + var qccfg = cfgRoot["QueryCommand"]; + if (qccfg == null) + { + _qCmd = null; + _qAccess = null; + _qDefaultAnswer = EventType.None; + } + else if (arcfg.Type == JTokenType.Object) + { + _qCmd = arcfg["Command"]?.Value(); + if (string.IsNullOrWhiteSpace(_qCmd)) + throw new RuleImportException("Query command option must have a value."); + if (_qCmd.Contains(" ")) + throw new RuleImportException("Query command must not contain spaces."); + + var acl = arcfg["AllowedUsers"]; + if (acl == null) _qAccess = null; + else _qAccess = new EntityList(acl); + + // TODO make optional + string ansval = arcfg["DefaultEvents"]?.Value(); + _qDefaultAnswer = GetTypesFromString(ansval); + } + else + { + throw new RuleImportException("Section for QueryCommand is not correctly defined."); + } + } + + public static EventType GetTypesFromString(string input) + { + if (string.IsNullOrWhiteSpace(input)) + throw new RuleImportException("Types are not properly defined."); + + var strTypes = input.Split( + new char[] { ' ', ',', '/', '+' }, // and more? + StringSplitOptions.RemoveEmptyEntries); + + EventType endResult = EventType.None; + foreach (var item in strTypes) + { + try + { + var result = Enum.Parse(item, true); + endResult |= result; + } + catch (ArgumentException) + { + throw new RuleImportException($"Unable to determine the given event type \"{item}\""); + } + } + + return endResult; + } + } +} diff --git a/Module/ModLogs/MessageCache.cs b/Module/ModLogs/MessageCache.cs index 23cf58b..6ab2dfb 100644 --- a/Module/ModLogs/MessageCache.cs +++ b/Module/ModLogs/MessageCache.cs @@ -9,9 +9,9 @@ using System.Threading.Tasks; namespace Noikoio.RegexBot.Module.ModLogs { /// - /// Helper class for . Keeps a database-backed cache of recent messages and assists + /// Helper class for . Keeps a database-backed cache of recent messages for use /// in reporting message changes and deletions, if configured to do so. - /// Does not manipulate the moderation log managed by the main class, but rather provides supplemental features. + /// Despite its place, it does not manipulate moderation logs. It simply pulls from the same configuration. /// class MessageCache { @@ -66,6 +66,7 @@ namespace Noikoio.RegexBot.Module.ModLogs private async Task Client_MessageDeleted(Cacheable msg, ISocketMessageChannel channel) { + if (channel is IDMChannel) return; // No DMs await ProcessReportMessage(true, msg.Id, channel, null); } #endregion @@ -84,11 +85,13 @@ namespace Noikoio.RegexBot.Module.ModLogs else return; // Check if this feature is enabled before doing anything else. - var rptTarget = _outGetConfig(guildId) as ConfigItem.EntityName?; - if (!rptTarget.HasValue) return; + var cfg = _outGetConfig(guildId) as GuildConfig; + if (cfg == null) return; + if (isDelete && (cfg.RptTypes & EventType.MsgDelete) == 0) return; + if (!isDelete && (cfg.RptTypes & EventType.MsgEdit) == 0) return; // Ignore if it's a message being deleted withing the reporting channel. - if (isDelete && rptTarget.Value.Id.Value == ch.Id) return; + if (isDelete && cfg.RptTarget.Value.Id.Value == ch.Id) return; // Regardless of delete or edit, it is necessary to get the equivalent database information. EntityCache.CacheUser ucd = null; @@ -128,11 +131,11 @@ namespace Noikoio.RegexBot.Module.ModLogs } // Find target channel, prepare and send out message - var rptTargetChannel = _dClient.GetGuild(guildId)?.GetTextChannel(rptTarget.Value.Id.Value); + var g = _dClient.GetGuild(guildId); + var rptTargetChannel = g?.GetTextChannel(cfg.RptTarget.Value.Id.Value); if (rptTargetChannel == null) { - await _outLog("Target channel not found."); - // TODO make a more descriptive error message + await _outLog($"WARNING: Reporting channel {cfg.RptTarget.Value.ToString()} could not be determined."); return; } var em = CreateReportEmbed(isDelete, ucd, messageId, ch, (cacheMsg, editMsg)); diff --git a/Module/ModLogs/ModLogs.cs b/Module/ModLogs/ModLogs.cs index acfc8ff..4eeb3e4 100644 --- a/Module/ModLogs/ModLogs.cs +++ b/Module/ModLogs/ModLogs.cs @@ -23,9 +23,11 @@ namespace Noikoio.RegexBot.Module.ModLogs // Do nothing if database unavailable. The user will be informed by ProcessConfiguration. if (!RegexBot.Config.DatabaseAvailable) return; + // MessageCache (reporting of MessageEdit, MessageDelete) handled by helper class _msgCacheInstance = new MessageCache(client, Log, GetConfig); - //throw new NotImplementedException(); + // TODO add handlers for detecting joins, leaves, bans, kicks, user edits (nick/username/discr) + // TODO add handler for processing the log query command } [ConfigSection("ModLogs")] @@ -33,46 +35,14 @@ namespace Noikoio.RegexBot.Module.ModLogs { if (configSection.Type != JTokenType.Object) throw new RuleImportException("Configuration for this section is invalid."); - var conf = (JObject)configSection; if (!RegexBot.Config.DatabaseAvailable) { await Log("Database access is not available. This module be unavailable."); return null; } - - try - { - // MessageCache testing: will store an EntityName or die trying - EntityName? mctarget = new EntityName(conf["mctarget"].Value(), EntityType.Channel); - await Log("Enabled MessageCache test on " + mctarget.Value.ToString()); - return mctarget; - } - catch (Exception) - { - // well, not really die - return null; - } - /* - * Concept: - * "ModLogs": { - * "AutoReporting": { - * // behavior for how to output to the reporting channel - * // MessageCache looks for configuration values within here. - * "Channel": "something compatible with EntityName", - * "Events": "perhaps a single string of separated event types" - * }, - * "QueryOptions": { - * // Behavior for the query command (which is defined here rather than ModTools) - * // Need to stress in the documentation that "msgedit" and "msgdelete" events - * // are not kept and cannot be queried - * "QueryCommand": "!modlogs", - * "Permission": "Moderators", // either a string that says "Moderators" or an EntityList - * "DefaultQueryEvents": "another single string of separated event types", - * } - * } - */ + return new GuildConfig((JObject)configSection); } } } \ No newline at end of file