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",
|
"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}/bin/Debug/net6.0/RegexBot.dll",
|
"program": "${workspaceFolder}/bin/Debug/net6.0/RegexBot.dll",
|
||||||
"args": [],
|
"args": [ "-c", "${workspaceFolder}/bin/Debug/net6.0/config.json" ],
|
||||||
"cwd": "${workspaceFolder}",
|
"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",
|
||||||
|
|
|
@ -1,14 +1,9 @@
|
||||||
using CommandLine;
|
using CommandLine;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace RegexBot;
|
namespace RegexBot;
|
||||||
/// <summary>
|
class Configuration {
|
||||||
/// Contains essential instance configuration for this bot including Discord connection settings, service configuration,
|
|
||||||
/// and command-line options.
|
|
||||||
/// </summary>
|
|
||||||
class InstanceConfig {
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Token used for Discord authentication.
|
/// Token used for Discord authentication.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -19,18 +14,20 @@ class InstanceConfig {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal IReadOnlyList<string> Assemblies { get; }
|
internal IReadOnlyList<string> Assemblies { get; }
|
||||||
|
|
||||||
public string? SqlHost { get; }
|
public JObject ServerConfigs { get; }
|
||||||
public string? SqlDatabase { get; }
|
|
||||||
public string SqlUsername { get; }
|
// SQL properties:
|
||||||
public string SqlPassword { get; }
|
public string? Host { get; }
|
||||||
|
public string? Database { get; }
|
||||||
|
public string Username { get; }
|
||||||
|
public string Password { get; }
|
||||||
|
|
||||||
/// <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() {
|
internal Configuration() {
|
||||||
var args = CommandLineParameters.Parse(Environment.GetCommandLineArgs());
|
var args = CommandLineParameters.Parse(Environment.GetCommandLineArgs());
|
||||||
var path = args?.ConfigFile ?? Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location)
|
var path = args?.ConfigFile!;
|
||||||
+ Path.DirectorySeparatorChar + "." + Path.DirectorySeparatorChar + "instance.json";
|
|
||||||
|
|
||||||
JObject conf;
|
JObject conf;
|
||||||
try {
|
try {
|
||||||
|
@ -54,10 +51,16 @@ class InstanceConfig {
|
||||||
throw new Exception($"'{nameof(Assemblies)}' is not properly specified in configuration.");
|
throw new Exception($"'{nameof(Assemblies)}' is not properly specified in configuration.");
|
||||||
}
|
}
|
||||||
|
|
||||||
SqlHost = ReadConfKey<string>(conf, nameof(SqlHost), false);
|
var dbconf = conf["DatabaseOptions"]?.Value<JObject>();
|
||||||
SqlDatabase = ReadConfKey<string?>(conf, nameof(SqlDatabase), false);
|
if (dbconf == null) throw new Exception("Database settings were not specified in configuration.");
|
||||||
SqlUsername = ReadConfKey<string>(conf, nameof(SqlUsername), true);
|
// TODO more detailed database configuration? password file, other advanced authentication settings... look into this.
|
||||||
SqlPassword = ReadConfKey<string>(conf, nameof(SqlPassword), true);
|
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) {
|
private static T? ReadConfKey<T>(JObject jc, string key, [DoesNotReturnIf(true)] bool failOnEmpty) {
|
||||||
|
@ -67,8 +70,8 @@ class InstanceConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
class CommandLineParameters {
|
class CommandLineParameters {
|
||||||
[Option('c', "config", Default = null)]
|
[Option('c', "config", Default = "config.json")]
|
||||||
public string? ConfigFile { get; set; } = null!;
|
public string? ConfigFile { get; set; } = null;
|
||||||
|
|
||||||
public static CommandLineParameters? Parse(string[] args) {
|
public static CommandLineParameters? Parse(string[] args) {
|
||||||
CommandLineParameters? result = null;
|
CommandLineParameters? result = null;
|
|
@ -10,15 +10,16 @@ public class BotDatabaseContext : DbContext {
|
||||||
|
|
||||||
static BotDatabaseContext() {
|
static BotDatabaseContext() {
|
||||||
// Get our own config loaded just for the SQL stuff
|
// 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() {
|
_connectionString = new NpgsqlConnectionStringBuilder() {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
IncludeErrorDetail = true,
|
IncludeErrorDetail = true,
|
||||||
#endif
|
#endif
|
||||||
Host = conf.SqlHost ?? "localhost", // default to localhost
|
Host = conf.Host ?? "localhost", // default to localhost
|
||||||
Database = conf.SqlDatabase,
|
Database = conf.Database,
|
||||||
Username = conf.SqlUsername,
|
Username = conf.Username,
|
||||||
Password = conf.SqlPassword
|
Password = conf.Password
|
||||||
}.ToString();
|
}.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ 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.
|
||||||
/// </summary>
|
/// </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 path = Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location) + Path.DirectorySeparatorChar;
|
||||||
var modules = new List<RegexbotModule>();
|
var modules = new List<RegexbotModule>();
|
||||||
|
|
||||||
|
|
|
@ -15,9 +15,9 @@ class Program {
|
||||||
StartTime = DateTimeOffset.UtcNow;
|
StartTime = DateTimeOffset.UtcNow;
|
||||||
Console.WriteLine("Bot start time: " + StartTime.ToString("u"));
|
Console.WriteLine("Bot start time: " + StartTime.ToString("u"));
|
||||||
|
|
||||||
InstanceConfig cfg;
|
Configuration cfg;
|
||||||
try {
|
try {
|
||||||
cfg = new InstanceConfig(); // Program may exit within here.
|
cfg = new Configuration(); // 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;
|
||||||
|
|
|
@ -11,14 +11,6 @@
|
||||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Remove="DefaultGuildConfig.json" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<EmbeddedResource Include="DefaultGuildConfig.json" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||||
<PackageReference Include="Discord.Net" Version="3.8.1" />
|
<PackageReference Include="Discord.Net" Version="3.8.1" />
|
||||||
|
|
|
@ -8,7 +8,7 @@ public partial class RegexbotClient {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets application instance configuration.
|
/// Gets application instance configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal InstanceConfig Config { get; }
|
internal Configuration Config { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the Discord client instance.
|
/// Gets the Discord client instance.
|
||||||
|
@ -20,14 +20,14 @@ public partial class RegexbotClient {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal IReadOnlyCollection<RegexbotModule> Modules { get; }
|
internal IReadOnlyCollection<RegexbotModule> Modules { get; }
|
||||||
|
|
||||||
internal RegexbotClient(InstanceConfig conf, DiscordSocketClient client) {
|
internal RegexbotClient(Configuration conf, DiscordSocketClient client) {
|
||||||
Config = conf;
|
Config = conf;
|
||||||
DiscordClient = client;
|
DiscordClient = client;
|
||||||
|
|
||||||
// Get all services started up
|
// Get all services started up
|
||||||
_svcLogging = new Services.Logging.LoggingService(this);
|
_svcLogging = new Services.Logging.LoggingService(this);
|
||||||
_svcSharedEvents = new Services.SharedEventService.SharedEventService(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);
|
_svcCommonFunctions = new Services.CommonFunctions.CommonFunctionsService(this);
|
||||||
_svcEntityCache = new Services.EntityCache.EntityCacheService(this);
|
_svcEntityCache = new Services.EntityCache.EntityCacheService(this);
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,18 @@
|
||||||
using Newtonsoft.Json;
|
using RegexBot.Common;
|
||||||
using RegexBot.Common;
|
|
||||||
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.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class ModuleStateService : Service {
|
class ModuleStateService : Service {
|
||||||
private readonly object _storageLock = new();
|
|
||||||
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;
|
||||||
|
private readonly JObject _serverConfs;
|
||||||
|
|
||||||
public ModuleStateService(RegexbotClient bot) : base(bot) {
|
public ModuleStateService(RegexbotClient bot, JObject servers) : base(bot) {
|
||||||
_moderators = new();
|
_moderators = new();
|
||||||
_stateData = new();
|
_stateData = new();
|
||||||
|
_serverConfs = servers;
|
||||||
|
|
||||||
bot.DiscordClient.GuildAvailable += RefreshGuildState;
|
bot.DiscordClient.GuildAvailable += RefreshGuildState;
|
||||||
bot.DiscordClient.JoinedGuild += RefreshGuildState;
|
bot.DiscordClient.JoinedGuild += RefreshGuildState;
|
||||||
|
@ -27,45 +25,31 @@ class ModuleStateService : Service {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task RemoveGuildData(SocketGuild arg) {
|
private Task RemoveGuildData(SocketGuild arg) {
|
||||||
lock (_storageLock) {
|
|
||||||
_stateData.Remove(arg.Id);
|
_stateData.Remove(arg.Id);
|
||||||
_moderators.Remove(arg.Id);
|
_moderators.Remove(arg.Id);
|
||||||
}
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hooked
|
// Hooked
|
||||||
public T? DoGetStateObj<T>(ulong guildId, Type t) {
|
public T? DoGetStateObj<T>(ulong guildId, Type t) {
|
||||||
lock (_storageLock) {
|
|
||||||
if (_stateData.ContainsKey(guildId) && _stateData[guildId].ContainsKey(t)) {
|
if (_stateData.ContainsKey(guildId) && _stateData[guildId].ContainsKey(t)) {
|
||||||
// Leave handling of potential InvalidCastException to caller.
|
// Leave handling of potential InvalidCastException to caller.
|
||||||
return (T?)_stateData[guildId][t];
|
return (T?)_stateData[guildId][t];
|
||||||
}
|
}
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Hooked
|
// Hooked
|
||||||
public EntityList DoGetModlist(ulong guildId) {
|
public EntityList DoGetModlist(ulong guildId) {
|
||||||
lock (_storageLock) {
|
|
||||||
if (_moderators.TryGetValue(guildId, out var mods)) return mods;
|
if (_moderators.TryGetValue(guildId, out var mods)) return mods;
|
||||||
else return new EntityList();
|
else return new EntityList();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> ProcessConfiguration(SocketGuild guild) {
|
private async Task<bool> ProcessConfiguration(SocketGuild guild) {
|
||||||
var jstr = await LoadConfigFile(guild);
|
var guildConf = _serverConfs[guild.Id.ToString()]?.Value<JObject>();
|
||||||
JObject guildConf;
|
if (guildConf == null) {
|
||||||
try {
|
Log($"{guild.Name} ({guild.Id}) has no configuration. Add config or consider removing bot from server.");
|
||||||
var tok = JToken.Parse(jstr);
|
return true;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load moderator list
|
// Load moderator list
|
||||||
|
@ -86,38 +70,8 @@ class ModuleStateService : Service {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lock (_storageLock) {
|
|
||||||
_moderators[guild.Id] = mods;
|
_moderators[guild.Id] = mods;
|
||||||
_stateData[guild.Id] = newStates;
|
_stateData[guild.Id] = newStates;
|
||||||
}
|
|
||||||
return true;
|
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