Merge pull request #4 from NoiTheCat/consolidate-config
Consolidate bot and guild configuration into one file
This commit is contained in:
commit
495636baec
12 changed files with 154 additions and 169 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;
|
44
ConfigurationSchema.json
Normal file
44
ConfigurationSchema.json
Normal file
|
@ -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" ]
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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:"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
57
SampleConfiguration.json
Normal file
57
SampleConfiguration.json
Normal file
|
@ -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:"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -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