From 681e7024060480a21182ffff679a936a751e34e8 Mon Sep 17 00:00:00 2001 From: Noi Date: Sun, 4 Dec 2022 16:30:50 -0800 Subject: [PATCH] Merve instance and guild configs into single file Previous method only really made sense when plans for this bot were far more ambitious than they are now. --- .vscode/launch.json | 2 +- InstanceConfig.cs => Configuration.cs | 41 ++++++----- Data/BotDatabaseContext.cs | 11 +-- ModuleLoader.cs | 2 +- Program.cs | 4 +- RegexBot.csproj | 8 --- RegexbotClient.cs | 6 +- Services/ModuleState/ModuleStateService.cs | 82 +++++----------------- 8 files changed, 53 insertions(+), 103 deletions(-) rename InstanceConfig.cs => Configuration.cs (66%) 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/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/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/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); - } - } }