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