diff --git a/DisCatSharp.Configuration/ConfigurationExtensions.cs b/DisCatSharp.Configuration/ConfigurationExtensions.cs index 0f7729d29..bd7d8f40f 100644 --- a/DisCatSharp.Configuration/ConfigurationExtensions.cs +++ b/DisCatSharp.Configuration/ConfigurationExtensions.cs @@ -1,313 +1,315 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections; using System.Linq; using DisCatSharp.Configuration.Models; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace DisCatSharp.Configuration { /// /// The configuration extensions. /// internal static class ConfigurationExtensions { /// /// The factory error message. /// private const string FACTORY_ERROR_MESSAGE = "Require a function which provides a default entity to work with"; /// /// The default root lib. /// public const string DEFAULT_ROOT_LIB = "DisCatSharp"; /// /// The config suffix. /// private const string CONFIG_SUFFIX = "Configuration"; /// /// Easily piece together paths that will work within /// /// (not used - only for adding context based functionality) /// The strings to piece together /// Strings joined together via ':' public static string ConfigPath(this IConfiguration config, params string[] values) => string.Join(":", values); /// /// Skims over the configuration section and only overrides values that are explicitly defined within the config /// /// Instance of config /// Section which contains values for private static void HydrateInstance(ref object config, ConfigSection section) { var props = config.GetType().GetProperties(); foreach (var prop in props) { // Must have a set method for this to work, otherwise continue on if (prop.SetMethod == null) continue; var entry = section.GetValue(prop.Name); object? value = null; if (typeof(string) == prop.PropertyType) { // We do NOT want to override value if nothing was provided if (!string.IsNullOrEmpty(entry)) prop.SetValue(config, entry); continue; } // We need to address collections a bit differently // They can come in the form of "root:section:name" with a string representation OR // "root:section:name:0" <--- this is not detectable when checking the above path if (typeof(IEnumerable).IsAssignableFrom(prop.PropertyType)) { value = string.IsNullOrEmpty(section.GetValue(prop.Name)) ? section.Config .GetSection(section.GetPath(prop.Name)).Get(prop.PropertyType) : Newtonsoft.Json.JsonConvert.DeserializeObject(entry, prop.PropertyType); if (value == null) continue; prop.SetValue(config, value); } // From this point onward we require the 'entry' value to have something useful if (string.IsNullOrEmpty(entry)) continue; try { // Primitive types are simple to convert if (prop.PropertyType.IsPrimitive) value = Convert.ChangeType(entry, prop.PropertyType); else { // The following types require a different approach if (prop.PropertyType.IsEnum) value = Enum.Parse(prop.PropertyType, entry); else if (typeof(TimeSpan) == prop.PropertyType) value = TimeSpan.Parse(entry); else if (typeof(DateTime) == prop.PropertyType) value = DateTime.Parse(entry); else if (typeof(DateTimeOffset) == prop.PropertyType) value = DateTimeOffset.Parse(entry); } // Update value within our config instance prop.SetValue(config, value); } catch (Exception ex) { Console.Error.WriteLine( $"Unable to convert value of '{entry}' to type '{prop.PropertyType.Name}' for prop '{prop.Name}' in config '{config.GetType().Name}'\n\t\t{ex.Message}"); } } } /// /// Instantiate an entity using then walk through the specified /// and translate user-defined config values to the instantiated instance from /// /// Section containing values for targeted config /// Function which generates a default entity /// Hydrated instance of an entity which contains user-defined values (if any) /// When is null public static object ExtractConfig(this ConfigSection section, Func factory) { if (factory == null) throw new ArgumentNullException(nameof(factory), FACTORY_ERROR_MESSAGE); // Create default instance var config = factory(); HydrateInstance(ref config, section); return config; } /// /// Instantiate an entity using then walk through the specified /// in . Translate user-defined config values to the instantiated instance from /// /// Loaded App Configuration /// Name of section to load /// Function which creates a default entity to work with /// (Optional) Used when section is nested within another. Default value is /// Hydrated instance of an entity which contains user-defined values (if any) /// When is null public static object ExtractConfig(this IConfiguration config, string sectionName, Func factory, string? rootSectionName = DEFAULT_ROOT_LIB) { if (factory == null) throw new ArgumentNullException(nameof(factory), FACTORY_ERROR_MESSAGE); // create default instance var instance = factory(); HydrateInstance(ref instance, new ConfigSection(ref config, sectionName, rootSectionName)); return instance; } /// /// Instantiate a new instance of , then walk through the specified /// in . Translate user-defined config values to the instance. /// /// Loaded App Configuration /// /// Name of section to load /// (Optional) Used when section is nested with another. Default value is /// Type of instance that represents /// Hydrated instance of which contains the user-defined values (if any). public static TConfig ExtractConfig(this IConfiguration config, IServiceProvider serviceProvider, string sectionName, string? rootSectionName = DEFAULT_ROOT_LIB) where TConfig : new() { // Default values should hopefully be provided from the constructor var configInstance = ActivatorUtilities.CreateInstance(serviceProvider, typeof(TConfig)); HydrateInstance(ref configInstance, new ConfigSection(ref config, sectionName, rootSectionName)); return (TConfig)configInstance; } /// /// Instantiate a new instance of , then walk through the specified /// in . Translate user-defined config values to the instance. /// /// Loaded App Configuration /// Name of section to load /// (Optional) Used when section is nested with another. Default value is /// Type of instance that represents /// Hydrated instance of which contains the user-defined values (if any). public static TConfig ExtractConfig(this IConfiguration config, string sectionName, string? rootSectionName = DEFAULT_ROOT_LIB) where TConfig : new() { // Default values should hopefully be provided from the constructor object configInstance = new TConfig(); HydrateInstance(ref configInstance, new ConfigSection(ref config, sectionName, rootSectionName)); return (TConfig)configInstance; } /// /// Determines if contains a particular section/object (not value) /// /// /// /// { /// "Discord": { // this is a section/object /// /// }, /// "Value": "something" // this is not a section/object /// } /// /// /// /// /// True if section exists, otherwise false public static bool HasSection(this IConfiguration config, params string[] values) { if (!values.Any()) return false; if (values.Length == 1) return config.GetChildren().Any(x => x.Key == values[0]); if (config.GetChildren().All(x => x.Key != values[0])) return false; var current = config.GetSection(values[0]); for (var i = 1; i < values.Length - 1; i++) { if (current.GetChildren().All(x => x.Key != values[i])) return false; current = current.GetSection(values[i]); } return current.GetChildren().Any(x => x.Key == values[^1]); } /// /// Instantiates an instance of , then consumes any custom /// configuration from user/developer from .
/// View remarks for more info ///
/// /// This is an example of how your JSON structure should look if you wish /// to override one or more of the default values from /// /// { /// "DisCatSharp": { /// "Discord": { } /// } /// } /// ///
/// Alternatively, you can use the type name itself /// /// { /// "DisCatSharp": { /// "DiscordConfiguration": { } /// } /// } /// /// /// { /// "botSectionName": { /// "DiscordConfiguration": { } /// } /// } /// ///
/// /// /// + /// /// Instance of public static DiscordClient BuildClient(this IConfiguration config, IServiceProvider serviceProvider, - string botSectionName = DEFAULT_ROOT_LIB) + string botSectionName = DEFAULT_ROOT_LIB, ILoggerFactory? logger = null) { var section = config.HasSection(botSectionName, "Discord") ? "Discord" : config.HasSection(botSectionName, $"Discord{CONFIG_SUFFIX}") ? $"Discord:{CONFIG_SUFFIX}" : null; - return string.IsNullOrEmpty(section) - ? new DiscordClient(new DiscordConfiguration(serviceProvider)) - : new DiscordClient(config.ExtractConfig(serviceProvider, section, botSectionName)); + var cfg = string.IsNullOrEmpty(section) ? new DiscordConfiguration(serviceProvider) : config.ExtractConfig(serviceProvider, section, botSectionName); + cfg.LoggerFactory = logger; + return new DiscordClient(cfg); } } } diff --git a/DisCatSharp.Hosting/BaseHostedService.cs b/DisCatSharp.Hosting/BaseHostedService.cs index a5f0720ca..73fbe2044 100644 --- a/DisCatSharp.Hosting/BaseHostedService.cs +++ b/DisCatSharp.Hosting/BaseHostedService.cs @@ -1,206 +1,210 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; using DisCatSharp.Configuration; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace DisCatSharp.Hosting { /// /// Contains the common logic between having a or /// as a Hosted Service /// public abstract class BaseHostedService : BackgroundService { protected readonly ILogger Logger; protected readonly IHostApplicationLifetime ApplicationLifetime; protected readonly IConfiguration Configuration; protected readonly IServiceProvider ServiceProvider; protected readonly string BotSection; + protected readonly ILoggerFactory? LoggerFactory; /// /// Initializes a new instance of the class. /// /// The config. /// The logger. /// The service provider. /// The application lifetime. /// The config bot section. + /// The logger factory. internal BaseHostedService(IConfiguration config, ILogger logger, IServiceProvider serviceProvider, IHostApplicationLifetime applicationLifetime, - string configBotSection = DisCatSharp.Configuration.ConfigurationExtensions.DEFAULT_ROOT_LIB) + string configBotSection = DisCatSharp.Configuration.ConfigurationExtensions.DEFAULT_ROOT_LIB, + ILoggerFactory? loggerFactory = null) { this.Configuration = config; this.Logger = logger; this.ApplicationLifetime = applicationLifetime; this.ServiceProvider = serviceProvider; this.BotSection = configBotSection; + this.LoggerFactory = loggerFactory; } /// /// When the bot(s) fail to start, this method will be invoked. (Default behavior is to shutdown) /// /// The exception/reason for not starting protected virtual void OnInitializationError(Exception ex) => this.ApplicationLifetime.StopApplication(); /// /// Connect your client(s) to Discord /// /// Task protected abstract Task ConnectAsync(); /// /// Dynamically load extensions by using and /// /// /// Client to add extension method(s) to /// Task protected Task InitializeExtensions(DiscordClient client) { var typeMap = this.Configuration.FindImplementedExtensions(this.BotSection); this.Logger.LogDebug($"Found the following config types: {string.Join("\n\t", typeMap.Keys)}"); foreach (var typePair in typeMap) try { /* If section is null --> utilize the default constructor This means the extension was explicitly added in the 'Using' array, but user did not wish to override any value(s) in the extension's config */ var configInstance = typePair.Value.Section.HasValue ? typePair.Value.Section.Value.ExtractConfig(() => ActivatorUtilities.CreateInstance(this.ServiceProvider, typePair.Value.ConfigType)) : ActivatorUtilities.CreateInstance(this.ServiceProvider, typePair.Value.ConfigType); /* Explanation for bindings Internal Constructors --> NonPublic Public Constructors --> Public Constructors --> Instance */ var flags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance; var ctors = typePair.Value.ImplementationType.GetConstructors(flags); var instance = ctors.Any(x => x.GetParameters().Length == 1 && x.GetParameters().First().ParameterType == typePair.Value.ConfigType) ? Activator.CreateInstance(typePair.Value.ImplementationType, flags, null, new[] { configInstance }, null) : Activator.CreateInstance(typePair.Value.ImplementationType, true); /* Certain extensions do not require a configuration argument Those who do -- pass config instance in, Those who don't -- simply instantiate ActivatorUtilities requires a public constructor, anything with internal breaks */ if (instance == null) { this.Logger.LogError($"Unable to instantiate '{typePair.Value.ImplementationType.Name}'"); continue; } // Add an easy reference to our extensions for later use client.AddExtension((BaseExtension)instance); } catch (Exception ex) { this.Logger.LogError($"Unable to register '{typePair.Value.ImplementationType.Name}': \n\t{ex.Message}"); this.OnInitializationError(ex); } return Task.CompletedTask; } /// /// Configure / Initialize the or /// /// /// protected abstract Task ConfigureAsync(); /// /// Configure the extensions for your or /// /// /// protected abstract Task ConfigureExtensionsAsync(); /// /// Runs just prior to . /// /// protected virtual Task PreConnectAsync() => Task.CompletedTask; /// /// Runs immediately after . /// /// Task protected virtual Task PostConnectAsync() => Task.CompletedTask; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { try { await this.ConfigureAsync(); await this.PreConnectAsync(); await this.ConnectAsync(); await this.ConfigureExtensionsAsync(); await this.PostConnectAsync(); } catch (Exception ex) { /* * Anything before DOTNET 6 will * fail silently despite throwing an exception in this method * So to overcome this obstacle we need to log what happens and * manually exit */ this.Logger.LogError($"Was unable to start {this.GetType().Name} Bot as a Hosted Service"); // Power given to developer for handling exception this.OnInitializationError(ex); } // Wait indefinitely -- but use stopping token so we can properly cancel if needed await Task.Delay(-1, stoppingToken); } } } diff --git a/DisCatSharp.Hosting/DiscordHostedService.cs b/DisCatSharp.Hosting/DiscordHostedService.cs index c55b53a8e..932e50562 100644 --- a/DisCatSharp.Hosting/DiscordHostedService.cs +++ b/DisCatSharp.Hosting/DiscordHostedService.cs @@ -1,84 +1,86 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Threading.Tasks; using DisCatSharp.Configuration; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace DisCatSharp.Hosting { /// /// Simple implementation for to work as a /// public abstract class DiscordHostedService : BaseHostedService, IDiscordHostedService { /// public DiscordClient Client { get; protected set; } #pragma warning disable 8618 /// /// Initializes a new instance of the class. /// /// IConfiguration provided via Dependency Injection. Aggregate method to access configuration files /// An ILogger to work with, provided via Dependency Injection /// ServiceProvider reference which contains all items currently registered for Dependency Injection /// Contains the appropriate methods for disposing / stopping BackgroundServices during runtime /// The name of the JSON/Config Key which contains the configuration for this Discord Service + /// The logger factory to use for the bots logging. protected DiscordHostedService(IConfiguration config, ILogger logger, IServiceProvider serviceProvider, IHostApplicationLifetime applicationLifetime, - string configBotSection = DisCatSharp.Configuration.ConfigurationExtensions.DEFAULT_ROOT_LIB) - : base(config, logger, serviceProvider, applicationLifetime, configBotSection) + string configBotSection = DisCatSharp.Configuration.ConfigurationExtensions.DEFAULT_ROOT_LIB, + ILoggerFactory? loggerFactory = null) + : base(config, logger, serviceProvider, applicationLifetime, configBotSection, loggerFactory) { } #pragma warning restore 8618 protected override Task ConfigureAsync() { try { - this.Client = this.Configuration.BuildClient(this.ServiceProvider, this.BotSection); + this.Client = this.Configuration.BuildClient(this.ServiceProvider, this.BotSection, this.LoggerFactory); } catch (Exception ex) { this.Logger.LogError($"Was unable to build {nameof(DiscordClient)} for {this.GetType().Name}"); this.OnInitializationError(ex); } return Task.CompletedTask; } protected sealed override async Task ConnectAsync() => await this.Client.ConnectAsync(); protected override Task ConfigureExtensionsAsync() { this.InitializeExtensions(this.Client); return Task.CompletedTask; } } } diff --git a/DisCatSharp.Hosting/DiscordSharedHostedService.cs b/DisCatSharp.Hosting/DiscordSharedHostedService.cs index d4a15b019..0653f6285 100644 --- a/DisCatSharp.Hosting/DiscordSharedHostedService.cs +++ b/DisCatSharp.Hosting/DiscordSharedHostedService.cs @@ -1,89 +1,92 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Threading.Tasks; using DisCatSharp.Configuration; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace DisCatSharp.Hosting { /// /// Simple Implementation for to work as a /// public abstract class DiscordShardedHostedService : BaseHostedService, IDiscordHostedShardService { public DiscordShardedClient ShardedClient { get; protected set; } #pragma warning disable 8618 /// /// Initializes a new instance of the class. /// /// The config. /// The logger. /// The service provider. /// The application lifetime. /// The config bot section. + /// protected DiscordShardedHostedService(IConfiguration config, ILogger logger, IServiceProvider serviceProvider, IHostApplicationLifetime applicationLifetime, - string configBotSection = DisCatSharp.Configuration.ConfigurationExtensions.DEFAULT_ROOT_LIB) - : base(config, logger, serviceProvider, applicationLifetime, configBotSection) + string configBotSection = DisCatSharp.Configuration.ConfigurationExtensions.DEFAULT_ROOT_LIB, + ILoggerFactory? loggerFactory = null) + : base(config, logger, serviceProvider, applicationLifetime, configBotSection, loggerFactory) { } #pragma warning restore 8618 protected override Task ConfigureAsync() { try { var config = this.Configuration.ExtractConfig(this.ServiceProvider, "Discord", this.BotSection); + config.LoggerFactory = this.LoggerFactory; this.ShardedClient = new DiscordShardedClient(config); } catch (Exception ex) { this.Logger.LogError($"Was unable to build {nameof(DiscordShardedClient)} for {this.GetType().Name}"); this.OnInitializationError(ex); } return Task.CompletedTask; } protected sealed override async Task ConnectAsync() => await this.ShardedClient.StartAsync(); protected override Task ConfigureExtensionsAsync() { foreach (var client in this.ShardedClient.ShardClients.Values) { this.InitializeExtensions(client); } return Task.CompletedTask; } } }