From 4a99d5a3d4447c796a3561ae14d0ce467d86ed8c Mon Sep 17 00:00:00 2001 From: Nikita Petko Date: Wed, 9 Oct 2024 11:37:49 +0100 Subject: [PATCH] #336: Settings Text command. #!components: grid-bot Settings.cs: ~ This command allows you to: 1. Get environment information about the current application configuration. 2. List all available settings provider names. 3. List all values within a settings provider (cached, can optionally be refreshed just before fetch) -- For now it only functions in Vault-based environments. 4. Get the value of a specific setting in a provider, supported on both Vault and Env var based environments. 5. Support for setting a specific setting value in a provider, for now only supported by Vault providers as base class is only Vault and the base class does not fall back to EnvVar provider when Vault refresh is not enabled within set commands. 6. Support for refreshing all registered auto refresh providers immediately, or in turn refreshing a specific provider (will possibly refresh twice if the refresh thread hits is around the same time) --- .../PrivateModules/Commands/Settings.cs | 311 ++++++++++++++++++ .../Providers/BaseSettingsProvider.cs | 8 + 2 files changed, 319 insertions(+) create mode 100755 services/grid-bot/lib/commands/PrivateModules/Commands/Settings.cs diff --git a/services/grid-bot/lib/commands/PrivateModules/Commands/Settings.cs b/services/grid-bot/lib/commands/PrivateModules/Commands/Settings.cs new file mode 100755 index 00000000..7d7e8ba7 --- /dev/null +++ b/services/grid-bot/lib/commands/PrivateModules/Commands/Settings.cs @@ -0,0 +1,311 @@ +namespace Grid.Bot.Commands.Private; + +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Reflection; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +using Discord; +using Discord.Commands; + +using Newtonsoft.Json; + +using Vault; +using Configuration; + +using Utility; +using Extensions; + +/// +/// Represents the interaction for settings. +/// +[LockDownCommand(BotRole.Owner)] +[RequireBotRole(BotRole.Owner)] +[Group("settings"), Summary("Commands used for managing app settings.")] +public partial class Settings(IServiceProvider services) : ModuleBase +{ + private class ProviderStringConverter : BaseProvider + { + public static ProviderStringConverter Singleton = new(); + + public object ConvertToPub(string value, Type type) => ConvertTo(value, type); + public string ConvertFromPub(object value, Type type) => ConvertFrom(value, type); + + protected override bool GetRawValue(string key, out string value) + { + throw new NotImplementedException(); + } + protected override void SetRawValue(string key, T value) + { + throw new NotImplementedException(); + } + } + + private readonly IServiceProvider _services = services ?? throw new ArgumentNullException(nameof(services)); + + private const string _namespace = "Grid.Bot"; + private static readonly Assembly _settingsAssembly = Assembly.Load("Shared.Settings"); + private static readonly Assembly _configAssembly = Assembly.Load("Configuration"); + + [GeneratedRegex(@"^([a-zA-Z]+)Settings$", RegexOptions.IgnoreCase | RegexOptions.Compiled)] + private partial Regex GetProviderNameRegex(); + + /// + /// Gets a list of settings providers that can be modified. + /// + [Command("info"), Summary("Gets information about settings such as environment and versions.")] + public async Task GetInformationAsync() + { + var builder = new EmbedBuilder() + .WithTitle("Configuration Information") + .WithAuthor(Context.User) + .WithCurrentTimestamp() + .WithColor(Color.Green); + + var isUsingVault = VaultClientFactory.Singleton.GetClient() != null ? "yes" : "no"; + var settingsAssemblyVersion = _settingsAssembly.GetName().Version; + var configurationAssemblyVersion = _configAssembly.GetName().Version; + + var environment = Grid.Bot.EnvironmentProvider.EnvironmentName; + + builder.AddField("Settings Version", settingsAssemblyVersion, true) + .AddField("Configuration Version", configurationAssemblyVersion, true) + .AddField("Environment", environment, true) + .AddField("Is using Vault", isUsingVault, true); + + await this.ReplyWithReferenceAsync(embed: builder.Build()); + } + + /// + /// Gets a list of settings providers that can be modified. + /// + [Command("providers"), Summary("Lists the names of all available providers.")] + public async Task ListProvidersAsync() + { + var providerTypes = _settingsAssembly.GetTypes().Where(type => type.BaseType == typeof(BaseSettingsProvider)); + + var builder = new EmbedBuilder() + .WithTitle("Providers list") + .WithAuthor(Context.User) + .WithCurrentTimestamp() + .WithColor(Color.Green); + + var desc = "```\n"; + + foreach (var provider in providerTypes) + desc += $"{GetProviderNameRegex().Match(provider.Name).Groups[1]}\n"; + + desc += "```"; + + builder.WithDescription(desc); + + await this.ReplyWithReferenceAsync(embed: builder.Build()); + } + + + /// + /// Gets the settings for the specified provider. + /// + /// The name of the provider. + /// Should the prvoider be refreshed beforehand? + [Command("all"), Summary("Gets all settings for the specified provider."), Alias("list")] + public async Task GetAllAsync(string provider, bool refresh = true) + { + using var _ = Context.Channel.EnterTypingState(); + + var fullName = $"{provider}settings"; + + var type = _settingsAssembly.GetType($"{_namespace}.{fullName}", false, true); + if (type == null) + { + await this.ReplyWithReferenceAsync($"The settings provider with the name {provider} was not found!"); + + return; + } + + var instance = _services.GetService(type) as BaseSettingsProvider; + if (type == null) + { + await this.ReplyWithReferenceAsync($"The settings provider with the name {provider} was not found!"); + + return; + } + + if (refresh) instance.Refresh(); + + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(instance.GetRawValues(), Formatting.Indented))); + + await this.ReplyWithFileAsync(stream, $"{provider}.json", "Here are the settings for the specified provider."); + } + + /// + /// Get the value of the specified setting. + /// + /// The name of the provider + /// The name of the setting + /// Should the prvoider be refreshed beforehand? + [Command("get"), Summary("Get the value of the specified setting.")] + public async Task GetSettingAsync(string provider, string settingName, bool refresh = true) + { + using var _ = Context.Channel.EnterTypingState(); + + var fullName = $"{provider}settings"; + + var type = _settingsAssembly.GetType($"{_namespace}.{fullName}", false, true); + if (type == null) + { + await this.ReplyWithReferenceAsync($"The settings provider with the name {provider} was not found!"); + + return; + } + + var instance = _services.GetService(type) as BaseSettingsProvider; + if (type == null) + { + await this.ReplyWithReferenceAsync($"The settings provider with the name {provider} was not found!"); + + return; + } + + var property = type.GetProperty(settingName, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public); + if (property == null) + { + await this.ReplyWithReferenceAsync($"The settings provider with the name {provider} does not define the setting {settingName}!"); + + return; + } + + if (refresh) instance.Refresh(); + + var value = property.GetMethod.Invoke(instance, []); + if (value is not string) value = JsonConvert.SerializeObject(value); + if (string.IsNullOrEmpty(value as string)) value = "(empty)"; + + var embed = new EmbedBuilder() + .WithTitle($"{type.Name}.{property.Name} ({property.PropertyType.Name})") + .WithDescription($"```{value}```") + .WithAuthor(Context.User) + .WithCurrentTimestamp() + .WithColor(Color.Green) + .Build(); + + await this.ReplyWithReferenceAsync(embed: embed); + } + + /// + /// Sets the specified setting to the specified value. + /// + /// The name of the provider + /// The name of the setting + /// The new value of the setting + /// Should the prvoider be refreshed beforehand? + [Command("set"), Summary("Sets the specified setting to the specified value.")] + public async Task SetSettingAsync(string provider, string settingName, string newValue = "", bool refresh = true) + { + using var _ = Context.Channel.EnterTypingState(); + + var fullName = $"{provider}settings"; + + var type = _settingsAssembly.GetType($"{_namespace}.{fullName}", false, true); + if (type == null) + { + await this.ReplyWithReferenceAsync($"The settings provider with the name {provider} was not found!"); + + return; + } + + var instance = _services.GetService(type) as BaseSettingsProvider; + if (type == null) + { + await this.ReplyWithReferenceAsync($"The settings provider with the name {provider} was not found!"); + + return; + } + + var property = type.GetProperty(settingName, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public); + if (property == null) + { + await this.ReplyWithReferenceAsync($"The settings provider with the name {provider} does not define the setting {settingName}!"); + + return; + } + + if (refresh) instance.Refresh(); + + var value = property.GetMethod.Invoke(instance, []); + var converted = ProviderStringConverter.Singleton.ConvertToPub(newValue, property.PropertyType); + + if (value.Equals(converted)) + { + await this.ReplyWithReferenceAsync("The value is identical to the current value, not changing!"); + + return; + } + + var attribute = property.GetCustomAttribute(); + var name = attribute?.Name ?? property.Name; + + var genericSet = type.GetMethod("Set", BindingFlags.Instance | BindingFlags.Public).MakeGenericMethod([ property.PropertyType ]); + + genericSet.Invoke(instance, [name, converted]); + + if (value is not string) value = JsonConvert.SerializeObject(value); + if (string.IsNullOrEmpty(value as string)) value = "(empty)"; + + var embed = new EmbedBuilder() + .WithTitle($"{type.Name}.{property.Name} ({property.PropertyType.Name})") + .AddField("Before", $"```{value}```") + .AddField("After", $"```{newValue}```") + .WithAuthor(Context.User) + .WithCurrentTimestamp() + .WithColor(Color.Green) + .Build(); + + await this.ReplyWithReferenceAsync(embed: embed); + } + + /// + /// Refreshes the specified provider or all registered providers. + /// + /// The name of the provider. + [Command("refresh"), Summary("Refreshes the specified provider or all registered providers.")] + public async Task RefreshAsync(string provider = "") + { + using var _ = Context.Channel.EnterTypingState(); + + if (string.IsNullOrEmpty(provider)) + { + VaultProvider.RefreshAllProviders(); + + await this.ReplyWithReferenceAsync("Refreshed all registered providers!"); + + return; + } + + var fullName = $"{provider}settings"; + + var type = _settingsAssembly.GetType($"{_namespace}.{fullName}", false, true); + if (type == null) + { + await this.ReplyWithReferenceAsync($"The settings provider with the name {provider} was not found!"); + + return; + } + + var instance = _services.GetService(type) as BaseSettingsProvider; + if (type == null) + { + await this.ReplyWithReferenceAsync($"The settings provider with the name {provider} was not found!"); + + return; + } + + instance.Refresh(); + + await this.ReplyWithReferenceAsync($"Successfully refreshed the {provider} settings provider!"); + } +} diff --git a/services/grid-bot/lib/settings/Providers/BaseSettingsProvider.cs b/services/grid-bot/lib/settings/Providers/BaseSettingsProvider.cs index 7b09e472..b1446db7 100755 --- a/services/grid-bot/lib/settings/Providers/BaseSettingsProvider.cs +++ b/services/grid-bot/lib/settings/Providers/BaseSettingsProvider.cs @@ -1,5 +1,7 @@ namespace Grid.Bot; +using System.Collections.Generic; + using Configuration; /// @@ -17,4 +19,10 @@ protected BaseSettingsProvider() : base(Logging.Logger.Singleton) { } + + /// + /// Gets the raw values associated with this settings provider. + /// + /// Te raw values. + public IDictionary GetRawValues() => _CachedValues; }