From fc88ab5cf9659a5c3e5a2b237002b4df4ddc8096 Mon Sep 17 00:00:00 2001 From: Noi Date: Wed, 20 Jul 2022 20:34:29 -0700 Subject: [PATCH] Update modules and module loading Changed accessibility of included modules to 'internal' to suppress documentation warnings. Additionally, increase logging feedback as configuration is loaded. --- {Modules => Common}/RateLimit.cs | 28 +++++++++++++++--------- InstanceConfig.cs | 14 ++++++------ ModuleLoader.cs | 10 +++++---- Modules/AutoResponder/AutoResponder.cs | 2 +- Modules/AutoResponder/Definition.cs | 2 +- Modules/EntryRole/EntryRole.cs | 14 +++++++++--- Modules/ModCommands/ModCommands.cs | 8 +++---- Modules/ModLogs/ModLogs.cs | 4 ++-- Modules/ModLogs/ModLogs_Messages.cs | 2 +- Modules/PendingOutRole/PendingOutRole.cs | 3 ++- Modules/RegexModerator/RegexModerator.cs | 2 +- Modules/VoiceRoleSync/ModuleConfig.cs | 2 ++ Modules/VoiceRoleSync/VoiceRoleSync.cs | 7 ++++-- RegexbotModule.cs | 2 +- 14 files changed, 61 insertions(+), 39 deletions(-) rename {Modules => Common}/RateLimit.cs (53%) diff --git a/Modules/RateLimit.cs b/Common/RateLimit.cs similarity index 53% rename from Modules/RateLimit.cs rename to Common/RateLimit.cs index 8657a1b..72377d8 100644 --- a/Modules/RateLimit.cs +++ b/Common/RateLimit.cs @@ -1,21 +1,30 @@ -namespace RegexBot.Modules; - +namespace RegexBot.Common; /// /// Helper class for managing rate limit data. -/// More accurately, this class holds entries, not allowing the same entry to be held more than once until a specified +/// Specifically, this class holds entries and does not allow the same entry to be held more than once until a specified /// amount of time has passed since the entry was originally tracked; useful for a rate limit system. /// -class RateLimit where T : notnull { - public const ushort DefaultTimeout = 20; // Skeeter's a cool guy and you can't convince me otherwise. +public class RateLimit where T : notnull { + private const int DefaultTimeout = 20; // Skeeter's a cool guy and you can't convince me otherwise. /// /// Time until an entry within this instance expires, in seconds. /// - public uint Timeout { get; } + public int Timeout { get; } private Dictionary Entries { get; } = new Dictionary(); + /// + /// Creates a new instance with the default timeout value. + /// public RateLimit() : this(DefaultTimeout) { } - public RateLimit(uint timeout) => Timeout = timeout; + /// + /// Creates a new instance with the given timeout value. + /// + /// Time until an entry within this instance will expire, in seconds. + public RateLimit(int timeout) { + if (timeout < 0) throw new ArgumentOutOfRangeException(nameof(timeout), "Timeout valie cannot be negative."); + Timeout = timeout; + } /// /// Checks if the given value is permitted through the rate limit. @@ -30,9 +39,8 @@ class RateLimit where T : notnull { var expired = Entries.Where(x => x.Value.AddSeconds(Timeout) <= now).Select(x => x.Key).ToList(); foreach (var item in expired) Entries.Remove(item); - if (Entries.ContainsKey(value)) { - return false; - } else { + if (Entries.ContainsKey(value)) return false; + else { Entries.Add(value, DateTime.Now); return true; } diff --git a/InstanceConfig.cs b/InstanceConfig.cs index 91f37eb..de5a9e1 100644 --- a/InstanceConfig.cs +++ b/InstanceConfig.cs @@ -17,7 +17,7 @@ class InstanceConfig { /// /// List of assemblies to load, by file. Paths are always relative to the bot directory. /// - internal string[] Assemblies { get; } + internal IReadOnlyList Assemblies { get; } /// /// Webhook URL for bot log reporting. @@ -31,7 +31,7 @@ class InstanceConfig { /// internal InstanceConfig() { var path = Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location) - + "." + Path.DirectorySeparatorChar + "config.json"; + + "." + Path.DirectorySeparatorChar + "instance.json"; JObject conf; try { @@ -59,12 +59,12 @@ class InstanceConfig { if (string.IsNullOrEmpty(InstanceLogTarget)) throw new Exception($"'{nameof(InstanceLogTarget)}' is not properly specified in configuration."); - var asmList = conf[nameof(Assemblies)]; - if (asmList == null || asmList.Type != JTokenType.Array) { + try { + Assemblies = Common.Utilities.LoadStringOrStringArray(conf[nameof(Assemblies)]).AsReadOnly(); + } catch (ArgumentNullException) { + Assemblies = Array.Empty(); + } catch (ArgumentException) { throw new Exception($"'{nameof(Assemblies)}' is not properly specified in configuration."); } - var asmListImport = new List(); - foreach (var line in asmList.Values()) if (!string.IsNullOrEmpty(line)) asmListImport.Add(line); - Assemblies = asmListImport.ToArray(); } } diff --git a/ModuleLoader.cs b/ModuleLoader.cs index 3069abe..e3c79d5 100644 --- a/ModuleLoader.cs +++ b/ModuleLoader.cs @@ -7,10 +7,13 @@ static class ModuleLoader { /// /// Given the instance configuration, loads all appropriate types from file specified in it. /// - internal static ReadOnlyCollection Load(InstanceConfig conf, RegexbotClient k) { + internal static ReadOnlyCollection Load(InstanceConfig conf, RegexbotClient rb) { var path = Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location) + Path.DirectorySeparatorChar; var modules = new List(); + // Load self, then others if defined + modules.AddRange(LoadModulesFromAssembly(Assembly.GetExecutingAssembly(), rb)); + foreach (var file in conf.Assemblies) { Assembly? a = null; try { @@ -24,7 +27,7 @@ static class ModuleLoader { IEnumerable? amods = null; try { - amods = LoadModulesFromAssembly(a, k); + amods = LoadModulesFromAssembly(a, rb); } catch (Exception ex) { Console.WriteLine("An error occurred when attempting to create a module instance."); Console.WriteLine(ex.ToString()); @@ -40,9 +43,8 @@ static class ModuleLoader { where !type.IsAssignableFrom(typeof(RegexbotModule)) where type.GetCustomAttribute() != null select type; - rb._svcLogging.DoLog(false, nameof(ModuleLoader), $"Scanning {asm.GetName().Name}"); - var newreport = new StringBuilder("---> Found module(s):"); + var newreport = new StringBuilder($"---> Modules in {asm.GetName().Name}:"); var newmods = new List(); foreach (var t in eligibleTypes) { var mod = Activator.CreateInstance(t, rb)!; diff --git a/Modules/AutoResponder/AutoResponder.cs b/Modules/AutoResponder/AutoResponder.cs index 239823a..16b1ac5 100644 --- a/Modules/AutoResponder/AutoResponder.cs +++ b/Modules/AutoResponder/AutoResponder.cs @@ -8,7 +8,7 @@ namespace RegexBot.Modules.AutoResponder; /// fit for non-moderation use cases and has specific features suitable to that end. /// [RegexbotModule] -public class AutoResponder : RegexbotModule { +internal class AutoResponder : RegexbotModule { public AutoResponder(RegexbotClient bot) : base(bot) { DiscordClient.MessageReceived += DiscordClient_MessageReceived; } diff --git a/Modules/AutoResponder/Definition.cs b/Modules/AutoResponder/Definition.cs index e31f5f7..5ceaf86 100644 --- a/Modules/AutoResponder/Definition.cs +++ b/Modules/AutoResponder/Definition.cs @@ -84,7 +84,7 @@ class Definition { // Rate limiting var rlconf = def[nameof(RateLimit)]; if (rlconf?.Type == JTokenType.Integer) { - var rlval = rlconf.Value(); + var rlval = rlconf.Value(); RateLimit = new RateLimit(rlval); } else if (rlconf != null) { throw new ModuleLoadException($"'{nameof(RateLimit)}' must be a non-negative integer{errpostfx}"); diff --git a/Modules/EntryRole/EntryRole.cs b/Modules/EntryRole/EntryRole.cs index 7ac8243..0042099 100644 --- a/Modules/EntryRole/EntryRole.cs +++ b/Modules/EntryRole/EntryRole.cs @@ -6,7 +6,7 @@ namespace RegexBot.Modules.EntryRole; /// Automatically sets a role onto users entering the guild after a predefined amount of time. /// [RegexbotModule] -public sealed class EntryRole : RegexbotModule, IDisposable { +internal sealed class EntryRole : RegexbotModule, IDisposable { readonly Task _workerTask; readonly CancellationTokenSource _workerTaskToken; @@ -57,11 +57,19 @@ public sealed class EntryRole : RegexbotModule, IDisposable { if (config.Type != JTokenType.Object) throw new ModuleLoadException("Configuration is not properly defined."); + var g = DiscordClient.GetGuild(guildID); // Attempt to preserve existing timer list on reload var oldconf = GetGuildState(guildID); - if (oldconf == null) return Task.FromResult(new GuildData((JObject)config)); - else return Task.FromResult(new GuildData((JObject)config, oldconf.WaitingList)); + if (oldconf == null) { + var newconf = new GuildData((JObject)config); + Log(g, $"Configured for {newconf.WaitTime} seconds."); + return Task.FromResult(newconf); + } else { + var newconf = new GuildData((JObject)config, oldconf.WaitingList); + Log(g, $"Reconfigured for {newconf.WaitTime} seconds; keeping {newconf.WaitingList.Count} existing timers."); + return Task.FromResult(newconf); + } } /// diff --git a/Modules/ModCommands/ModCommands.cs b/Modules/ModCommands/ModCommands.cs index 65fbffc..ccaf0b5 100644 --- a/Modules/ModCommands/ModCommands.cs +++ b/Modules/ModCommands/ModCommands.cs @@ -3,7 +3,7 @@ /// Provides a way to define highly configurable text-based commands for use by moderators within a guild. /// [RegexbotModule] -public class ModCommands : RegexbotModule { +internal class ModCommands : RegexbotModule { public ModCommands(RegexbotClient bot) : base(bot) { DiscordClient.MessageReceived += Client_MessageReceived; } @@ -20,12 +20,10 @@ public class ModCommands : RegexbotModule { var conf = new ModuleConfig(this, config); if (conf.Commands.Count > 0) { - Log($"{conf.Commands.Count} commands loaded."); + Log(DiscordClient.GetGuild(guildID), $"{conf.Commands.Count} commands loaded."); return Task.FromResult(conf); - } else { - Log("'{nameof(ModLogs)}' is defined, but no command configuration exists."); - return Task.FromResult(null); } + return Task.FromResult(null); } private async Task CommandCheckInvoke(ModuleConfig cfg, SocketMessage arg) { diff --git a/Modules/ModLogs/ModLogs.cs b/Modules/ModLogs/ModLogs.cs index efd9bc6..213611a 100644 --- a/Modules/ModLogs/ModLogs.cs +++ b/Modules/ModLogs/ModLogs.cs @@ -6,7 +6,7 @@ namespace RegexBot.Modules.ModLogs; /// Makes use of a helper class, . /// [RegexbotModule] -public partial class ModLogs : RegexbotModule { +internal partial class ModLogs : RegexbotModule { // TODO consider resurrecting 2.x idea of logging actions to db, making it searchable? public ModLogs(RegexbotClient bot) : base(bot) { @@ -20,7 +20,7 @@ public partial class ModLogs : RegexbotModule { if (config.Type != JTokenType.Object) throw new ModuleLoadException("Configuration for this section is invalid."); var newconf = new ModuleConfig((JObject)config); - Log($"Writing logs to {newconf.ReportingChannel}."); + Log(DiscordClient.GetGuild(guildID), $"Writing logs to {newconf.ReportingChannel}."); return Task.FromResult(new ModuleConfig((JObject)config)); } diff --git a/Modules/ModLogs/ModLogs_Messages.cs b/Modules/ModLogs/ModLogs_Messages.cs index 3981656..7330866 100644 --- a/Modules/ModLogs/ModLogs_Messages.cs +++ b/Modules/ModLogs/ModLogs_Messages.cs @@ -5,7 +5,7 @@ using System.Text; namespace RegexBot.Modules.ModLogs; // Contains handlers and all logic relating to logging message edits and deletions -public partial class ModLogs { +internal partial class ModLogs { const string PreviewCutoffNotify = "**Message too long to preview; showing first {0} characters.**\n\n"; const string NotCached = "Message not cached."; diff --git a/Modules/PendingOutRole/PendingOutRole.cs b/Modules/PendingOutRole/PendingOutRole.cs index 95e6698..f02db71 100644 --- a/Modules/PendingOutRole/PendingOutRole.cs +++ b/Modules/PendingOutRole/PendingOutRole.cs @@ -5,7 +5,7 @@ /// that is, the user has passed the requirements needed to fully access the guild such as welcome messages, etc. /// [RegexbotModule] -public class PendingOutRole : RegexbotModule { +internal class PendingOutRole : RegexbotModule { public PendingOutRole(RegexbotClient bot) : base(bot) { DiscordClient.GuildMembersDownloaded += DiscordClient_GuildMembersDownloaded; DiscordClient.GuildMemberUpdated += DiscordClient_GuildMemberUpdated; @@ -45,6 +45,7 @@ public class PendingOutRole : RegexbotModule { if (config == null) return Task.FromResult(null); if (config.Type != JTokenType.Object) throw new ModuleLoadException("Configuration for this section is invalid."); + Log(DiscordClient.GetGuild(guildID), "Active."); return Task.FromResult(new ModuleConfig((JObject)config)); } } diff --git a/Modules/RegexModerator/RegexModerator.cs b/Modules/RegexModerator/RegexModerator.cs index cb76ac2..a7ba876 100644 --- a/Modules/RegexModerator/RegexModerator.cs +++ b/Modules/RegexModerator/RegexModerator.cs @@ -6,7 +6,7 @@ namespace RegexBot.Modules.RegexModerator; /// When triggered, one or more actions are executed as defined in its configuration. /// [RegexbotModule] -public class RegexModerator : RegexbotModule { +internal class RegexModerator : RegexbotModule { public RegexModerator(RegexbotClient bot) : base(bot) { DiscordClient.MessageReceived += DiscordClient_MessageReceived; DiscordClient.MessageUpdated += DiscordClient_MessageUpdated; diff --git a/Modules/VoiceRoleSync/ModuleConfig.cs b/Modules/VoiceRoleSync/ModuleConfig.cs index abd5f25..0e584a6 100644 --- a/Modules/VoiceRoleSync/ModuleConfig.cs +++ b/Modules/VoiceRoleSync/ModuleConfig.cs @@ -7,6 +7,8 @@ namespace RegexBot.Modules.VoiceRoleSync; class ModuleConfig { private readonly ReadOnlyDictionary _values; + public int Count { get => _values.Count; } + public ModuleConfig(JObject config) { // Configuration format is expected to be an object that contains other objects. // The objects themselves should have their name be the voice channel, diff --git a/Modules/VoiceRoleSync/VoiceRoleSync.cs b/Modules/VoiceRoleSync/VoiceRoleSync.cs index 6856c81..e92a841 100644 --- a/Modules/VoiceRoleSync/VoiceRoleSync.cs +++ b/Modules/VoiceRoleSync/VoiceRoleSync.cs @@ -4,7 +4,7 @@ namespace RegexBot.Modules.VoiceRoleSync; /// In other words: applies a role to a user entering a voice channel. Removes the role when exiting. /// [RegexbotModule] -public class VoiceRoleSync : RegexbotModule { +internal class VoiceRoleSync : RegexbotModule { // TODO wishlist? specify multiple definitions - multiple channels associated with multiple roles. public VoiceRoleSync(RegexbotClient bot) : base(bot) { @@ -48,6 +48,9 @@ public class VoiceRoleSync : RegexbotModule { if (config == null) return Task.FromResult(null); if (config.Type != JTokenType.Object) throw new ModuleLoadException("Configuration for this section is invalid."); - return Task.FromResult(new ModuleConfig((JObject)config)); + + var newconf = new ModuleConfig((JObject)config); + Log(DiscordClient.GetGuild(guildID), $"Configured with {newconf.Count} pairing(s)."); + return Task.FromResult(newconf); } } diff --git a/RegexbotModule.cs b/RegexbotModule.cs index 4ebe99d..726f6ff 100644 --- a/RegexbotModule.cs +++ b/RegexbotModule.cs @@ -73,7 +73,7 @@ public abstract class RegexbotModule { /// The log message to send. Multi-line messages are acceptable. protected void Log(SocketGuild guild, string? message) { var gname = guild.Name ?? $"Guild ID {guild.Id}"; - Bot._svcLogging.DoLog(false, $"{Name}] [{gname}", message); + Bot._svcLogging.DoLog(false, $"{gname}] [{Name}", message); } ///