Changed module configuration model

-Changed a few method names to better reflect its capabilities.
--Specifically, references to "Config" were renamed to "State".
-Changed the way that what is now CreateInstanceState is called
--It is now always called instead of only when configuration exists
--A basic null check was added to prevent issues resulting from this
--General exception handler added to configuration loader
This commit is contained in:
Noikoio 2018-03-22 00:30:22 -07:00
parent 287bb33d77
commit 1773fd2fc2
10 changed files with 50 additions and 54 deletions

View file

@ -24,37 +24,35 @@ namespace Noikoio.RegexBot
} }
/// <summary> /// <summary>
/// Processes module-specific configuration. /// This method is called on each module when configuration is (re)loaded.
/// This method is not called if the user did not provide configuration for the module. /// 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.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Module code <i>should not</i> hold on to this data, but instead use <see cref="GetConfig(ulong)"/> to retrieve /// Module code <i>should not</i> hold on state or configuration data on its own, but instead use
/// them. This is in the event that configuration is reverted to an earlier state and allows for the /// <see cref="GetState{T}(ulong)"/> to retrieve its state object. This is to provide the user
/// all modules to revert to previously used configuration values with no effort on the part of the /// with the ability to maintain the current bot state in the event that a configuration reload fails.
/// module code itself.
/// </remarks> /// </remarks>
/// <returns> /// <param name="configSection">
/// Processed configuration data prepared for later use. /// Configuration data for this module, for this guild. Is null if none was defined.
/// </returns> /// </param>
/// <exception cref="ConfigItem.RuleImportException"> /// <returns>An object that may later be retrieved by <see cref="GetState{T}(ulong)"/>.</returns>
/// This method should throw <see cref="ConfigItem.RuleImportException"/> public virtual Task<object> CreateInstanceState(JToken configSection) => Task.FromResult<object>(null);
/// in the event of configuration errors. The exception message will be properly displayed.
/// </exception>
public abstract Task<object> ProcessConfiguration(JToken configSection);
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <returns> /// <returns>
/// The stored configuration data, or null if none exists. /// The stored state data, or null/default if none exists.
/// </returns> /// </returns>
protected object GetConfig(ulong guildId) protected T GetState<T>(ulong guildId)
{ {
// TODO investigate if locking may be necessary
var sc = RegexBot.Config.Servers.FirstOrDefault(g => g.Id == guildId); 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; if (sc.ModuleConfigs.TryGetValue(this, out var item)) return (T)item;
else return null; else return default(T);
} }
/// <summary> /// <summary>

View file

@ -158,31 +158,30 @@ namespace Noikoio.RegexBot
EntityList mods = new EntityList(sconf["moderators"]); EntityList mods = new EntityList(sconf["moderators"]);
if (sconf["moderators"] != null) await SLog("Moderator " + mods.ToString()); if (sconf["moderators"] != null) await SLog("Moderator " + mods.ToString());
// Load module configurations // Set up module state / load configurations
Dictionary<BotModule, object> customConfs = new Dictionary<BotModule, object>(); Dictionary<BotModule, object> customConfs = new Dictionary<BotModule, object>();
foreach (var item in _bot.Modules) foreach (var item in _bot.Modules)
{ {
var confSection = item.Name; var confSection = item.Name;
var section = sconf[confSection]; var section = sconf[confSection];
if (section == null) await SLog("Setting up " + item.Name);
{
// 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);
object result; object result;
try try
{ {
result = await item.ProcessConfiguration(section); result = await item.CreateInstanceState(section);
} }
catch (RuleImportException ex) catch (RuleImportException ex)
{ {
await SLog($"{item.Name} failed to load configuration: " + ex.Message); await SLog($"{item.Name} failed to load configuration: " + ex.Message);
return false; return false;
} }
catch (Exception ex)
{
await SLog("Encountered unhandled exception:");
await SLog(ex.ToString());
return false;
}
customConfs.Add(item, result); customConfs.Add(item, result);
} }

View file

@ -32,8 +32,6 @@ namespace Noikoio.RegexBot.EntityCache
Log("No database storage available.").Wait(); Log("No database storage available.").Wait();
} }
} }
public override Task<object> ProcessConfiguration(JToken configSection) => Task.FromResult<object>(null);
// Guild and guild member information has become available. // Guild and guild member information has become available.
// This is a very expensive operation, especially when joining larger guilds. // This is a very expensive operation, especially when joining larger guilds.

View file

