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>
/// 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.
/// </summary>
/// <remarks>
/// Module code <i>should not</i> hold on to this data, but instead use <see cref="GetConfig(ulong)"/> 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 <i>should not</i> hold on state or configuration data on its own, but instead use
/// <see cref="GetState{T}(ulong)"/> 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.
/// </remarks>
/// <returns>
/// Processed configuration data prepared for later use.
/// </returns>
/// <exception cref="ConfigItem.RuleImportException">
/// This method should throw <see cref="ConfigItem.RuleImportException"/>
/// in the event of configuration errors. The exception message will be properly displayed.
/// </exception>
public abstract Task<object> ProcessConfiguration(JToken configSection);
/// <param name="configSection">
/// Configuration data for this module, for this guild. Is null if none was defined.
/// </param>
/// <returns>An object that may later be retrieved by <see cref="GetState{T}(ulong)"/>.</returns>
public virtual Task<object> CreateInstanceState(JToken configSection) => Task.FromResult<object>(null);
/// <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>
/// <returns>
/// The stored configuration data, or null if none exists.
/// The stored state data, or null/default if none exists.
/// </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);
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);
}
/// <summary>

View file

@ -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<BotModule, object> customConfs = new Dictionary<BotModule, object>();
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);
}

View file

@ -33,8 +33,6 @@ namespace Noikoio.RegexBot.EntityCache
}
}
public override Task<object> ProcessConfiguration(JToken configSection) => Task.FromResult<object>(null);
// Guild and guild member information has become available.
// This is a very expensive operation, especially when joining larger guilds.
private async Task Client_GuildAvailable(SocketGuild arg)

View file

@ -22,8 +22,9 @@ namespace Noikoio.RegexBot.Module.AutoMod
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>();
foreach (var def in configSection.Children<JProperty>())
@ -52,7 +53,7 @@ namespace Noikoio.RegexBot.Module.AutoMod
if (ch == null) return;
// Get rules
var rules = GetConfig(ch.Guild.Id) as IEnumerable<ConfigItem>;
var rules = GetState<IEnumerable<ConfigItem>>(ch.Guild.Id);
if (rules == null) return;
foreach (var rule in rules)

View file

@ -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<ConfigItem>;
var defs = GetState<IEnumerable<ConfigItem>>(ch.Guild.Id);
if (defs == null) return;
foreach (var def in defs)
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>();
foreach (var def in configSection.Children<JProperty>())
{

View file

@ -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<object> ProcessConfiguration(JToken configSection) => Task.FromResult<object>(null);
private async Task ProcessMessage(SocketMessage arg, bool edited)
{
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);
}
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)
{
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<ModuleConfig>(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<object>(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<object>(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<ModuleConfig>(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<ModuleConfig>(g.Id);
if (conf == null) return null;
var roleInfo = conf.Role;

View file

@ -32,8 +32,10 @@ namespace Noikoio.RegexBot.Module.ModCommands
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
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<ConfigItem>(g.Id).Commands.TryGetValue(cmdchk, out var c))
{
try
{

View file

@ -17,11 +17,11 @@ namespace Noikoio.RegexBot.Module.ModLogs
{
private readonly DiscordSocketClient _dClient;
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.
public MessageCache(DiscordSocketClient client, AsyncLogger logger, Func<ulong, object> getConfFunc)
public MessageCache(DiscordSocketClient client, AsyncLogger logger, Func<ulong, GuildConfig> 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;

View file

@ -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<GuildConfig>(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<object> ProcessConfiguration(JToken configSection)
public override async Task<object> CreateInstanceState(JToken configSection)
{
if (configSection == null) return null;
if (configSection.Type != JTokenType.Object)
throw new RuleImportException("Configuration for this section is invalid.");