using Newtonsoft.Json;
using RegexBot.Common;
using System.Reflection;
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;
public ModuleStateService(RegexbotClient bot) : base(bot) {
_moderators = new();
_stateData = new();
bot.DiscordClient.GuildAvailable += RefreshGuildState;
bot.DiscordClient.JoinedGuild += RefreshGuildState;
bot.DiscordClient.LeftGuild += RemoveGuildData;
}
private async Task RefreshGuildState(SocketGuild arg) {
if (await ProcessConfiguration(arg)) Log($"'{arg.Name}': Configuration refreshed.");
else Log($"'{arg.Name}': Configuration refresh failed. Retaining existing configuration and state, if any.");
}
private Task RemoveGuildData(SocketGuild arg) {
lock (_storageLock) {
_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;
}
}
// Hooked
public EntityList DoGetModlist(ulong guildId) {
lock (_storageLock) {
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;
}
// Load moderator list
var mods = new EntityList(guildConf["Moderators"]!, true);
// Create guild state objects for all existing modules
var newStates = new Dictionary();
foreach (var module in BotClient.Modules) {
var t = module.GetType();
try {
var state = await module.CreateGuildStateAsync(guild.Id, guildConf[module.Name]!);
newStates.Add(t, state);
} catch (ModuleLoadException ex) {
Log($"{guild.Id}: Error reading configuration regarding {module.Name}: {ex.Message}");
return false;
} catch (Exception ex) when (ex is not ModuleLoadException) {
Log("Unhandled exception while initializing guild state for module:\n" +
$"Module: {module.Name} | " +
$"Guild: {guild.Id} ({BotClient.DiscordClient.GetGuild(guild.Id)?.Name ?? "unknown name"})\n" +
$"```\n{ex}\n```");
return false;
}
}
lock (_storageLock) {
_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);
}
}
}