Reorganized project

Moved modules into the assembly itself to simplify development of
further features and reduce complexity in building this project.

Additionally, many small adjustments were made, including:
- Add documentation to most public methods that had it missing
- Minor style updates
- Updated readme to reflect near-completion of this rewrite
- Remove any last remaining references to old project name Kerobot
- Update dependencies
This commit is contained in:
Noi 2022-07-20 18:55:08 -07:00
parent 53e0301edd
commit 1149f2800d
73 changed files with 333 additions and 313 deletions

4
.vscode/launch.json vendored
View file

@ -10,9 +10,9 @@
"request": "launch", "request": "launch",
"preLaunchTask": "build", "preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path. // If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/output/Debug/net6.0/RegexBot.dll", "program": "${workspaceFolder}/bin/Debug/net6.0/RegexBot.dll",
"args": [], "args": [],
"cwd": "${workspaceFolder}/RegexBot", "cwd": "${workspaceFolder}",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
"console": "internalConsole", "console": "internalConsole",
"stopAtEntry": false "stopAtEntry": false

6
.vscode/tasks.json vendored
View file

@ -7,7 +7,7 @@
"type": "process", "type": "process",
"args": [ "args": [
"build", "build",
"${workspaceFolder}/RegexBot.sln", "${workspaceFolder}/RegexBot.csproj",
"/property:GenerateFullPaths=true", "/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary" "/consoleloggerparameters:NoSummary"
], ],
@ -19,7 +19,7 @@
"type": "process", "type": "process",
"args": [ "args": [
"publish", "publish",
"${workspaceFolder}/RegexBot.sln", "${workspaceFolder}/RegexBot.csproj",
"/property:GenerateFullPaths=true", "/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary" "/consoleloggerparameters:NoSummary"
], ],
@ -33,7 +33,7 @@
"watch", "watch",
"run", "run",
"--project", "--project",
"${workspaceFolder}/RegexBot.sln" "${workspaceFolder}/RegexBot.csproj"
], ],
"problemMatcher": "$msCompile" "problemMatcher": "$msCompile"
} }

View file

