diff --git a/.vscode/launch.json b/.vscode/launch.json
index f29b948..eb527f9 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -11,7 +11,7 @@
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/bin/Debug/net6.0/RegexBot.dll",
- "args": [],
+ "args": [ "-c", "${workspaceFolder}/bin/Debug/net6.0/config.json" ],
"cwd": "${workspaceFolder}",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
"console": "internalConsole",
diff --git a/InstanceConfig.cs b/Configuration.cs
similarity index 66%
rename from InstanceConfig.cs
rename to Configuration.cs
index 413633a..eef0dbe 100644
--- a/InstanceConfig.cs
+++ b/Configuration.cs
@@ -1,14 +1,9 @@
using CommandLine;
using Newtonsoft.Json;
using System.Diagnostics.CodeAnalysis;
-using System.Reflection;
namespace RegexBot;
-///
-/// Contains essential instance configuration for this bot including Discord connection settings, service configuration,
-/// and command-line options.
-///
-class InstanceConfig {
+class Configuration {
///
/// Token used for Discord authentication.
///
@@ -19,18 +14,20 @@ class InstanceConfig {
///
internal IReadOnlyList Assemblies { get; }
- public string? SqlHost { get; }
- public string? SqlDatabase { get; }
- public string SqlUsername { get; }
- public string SqlPassword { get; }
+ public JObject ServerConfigs { get; }
+
+ // SQL properties:
+ public string? Host { get; }
+ public string? Database { get; }
+ public string Username { get; }
+ public string Password { get; }
///
/// Sets up instance configuration object from file and command line parameters.
///
- internal InstanceConfig() {
+ internal Configuration() {
var args = CommandLineParameters.Parse(Environment.GetCommandLineArgs());
- var path = args?.ConfigFile ?? Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location)
- + Path.DirectorySeparatorChar + "." + Path.DirectorySeparatorChar + "instance.json";
+ var path = args?.ConfigFile!;
JObject conf;
try {
@@ -54,10 +51,16 @@ class InstanceConfig {
throw new Exception($"'{nameof(Assemblies)}' is not properly specified in configuration.");
}
- SqlHost = ReadConfKey(conf, nameof(SqlHost), false);
- SqlDatabase = ReadConfKey(conf, nameof(SqlDatabase), false);
- SqlUsername = ReadConfKey(conf, nameof(SqlUsername), true);
- SqlPassword = ReadConfKey(conf, nameof(SqlPassword), true);
+ var dbconf = conf["DatabaseOptions"]?.Value();
+ if (dbconf == null) throw new Exception("Database settings were not specified in configuration.");
+ // TODO more detailed database configuration? password file, other advanced authentication settings... look into this.
+ Host = ReadConfKey(dbconf, nameof(Host), false);
+ Database = ReadConfKey(dbconf, nameof(Database), false);
+ Username = ReadConfKey(dbconf, nameof(Username), true);
+ Password = ReadConfKey(dbconf, nameof(Password), true);
+
+ ServerConfigs = conf["Servers"]?.Value();
+ if (ServerConfigs == null) throw new Exception("No server configurations were specified.");
}
private static T? ReadConfKey(JObject jc, string key, [DoesNotReturnIf(true)] bool failOnEmpty) {
@@ -67,8 +70,8 @@ class InstanceConfig {
}
class CommandLineParameters {
- [Option('c', "config", Default = null)]
- public string? ConfigFile { get; set; } = null!;
+ [Option('c', "config", Default = "config.json")]
+ public string? ConfigFile { get; set; } = null;
public static CommandLineParameters? Parse(string[] args) {
CommandLineParameters? result = null;
diff --git a/ConfigurationSchema.json b/ConfigurationSchema.json
new file mode 100644
index 0000000..6c126e3
--- /dev/null
+++ b/ConfigurationSchema.json
@@ -0,0 +1,44 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "type": "object",
+ "properties": {
+ "BotToken": {
+ "type": "string",
+ "description": "The token used by the bot to connect to Discord."
+ },
+ "Assemblies": {
+ "type": "array",
+ "description": "A list of additional files to be loaded to extend the bot's functionality.",
+ "default": [ "RegexBot.dll" ]
+ },
+ "DatabaseOptions": {
+ "type": "object",
+ "description": "A set of options for the SQL database connection.",
+ "properties": {
+ "Host": {
+ "type": "string",
+ "description": "The SQL host, whether a hostname, IP address, or path to a socket."
+ },
+ "Database": {
+ "type": "string",
+ "description": "The target SQL database name to connect to, if different from the default."
+ },
+ "Username": {
+ "type": "string",
+ "description": "The username used for SQL server authentication."
+ },
+ "Password": {
+ "type": "string",
+ "description": "The password used for SQL server authentication."
+ }
+ },
+ "required": [ "Username", "Password" ]
+ },
+ "Servers": {
+ "type": "object",
+ "description": "A collection of server configurations with keys representing server IDs and values containing the respective server's configuration."
+ /* TODO unfinished */
+ }
+ },
+ "required": [ "BotToken", "DatabaseOptions", "Servers" ]
+}
\ No newline at end of file
diff --git a/Data/BotDatabaseContext.cs b/Data/BotDatabaseContext.cs
index 319594d..5e2a775 100644
--- a/Data/BotDatabaseContext.cs
+++ b/Data/BotDatabaseContext.cs
@@ -10,15 +10,16 @@ public class BotDatabaseContext : DbContext {
static BotDatabaseContext() {
// Get our own config loaded just for the SQL stuff
- var conf = new InstanceConfig();
+ // TODO this should probably be cached, or otherwise loaded in a better way
+ var conf = new Configuration();
_connectionString = new NpgsqlConnectionStringBuilder() {
#if DEBUG
IncludeErrorDetail = true,
#endif
- Host = conf.SqlHost ?? "localhost", // default to localhost
- Database = conf.SqlDatabase,
- Username = conf.SqlUsername,
- Password = conf.SqlPassword
+ Host = conf.Host ?? "localhost", // default to localhost
+ Database = conf.Database,
+ Username = conf.Username,
+ Password = conf.Password
}.ToString();
}
diff --git a/DefaultGuildConfig.json b/DefaultGuildConfig.json
deleted file mode 100644
index 07ddb10..0000000
--- a/DefaultGuildConfig.json
+++ /dev/null
@@ -1,45 +0,0 @@
-{
- "$schema": "https://raw.githubusercontent.com/NoiTheCat/RegexBot/main/ServerConfigSchema.json",
- "Name": "SERVER NAME", // Server name is optional, but useful as a reference
- "Moderators": [
- // Users and roles are accepted here.
- "MODERATOR"
- ],
-
- /*
- The following configuration is provided as a sample for commonly-used features.
- For a detailed reference which includes all possible configuration settings, see:
- (TODO put documentation link here)
- */
- "RegexModerator": [
- {
- "Label": "No cheese",
- "Regex": "cheese",
- "Response": [
- "say #_ You can't say that, that's illegal",
- "delete"
- ]
- },
- {
- "Label": "Secret club initiation",
- "Regex": "my name is .* and I dislike cheese",
- "Response": [
- "say @_ We welcome you.",
- "addrole &00000::Secret Club member"
- ]
- }
- ],
-
- "AutoResponder": [
- {
- "Label": "Infinite no u",
- "Regex": "no u",
- "Reply": "no u"
- },
- {
- "Label": "Acknowledge praise",
- "Regex": "yes u",
- "Reply": ":blush:"
- }
- ]
-}
\ No newline at end of file
diff --git a/ModuleLoader.cs b/ModuleLoader.cs
index e7a46d7..9bc9e34 100644
--- a/ModuleLoader.cs
+++ b/ModuleLoader.cs
@@ -7,7 +7,7 @@ static class ModuleLoader {
///
/// Given the instance configuration, loads all appropriate types from file specified in it.
///
- internal static ReadOnlyCollection Load(InstanceConfig conf, RegexbotClient rb) {
+ internal static ReadOnlyCollection Load(Configuration conf, RegexbotClient rb) {
var path = Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location) + Path.DirectorySeparatorChar;
var modules = new List();
diff --git a/Program.cs b/Program.cs
index 7fee3e8..f680771 100644
--- a/Program.cs
+++ b/Program.cs
@@ -15,9 +15,9 @@ class Program {
StartTime = DateTimeOffset.UtcNow;
Console.WriteLine("Bot start time: " + StartTime.ToString("u"));
- InstanceConfig cfg;
+ Configuration cfg;
try {
- cfg = new InstanceConfig(); // Program may exit within here.
+ cfg = new Configuration(); // Program may exit within here.
} catch (Exception ex) {
Console.WriteLine(ex.Message);
Environment.ExitCode = 1;
diff --git a/RegexBot.csproj b/RegexBot.csproj
index 44d59b5..2bddcf8 100644
--- a/RegexBot.csproj
+++ b/RegexBot.csproj
@@ -11,14 +11,6 @@
True
-
-
-
-
-
-
-
-
diff --git a/RegexbotClient.cs b/RegexbotClient.cs
index bb5d161..bef14d4 100644
--- a/RegexbotClient.cs
+++ b/RegexbotClient.cs
@@ -8,7 +8,7 @@ public partial class RegexbotClient {
///
/// Gets application instance configuration.
///
- internal InstanceConfig Config { get; }
+ internal Configuration Config { get; }
///
/// Gets the Discord client instance.
@@ -20,14 +20,14 @@ public partial class RegexbotClient {
///
internal IReadOnlyCollection Modules { get; }
- internal RegexbotClient(InstanceConfig conf, DiscordSocketClient client) {
+ internal RegexbotClient(Configuration conf, DiscordSocketClient client) {
Config = conf;
DiscordClient = client;
// Get all services started up
_svcLogging = new Services.Logging.LoggingService(this);
_svcSharedEvents = new Services.SharedEventService.SharedEventService(this);
- _svcGuildState = new Services.ModuleState.ModuleStateService(this);
+ _svcGuildState = new Services.ModuleState.ModuleStateService(this, conf.ServerConfigs);
_svcCommonFunctions = new Services.CommonFunctions.CommonFunctionsService(this);
_svcEntityCache = new Services.EntityCache.EntityCacheService(this);
diff --git a/SampleConfiguration.json b/SampleConfiguration.json
new file mode 100644
index 0000000..48c5564
--- /dev/null
+++ b/SampleConfiguration.json
@@ -0,0 +1,57 @@
+{
+ "$schema": "https://raw.githubusercontent.com/NoiTheCat/RegexBot/main/ConfigurationSchema.json",
+
+ "BotToken": "12345678901234567890qwertyuiop.1234567890",
+ //"Assemblies": [ "RegexBot.dll" ],
+ "DatabaseOptions": {
+ "Username": "regexbot",
+ "Password": "regexbot"
+ },
+
+ "Servers": {
+ "00000000": { // Place server ID here
+ "Name": "SERVER NAME", // Server name is unused by the bot, but is useful as a reference.
+ "Moderators": [
+ // Users and roles are accepted here.
+ "MODERATOR"
+ ],
+
+ /*
+ The following configuration is provided as a sample for commonly-used features.
+ For a detailed reference which includes all possible configuration settings, see
+ this project's documentation.
+ */
+ "RegexModerator": [
+ {
+ "Label": "No cheese",
+ "Regex": "cheese",
+ "Response": [
+ "say #_ You can't say that, that's illegal",
+ "delete"
+ ]
+ },
+ {
+ "Label": "Secret club initiation",
+ "Regex": "my name is .* and I (hate|dislike) cheese",
+ "Response": [
+ "say @_ We welcome you.",
+ "addrole &00000::Secret Club member"
+ ]
+ }
+ ],
+
+ "AutoResponder": [
+ {
+ "Label": "Infinite no u",
+ "Regex": "no u",
+ "Reply": "no u"
+ },
+ {
+ "Label": "Acknowledge praise",
+ "Regex": "yes u",
+ "Reply": ":blush:"
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/ServerConfigSchema.json b/ServerConfigSchema.json
deleted file mode 100644
index ec2f8ad..0000000
--- a/ServerConfigSchema.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "$schema": "http://json-schema.org/draft-04/schema#",
- "type": "object",
- "properties": {
- "Name": {
- "type": "string",
- "description": "The server's name. The value is unused by the application and is only for user reference."
- },
- "Moderators": {
- "type": "array",
- "description": "A list of entities which the bot should recognize as moderators.",
- "items": {
- "type": "string"
- }
- }
- /* schema still a work in progress */
- },
- "required": [
- "Moderators"
- ]
-}
\ No newline at end of file
diff --git a/Services/ModuleState/ModuleStateService.cs b/Services/ModuleState/ModuleStateService.cs
index ecaf395..93623e7 100644
--- a/Services/ModuleState/ModuleStateService.cs
+++ b/Services/ModuleState/ModuleStateService.cs
@@ -1,20 +1,18 @@
-using Newtonsoft.Json;
-using RegexBot.Common;
-using System.Reflection;
+using RegexBot.Common;
namespace RegexBot.Services.ModuleState;
///
/// 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.
///
class ModuleStateService : Service {
- private readonly object _storageLock = new();
private readonly Dictionary _moderators;
private readonly Dictionary> _stateData;
+ private readonly JObject _serverConfs;
- public ModuleStateService(RegexbotClient bot) : base(bot) {
+ public ModuleStateService(RegexbotClient bot, JObject servers) : base(bot) {
_moderators = new();
_stateData = new();
+ _serverConfs = servers;
bot.DiscordClient.GuildAvailable += RefreshGuildState;
bot.DiscordClient.JoinedGuild += RefreshGuildState;
@@ -27,45 +25,31 @@ class ModuleStateService : Service {
}
private Task RemoveGuildData(SocketGuild arg) {
- lock (_storageLock) {
- _stateData.Remove(arg.Id);
- _moderators.Remove(arg.Id);
- }
+ _stateData.Remove(arg.Id);
+ _moderators.Remove(arg.Id);
return Task.CompletedTask;
}
// Hooked
public T? DoGetStateObj(ulong guildId, Type t) {
- lock (_storageLock) {
- if (_stateData.ContainsKey(guildId) && _stateData[guildId].ContainsKey(t)) {
- // Leave handling of potential InvalidCastException to caller.
- return (T?)_stateData[guildId][t];
- }
- return default;
+ if (_stateData.ContainsKey(guildId) && _stateData[guildId].ContainsKey(t)) {
+ // Leave handling of potential InvalidCastException to caller.
+ return (T?)_stateData[guildId][t];
}
+ return default;
}
// Hooked
public EntityList DoGetModlist(ulong guildId) {
- lock (_storageLock) {
- if (_moderators.TryGetValue(guildId, out var mods)) return mods;
- else return new EntityList();
- }
+ if (_moderators.TryGetValue(guildId, out var mods)) return mods;
+ else return new EntityList();
}
private async Task ProcessConfiguration(SocketGuild guild) {
- var jstr = await LoadConfigFile(guild);
- JObject guildConf;
- try {
- var tok = JToken.Parse(jstr);
- if (tok.Type == JTokenType.Object) {
- guildConf = (JObject)tok;
- } else {
- throw new InvalidCastException("Configuration is not valid JSON.");
- }
- } catch (Exception ex) when (ex is JsonReaderException or InvalidCastException) {
- Log($"Error loading configuration for server ID {guild.Id}: {ex.Message}");
- return false;
+ var guildConf = _serverConfs[guild.Id.ToString()]?.Value();
+ if (guildConf == null) {
+ Log($"{guild.Name} ({guild.Id}) has no configuration. Add config or consider removing bot from server.");
+ return true;
}
// Load moderator list
@@ -86,38 +70,8 @@ class ModuleStateService : Service {
return false;
}
}
- lock (_storageLock) {
- _moderators[guild.Id] = mods;
- _stateData[guild.Id] = newStates;
- }
+ _moderators[guild.Id] = mods;
+ _stateData[guild.Id] = newStates;
return true;
}
-
- private async Task LoadConfigFile(SocketGuild guild) {
- // Per-guild configuration exists under `config/(guild ID).json`
- var basePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly()!.Location) +
- Path.DirectorySeparatorChar + "config" + Path.DirectorySeparatorChar;
- if (!Directory.Exists(basePath)) Directory.CreateDirectory(basePath);
- var path = basePath + guild.Id + ".json";
- if (File.Exists(path)) {
- return await File.ReadAllTextAsync(path);
- } else { // Write default configuration to new file
- string fileContents;
- using (var resStream = Assembly.GetExecutingAssembly()
- .GetManifestResourceStream($"{nameof(RegexBot)}.DefaultGuildConfig.json")!) {
- using var readin = new StreamReader(resStream, encoding: System.Text.Encoding.UTF8);
- fileContents = readin.ReadToEnd();
- }
- var userex = BotClient.DiscordClient.CurrentUser;
- fileContents = fileContents.Replace("SERVER NAME", guild.Name).Replace("MODERATOR", $"@{userex.Id}::{userex.Username}");
- using (var newFile = File.OpenWrite(path)) {
- var w = new StreamWriter(newFile);
- w.Write(fileContents);
- w.Flush();
- w.Close();
- }
- Log($"Created initial configuration file in config{Path.DirectorySeparatorChar}{guild.Id}.json");
- return await LoadConfigFile(guild);
- }
- }
}