@ -22,8 +22,9 @@ namespace Noikoio.RegexBot.Module.AutoMod
client.MessageUpdated += CMessageUpdated; client.MessageUpdated += CMessageUpdated;
} }
public override async Task<object> ProcessConfiguration(JToken configSection) public override async Task<object> CreateInstanceState(JToken configSection)
{ {
if (configSection == null) return null;
List<ConfigItem> rules = new List<ConfigItem>(); List<ConfigItem> rules = new List<ConfigItem>();
foreach (var def in configSection.Children<JProperty>()) foreach (var def in configSection.Children<JProperty>())
@ -52,7 +53,7 @@ namespace Noikoio.RegexBot.Module.AutoMod
if (ch == null) return; if (ch == null) return;
// Get rules // Get rules
var rules = GetConfig(ch.Guild.Id) as IEnumerable<ConfigItem>; var rules = GetState<IEnumerable<ConfigItem>>(ch.Guild.Id);
if (rules == null) return; if (rules == null) return;
foreach (var rule in rules) foreach (var rule in rules)

View file

@ -33,15 +33,16 @@ namespace Noikoio.RegexBot.Module.AutoRespond
var ch = arg.Channel as SocketGuildChannel; var ch = arg.Channel as SocketGuildChannel;
if (ch == null) return; if (ch == null) return;
var defs = GetConfig(ch.Guild.Id) as IEnumerable<ConfigItem>; var defs = GetState<IEnumerable<ConfigItem>>(ch.Guild.Id);
if (defs == null) return; if (defs == null) return;
foreach (var def in defs) foreach (var def in defs)
await Task.Run(async () => await ProcessMessage(arg, def)); await Task.Run(async () => await ProcessMessage(arg, def));
} }
public override async Task<object> ProcessConfiguration(JToken configSection) public override async Task<object> CreateInstanceState(JToken configSection)
{ {
if (configSection == null) return null;
var responses = new List<ConfigItem>(); var responses = new List<ConfigItem>();
foreach (var def in configSection.Children<JProperty>()) foreach (var def in configSection.Children<JProperty>())
{ {

View file

@ -1,6 +1,5 @@
using Discord; using Discord;
using Discord.WebSocket; using Discord.WebSocket;
using Newtonsoft.Json.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -34,8 +33,6 @@ namespace Noikoio.RegexBot.Module.DMLogger
await ProcessMessage(arg2, true); await ProcessMessage(arg2, true);
} }
public override Task<object> ProcessConfiguration(JToken configSection) => Task.FromResult<object>(null);
private async Task ProcessMessage(SocketMessage arg, bool edited) private async Task ProcessMessage(SocketMessage arg, bool edited)
{ {
var result = new StringBuilder(); var result = new StringBuilder();

View file

@ -36,8 +36,9 @@ namespace Noikoio.RegexBot.Module.EntryAutoRole
Task.Factory.StartNew(Worker, _workerCancel.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); Task.Factory.StartNew(Worker, _workerCancel.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
} }
public override Task<object> ProcessConfiguration(JToken configSection) public override Task<object> CreateInstanceState(JToken configSection)
{ {
if (configSection == null) return Task.FromResult<object>(null);
if (configSection.Type != JTokenType.Object) if (configSection.Type != JTokenType.Object)
{ {
throw new RuleImportException("Configuration for this section is invalid."); throw new RuleImportException("Configuration for this section is invalid.");
@ -47,7 +48,7 @@ namespace Noikoio.RegexBot.Module.EntryAutoRole
private Task Client_GuildAvailable(SocketGuild arg) private Task Client_GuildAvailable(SocketGuild arg)
{ {
var conf = (ModuleConfig)GetConfig(arg.Id); var conf = GetState<ModuleConfig>(arg.Id);
if (conf == null) return Task.CompletedTask; if (conf == null) return Task.CompletedTask;
SocketRole trole = GetRole(arg); SocketRole trole = GetRole(arg);
if (trole == null) return Task.CompletedTask; if (trole == null) return Task.CompletedTask;
@ -71,19 +72,19 @@ namespace Noikoio.RegexBot.Module.EntryAutoRole
private Task Client_UserLeft(SocketGuildUser arg) private Task Client_UserLeft(SocketGuildUser arg)
{ {
if (GetConfig(arg.Guild.Id) == null) return Task.CompletedTask; if (GetState<object>(arg.Guild.Id) == null) return Task.CompletedTask;
lock (_roleWaitLock) _roleWaitlist.RemoveAll(m => m.GuildId == arg.Guild.Id && m.UserId == arg.Id); lock (_roleWaitLock) _roleWaitlist.RemoveAll(m => m.GuildId == arg.Guild.Id && m.UserId == arg.Id);
return Task.CompletedTask; return Task.CompletedTask;
} }
private Task Client_UserJoined(SocketGuildUser arg) private Task Client_UserJoined(SocketGuildUser arg)
{ {
if (GetConfig(arg.Guild.Id) == null) return Task.CompletedTask; if (GetState<object>(arg.Guild.Id) == null) return Task.CompletedTask;
lock (_roleWaitLock) _roleWaitlist.Add(new AutoRoleEntry() lock (_roleWaitLock) _roleWaitlist.Add(new AutoRoleEntry()
{ {
GuildId = arg.Guild.Id, GuildId = arg.Guild.Id,
UserId = arg.Id, UserId = arg.Id,
ExpireTime = DateTimeOffset.UtcNow.AddSeconds(((ModuleConfig)GetConfig(arg.Guild.Id)).TimeDelay) ExpireTime = DateTimeOffset.UtcNow.AddSeconds((GetState<ModuleConfig>(arg.Guild.Id)).TimeDelay)
}); });
return Task.CompletedTask; return Task.CompletedTask;
} }
@ -91,7 +92,7 @@ namespace Noikoio.RegexBot.Module.EntryAutoRole
// can return null // can return null
private SocketRole GetRole(SocketGuild g) private SocketRole GetRole(SocketGuild g)
{ {
var conf = (ModuleConfig)GetConfig(g.Id); var conf = GetState<ModuleConfig>(g.Id);
if (conf == null) return null; if (conf == null) return null;
var roleInfo = conf.Role; var roleInfo = conf.Role;

View file

@ -32,8 +32,10 @@ namespace Noikoio.RegexBot.Module.ModCommands
if (arg.Channel is IGuildChannel) await CommandCheckInvoke(arg); if (arg.Channel is IGuildChannel) await CommandCheckInvoke(arg);
} }
public override async Task<object> ProcessConfiguration(JToken configSection) public override async Task<object> CreateInstanceState(JToken configSection)
{ {
if (configSection == null) return null;
// Constructor throws exception on config errors // Constructor throws exception on config errors
var conf = new ConfigItem(this, configSection); var conf = new ConfigItem(this, configSection);
@ -44,8 +46,6 @@ namespace Noikoio.RegexBot.Module.ModCommands
return conf; return conf;
} }
private new ConfigItem GetConfig(ulong guildId) => (ConfigItem)base.GetConfig(guildId);
public new Task Log(string text) => base.Log(text); public new Task Log(string text) => base.Log(text);
private async Task CommandCheckInvoke(SocketMessage arg) private async Task CommandCheckInvoke(SocketMessage arg)
@ -67,7 +67,7 @@ namespace Noikoio.RegexBot.Module.ModCommands
int spc = arg.Content.IndexOf(' '); int spc = arg.Content.IndexOf(' ');
if (spc != -1) cmdchk = arg.Content.Substring(0, spc); if (spc != -1) cmdchk = arg.Content.Substring(0, spc);
else cmdchk = arg.Content; else cmdchk = arg.Content;
if (GetConfig(g.Id).Commands.TryGetValue(cmdchk, out var c)) if (GetState<ConfigItem>(g.Id).Commands.TryGetValue(cmdchk, out var c))
{ {
try try
{ {

View file

@ -17,11 +17,11 @@ namespace Noikoio.RegexBot.Module.ModLogs
{ {
private readonly DiscordSocketClient _dClient; private readonly DiscordSocketClient _dClient;
private readonly AsyncLogger _outLog; private readonly AsyncLogger _outLog;
private readonly Func<ulong, object> _outGetConfig; private readonly Func<ulong, GuildConfig> _outGetConfig;
// TODO: How to clear the cache after a time? Can't hold on to this forever. // TODO: How to clear the cache after a time? Can't hold on to this forever.
public MessageCache(DiscordSocketClient client, AsyncLogger logger, Func<ulong, object> getConfFunc) public MessageCache(DiscordSocketClient client, AsyncLogger logger, Func<ulong, GuildConfig> getConfFunc)
{ {
_dClient = client; _dClient = client;
_outLog = logger; _outLog = logger;
@ -85,7 +85,7 @@ namespace Noikoio.RegexBot.Module.ModLogs
else return; else return;
// Check if this feature is enabled before doing anything else. // 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 (cfg == null) return;
if (isDelete && (cfg.RptTypes & EventType.MsgDelete) == 0) return; if (isDelete && (cfg.RptTypes & EventType.MsgDelete) == 0) return;
if (!isDelete && (cfg.RptTypes & EventType.MsgEdit) == 0) return; if (!isDelete && (cfg.RptTypes & EventType.MsgEdit) == 0) return;

View file

@ -19,14 +19,15 @@ namespace Noikoio.RegexBot.Module.ModLogs
if (!RegexBot.Config.DatabaseAvailable) return; if (!RegexBot.Config.DatabaseAvailable) return;
// MessageCache (reporting of MessageEdit, MessageDelete) handled by helper class // 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<GuildConfig>(id); });
// TODO add handlers for detecting joins, leaves, bans, kicks, user edits (nick/username/discr) // TODO add handlers for detecting joins, leaves, bans, kicks, user edits (nick/username/discr)
// TODO add handler for processing the log query command // TODO add handler for processing the log query command
} }
public override async Task<object> ProcessConfiguration(JToken configSection) public override async Task<object> CreateInstanceState(JToken configSection)
{ {
if (configSection == null) return null;
if (configSection.Type != JTokenType.Object) if (configSection.Type != JTokenType.Object)
throw new RuleImportException("Configuration for this section is invalid."); throw new RuleImportException("Configuration for this section is invalid.");