diff --git a/BotModule.cs b/BotModule.cs index 7983dca..808eb25 100644 --- a/BotModule.cs +++ b/BotModule.cs @@ -24,37 +24,35 @@ namespace Noikoio.RegexBot } /// - /// Processes module-specific configuration. - /// This method is not called if the user did not provide configuration for the module. + /// This method is called on each module when configuration is (re)loaded. + /// The module is expected to use this opportunity to set up an object that will hold state data + /// for a particular guild, using the incoming configuration object as needed in order to do so. /// /// - /// Module code should not hold on to this data, but instead use to retrieve - /// them. This is in the event that configuration is reverted to an earlier state and allows for the - /// all modules to revert to previously used configuration values with no effort on the part of the - /// module code itself. + /// Module code should not hold on state or configuration data on its own, but instead use + /// to retrieve its state object. This is to provide the user + /// with the ability to maintain the current bot state in the event that a configuration reload fails. /// - /// - /// Processed configuration data prepared for later use. - /// - /// - /// This method should throw - /// in the event of configuration errors. The exception message will be properly displayed. - /// - public abstract Task ProcessConfiguration(JToken configSection); + /// + /// Configuration data for this module, for this guild. Is null if none was defined. + /// + /// An object that may later be retrieved by . + public virtual Task CreateInstanceState(JToken configSection) => Task.FromResult(null); /// - /// Gets this module's relevant configuration data associated with the given Discord guild. + /// Retrieves this module's relevant state data associated with the given Discord guild. /// /// - /// The stored configuration data, or null if none exists. + /// The stored state data, or null/default if none exists. /// - protected object GetConfig(ulong guildId) + protected T GetState(ulong guildId) { + // TODO investigate if locking may be necessary var sc = RegexBot.Config.Servers.FirstOrDefault(g => g.Id == guildId); - if (sc == null) return null; + if (sc == null) return default(T); - if (sc.ModuleConfigs.TryGetValue(this, out var item)) return item; - else return null; + if (sc.ModuleConfigs.TryGetValue(this, out var item)) return (T)item; + else return default(T); } /// diff --git a/Configuration.cs b/Configuration.cs index f371e6c..8fee47f 100644 --- a/Configuration.cs +++ b/Configuration.cs @@ -158,31 +158,30 @@ namespace Noikoio.RegexBot EntityList mods = new EntityList(sconf["moderators"]); if (sconf["moderators"] != null) await SLog("Moderator " + mods.ToString()); - // Load module configurations + // Set up module state / load configurations Dictionary customConfs = new Dictionary(); foreach (var item in _bot.Modules) { var confSection = item.Name; var section = sconf[confSection]; - if (section == null) - { - // Section not in config. Do not call loader method. - await SLog("Additional configuration not defined for " + item.Name); - continue; - } - - await SLog("Loading additional configuration for " + item.Name); + await SLog("Setting up " + item.Name); object result; try { - result = await item.ProcessConfiguration(section); + result = await item.CreateInstanceState(section); } catch (RuleImportException ex) { await SLog($"{item.Name} failed to load configuration: " + ex.Message); return false; } + catch (Exception ex) + { + await SLog("Encountered unhandled exception:"); + await SLog(ex.ToString()); + return false; + } customConfs.Add(item, result); } diff --git a/EntityCache/Module.cs b/EntityCache/Module.cs index c4945ab..845dd87 100644 --- a/EntityCache/Module.cs +++ b/EntityCache/Module.cs @@ -32,8 +32,6 @@ namespace Noikoio.RegexBot.EntityCache Log("No database storage available.").Wait(); } } - - public override Task ProcessConfiguration(JToken configSection) => Task.FromResult(null); // Guild and guild member information has become available. // This is a very expensive operation, especially when joining larger guilds. diff --git a/Module/AutoMod/AutoMod.cs b/Module/AutoMod/AutoMod.cs index 88c4b14..4e48525 100644 --- a/Module/AutoMod/AutoMod.cs +++ b/Module/AutoMod/AutoMod.cs @@ -22,8 +22,9 @@ namespace Noikoio.RegexBot.Module.AutoMod client.MessageUpdated += CMessageUpdated; } - public override async Task ProcessConfiguration(JToken configSection) + public override async Task CreateInstanceState(JToken configSection) { + if (configSection == null) return null; List rules = new List(); foreach (var def in configSection.Children()) @@ -52,7 +53,7 @@ namespace Noikoio.RegexBot.Module.AutoMod if (ch == null) return; // Get rules - var rules = GetConfig(ch.Guild.Id) as IEnumerable; + var rules = GetState>(ch.Guild.Id); if (rules == null) return; foreach (var rule in rules) diff --git a/Module/AutoRespond/AutoRespond.cs b/Module/AutoRespond/AutoRespond.cs index e5cfb5a..e9baceb 100644 --- a/Module/AutoRespond/AutoRespond.cs +++ b/Module/AutoRespond/AutoRespond.cs @@ -33,15 +33,16 @@ namespace Noikoio.RegexBot.Module.AutoRespond var ch = arg.Channel as SocketGuildChannel; if (ch == null) return; - var defs = GetConfig(ch.Guild.Id) as IEnumerable; + var defs = GetState>(ch.Guild.Id); if (defs == null) return; foreach (var def in defs) await Task.Run(async () => await ProcessMessage(arg, def)); } - public override async Task ProcessConfiguration(JToken configSection) + public override async Task CreateInstanceState(JToken configSection) { + if (configSection == null) return null; var responses = new List(); foreach (var def in configSection.Children()) { diff --git a/Module/DMLogger/DMLogger.cs b/Module/DMLogger/DMLogger.cs index 66bde53..2599b9d 100644 --- a/Module/DMLogger/DMLogger.cs +++ b/Module/DMLogger/DMLogger.cs @@ -1,6 +1,5 @@ using Discord; using Discord.WebSocket; -using Newtonsoft.Json.Linq; using System.Text; using System.Threading.Tasks; @@ -34,8 +33,6 @@ namespace Noikoio.RegexBot.Module.DMLogger await ProcessMessage(arg2, true); } - public override Task ProcessConfiguration(JToken configSection) => Task.FromResult(null); - private async Task ProcessMessage(SocketMessage arg, bool edited) { var result = new StringBuilder(); diff --git a/Module/EntryAutoRole/EntryAutoRole.cs b/Module/EntryAutoRole/EntryAutoRole.cs index 19b5bec..22e09e6 100644 --- a/Module/EntryAutoRole/EntryAutoRole.cs +++ b/Module/EntryAutoRole/EntryAutoRole.cs @@ -36,8 +36,9 @@ namespace Noikoio.RegexBot.Module.EntryAutoRole Task.Factory.StartNew(Worker, _workerCancel.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); } - public override Task ProcessConfiguration(JToken configSection) + public override Task CreateInstanceState(JToken configSection) { + if (configSection == null) return Task.FromResult(null); if (configSection.Type != JTokenType.Object) { throw new RuleImportException("Configuration for this section is invalid."); @@ -47,7 +48,7 @@ namespace Noikoio.RegexBot.Module.EntryAutoRole private Task Client_GuildAvailable(SocketGuild arg) { - var conf = (ModuleConfig)GetConfig(arg.Id); + var conf = GetState(arg.Id); if (conf == null) return Task.CompletedTask; SocketRole trole = GetRole(arg); if (trole == null) return Task.CompletedTask; @@ -71,19 +72,19 @@ namespace Noikoio.RegexBot.Module.EntryAutoRole private Task Client_UserLeft(SocketGuildUser arg) { - if (GetConfig(arg.Guild.Id) == null) return Task.CompletedTask; + if (GetState(arg.Guild.Id) == null) return Task.CompletedTask; lock (_roleWaitLock) _roleWaitlist.RemoveAll(m => m.GuildId == arg.Guild.Id && m.UserId == arg.Id); return Task.CompletedTask; } private Task Client_UserJoined(SocketGuildUser arg) { - if (GetConfig(arg.Guild.Id) == null) return Task.CompletedTask; + if (GetState(arg.Guild.Id) == null) return Task.CompletedTask; lock (_roleWaitLock) _roleWaitlist.Add(new AutoRoleEntry() { GuildId = arg.Guild.Id, UserId = arg.Id, - ExpireTime = DateTimeOffset.UtcNow.AddSeconds(((ModuleConfig)GetConfig(arg.Guild.Id)).TimeDelay) + ExpireTime = DateTimeOffset.UtcNow.AddSeconds((GetState(arg.Guild.Id)).TimeDelay) }); return Task.CompletedTask; } @@ -91,7 +92,7 @@ namespace Noikoio.RegexBot.Module.EntryAutoRole // can return null private SocketRole GetRole(SocketGuild g) { - var conf = (ModuleConfig)GetConfig(g.Id); + var conf = GetState(g.Id); if (conf == null) return null; var roleInfo = conf.Role; diff --git a/Module/ModCommands/ModCommands.cs b/Module/ModCommands/ModCommands.cs index 82e4773..d63a1b9 100644 --- a/Module/ModCommands/ModCommands.cs +++ b/Module/ModCommands/ModCommands.cs @@ -32,8 +32,10 @@ namespace Noikoio.RegexBot.Module.ModCommands if (arg.Channel is IGuildChannel) await CommandCheckInvoke(arg); } - public override async Task ProcessConfiguration(JToken configSection) + public override async Task CreateInstanceState(JToken configSection) { + if (configSection == null) return null; + // Constructor throws exception on config errors var conf = new ConfigItem(this, configSection); @@ -44,8 +46,6 @@ namespace Noikoio.RegexBot.Module.ModCommands return conf; } - private new ConfigItem GetConfig(ulong guildId) => (ConfigItem)base.GetConfig(guildId); - public new Task Log(string text) => base.Log(text); private async Task CommandCheckInvoke(SocketMessage arg) @@ -67,7 +67,7 @@ namespace Noikoio.RegexBot.Module.ModCommands int spc = arg.Content.IndexOf(' '); if (spc != -1) cmdchk = arg.Content.Substring(0, spc); else cmdchk = arg.Content; - if (GetConfig(g.Id).Commands.TryGetValue(cmdchk, out var c)) + if (GetState(g.Id).Commands.TryGetValue(cmdchk, out var c)) { try { diff --git a/Module/ModLogs/MessageCache.cs b/Module/ModLogs/MessageCache.cs index 6ab2dfb..8b75462 100644 --- a/Module/ModLogs/MessageCache.cs +++ b/Module/ModLogs/MessageCache.cs @@ -17,11 +17,11 @@ namespace Noikoio.RegexBot.Module.ModLogs { private readonly DiscordSocketClient _dClient; private readonly AsyncLogger _outLog; - private readonly Func _outGetConfig; + private readonly Func _outGetConfig; // TODO: How to clear the cache after a time? Can't hold on to this forever. - public MessageCache(DiscordSocketClient client, AsyncLogger logger, Func getConfFunc) + public MessageCache(DiscordSocketClient client, AsyncLogger logger, Func getConfFunc) { _dClient = client; _outLog = logger; @@ -85,7 +85,7 @@ namespace Noikoio.RegexBot.Module.ModLogs else return; // Check if this feature is enabled before doing anything else. - var cfg = _outGetConfig(guildId) as GuildConfig; + var cfg = _outGetConfig(guildId); if (cfg == null) return; if (isDelete && (cfg.RptTypes & EventType.MsgDelete) == 0) return; if (!isDelete && (cfg.RptTypes & EventType.MsgEdit) == 0) return; diff --git a/Module/ModLogs/ModLogs.cs b/Module/ModLogs/ModLogs.cs index 7f4c130..7ae1136 100644 --- a/Module/ModLogs/ModLogs.cs +++ b/Module/ModLogs/ModLogs.cs @@ -19,14 +19,15 @@ namespace Noikoio.RegexBot.Module.ModLogs if (!RegexBot.Config.DatabaseAvailable) return; // MessageCache (reporting of MessageEdit, MessageDelete) handled by helper class - _msgCacheInstance = new MessageCache(client, Log, GetConfig); + _msgCacheInstance = new MessageCache(client, Log, delegate (ulong id) { return GetState(id); }); // TODO add handlers for detecting joins, leaves, bans, kicks, user edits (nick/username/discr) // TODO add handler for processing the log query command } - public override async Task ProcessConfiguration(JToken configSection) + public override async Task CreateInstanceState(JToken configSection) { + if (configSection == null) return null; if (configSection.Type != JTokenType.Object) throw new RuleImportException("Configuration for this section is invalid.");