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.
This commit is contained in:
Noi 2022-12-04 16:30:50 -08:00
parent 8b5f0e83c4
commit 681e702406
8 changed files with 53 additions and 103 deletions

2
.vscode/launch.json vendored
View file

@ -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",

View file

@ -1,14 +1,9 @@
using CommandLine;
using Newtonsoft.Json;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
namespace RegexBot;
/// <summary>
/// Contains essential instance configuration for this bot including Discord connection settings, service configuration,
/// and command-line options.
/// </summary>
class InstanceConfig {
class Configuration {
/// <summary>
/// Token used for Discord authentication.
/// </summary>
@ -19,18 +14,20 @@ class InstanceConfig {
/// </summary>
internal IReadOnlyList<string> 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; }
/// <summary>
/// Sets up instance configuration object from file and command line parameters.
/// </summary>
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<string>(conf, nameof(SqlHost), false);
SqlDatabase = ReadConfKey<string?>(conf, nameof(SqlDatabase), false);
SqlUsername = ReadConfKey<string>(conf, nameof(SqlUsername), true);
SqlPassword = ReadConfKey<string>(conf, nameof(SqlPassword), true);
var dbconf = conf["DatabaseOptions"]?.Value<JObject>();
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<string>(dbconf, nameof(Host), false);
Database = ReadConfKey<string?>(dbconf, nameof(Database), false);
Username = ReadConfKey<string>(dbconf, nameof(Username), true);
Password = ReadConfKey<string>(dbconf, nameof(Password), true);
ServerConfigs = conf["Servers"]?.Value<JObject>();
if (ServerConfigs == null) throw new Exception("No server configurations were specified.");
}
private static T? ReadConfKey<T>(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;

View file

@ -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();
}

View file

@ -7,7 +7,7 @@ static class ModuleLoader {
/// <summary>
/// Given the instance configuration, loads all appropriate types from file specified in it.
/// </summary>
internal static ReadOnlyCollection<RegexbotModule> Load(InstanceConfig conf, RegexbotClient rb) {
internal static ReadOnlyCollection<RegexbotModule> Load(Configuration conf, RegexbotClient rb) {
var path = Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location) + Path.DirectorySeparatorChar;
var modules = new List<RegexbotModule>();

View file

@ -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;

View file

@ -11,14 +11,6 @@
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<None Remove="DefaultGuildConfig.json" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="DefaultGuildConfig.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="Discord.Net" Version="3.8.1" />

View file

@ -8,7 +8,7 @@ public partial class RegexbotClient {
/// <summary>
/// Gets application instance configuration.
/// </summary>
internal InstanceConfig Config { get; }
internal Configuration Config { get; }
/// <summary>
/// Gets the Discord client instance.
@ -20,14 +20,14 @@ public partial class RegexbotClient {
/// </summary>
internal IReadOnlyCollection<RegexbotModule> 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);

View file

@ -1,20 +1,18 @@
using Newtonsoft.Json;
using RegexBot.Common;
using System.Reflection;
using RegexBot.Common;
namespace RegexBot.Services.ModuleState;
/// <summary>
/// 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.
/// </summary>
class ModuleStateService : Service {
private readonly object _storageLock = new();
private readonly Dictionary<ulong, EntityList> _moderators;
private readonly Dictionary<ulong, Dictionary<Type, object?>> _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<T>(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<bool> 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<JObject>();
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<string> 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);
}
}
}