@ -1,9 +1,6 @@
using Discord.WebSocket; using System.Collections;
using Newtonsoft.Json.Linq;
using System.Collections;
namespace RegexBot.Common; namespace RegexBot.Common;
/// <summary> /// <summary>
/// Represents a commonly-used configuration structure: an array of strings consisting of <see cref="EntityName"/> values. /// Represents a commonly-used configuration structure: an array of strings consisting of <see cref="EntityName"/> values.
/// </summary> /// </summary>
@ -64,6 +61,7 @@ public class EntityList : IEnumerable<EntityName> {
/// Checks if the parameters of the given <see cref="SocketMessage"/> matches with /// Checks if the parameters of the given <see cref="SocketMessage"/> matches with
/// any entity specified in this list. /// any entity specified in this list.
/// </summary> /// </summary>
/// <param name="msg">The incoming message object with which to scan for a match.</param>
/// <param name="keepId"> /// <param name="keepId">
/// Specifies if EntityName instances within this list should have their internal ID value /// Specifies if EntityName instances within this list should have their internal ID value
/// updated if found during the matching process. /// updated if found during the matching process.

View file

@ -1,7 +1,4 @@
using Discord.WebSocket; namespace RegexBot.Common;
namespace RegexBot.Common;
/// <summary> /// <summary>
/// Helper class that holds an entity's name, ID, or both. /// Helper class that holds an entity's name, ID, or both.
/// Meant to be used during configuration processing in cases where the configuration expects /// Meant to be used during configuration processing in cases where the configuration expects

20
Common/EntityType.cs Normal file
View file

@ -0,0 +1,20 @@
namespace RegexBot.Common;
/// <summary>
/// The type of entity specified in an <see cref="EntityName"/>.
/// </summary>
public enum EntityType {
/// <summary>Default value. Is never referenced in regular usage.</summary>
Unspecified,
/// <summary>
/// Userd when the <see cref="EntityName"/> represents a role.
/// </summary>
Role,
/// <summary>
/// Used when the <see cref="EntityName"/> represents a channel.
/// </summary>
Channel,
/// <summary>
/// Used when the <see cref="EntityName"/> represents a user.
/// </summary>
User
}

View file

@ -1,16 +1,37 @@
using Discord.WebSocket; namespace RegexBot.Common;
using Newtonsoft.Json.Linq;
namespace RegexBot.Common;
/// <summary> /// <summary>
/// Represents commonly-used configuration regarding whitelist/blacklist filtering, including exemptions. /// Represents commonly-used configuration regarding whitelist/blacklist filtering, including exemptions.
/// </summary> /// </summary>
public class FilterList { public class FilterList {
public enum FilterMode { None, Whitelist, Blacklist } /// <summary>
/// The mode at which the <see cref="FilterList"/>'s filter criteria is operating.
/// </summary>
public enum FilterMode {
/// <summary>
/// A <see cref="FilterList"/> setting which does no filtering on the list.
/// </summary>
None,
/// <summary>
/// A <see cref="FilterList"/> setting which excludes only entites not in the list, excluding those exempted.
/// </summary>
Whitelist,
/// <summary>
/// A <see cref="FilterList"/> setting which allows all entities except those in the list, but allowing those exempted.
/// </summary>
Blacklist
}
/// <summary>
/// Gets the mode at which the <see cref="FilterList"/>'s filter criteria is operating.
/// </summary>
public FilterMode Mode { get; } public FilterMode Mode { get; }
/// <summary>
/// Gets the inner list that this instance is using for its filtering criteria.
/// </summary>
public EntityList FilteredList { get; } public EntityList FilteredList { get; }
/// <summary>
/// Gets the list of entities that may override filtering rules for this instance.
/// </summary>
public EntityList FilterExemptions { get; } public EntityList FilterExemptions { get; }
/// <summary> /// <summary>
@ -70,6 +91,9 @@ public class FilterList {
/// Determines if the parameters of the given message match up against the filtering /// Determines if the parameters of the given message match up against the filtering
/// rules described within this instance. /// rules described within this instance.
/// </summary> /// </summary>
/// <param name="msg">
/// The incoming message to be checked.
/// </param>
/// <param name="keepId"> /// <param name="keepId">
/// See equivalent documentation for <see cref="EntityList.IsListMatch(SocketMessage, bool)"/>. /// See equivalent documentation for <see cref="EntityList.IsListMatch(SocketMessage, bool)"/>.
/// </param> /// </param>

10
Common/Messages.cs Normal file
View file

@ -0,0 +1,10 @@
namespace RegexBot.Common;
/// <summary>
/// Commonly used strings used throughout the bot and modules.
/// </summary>
public static class Messages {
/// <summary>
/// Gets a string generally appropriate to display in the event of a 403 error.
/// </summary>
public const string ForbiddenGenericError = "Failed to perform the action due to a permissions issue.";
}

View file

@ -1,6 +1,4 @@
using Discord; using Discord;
using Discord.WebSocket;
using Newtonsoft.Json.Linq;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -9,9 +7,24 @@ namespace RegexBot.Common;
/// Miscellaneous utility methods useful for the bot and modules. /// Miscellaneous utility methods useful for the bot and modules.
/// </summary> /// </summary>
public static class Utilities { public static class Utilities {
/// <summary>
/// Gets a compiled regex that matches a channel tag and pulls its snowflake value.
/// </summary>
public static Regex ChannelMention { get; } = new(@"<#(?<snowflake>\d+)>", RegexOptions.Compiled); public static Regex ChannelMention { get; } = new(@"<#(?<snowflake>\d+)>", RegexOptions.Compiled);
/// <summary>
/// Gets a compiled regex that matches a custom emoji and pulls its name and ID.
/// </summary>
public static Regex CustomEmoji { get; } = new(@"<:(?<name>[A-Za-z0-9_]{2,}):(?<ID>\d+)>", RegexOptions.Compiled); public static Regex CustomEmoji { get; } = new(@"<:(?<name>[A-Za-z0-9_]{2,}):(?<ID>\d+)>", RegexOptions.Compiled);
/// <summary>
/// Gets a compiled regex that matches a fully formed Discord handle, extracting the name and discriminator.
/// </summary>
public static Regex DiscriminatorSearch { get; } = new(@"(.+)#(\d{4}(?!\d))", RegexOptions.Compiled); public static Regex DiscriminatorSearch { get; } = new(@"(.+)#(\d{4}(?!\d))", RegexOptions.Compiled);
/// <summary>
/// Gets a compiled regex that matches a user tag and pulls its snowflake value.
/// </summary>
public static Regex UserMention { get; } = new(@"<@!?(?<snowflake>\d+)>", RegexOptions.Compiled); public static Regex UserMention { get; } = new(@"<@!?(?<snowflake>\d+)>", RegexOptions.Compiled);
/// <summary> /// <summary>

View file

@ -1,7 +1,9 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace RegexBot.Data; namespace RegexBot.Data;
/// <summary>
/// Represents a database connection using the settings defined in the bot's global configuration.
/// </summary>
public class BotDatabaseContext : DbContext { public class BotDatabaseContext : DbContext {
private static string? _npgsqlConnectionString; private static string? _npgsqlConnectionString;
internal static string PostgresConnectionString { internal static string PostgresConnectionString {
@ -17,21 +19,31 @@ public class BotDatabaseContext : DbContext {
set => _npgsqlConnectionString ??= value; set => _npgsqlConnectionString ??= value;
} }
public DbSet<GuildLogLine> GuildLog { get; set; } = null!; /// <summary>
/// Retrieves the <seealso cref="CachedUser">user cache</seealso>.
/// </summary>
public DbSet<CachedUser> UserCache { get; set; } = null!; public DbSet<CachedUser> UserCache { get; set; } = null!;
/// <summary>
/// Retrieves the <seealso cref="CachedGuildUser">guild user cache</seealso>.
/// </summary>
public DbSet<CachedGuildUser> GuildUserCache { get; set; } = null!; public DbSet<CachedGuildUser> GuildUserCache { get; set; } = null!;
/// <summary>
/// Retrieves the <seealso cref="CachedGuildMessage">guild message cache</seealso>.
/// </summary>
public DbSet<CachedGuildMessage> GuildMessageCache { get; set; } = null!; public DbSet<CachedGuildMessage> GuildMessageCache { get; set; } = null!;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) /// <inheritdoc />
protected sealed override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder => optionsBuilder
.UseNpgsql(PostgresConnectionString) .UseNpgsql(PostgresConnectionString)
.UseSnakeCaseNamingConvention(); .UseSnakeCaseNamingConvention();
/// <inheritdoc />
protected override void OnModelCreating(ModelBuilder modelBuilder) { protected override void OnModelCreating(ModelBuilder modelBuilder) {
modelBuilder.Entity<GuildLogLine>(entity => entity.Property(e => e.Timestamp).HasDefaultValueSql("now()"));
modelBuilder.Entity<CachedUser>(entity => entity.Property(e => e.Discriminator).HasMaxLength(4).IsFixedLength()); modelBuilder.Entity<CachedUser>(entity => entity.Property(e => e.Discriminator).HasMaxLength(4).IsFixedLength());
modelBuilder.Entity<CachedGuildUser>(entity => { modelBuilder.Entity<CachedGuildUser>(entity => {
entity.Navigation(e => e.User).AutoInclude();
entity.HasKey(e => new { e.UserId, e.GuildId }); entity.HasKey(e => new { e.UserId, e.GuildId });
entity.Property(e => e.FirstSeenTime).HasDefaultValueSql("now()"); entity.Property(e => e.FirstSeenTime).HasDefaultValueSql("now()");
}); });

View file

@ -0,0 +1,62 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RegexBot.Data;
/// <summary>
/// Represents an item in the guild message cache.
/// </summary>
[Table("cache_guildmessages")]
public class CachedGuildMessage {
/// <summary>
/// Gets the message's snowflake ID.
/// </summary>
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public long MessageId { get; set; }
/// <summary>
/// Gets the message author's snowflake ID.
/// </summary>
public long AuthorId { get; set; }
/// <summary>
/// Gets the associated guild's snowflake ID.
/// </summary>
public long GuildId { get; set; }
/// <summary>
/// Gets the corresponding channel's snowflake ID.
/// </summary>
public long ChannelId { get; set; }
/// <summary>
/// Gets the timestamp showing when this message was originally created.
/// </summary>
/// <remarks>
/// Though it's possible to draw this from <see cref="MessageId"/>, it is stored in the database
/// as a separate field for any possible necessary use via database queries.
/// </remarks>
public DateTimeOffset CreatedAt { get; set; }
/// <summary>
/// Gets the timestamp, if any, showing when this message was last edited.
/// </summary>
public DateTimeOffset? EditedAt { get; set; }
/// <summary>
/// Gets a list of file names thata were attached to this message.
/// </summary>
public List<string> AttachmentNames { get; set; } = null!;
/// <summary>
/// Gets this message's content.
/// </summary>
public string Content { get; set; } = null!;
/// <summary>
/// If included in the query, references the associated <seealso cref="CachedUser"/> for this entry.
/// </summary>
[ForeignKey(nameof(AuthorId))]
[InverseProperty(nameof(CachedUser.GuildMessages))]
public CachedUser Author { get; set; } = null!;
}

36
Data/CachedGuildUser.cs Normal file
View file

@ -0,0 +1,36 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace RegexBot.Data;
/// <summary>
/// Represents an item in the guild user cache.
/// </summary>
[Table("cache_usersinguild")]
public class CachedGuildUser {
/// <inheritdoc cref="CachedUser.UserId"/>
public long UserId { get; set; }
/// <summary>
/// Gets the associated guild's snowflake ID.
/// </summary>
public long GuildId { get; set; }
/// <inheritdoc cref="CachedUser.ULastUpdateTime"/>
public DateTimeOffset GULastUpdateTime { get; set; }
/// <summary>
/// Gets the timestamp showing when this cache entry was first added into the database.
/// </summary>
public DateTimeOffset FirstSeenTime { get; set; }
/// <summary>
/// Gets the user's cached nickname in the guild.
/// </summary>
public string? Nickname { get; set; }
/// <summary>
/// If included in the query, references the associated <seealso cref="CachedUser"/> for this entry.
/// </summary>
[ForeignKey(nameof(UserId))]
[InverseProperty(nameof(CachedUser.Guilds))]
public CachedUser User { get; set; } = null!;
}

48
Data/CachedUser.cs Normal file
View file

@ -0,0 +1,48 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RegexBot.Data;
/// <summary>
/// Represents an item in the user cache.
/// </summary>
[Table("cache_users")]
public class CachedUser {
/// <summary>
/// Gets the user's snowflake ID.
/// </summary>
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public long UserId { get; set; }
/// <summary>
/// Gets the timestamp showing when this cache entry was last updated.
/// </summary>
public DateTimeOffset ULastUpdateTime { get; set; }
/// <summary>
/// Gets the user's username value, without the discriminator.
/// </summary>
public string Username { get; set; } = null!;
/// <summary>
/// Gets the user's discriminator value.
/// </summary>
public string Discriminator { get; set; } = null!;
/// <summary>
/// Gets the avatar URL, if any, for the associated user.
/// </summary>
public string? AvatarUrl { get; set; }
/// <summary>
/// If included in the query, gets the list of associated <seealso cref="CachedGuildUser"/> entries for this entry.
/// </summary>
[InverseProperty(nameof(CachedGuildUser.User))]
public ICollection<CachedGuildUser> Guilds { get; set; } = null!;
/// <summary>
/// If included in the query, gets the list of associated <seealso cref="CachedGuildMessage"/> entries for this entry.
/// </summary>
[InverseProperty(nameof(CachedGuildMessage.Author))]
public ICollection<CachedGuildMessage> GuildMessages { get; set; } = null!;
}

View file

@ -0,0 +1,3 @@
[*.cs]
dotnet_analyzer_diagnostic.category-CodeQuality.severity = none
dotnet_diagnostic.CS1591.severity = none

View file

@ -1,5 +1,4 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using RegexBot.Data; using RegexBot.Data;
using System.Reflection; using System.Reflection;
@ -30,14 +29,9 @@ class InstanceConfig {
/// <summary> /// <summary>
/// Sets up instance configuration object from file and command line parameters. /// Sets up instance configuration object from file and command line parameters.
/// </summary> /// </summary>
internal InstanceConfig(string[] cmdline) { internal InstanceConfig() {
var opts = Options.ParseOptions(cmdline); var path = Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location)
var path = opts.ConfigFile;
if (path == null) { // default: config.json in working directory
path = Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location)
+ "." + Path.DirectorySeparatorChar + "config.json"; + "." + Path.DirectorySeparatorChar + "config.json";
}
JObject conf; JObject conf;
try { try {

View file

@ -1,7 +1,8 @@
namespace RegexBot; namespace RegexBot;
/// <summary> /// <summary>
/// Represents errors that occur when a module attempts to create a new guild state object. /// Represents an error occurring when a module attempts to create a new guild state object
/// (that is, read or refresh its configuration).
/// </summary> /// </summary>
public class ModuleLoadException : Exception { public class ModuleLoadException : Exception {
/// <summary> /// <summary>

View file

@ -3,7 +3,6 @@ using System.Reflection;
using System.Text; using System.Text;
namespace RegexBot; namespace RegexBot;
static class ModuleLoader { 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.
@ -36,21 +35,21 @@ static class ModuleLoader {
return modules.AsReadOnly(); return modules.AsReadOnly();
} }
static IEnumerable<RegexbotModule> LoadModulesFromAssembly(Assembly asm, RegexbotClient k) { static IEnumerable<RegexbotModule> LoadModulesFromAssembly(Assembly asm, RegexbotClient rb) {
var eligibleTypes = from type in asm.GetTypes() var eligibleTypes = from type in asm.GetTypes()
where !type.IsAssignableFrom(typeof(RegexbotModule)) where !type.IsAssignableFrom(typeof(RegexbotModule))
where type.GetCustomAttribute<RegexbotModuleAttribute>() != null where type.GetCustomAttribute<RegexbotModuleAttribute>() != null
select type; select type;
k._svcLogging.DoLog(false, nameof(ModuleLoader), $"Scanning {asm.GetName().Name}"); rb._svcLogging.DoLog(false, nameof(ModuleLoader), $"Scanning {asm.GetName().Name}");
var newreport = new StringBuilder("---> Found module(s):"); var newreport = new StringBuilder("---> Found module(s):");
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, k)!; var mod = Activator.CreateInstance(t, rb)!;
newreport.Append($" {t.Name}"); newreport.Append($" {t.Name}");
newmods.Add((RegexbotModule)mod); newmods.Add((RegexbotModule)mod);
} }
k._svcLogging.DoLog(false, nameof(ModuleLoader), newreport.ToString()); rb._svcLogging.DoLog(false, nameof(ModuleLoader), newreport.ToString());
return newmods; return newmods;
} }
} }

View file

@ -1,4 +1,5 @@
using RegexBot.Data; using RegexBot.Common;
using RegexBot.Data;
namespace RegexBot.Modules.ModCommands.Commands; namespace RegexBot.Modules.ModCommands.Commands;
// Ban and kick commands are highly similar in implementation, and thus are handled in a single class. // Ban and kick commands are highly similar in implementation, and thus are handled in a single class.
@ -132,7 +133,7 @@ abstract class BanKick : CommandConfig {
await ContinueInvoke(g, msg, reason, targetId, targetQuery, targetUser); await ContinueInvoke(g, msg, reason, targetId, targetQuery, targetUser);
} catch (Discord.Net.HttpException ex) { } catch (Discord.Net.HttpException ex) {
if (ex.HttpCode == System.Net.HttpStatusCode.Forbidden) { if (ex.HttpCode == System.Net.HttpStatusCode.Forbidden) {
await msg.Channel.SendMessageAsync(":x: " + Strings.ForbiddenGenericError); await msg.Channel.SendMessageAsync(":x: " + Messages.ForbiddenGenericError);
} else if (ex.HttpCode == System.Net.HttpStatusCode.NotFound) { } else if (ex.HttpCode == System.Net.HttpStatusCode.NotFound) {
await msg.Channel.SendMessageAsync(":x: Encountered a 404 error when processing the request."); await msg.Channel.SendMessageAsync(":x: Encountered a 404 error when processing the request.");
} }

View file

@ -1,4 +1,6 @@
namespace RegexBot.Modules.ModCommands.Commands; using RegexBot.Common;
namespace RegexBot.Modules.ModCommands.Commands;
class Unban : CommandConfig { class Unban : CommandConfig {
private readonly string _usage; private readonly string _usage;
@ -43,7 +45,7 @@ class Unban : CommandConfig {
} catch (Discord.Net.HttpException ex) { } catch (Discord.Net.HttpException ex) {
const string FailPrefix = ":x: **Could not unban:** "; const string FailPrefix = ":x: **Could not unban:** ";
if (ex.HttpCode == System.Net.HttpStatusCode.Forbidden) if (ex.HttpCode == System.Net.HttpStatusCode.Forbidden)
await msg.Channel.SendMessageAsync(FailPrefix + Strings.ForbiddenGenericError); await msg.Channel.SendMessageAsync(FailPrefix + Messages.ForbiddenGenericError);
else if (ex.HttpCode == System.Net.HttpStatusCode.NotFound) else if (ex.HttpCode == System.Net.HttpStatusCode.NotFound)
await msg.Channel.SendMessageAsync(FailPrefix + "The specified user does not exist or is not in the ban list."); await msg.Channel.SendMessageAsync(FailPrefix + "The specified user does not exist or is not in the ban list.");
else throw; else throw;

View file

@ -67,7 +67,7 @@ class ResponseExecutor {
var result = await runLine(param); var result = await runLine(param);
_reports.Add((cmd, result)); _reports.Add((cmd, result));
} catch (Discord.Net.HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.Forbidden) { } catch (Discord.Net.HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.Forbidden) {
_reports.Add((cmd, FromError(Strings.ForbiddenGenericError))); _reports.Add((cmd, FromError(Messages.ForbiddenGenericError)));
} }
} }
@ -141,7 +141,7 @@ class ResponseExecutor {
result = await _bot.KickAsync(_guild, $"Rule '{_rule.Label}'", _user.Id, result = await _bot.KickAsync(_guild, $"Rule '{_rule.Label}'", _user.Id,
parameter, _rule.NotifyUserOfRemoval); parameter, _rule.NotifyUserOfRemoval);
} }
if (result.ErrorForbidden) return FromError(Strings.ForbiddenGenericError); if (result.ErrorForbidden) return FromError(Messages.ForbiddenGenericError);
if (result.ErrorNotFound) return FromError("The target user is no longer in the server."); if (result.ErrorNotFound) return FromError("The target user is no longer in the server.");
if (_rule.NotifyChannelOfRemoval) await _msg.Channel.SendMessageAsync(result.GetResultString(_bot)); if (_rule.NotifyChannelOfRemoval) await _msg.Channel.SendMessageAsync(result.GetResultString(_bot));
return FromSuccess(result.MessageSendSuccess ? null : "Unable to send notification DM."); return FromSuccess(result.MessageSendSuccess ? null : "Unable to send notification DM.");

View file

@ -1,5 +1,6 @@
using Discord; global using Discord.WebSocket;
using Discord.WebSocket; global using Newtonsoft.Json.Linq;
using Discord;
namespace RegexBot; namespace RegexBot;
class Program { class Program {
@ -10,13 +11,13 @@ class Program {
static RegexbotClient _main = null!; static RegexbotClient _main = null!;
static async Task Main(string[] args) { static async Task Main() {
StartTime = DateTimeOffset.UtcNow; StartTime = DateTimeOffset.UtcNow;
Console.WriteLine("Bot start time: " + StartTime.ToString("u")); Console.WriteLine("Bot start time: " + StartTime.ToString("u"));
InstanceConfig cfg; InstanceConfig cfg;
try { try {
cfg = new InstanceConfig(args); // Program may exit within here. cfg = new InstanceConfig(); // Program may exit within here.
} catch (Exception ex) { } catch (Exception ex) {
Console.WriteLine(ex.Message); Console.WriteLine(ex.Message);
Environment.ExitCode = 1; Environment.ExitCode = 1;
@ -33,7 +34,6 @@ class Program {
AlwaysDownloadUsers = true AlwaysDownloadUsers = true
}); });
// Kerobot class initialization - will set up services and modules
_main = new RegexbotClient(cfg, client); _main = new RegexbotClient(cfg, client);
// Set up application close handler // Set up application close handler

View file

@ -1,26 +1,14 @@
# RegexBot # RegexBot
**This branch is still a major work in progress, and is highly incomplete. See the legacy branch for the current working version.** [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/J3J65TW2E)
RegexBot is a Discord moderation bot framework of sorts, inspired by the terrible state of Discord moderation tools a few years ago RegexBot is a Discord moderation bot framework of sorts, inspired by the terrible state of Discord moderation tools a few years ago
combined with my tendency to overengineer things until they into pseudo-libraries of their own right. combined with my tendency to overengineer things until they into pseudo-libraries of their own right.
### Features: This bot includes a number of features which assist in handling the tedious details in a busy server with the goal of minimizing
* Provides a sort of in-between interface to Discord.Net that allows modules to be written for it, its benefits being: the occurrence of hidden details, arbitrary restrictions, or annoyingly unmodifiable behavior. Its configuration allows for a very high
* Putting together disparate bot features under a common interface. level of flexibility, ensuring that the bot behaves in accordance to the exact needs of your server without compromise.
* Reducing duplicate code potentially leading to an inconsistent user experience.
* Versatile JSON-based configuration.
* High detail logging and record-keeping prevents gaps in moderation that might occur with large public bots.
This repository also contains... ### Features
# RegexBot-Modules
An optional set of features to add to RegexBot, some of them inspired by Reddit's Automoderator.
This module provides a number of features to assist in watching over the tedious details in a busy server with no hidden details,
arbitrary restrictions, or unmodifiable behavior. Its configuration allows for a very high level of flexibility, ensuring that the bot
behaves in accordance to the exact needs of your server.
### Features:
* Create rules based on regular expression patterns * Create rules based on regular expression patterns
* Follow up with custom responses ranging from sending a DM to disciplinary action * Follow up with custom responses ranging from sending a DM to disciplinary action
* Create pattern-based triggers to provide information and fun to your users * Create pattern-based triggers to provide information and fun to your users
@ -29,6 +17,14 @@ behaves in accordance to the exact needs of your server.
* Make things interesting by setting triggers that only activate at random * Make things interesting by setting triggers that only activate at random
* Individual rules and triggers can be whitelisted or blacklisted per-user, per-channel, or per-role * Individual rules and triggers can be whitelisted or blacklisted per-user, per-channel, or per-role
* Exemptions to these filters can be applied for additional flexibility * Exemptions to these filters can be applied for additional flexibility
* High detail logging and record-keeping prevents gaps in moderation that might occur with large public bots.
## Documentation ### Modules
As mentioned above, this bot also serves as a framework of sorts, allowing others to write their own modules and expand
the bot's feature set ever further. Its benefits are:
* Putting together disparate bot features under a common, consistent interface.
* Reducing duplicate code potentially leading to an inconsistent user experience.
* Versatile JSON-based configuration.
## User documentation
Coming soon? Coming soon?

View file

@ -1,2 +0,0 @@
global using Discord.WebSocket;
global using Newtonsoft.Json.Linq;

View file

@ -1,19 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>RegexBot.Modules</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Authors>NoiTheCat</Authors>
<Description>A set of standard modules for use with RegexBot.</Description>
<BaseOutputPath>$(SolutionDir)\output</BaseOutputPath>
<UseCommonOutputDirectory>true</UseCommonOutputDirectory>
<DebugType>embedded</DebugType>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\RegexBot\RegexBot.csproj" />
</ItemGroup>
</Project>

View file

@ -9,7 +9,6 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<GenerateDocumentationFile>True</GenerateDocumentationFile> <GenerateDocumentationFile>True</GenerateDocumentationFile>
<BaseOutputPath>$(SolutionDir)\output</BaseOutputPath>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -21,18 +20,17 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.9.1" /> <PackageReference Include="Discord.Net" Version="3.7.2" />
<PackageReference Include="Discord.Net" Version="3.7.0" />
<PackageReference Include="EFCore.NamingConventions" Version="6.0.0" /> <PackageReference Include="EFCore.NamingConventions" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.5"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.7">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.7" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Npgsql" Version="6.0.4" /> <PackageReference Include="Npgsql" Version="6.0.5" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.4" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.5" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
</ItemGroup> </ItemGroup>

View file

@ -1,28 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30114.105
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RegexBot", "RegexBot\RegexBot.csproj", "{F7CDACE1-C74E-451E-A9C2-ED717BF72C1C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RegexBot-Modules", "RegexBot-Modules\RegexBot-Modules.csproj", "{347A1912-4F7D-419B-A159-6671791568CC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{F7CDACE1-C74E-451E-A9C2-ED717BF72C1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F7CDACE1-C74E-451E-A9C2-ED717BF72C1C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F7CDACE1-C74E-451E-A9C2-ED717BF72C1C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F7CDACE1-C74E-451E-A9C2-ED717BF72C1C}.Release|Any CPU.Build.0 = Release|Any CPU
{347A1912-4F7D-419B-A159-6671791568CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{347A1912-4F7D-419B-A159-6671791568CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{347A1912-4F7D-419B-A159-6671791568CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{347A1912-4F7D-419B-A159-6671791568CC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View file

@ -1,12 +0,0 @@
namespace RegexBot.Common;
/// <summary>
/// The type of entity specified in an <see cref="EntityName"/>.
/// </summary>
public enum EntityType {
/// <summary>Default value. Is never referenced in regular usage.</summary>
Unspecified,
Role,
Channel,
User
}

View file

@ -1,6 +0,0 @@
/// <summary>
/// Commonly used strings used throughout the program and modules.
/// </summary>
public static class Strings {
public const string ForbiddenGenericError = "Failed to perform the action due to a permissions issue.";
}

View file

@ -1,30 +0,0 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RegexBot.Data;
[Table("cache_messages")]
public class CachedGuildMessage {
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public long MessageId { get; set; }
public long AuthorId { get; set; }
public long GuildId { get; set; }
public long ChannelId { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset? EditedAt { get; set; }
public List<string> AttachmentNames { get; set; } = null!;
public string Content { get; set; } = null!;
[ForeignKey(nameof(AuthorId))]
[InverseProperty(nameof(CachedUser.GuildMessages))]
public CachedUser Author { get; set; } = null!;
internal new CachedGuildMessage MemberwiseClone() => (CachedGuildMessage)base.MemberwiseClone();
}

View file

@ -1,16 +0,0 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace RegexBot.Data;
[Table("cache_userguild")]
public class CachedGuildUser {
public long UserId { get; set; }
public long GuildId { get; set; }
public DateTimeOffset GULastUpdateTime { get; set; }
public DateTimeOffset FirstSeenTime { get; set; }
public string? Nickname { get; set; }
[ForeignKey(nameof(UserId))]
[InverseProperty(nameof(CachedUser.Guilds))]
public CachedUser User { get; set; } = null!;
}

View file

@ -1,21 +0,0 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RegexBot.Data;
[Table("cache_user")]
public class CachedUser {
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public long UserId { get; set; }
public DateTimeOffset ULastUpdateTime { get; set; }
public string Username { get; set; } = null!;
public string Discriminator { get; set; } = null!;
public string? AvatarUrl { get; set; }
[InverseProperty(nameof(CachedGuildUser.User))]
public ICollection<CachedGuildUser> Guilds { get; set; } = null!;
[InverseProperty(nameof(CachedGuildMessage.Author))]
public ICollection<CachedGuildMessage> GuildMessages { get; set; } = null!;
}

View file

@ -1,14 +0,0 @@
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations.Schema;
namespace RegexBot.Data;
[Table("guild_log")]
[Index(nameof(GuildId))]
public class GuildLogLine {
public int Id { get; set; }
public long GuildId { get; set; }
public DateTimeOffset Timestamp { get; set; }
public string Source { get; set; } = null!;
public string Message { get; set; } = null!;
}

View file

@ -1,33 +0,0 @@
using CommandLine;
using CommandLine.Text;
namespace RegexBot;
/// <summary>
/// Command line options
/// </summary>
class Options {
[Option('c', "config", Default = null,
HelpText = "Custom path to instance configuration. Defaults to config.json in bot directory.")]
public string ConfigFile { get; set; } = null!;
/// <summary>
/// Command line arguments parsed here. Depending on inputs, the program can exit here.
/// </summary>
public static Options ParseOptions(string[] args) {
// Parser will not write out to console by itself
var parser = new Parser(config => config.HelpWriter = null);
Options? opts = null;
var result = parser.ParseArguments<Options>(args);
result.WithParsed(p => opts = p);
result.WithNotParsed(p => {
// Taking some extra steps to modify the header to make it resemble our welcome message.
var ht = HelpText.AutoBuild(result);
ht.Heading += " - https://github.com/NoiTheCat/RegexBot";
Console.WriteLine(ht.ToString());
Environment.Exit(1);
});
return opts!;
}
}

View file

@ -1,9 +0,0 @@
namespace RegexBot;
/// <summary>
/// Specifies to the Kerobot module loader that the target class should be treated as a module instance.
/// When the program scans an assembly which has been specified in its instance configuration to be loaded,
/// the program searches for classes implementing <see cref="RegexbotModule"/> that also contain this attribute.
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class RegexbotModuleAttribute : Attribute { }

View file

@ -1,8 +1,6 @@
using Discord.WebSocket; using System.Reflection;
using System.Reflection;
namespace RegexBot; namespace RegexBot;
/// <summary> /// <summary>
/// The RegexBot client instance. /// The RegexBot client instance.
/// </summary> /// </summary>

View file

@ -1,6 +1,4 @@
using Discord.WebSocket; using RegexBot.Common;
using Newtonsoft.Json.Linq;
using RegexBot.Common;
using System.Diagnostics; using System.Diagnostics;
namespace RegexBot; namespace RegexBot;

View file

@ -0,0 +1,8 @@
namespace RegexBot;
/// <summary>
/// Provides a hint to the module loader that the class it is applied to should be treated as a module instance.
/// When the program scans an assembly, it is scanned for classes which implement <see cref="RegexbotModule"/> and have this attribute.
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class RegexbotModuleAttribute : Attribute { }

View file

@ -1,4 +1,5 @@
using Discord.Net; using Discord.Net;
using RegexBot.Common;
// Instances of this class are created by CommonFunctionService and are meant to be sent to modules, // Instances of this class are created by CommonFunctionService and are meant to be sent to modules,
// therefore we put this in the root RegexBot namespace despite being specific to this service. // therefore we put this in the root RegexBot namespace despite being specific to this service.
@ -104,7 +105,7 @@ public class BanKickResult {
if (!MessageSendSuccess) msg += "\n(User was unable to receive notification message.)"; if (!MessageSendSuccess) msg += "\n(User was unable to receive notification message.)";
} else { } else {
if (ErrorNotFound) msg += ": The specified user could not be found."; if (ErrorNotFound) msg += ": The specified user could not be found.";
else if (ErrorForbidden) msg += ": " + Strings.ForbiddenGenericError; else if (ErrorForbidden) msg += ": " + Messages.ForbiddenGenericError;
} }
return msg; return msg;

View file

@ -1,15 +1,10 @@
using Discord.Net; using Discord.Net;
using Discord.WebSocket;
namespace RegexBot.Services.CommonFunctions; namespace RegexBot.Services.CommonFunctions;
/// <summary> /// <summary>
/// Implements certain common actions that modules may want to perform. Using this service to perform those /// Implements certain common actions that modules may want to perform. Using this service to perform those
/// functions may help enforce a sense of consistency across modules when performing common actions, and may /// functions may help enforce a sense of consistency across modules when performing common actions, and may
/// inform services which provide any additional features the ability to respond to those actions ahead of time. /// inform services which provide any additional features the ability to respond to those actions ahead of time.
///
/// This is currently an experimental section. If it turns out to not be necessary, this service will be removed and
/// modules may resume executing common actions on their own.
/// </summary> /// </summary>
internal class CommonFunctionsService : Service { internal class CommonFunctionsService : Service {
public CommonFunctionsService(RegexbotClient bot) : base(bot) { } public CommonFunctionsService(RegexbotClient bot) : base(bot) { }

View file

@ -1,5 +1,4 @@
using Discord.WebSocket; using RegexBot.Services.CommonFunctions;
using RegexBot.Services.CommonFunctions;
namespace RegexBot; namespace RegexBot;
partial class RegexbotClient { partial class RegexbotClient {
@ -29,7 +28,12 @@ partial class RegexbotClient {
/// Similar to <see cref="BanAsync(SocketGuild, string, ulong, int, string, bool)"/>, but making use of an /// Similar to <see cref="BanAsync(SocketGuild, string, ulong, int, string, bool)"/>, but making use of an
/// EntityCache lookup to determine the target. /// EntityCache lookup to determine the target.
/// </summary> /// </summary>
/// <param name="targetSearch">The EntityCache search string.</param> /// <param name="guild">The guild in which to attempt the ban.</param>
/// <param name="source">The user, module, or service which is requesting this action to be taken.</param>
/// <param name="targetSearch">The user which to perform the action to (as a query to the entity cache).</param>
/// <param name="purgeDays">Number of days of prior post history to delete on ban. Must be between 0-7.</param>
/// <param name="reason">Reason for the action. Sent to the Audit Log and user (if specified).</param>
/// <param name="sendDMToTarget">Specify whether to send a direct message to the target user informing them of the action.</param>
public async Task<BanKickResult> BanAsync(SocketGuild guild, public async Task<BanKickResult> BanAsync(SocketGuild guild,
string source, string source,
string targetSearch, string targetSearch,
@ -54,10 +58,7 @@ partial class RegexbotClient {
/// Reason for the action. Sent to the guild's audit log and, if /// Reason for the action. Sent to the guild's audit log and, if
/// <paramref name="sendDMToTarget"/> is <see langword="true"/>, the target. /// <paramref name="sendDMToTarget"/> is <see langword="true"/>, the target.
/// </param> /// </param>
/// <param name="sendDMToTarget"> /// <param name="sendDMToTarget">Specify whether to send a direct message to the target user informing them of the action.</param>
/// Specify whether to send a direct message to the target user informing them of the action
/// (that is, a ban/kick message).
/// </param>
public Task<BanKickResult> KickAsync(SocketGuild guild, public Task<BanKickResult> KickAsync(SocketGuild guild,
string source, string source,
ulong targetUser, ulong targetUser,
@ -69,7 +70,14 @@ partial class RegexbotClient {
/// Similar to <see cref="KickAsync(SocketGuild, string, ulong, string, bool)"/>, but making use of an /// Similar to <see cref="KickAsync(SocketGuild, string, ulong, string, bool)"/>, but making use of an
/// EntityCache lookup to determine the target. /// EntityCache lookup to determine the target.
/// </summary> /// </summary>
/// <param name="targetSearch">The EntityCache search string.</param> /// <param name="guild">The guild in which to attempt the kick.</param>
/// <param name="source">The user, module, or service which is requesting this action to be taken.</param>
/// <param name="targetSearch">The user which to perform the action towards (processed as a query to the entity cache).</param>
/// <param name="reason">
/// Reason for the action. Sent to the guild's audit log and, if
/// <paramref name="sendDMToTarget"/> is <see langword="true"/>, the target.
/// </param>
/// <param name="sendDMToTarget">Specify whether to send a direct message to the target user informing them of the action.</param>
public async Task<BanKickResult> KickAsync(SocketGuild guild, public async Task<BanKickResult> KickAsync(SocketGuild guild,
string source, string source,
string targetSearch, string targetSearch,

View file

@ -1,9 +1,10 @@
// Despite specific to CommonFunctionsService, this enum is meant to be visible by modules too, namespace RegexBot;
// thus it is placed within the root namespace.
namespace RegexBot;
/// <summary> /// <summary>
/// Specifies possible outcomes for the removal of a user from a guild. /// Specifies possible outcomes for the removal of a user from a guild.
/// </summary> /// </summary>
// Despite specific to CommonFunctionsService, this enum is meant to be visible by modules too,
// thus it is placed within the root namespace.
// TODO Tends to be unused except internally. Look into removing.
public enum RemovalType { public enum RemovalType {
/// <summary> /// <summary>
/// Default value. Not used in any actual circumstances. /// Default value. Not used in any actual circumstances.

View file

@ -2,9 +2,7 @@
namespace RegexBot.Services.EntityCache; namespace RegexBot.Services.EntityCache;
/// <summary> /// <summary>
/// Provides and maintains a database-backed cache of entities. Portions of information collected by this /// Provides and maintains a database-backed cache of entities.
/// service may be used by modules, while other portions are useful only for external applications which may
/// require this information, such as an external web interface.
/// </summary> /// </summary>
class EntityCacheService : Service { class EntityCacheService : Service {
private readonly UserCachingSubservice _uc; private readonly UserCachingSubservice _uc;

View file

@ -1,5 +1,4 @@
using Discord.WebSocket; using RegexBot.Data;
using RegexBot.Data;
using RegexBot.Services.EntityCache; using RegexBot.Services.EntityCache;
namespace RegexBot; namespace RegexBot;

View file

@ -1,5 +1,4 @@
using Discord; using Discord;
using Discord.WebSocket;
using RegexBot.Data; using RegexBot.Data;
using static RegexBot.RegexbotClient; using static RegexBot.RegexbotClient;

View file

@ -1,4 +1,4 @@
using Discord.WebSocket; using Microsoft.EntityFrameworkCore;
using RegexBot.Common; using RegexBot.Common;
using RegexBot.Data; using RegexBot.Data;
@ -101,8 +101,7 @@ class UserCachingSubservice {
internal CachedGuildUser? DoGuildUserQuery(ulong guildId, string search) { internal CachedGuildUser? DoGuildUserQuery(ulong guildId, string search) {
static CachedGuildUser? innerQuery(ulong guildId, ulong? sID, (string name, string? disc)? nameSearch) { static CachedGuildUser? innerQuery(ulong guildId, ulong? sID, (string name, string? disc)? nameSearch) {
var db = new BotDatabaseContext(); var db = new BotDatabaseContext();
var query = db.GuildUserCache.Include(gu => gu.User).Where(c => c.GuildId == (long)guildId);
var query = db.GuildUserCache.Where(c => c.GuildId == (long)guildId);
if (sID.HasValue) if (sID.HasValue)
query = query.Where(c => c.UserId == (long)sID.Value); query = query.Where(c => c.UserId == (long)sID.Value);
if (nameSearch != null) { if (nameSearch != null) {

View file

@ -1,7 +1,6 @@
using RegexBot.Services.Logging; using RegexBot.Services.Logging;
namespace RegexBot; namespace RegexBot;
partial class RegexbotClient { partial class RegexbotClient {
// Access set to internal for ModuleBase and Service base class // Access set to internal for ModuleBase and Service base class
internal readonly LoggingService _svcLogging; internal readonly LoggingService _svcLogging;

View file

@ -1,7 +1,6 @@
using RegexBot.Services.ModuleState; using RegexBot.Services.ModuleState;
namespace RegexBot; namespace RegexBot;
partial class RegexbotClient { partial class RegexbotClient {
// Access set to internal for ModuleBase // Access set to internal for ModuleBase
internal readonly ModuleStateService _svcGuildState; internal readonly ModuleStateService _svcGuildState;

View file

@ -1,11 +1,8 @@
using Discord.WebSocket; using Newtonsoft.Json;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using RegexBot.Common; using RegexBot.Common;
using System.Reflection; using System.Reflection;
namespace RegexBot.Services.ModuleState; namespace RegexBot.Services.ModuleState;
/// <summary> /// <summary>
/// Implements per-module storage and retrieval of guild-specific state data, most typically but not limited to configuration data. /// Implements per-module storage and retrieval of guild-specific state data, most typically but not limited to configuration data.
/// To that end, this service handles loading and validation of per-guild configuration files. /// To that end, this service handles loading and validation of per-guild configuration files.
@ -15,8 +12,6 @@ class ModuleStateService : Service {
private readonly Dictionary<ulong, EntityList> _moderators; private readonly Dictionary<ulong, EntityList> _moderators;
private readonly Dictionary<ulong, Dictionary<Type, object?>> _stateData; private readonly Dictionary<ulong, Dictionary<Type, object?>> _stateData;
const string GuildLogSource = "Configuration loader";
public ModuleStateService(RegexbotClient bot) : base(bot) { public ModuleStateService(RegexbotClient bot) : base(bot) {
_moderators = new(); _moderators = new();
_stateData = new(); _stateData = new();

View file

@ -1,5 +1,4 @@
namespace RegexBot.Services; namespace RegexBot.Services;
/// <summary> /// <summary>
/// Base class for services. /// Base class for services.
/// </summary> /// </summary>