diff --git a/DisCatSharp.Hosting.Tests/HostTests.cs b/DisCatSharp.Hosting.Tests/HostTests.cs index db361127c..8cc9ae159 100644 --- a/DisCatSharp.Hosting.Tests/HostTests.cs +++ b/DisCatSharp.Hosting.Tests/HostTests.cs @@ -1,145 +1,140 @@ // This file is part of the DisCatSharp project, a fork of DSharpPlus. // // Copyright (c) 2021 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.Collections.Generic; using DisCatSharp.Interactivity; using DisCatSharp.Lavalink; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Xunit; namespace DisCatSharp.Hosting.Tests { public class HostTests { private Dictionary DefaultDiscord() => new() { { "DisCatSharp:Discord:Token", "1234567890" }, { "DisCatSharp:Discord:TokenType", "Bot" }, { "DisCatSharp:Discord:MinimumLogLevel", "Information" }, { "DisCatSharp:Discord:UseRelativeRateLimit", "true" }, { "DisCatSharp:Discord:LogTimestampFormat", "yyyy-MM-dd HH:mm:ss zzz" }, { "DisCatSharp:Discord:LargeThreshold", "250" }, { "DisCatSharp:Discord:AutoReconnect", "true" }, { "DisCatSharp:Discord:ShardId", "123123" }, { "DisCatSharp:Discord:GatewayCompressionLevel", "Stream" }, { "DisCatSharp:Discord:MessageCacheSize", "1024" }, { "DisCatSharp:Discord:HttpTimeout", "00:00:20" }, { "DisCatSharp:Discord:ReconnectIndefinitely", "false" }, { "DisCatSharp:Discord:AlwaysCacheMembers", "true" }, { "DisCatSharp:Discord:DiscordIntents", "AllUnprivileged" }, { "DisCatSharp:Discord:MobileStatus", "false" }, { "DisCatSharp:Discord:UseCanary", "false" }, { "DisCatSharp:Discord:AutoRefreshChannelCache", "false" }, { "DisCatSharp:Discord:Intents", "AllUnprivileged" } }; public Dictionary DiscordInteractivity() => new (this.DefaultDiscord()) { {"DisCatSharp:Using","[\"DisCatSharp.Interactivity\"]"}, }; public Dictionary DiscordInteractivityAndLavalink() => new (this.DefaultDiscord()) { {"DisCatSharp:Using","[\"DisCatSharp.Interactivity\", \"DisCatSharp.Lavalink\"]"}, }; IHostBuilder Create(Dictionary configValues) => Host.CreateDefaultBuilder() .ConfigureServices(services => { services.AddSingleton(); }) .ConfigureHostConfiguration(builder => { builder.AddInMemoryCollection(configValues); }); [Fact] public void TestNoExtensions() { IHost? host = null; try { host = this.Create(this.DefaultDiscord()).Build(); var service = host.Services.GetRequiredService(); Assert.NotNull(service); Assert.NotNull(service.Client); } finally { host?.Dispose(); } } [Fact] public void TestInteractivityExtension() { IHost? host = null; try { host = this.Create(this.DiscordInteractivity()).Build(); var service = host.Services.GetRequiredService(); Assert.NotNull(service); Assert.NotNull(service.Client); Assert.NotNull(service.Client.GetExtension()); - - // Also verify IDiscordHostedService.Extensions - Assert.Single(service.Extensions); } finally { host?.Dispose(); } } [Fact] public void TestInteractivityLavalinkExtensions() { IHost? host = null; try { host = this.Create(this.DiscordInteractivityAndLavalink()).Build(); var service = host.Services.GetRequiredService(); Assert.NotNull(service); Assert.NotNull(service.Client); Assert.NotNull(service.Client.GetExtension()); Assert.NotNull(service.Client.GetExtension()); - - Assert.Equal(2, service.Extensions.Count); } finally { host?.Dispose(); } } } } diff --git a/DisCatSharp.Hosting/ConfigurationExtensions.cs b/DisCatSharp.Hosting/ConfigurationExtensions.cs index c3d48c0aa..16668288d 100644 --- a/DisCatSharp.Hosting/ConfigurationExtensions.cs +++ b/DisCatSharp.Hosting/ConfigurationExtensions.cs @@ -1,133 +1,196 @@ // This file is part of the DisCatSharp project, a fork of DSharpPlus. // // Copyright (c) 2021 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.Generic; using System.Linq; +using System.Reflection; using DisCatSharp.Configuration; using DisCatSharp.Configuration.Models; using Microsoft.Extensions.Configuration; namespace DisCatSharp.Hosting { internal struct ExtensionConfigResult { public ConfigSection Section { get; set; } public Type ConfigType { get; set; } public Type ImplementationType { get; set; } } internal static class ConfigurationExtensions { public static bool HasSection(this IConfiguration config, params string[] values) { // We require something to be passed in if (!values.Any()) return false; Queue queue = new(values); IConfigurationSection section = config.GetSection(queue.Dequeue()); while (section != null && queue.Any()) config.GetSection(queue.Dequeue()); return section != null; } + /// + /// Find assemblies that match the names provided via . + /// + /// + /// In some cases the assembly the user is after could be used in the application but + /// not appear within the .
+ /// The workaround for this is to check the assemblies in the , as well as referenced + /// assemblies. If the targeted assembly is a reference, we need to load it into our workspace to get more info. + ///
+ /// Names of assemblies to look for + /// Assemblies which meet the given names. No duplicates + public static Assembly[] FindAssemblies(string[]? names) + { + /* + There is a possibility that an assembly can be referenced in multiple assemblies. + To alleviate duplicates we need to shrink our queue as we find things + */ + List results = new(); + + if (names is null) + return results.ToArray(); + + List queue = new(names); + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + if (!queue.Any()) + break; + + // Is the loaded assembly one we're looking for? + if (queue.Contains(assembly.GetName().Name)) + { + results.Add(assembly); + + // Shrink queue so we don't accidentally add the same assembly > 1 times + queue.Remove(assembly.GetName().Name); + continue; + } + + // We shall check referenced assembly names... just in case + foreach(var referencedAssembly in assembly.GetReferencedAssemblies() + .Where(x => queue.Contains(x.Name))) + try + { + // Must load the assembly into our workspace so we can do stuff with it later + results.Add(Assembly.Load(referencedAssembly)); + queue.Remove(referencedAssembly.Name); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Unable to load referenced assembly: '{referencedAssembly.Name}' \n\t{ex.Message}"); + } + } + + return results.ToArray(); + } + /// /// Easily identify which configuration types have been added to the
/// This way we can dynamically load extensions without explicitly doing so ///
/// /// /// Dictionary where Key -> Name of implemented type
Value ->
public static Dictionary FindImplementedExtensions(this IConfiguration configuration, string rootName = Configuration.ConfigurationExtensions.DefaultRootLib) { if (string.IsNullOrEmpty(rootName)) throw new ArgumentNullException(nameof(rootName), "Root name must be provided"); Dictionary results = new(); + string[]? assemblyNames; // Has the user defined a using section within the root name? - if (!configuration.HasSection(rootName, "Using") || - string.IsNullOrEmpty(configuration[configuration.ConfigPath(rootName,"Using")])) + if (!configuration.HasSection(rootName, "Using")) return results; - string usingAssemblies = configuration[configuration.ConfigPath(rootName, "Using")]; - - var assemblyNames = Newtonsoft.Json.JsonConvert.DeserializeObject(usingAssemblies); - - // Assemblies managed by DisCatSharp - var assemblies = AppDomain.CurrentDomain.GetAssemblies() - .Where(x => assemblyNames?.Contains(x.GetName().Name) ?? false); - - foreach (var assembly in assemblies) + /* + There are 2 ways a user could list which assemblies are used + "Using": "[\"Assembly.Name\"]" + "Using": ["Assembly.Name"] + + JSON or as Text. + */ + if (string.IsNullOrEmpty(configuration[configuration.ConfigPath(rootName, "Using")])) + assemblyNames = configuration.GetSection(configuration.ConfigPath(rootName, "Using")).Get(); + else + assemblyNames = + Newtonsoft.Json.JsonConvert.DeserializeObject( + configuration[configuration.ConfigPath(rootName, "Using")]); + + foreach (var assembly in FindAssemblies(assemblyNames)) { ExtensionConfigResult result = new(); foreach (var type in assembly.ExportedTypes .Where(x => x.Name.EndsWith(Constants.ConfigSuffix) && !x.IsAbstract && !x.IsInterface)) { string sectionName = type.Name; string prefix = type.Name.Replace(Constants.ConfigSuffix, ""); result.ConfigType = type; // Does a section exist with the classname? (DiscordConfiguration - for instance) if(configuration.HasSection(rootName, sectionName)) result.Section = new ConfigSection(ref configuration, type.Name, rootName); // Does a section exist with the classname minus Configuration? (Discord - for Instance) else if (configuration.HasSection(rootName, prefix)) result.Section = new ConfigSection(ref configuration, prefix, rootName); // We require the implemented type to exist so we'll continue onward else continue; /* Now we need to find the type which should consume our config In the event a user has some "fluff" between prefix and suffix we'll just check for beginning and ending values. Type should not be an interface or abstract, should also be assignable to BaseExtension */ var implementationType = assembly.ExportedTypes.FirstOrDefault(x => !x.IsAbstract && !x.IsInterface && x.Name.StartsWith(prefix) && x.Name.EndsWith(Constants.ExtensionSuffix) && x.IsAssignableTo(typeof(BaseExtension))); // If the implementation type was found we can add it to our result set if (implementationType != null) { result.ImplementationType = implementationType; results.Add(implementationType.Name, result); } } } return results; } } } diff --git a/DisCatSharp.Hosting/DiscordHostedService.cs b/DisCatSharp.Hosting/DiscordHostedService.cs index 5e4126dba..868927123 100644 --- a/DisCatSharp.Hosting/DiscordHostedService.cs +++ b/DisCatSharp.Hosting/DiscordHostedService.cs @@ -1,158 +1,151 @@ // This file is part of the DisCatSharp project, a fork of DSharpPlus. // // Copyright (c) 2021 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.Generic; 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 { /// /// Simple implementation for to work as a /// public class DiscordHostedService : BackgroundService, IDiscordHostedService { - /// + /// public DiscordClient Client { get; private set; } private readonly ILogger _logger; - /// - public Dictionary Extensions { get; } = new(); - #pragma warning disable 8618 public DiscordHostedService(IConfiguration config, ILogger logger, IServiceProvider provider) { this._logger = logger; this.Initialize(config, provider); } #pragma warning restore 8618 /// /// Automatically search for and configure /// /// /// private void Initialize(IConfiguration config, IServiceProvider provider) { var typeMap = config.FindImplementedExtensions(); this._logger.LogDebug($"Found the following config types: {string.Join("\n\t", typeMap.Keys)}"); var section = config.HasSection(Constants.LibName, "Discord") ? "Discord" : config.HasSection(config.ConfigPath(Constants.LibName, $"Discord{Constants.ConfigSuffix}")) ? $"Discord{Constants.ConfigSuffix}" : null; // If not section was provided we'll still just use the default config if (string.IsNullOrEmpty(section)) this.Client = new DiscordClient(new()); else this.Client = new DiscordClient(config.ExtractConfig(section)); foreach (var typePair in typeMap) try { // First retrieve our configuration! object configInstance = typePair.Value.Section.ExtractConfig(() => ActivatorUtilities.CreateInstance(provider, typePair.Value.ConfigType)); /* Explanation for bindings Internal Constructors --> NonPublic Public Constructors --> Public Constructors --> Instance */ BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance; var ctors = typePair.Value.ImplementationType.GetConstructors(flags); object? instance; /* 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 (ctors.Any(x => x.GetParameters().Length == 1 && x.GetParameters().First().ParameterType == typePair.Value.ConfigType)) instance = Activator.CreateInstance(typePair.Value.ImplementationType, flags, null, new[] { configInstance }, null); else instance = Activator.CreateInstance(typePair.Value.ImplementationType, true); if (instance == null) { this._logger.LogError($"Unable to instantiate '{typePair.Value.ImplementationType.Name}'"); continue; } // Add an easy reference to our extensions for later use - this.Extensions.Add(typePair.Value.ImplementationType.Name, (BaseExtension) instance); + this.Client.AddExtension((BaseExtension)instance); } catch (Exception ex) { this._logger.LogError($"Unable to register '{typePair.Value.ImplementationType.Name}': \n\t{ex.Message}"); } - - // Add of our extensions to our client - foreach (var extension in this.Extensions.Values) - this.Client.AddExtension(extension); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { if (this.Client == null) throw new NullReferenceException("Discord Client cannot be null"); await this.PreConnect(); await this.Client.ConnectAsync(); await this.PostConnect(); // Wait indefinitely -- but use stopping token so we can properly cancel if needed await Task.Delay(-1, stoppingToken); } /// /// Runs just prior to the bot connecting /// protected virtual Task PreConnect() => Task.CompletedTask; /// /// Runs immediately after the bot connects /// protected virtual Task PostConnect() => Task.CompletedTask; } } diff --git a/DisCatSharp.Hosting/IDiscordHostedService.cs b/DisCatSharp.Hosting/IDiscordHostedService.cs index 97c114047..353a4a99e 100644 --- a/DisCatSharp.Hosting/IDiscordHostedService.cs +++ b/DisCatSharp.Hosting/IDiscordHostedService.cs @@ -1,46 +1,38 @@ // This file is part of the DisCatSharp project, a fork of DSharpPlus. // // Copyright (c) 2021 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.Collections.Generic; namespace DisCatSharp.Hosting { /// /// Contract required for to work in a web hosting environment /// public interface IDiscordHostedService : Microsoft.Extensions.Hosting.IHostedService { /// /// Reference to connected client /// DiscordClient Client { get; } - - /// - /// Extensions cached by their implementation type - /// - /// - /// Key: "InteractivityExtension" Value: Instance of InteractivityExtension - /// - Dictionary Extensions { get; } } }