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:
parent
8b5f0e83c4
commit
681e702406
8 changed files with 53 additions and 103 deletions
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
|
@ -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",
|
||||
|
|
|
@ -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;
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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>();
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue