diff --git a/DisCatSharp.Hosting.DependencyInjection/DisCatSharp.Hosting.DependencyInjection.csproj b/DisCatSharp.Hosting.DependencyInjection/DisCatSharp.Hosting.DependencyInjection.csproj new file mode 100644 index 000000000..0a951bfe3 --- /dev/null +++ b/DisCatSharp.Hosting.DependencyInjection/DisCatSharp.Hosting.DependencyInjection.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + enable + + + + + + + + + + + diff --git a/DisCatSharp.Hosting.DependencyInjection/ServiceCollectionExtensions.cs b/DisCatSharp.Hosting.DependencyInjection/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..fbb58b3be --- /dev/null +++ b/DisCatSharp.Hosting.DependencyInjection/ServiceCollectionExtensions.cs @@ -0,0 +1,42 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace DisCatSharp.Hosting.DependencyInjection +{ + public static class ServiceCollectionExtensions + { + /// + /// Add as a background service + /// + /// + /// is scoped to .
+ /// Maps to implementation of + ///
+ /// + /// + public static IServiceCollection AddDiscordHostedService(this IServiceCollection services) + { + services.AddSingleton(); + services.AddHostedService(provider => provider.GetRequiredService()); + + return services; + } + + /// + /// Add as a background service + /// + /// + /// is scoped to .
+ /// Maps to Implementation of + ///
+ /// + /// + /// + public static IServiceCollection AddDiscordHostedService(this IServiceCollection services) + where TService : class, IDiscordHostedService + { + services.AddSingleton(); + services.AddHostedService(provider => provider.GetRequiredService()); + return services; + } + } +} diff --git a/DisCatSharp.Hosting.Tests/DisCatSharp.Hosting.Tests.csproj b/DisCatSharp.Hosting.Tests/DisCatSharp.Hosting.Tests.csproj new file mode 100644 index 000000000..1a4e43783 --- /dev/null +++ b/DisCatSharp.Hosting.Tests/DisCatSharp.Hosting.Tests.csproj @@ -0,0 +1,31 @@ + + + + net5.0 + enable + + false + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + diff --git a/DisCatSharp.Hosting.Tests/ExtensionTests.cs b/DisCatSharp.Hosting.Tests/ExtensionTests.cs new file mode 100644 index 000000000..0cf6cb951 --- /dev/null +++ b/DisCatSharp.Hosting.Tests/ExtensionTests.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Configuration; +using Xunit; +using DisCatSharp.Interactivity; +using DisCatSharp.Lavalink; + +namespace DisCatSharp.Hosting.Tests +{ + public class UnitTest1 + { + #region Reference to external assemblies - required to ensure they're loaded + private InteractivityConfiguration interactivityConfig = null; + private LavalinkConfiguration lavalinkConfig = null; + private DiscordConfiguration discordConfig = null; + #endregion + + 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 IConfiguration DiscordInteractivityConfiguration() => new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary(this.DefaultDiscord()) + { + {"DisCatSharp:Interactivity", ""} // this should be enough to automatically add the extension + }) + .Build(); + + public IConfiguration DiscordOnlyConfiguration() => new ConfigurationBuilder() + .AddInMemoryCollection(this.DefaultDiscord()) + .Build(); + + public IConfiguration DiscordInteractivityAndLavaLinkConfiguration() => new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary(this.DefaultDiscord()) + { + { "DisCatSharp:Interactivity", "" }, { "DisCatSharp:LavaLink", "" } + }) + .Build(); + + [Fact] + public void HasSection_Tests() + { + var source = this.DiscordInteractivityConfiguration(); + + bool hasDisCatSharp = source.HasSection("DisCatSharp"); + Assert.True(hasDisCatSharp); + + bool hasDiscord = source.HasSection("DisCatSharp", "Discord"); + Assert.True(hasDiscord); + } + + [Fact] + public void DiscoverExtensions_Interactivity() + { + var source = this.DiscordInteractivityConfiguration(); + var discovered = source.FindImplementedExtensions(); + + // Remember that DiscordConfiguration does not have an implementation type which is assignable to BaseExtension + Assert.Single(discovered); + var item = discovered.First(); + + Assert.Equal(typeof(InteractivityConfiguration), item.Value.ConfigType); + Assert.Equal(typeof(InteractivityExtension), item.Value.ImplementationType); + Assert.Equal("InteractivityExtension", item.Key); + } + + [Fact] + public void DiscoverExtensions_InteractivityAndLavaLink() + { + var source = this.DiscordInteractivityAndLavaLinkConfiguration(); + var discovered = source.FindImplementedExtensions(); + + Assert.Equal(2, discovered.Count); + var first = discovered.First(); + var last = discovered.Last(); + + Assert.Equal(typeof(InteractivityConfiguration), first.Value.ConfigType); + Assert.Equal(typeof(InteractivityExtension), first.Value.ImplementationType); + Assert.True("InteractivityExtension".Equals(first.Key, StringComparison.OrdinalIgnoreCase)); + + Assert.Equal(typeof(LavalinkConfiguration), last.Value.ConfigType); + Assert.Equal(typeof(LavalinkExtension), last.Value.ImplementationType); + Assert.True("LavalinkExtension".Equals(last.Key, StringComparison.OrdinalIgnoreCase)); + } + } +} + + diff --git a/DisCatSharp.Hosting.Tests/HostTests.cs b/DisCatSharp.Hosting.Tests/HostTests.cs new file mode 100644 index 000000000..5dc5a3804 --- /dev/null +++ b/DisCatSharp.Hosting.Tests/HostTests.cs @@ -0,0 +1,145 @@ +// 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:Interactivity", "" } // this should be enough to automatically add the extension + }; + + public Dictionary DiscordInteractivityAndLavalink() => new (this.DefaultDiscord()) + { + { "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/AssemblyProperties.cs b/DisCatSharp.Hosting/AssemblyProperties.cs new file mode 100644 index 000000000..71ebab38f --- /dev/null +++ b/DisCatSharp.Hosting/AssemblyProperties.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("DisCatSharp.Hosting")] +[assembly: InternalsVisibleTo("DisCatSharp.Hosting.Tests")] diff --git a/DisCatSharp.Hosting/ConfigurationExtensions.cs b/DisCatSharp.Hosting/ConfigurationExtensions.cs index 4001cb01c..b846f3e04 100644 --- a/DisCatSharp.Hosting/ConfigurationExtensions.cs +++ b/DisCatSharp.Hosting/ConfigurationExtensions.cs @@ -1,102 +1,124 @@ // 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 DisCatSharp.Configuration; using DisCatSharp.Configuration.Models; using Microsoft.Extensions.Configuration; namespace DisCatSharp.Hosting { internal struct ExtensionConfigResult { - public string Name { get; set; } 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; + } + /// /// Easily identify which configuration types have been added to the
/// This way we can dynamically load extensions without explicitly doing so ///
/// /// - /// Dictionary - public static Dictionary FindImplementedConfigs(this IConfiguration configuration, - string rootName = "DisCatSharp") + /// 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(); // Assemblies managed by DisCatSharp var assemblies = AppDomain.CurrentDomain.GetAssemblies() .Where(x => x.FullName != null && x.FullName.StartsWith(Configuration.ConfigurationExtensions.DefaultRootLib)); foreach (var assembly in assemblies) { ExtensionConfigResult result = new(); foreach (var type in assembly.ExportedTypes - .Where(x => x.Name.EndsWith("Configuration") && !x.IsAbstract && !x.IsInterface)) + .Where(x => x.Name.EndsWith(Constants.ConfigSuffix) && !x.IsAbstract && !x.IsInterface)) { - - // Strip name to be whatever prefix - result.Name = type.Name - .Replace("Configuration", ""); + string sectionName = type.Name; + string prefix = type.Name.Replace(Constants.ConfigSuffix, ""); result.ConfigType = type; - if (!string.IsNullOrEmpty(configuration[configuration.ConfigPath(rootName, type.Name)])) + // Does a section exist with the classname? (DiscordConfiguration - for instance) + if(configuration.HasSection(rootName, sectionName)) result.Section = new ConfigSection(ref configuration, type.Name, rootName); - else if (!string.IsNullOrEmpty(configuration[configuration.ConfigPath(rootName, result.Name)])) - result.Section = new ConfigSection(ref configuration, result.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 + 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(result.Name) && - x.Name.EndsWith("Extension")); + !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) { - results.Add(result.Name, result); result.ImplementationType = implementationType; + results.Add(implementationType.Name, result); } } } return results; } } } diff --git a/DisCatSharp.Hosting/IDiscordHostedService.cs b/DisCatSharp.Hosting/Constants.cs similarity index 82% copy from DisCatSharp.Hosting/IDiscordHostedService.cs copy to DisCatSharp.Hosting/Constants.cs index 36808f652..e7e9cbafc 100644 --- a/DisCatSharp.Hosting/IDiscordHostedService.cs +++ b/DisCatSharp.Hosting/Constants.cs @@ -1,32 +1,31 @@ // 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 Microsoft.Extensions.Configuration; - namespace DisCatSharp.Hosting { - - public interface IDiscordHostedService : Microsoft.Extensions.Hosting.IHostedService + internal static class Constants { - DiscordClient Client { get; } + public static string LibName => Configuration.ConfigurationExtensions.DefaultRootLib; + public static string ConfigSuffix => "Configuration"; + public static string ExtensionSuffix => "Extension"; } } diff --git a/DisCatSharp.Hosting/DiscordHostedService.cs b/DisCatSharp.Hosting/DiscordHostedService.cs index 59969481d..970e83800 100644 --- a/DisCatSharp.Hosting/DiscordHostedService.cs +++ b/DisCatSharp.Hosting/DiscordHostedService.cs @@ -1,88 +1,138 @@ // 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 { - public class DiscordHostedService : BackgroundService, IDiscordHostedService + /// + /// Simple implementation for to work as a + /// + public sealed class DiscordHostedService : BackgroundService, IDiscordHostedService { + /// public DiscordClient Client { get; private set; } private readonly ILogger _logger; - public Dictionary Extensions { get; } = new(); + /// + public Dictionary Extensions { get; } = new(); public DiscordHostedService(IConfiguration config, ILogger logger, IServiceProvider provider) { this._logger = logger; this.Initialize(config, provider); } - internal void Initialize(IConfiguration config, IServiceProvider provider) + /// + /// Automatically search for and configure + /// + /// + /// + private void Initialize(IConfiguration config, IServiceProvider provider) { - var typeMap = config.FindImplementedConfigs(); + 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 we must create an instance of the configuration type - object configInstance = ActivatorUtilities.CreateInstance(provider, typePair.Value.ConfigType); + // 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 + */ - // Secondly -- instantiate corresponding implemented types - if (typePair.Value.ImplementationType == typeof(DiscordClient)) - { - this.Client = (DiscordClient)ActivatorUtilities.CreateInstance(provider, - typePair.Value.ImplementationType, configInstance); + BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance; + var ctors = typePair.Value.ImplementationType.GetConstructors(flags); - continue; - } + object? instance; - object instance = ActivatorUtilities.CreateInstance(provider, typePair.Value.ImplementationType, configInstance); + /* + Certain extensions do not require a configuration argument + Those who do -- pass config instance in, + Those who don't -- simply instantiate - this.Extensions.Add(typePair.Value.ImplementationType.Name, instance); - this.Client.AddExtension((BaseExtension) instance); + ActivatorUtilities requires a public constructor, anything with internal breaks + */ + + if (ctors.Any(x => x.GetParameters().Length == 1)) + instance = Activator.CreateInstance(typePair.Value.ImplementationType, flags, null, + new[] { configInstance }, null); + else + instance = Activator.CreateInstance(typePair.Value.ImplementationType, true); + + // Add an easy reference to our extensions for later use + this.Extensions.Add(typePair.Value.ImplementationType.Name, (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.Client.ConnectAsync(); + // Wait indefinitely -- but use stopping token so we can properly cancel if needed await Task.Delay(-1, stoppingToken); } } } diff --git a/DisCatSharp.Hosting/IDiscordHostedService.cs b/DisCatSharp.Hosting/IDiscordHostedService.cs index 36808f652..97c114047 100644 --- a/DisCatSharp.Hosting/IDiscordHostedService.cs +++ b/DisCatSharp.Hosting/IDiscordHostedService.cs @@ -1,32 +1,46 @@ // 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 Microsoft.Extensions.Configuration; +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; } } } diff --git a/DisCatSharp.sln b/DisCatSharp.sln index b1b4c8f2e..48770a423 100644 --- a/DisCatSharp.sln +++ b/DisCatSharp.sln @@ -1,129 +1,141 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.29613.14 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp", "DisCatSharp\DisCatSharp.csproj", "{EB3D8310-DFAD-4295-97F9-82E253647583}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.VoiceNext", "DisCatSharp.VoiceNext\DisCatSharp.VoiceNext.csproj", "{FB6B9EE9-65FB-4DFB-8D51-06F0BE6C1BA5}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4255B64D-92EC-46B3-BC3B-ED2C3A8073EE}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig .gitattributes = .gitattributes .gitignore = .gitignore BUILDING.md = BUILDING.md CODE_OF_CONDUCT.md = CODE_OF_CONDUCT.md CONTRIBUTING.md = CONTRIBUTING.md LICENSE.md = LICENSE.md README.md = README.md SECURITY.md = SECURITY.md EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.CommandsNext", "DisCatSharp.CommandsNext\DisCatSharp.CommandsNext.csproj", "{C8ED55FB-E028-468D-955F-1534C20274EF}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.Interactivity", "DisCatSharp.Interactivity\DisCatSharp.Interactivity.csproj", "{DD32BEC3-0189-479F-86DC-CCF95E5634A9}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{F953F5D0-F0C9-41E6-ADBF-60A76D295899}" ProjectSection(SolutionItems) = preProject .nuget\NuGet.config = .nuget\NuGet.config EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build Items", "Build Items", "{84464D70-687B-40A8-836D-C4F737698969}" ProjectSection(SolutionItems) = preProject appveyor.yml = appveyor.yml .github\dependabot.yml = .github\dependabot.yml DisCatSharp.targets = DisCatSharp.targets docs-oneclick-rebuild.ps1 = docs-oneclick-rebuild.ps1 NuGet.targets = NuGet.targets oneclick-rebuild.ps1 = oneclick-rebuild.ps1 Package.targets = Package.targets rebuild-all.ps1 = rebuild-all.ps1 rebuild-docs.ps1 = rebuild-docs.ps1 rebuild-lib.ps1 = rebuild-lib.ps1 Version.targets = Version.targets EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{430C28D8-5F85-4D6E-AA68-211549435245}" ProjectSection(SolutionItems) = preProject .github\ISSUE_TEMPLATE\bug_report.md = .github\ISSUE_TEMPLATE\bug_report.md .github\ISSUE_TEMPLATE\feature_request.md = .github\ISSUE_TEMPLATE\feature_request.md .github\FUNDING.yml = .github\FUNDING.yml .github\pull_request_template.md = .github\pull_request_template.md EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.Lavalink", "DisCatSharp.Lavalink\DisCatSharp.Lavalink.csproj", "{A8B8FB09-C6AF-4F28-89B8-B53EE0DCE6E5}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.VoiceNext.Natives", "DisCatSharp.VoiceNext.Natives\DisCatSharp.VoiceNext.Natives.csproj", "{BEC47B41-71E4-41D1-A4F9-BB7C56A1B82B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.Common", "DisCatSharp.Common\DisCatSharp.Common.csproj", "{CD84A5C7-C7FF-48CA-B23D-FA726CF80E09}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.ApplicationCommands", "DisCatSharp.ApplicationCommands\DisCatSharp.ApplicationCommands.csproj", "{AD530FD0-523C-4DE7-9AF6-B9A3785492C2}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DisCatSharp.Configuration", "DisCatSharp.Configuration\DisCatSharp.Configuration.csproj", "{603287D3-1EF2-47F1-A611-C7F25869DE14}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DisCatSharp.Configuration.Tests", "DisCatSharp.Configuration.Tests\DisCatSharp.Configuration.Tests.csproj", "{E15E88B4-63AD-42DE-B685-D31697C62194}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DisCatSharp.Hosting", "DisCatSharp.Hosting\DisCatSharp.Hosting.csproj", "{72CCE5D5-926B-432A-876A-065FA2BC9B7B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DisCatSharp.Hosting.Tests", "DisCatSharp.Hosting.Tests\DisCatSharp.Hosting.Tests.csproj", "{D02B598A-F0C9-4A8C-B8DE-7C0BAC8C9B94}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DisCatSharp.Hosting.DependencyInjection", "DisCatSharp.Hosting.DependencyInjection\DisCatSharp.Hosting.DependencyInjection.csproj", "{2D67D1DD-E5B2-40C7-80E2-54D63730E7F0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {EB3D8310-DFAD-4295-97F9-82E253647583}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EB3D8310-DFAD-4295-97F9-82E253647583}.Debug|Any CPU.Build.0 = Debug|Any CPU {EB3D8310-DFAD-4295-97F9-82E253647583}.Release|Any CPU.ActiveCfg = Release|Any CPU {EB3D8310-DFAD-4295-97F9-82E253647583}.Release|Any CPU.Build.0 = Release|Any CPU {FB6B9EE9-65FB-4DFB-8D51-06F0BE6C1BA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FB6B9EE9-65FB-4DFB-8D51-06F0BE6C1BA5}.Debug|Any CPU.Build.0 = Debug|Any CPU {FB6B9EE9-65FB-4DFB-8D51-06F0BE6C1BA5}.Release|Any CPU.ActiveCfg = Release|Any CPU {FB6B9EE9-65FB-4DFB-8D51-06F0BE6C1BA5}.Release|Any CPU.Build.0 = Release|Any CPU {C8ED55FB-E028-468D-955F-1534C20274EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C8ED55FB-E028-468D-955F-1534C20274EF}.Debug|Any CPU.Build.0 = Debug|Any CPU {C8ED55FB-E028-468D-955F-1534C20274EF}.Release|Any CPU.ActiveCfg = Release|Any CPU {C8ED55FB-E028-468D-955F-1534C20274EF}.Release|Any CPU.Build.0 = Release|Any CPU {DD32BEC3-0189-479F-86DC-CCF95E5634A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DD32BEC3-0189-479F-86DC-CCF95E5634A9}.Debug|Any CPU.Build.0 = Debug|Any CPU {DD32BEC3-0189-479F-86DC-CCF95E5634A9}.Release|Any CPU.ActiveCfg = Release|Any CPU {DD32BEC3-0189-479F-86DC-CCF95E5634A9}.Release|Any CPU.Build.0 = Release|Any CPU {A8B8FB09-C6AF-4F28-89B8-B53EE0DCE6E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A8B8FB09-C6AF-4F28-89B8-B53EE0DCE6E5}.Debug|Any CPU.Build.0 = Debug|Any CPU {A8B8FB09-C6AF-4F28-89B8-B53EE0DCE6E5}.Release|Any CPU.ActiveCfg = Release|Any CPU {A8B8FB09-C6AF-4F28-89B8-B53EE0DCE6E5}.Release|Any CPU.Build.0 = Release|Any CPU {BEC47B41-71E4-41D1-A4F9-BB7C56A1B82B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BEC47B41-71E4-41D1-A4F9-BB7C56A1B82B}.Debug|Any CPU.Build.0 = Debug|Any CPU {BEC47B41-71E4-41D1-A4F9-BB7C56A1B82B}.Release|Any CPU.ActiveCfg = Release|Any CPU {BEC47B41-71E4-41D1-A4F9-BB7C56A1B82B}.Release|Any CPU.Build.0 = Release|Any CPU {CD84A5C7-C7FF-48CA-B23D-FA726CF80E09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CD84A5C7-C7FF-48CA-B23D-FA726CF80E09}.Debug|Any CPU.Build.0 = Debug|Any CPU {CD84A5C7-C7FF-48CA-B23D-FA726CF80E09}.Release|Any CPU.ActiveCfg = Release|Any CPU {CD84A5C7-C7FF-48CA-B23D-FA726CF80E09}.Release|Any CPU.Build.0 = Release|Any CPU {AD530FD0-523C-4DE7-9AF6-B9A3785492C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AD530FD0-523C-4DE7-9AF6-B9A3785492C2}.Debug|Any CPU.Build.0 = Debug|Any CPU {AD530FD0-523C-4DE7-9AF6-B9A3785492C2}.Release|Any CPU.ActiveCfg = Release|Any CPU {AD530FD0-523C-4DE7-9AF6-B9A3785492C2}.Release|Any CPU.Build.0 = Release|Any CPU {603287D3-1EF2-47F1-A611-C7F25869DE14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {603287D3-1EF2-47F1-A611-C7F25869DE14}.Debug|Any CPU.Build.0 = Debug|Any CPU {603287D3-1EF2-47F1-A611-C7F25869DE14}.Release|Any CPU.ActiveCfg = Release|Any CPU {603287D3-1EF2-47F1-A611-C7F25869DE14}.Release|Any CPU.Build.0 = Release|Any CPU {E15E88B4-63AD-42DE-B685-D31697C62194}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E15E88B4-63AD-42DE-B685-D31697C62194}.Debug|Any CPU.Build.0 = Debug|Any CPU {E15E88B4-63AD-42DE-B685-D31697C62194}.Release|Any CPU.ActiveCfg = Release|Any CPU {E15E88B4-63AD-42DE-B685-D31697C62194}.Release|Any CPU.Build.0 = Release|Any CPU {72CCE5D5-926B-432A-876A-065FA2BC9B7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {72CCE5D5-926B-432A-876A-065FA2BC9B7B}.Debug|Any CPU.Build.0 = Debug|Any CPU {72CCE5D5-926B-432A-876A-065FA2BC9B7B}.Release|Any CPU.ActiveCfg = Release|Any CPU {72CCE5D5-926B-432A-876A-065FA2BC9B7B}.Release|Any CPU.Build.0 = Release|Any CPU + {D02B598A-F0C9-4A8C-B8DE-7C0BAC8C9B94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D02B598A-F0C9-4A8C-B8DE-7C0BAC8C9B94}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D02B598A-F0C9-4A8C-B8DE-7C0BAC8C9B94}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D02B598A-F0C9-4A8C-B8DE-7C0BAC8C9B94}.Release|Any CPU.Build.0 = Release|Any CPU + {2D67D1DD-E5B2-40C7-80E2-54D63730E7F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D67D1DD-E5B2-40C7-80E2-54D63730E7F0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D67D1DD-E5B2-40C7-80E2-54D63730E7F0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D67D1DD-E5B2-40C7-80E2-54D63730E7F0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {430C28D8-5F85-4D6E-AA68-211549435245} = {4255B64D-92EC-46B3-BC3B-ED2C3A8073EE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {23F3A981-51B8-4285-A38C-3267F1D25FE7} EndGlobalSection EndGlobal