diff --git a/DisCatSharp.Hosting.DependencyInjection/ServiceCollectionExtensions.cs b/DisCatSharp.Hosting.DependencyInjection/ServiceCollectionExtensions.cs index c7cec0eb6..2f4d1aed7 100644 --- a/DisCatSharp.Hosting.DependencyInjection/ServiceCollectionExtensions.cs +++ b/DisCatSharp.Hosting.DependencyInjection/ServiceCollectionExtensions.cs @@ -1,49 +1,88 @@ using Microsoft.Extensions.DependencyInjection; namespace DisCatSharp.Hosting.DependencyInjection { /// /// The service collection extensions. /// public static class ServiceCollectionExtensions { /// /// Adds your bot as a BackgroundService, registered in Dependency Injection as /// /// /// is scoped to ServiceLifetime.Singleton.
/// Maps to Implementation of ///
/// /// - /// + /// Reference to for chaining purposes public static IServiceCollection AddDiscordHostedService(this IServiceCollection services) where TService : class, IDiscordHostedService { services.AddSingleton(); services.AddHostedService(provider => provider.GetRequiredService()); return services; } + /// + /// Adds your bot as a BackgroundService, registered in Dependency Injection as + /// + /// + /// is scoped to ServiceLifetime.Singleton.
+ /// Maps to Implementation of + ///
+ /// + /// + /// Reference to for chaining purposes + public static IServiceCollection AddDiscordHostedShardService(this IServiceCollection services) + where TService : class, IDiscordHostedShardService + { + services.AddSingleton(); + services.AddHostedService(provider => provider.GetRequiredService()); + return services; + } + /// /// Add as a background service which derives from /// and /// /// /// To retrieve your bot via Dependency Injection you can reference it via /// /// /// Interface which inherits from /// Your custom bot - /// + /// Reference to for chaining purposes public static IServiceCollection AddDiscordHostedService(this IServiceCollection services) where TInterface : class, IDiscordHostedService where TService : class, TInterface, IDiscordHostedService { services.AddSingleton(); services.AddHostedService(provider => provider.GetRequiredService()); return services; } + + /// + /// Add as a background service which derives from + /// and + /// + /// + /// To retrieve your bot via Dependency Injection you can reference it via + /// + /// + /// Interface which inherits from + /// Your custom bot + /// Reference to for chaining purposes + public static IServiceCollection AddDiscordHostedShardService( + this IServiceCollection services) + where TInterface : class, IDiscordHostedShardService + where TService : class, TInterface, IDiscordHostedShardService + { + services.AddSingleton(); + services.AddHostedService(provider => provider.GetRequiredService()); + return services; + } } } diff --git a/DisCatSharp.Hosting.Tests/HostTests.cs b/DisCatSharp.Hosting.Tests/HostTests.cs index 6a967793a..f513fcc18 100644 --- a/DisCatSharp.Hosting.Tests/HostTests.cs +++ b/DisCatSharp.Hosting.Tests/HostTests.cs @@ -1,254 +1,256 @@ // 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 DisCatSharp.Interactivity; using DisCatSharp.Lavalink; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Xunit; namespace DisCatSharp.Hosting.Tests { public sealed class Bot : DiscordHostedService { public Bot(IConfiguration config, ILogger logger, IServiceProvider provider, IHostApplicationLifetime lifetime) : base(config, logger, provider, lifetime) { - this.InitializeExtensions(); + this.PreConnectAsync().GetAwaiter().GetResult(); + this.PostConnectAsync().GetAwaiter().GetResult(); } } public sealed class MyCustomBot : DiscordHostedService { public MyCustomBot(IConfiguration config, ILogger logger, IServiceProvider provider, IHostApplicationLifetime lifetime) : base(config, logger, provider, lifetime, "MyCustomBot") { - this.InitializeExtensions(); + this.PreConnectAsync().GetAwaiter().GetResult(); + this.PostConnectAsync().GetAwaiter().GetResult(); } } public interface IBotTwoService : IDiscordHostedService { string GiveMeAResponse(); } public sealed class BotTwoService : DiscordHostedService, IBotTwoService { public BotTwoService(IConfiguration config, ILogger logger, IServiceProvider provider, IHostApplicationLifetime lifetime) : base(config, logger, provider, lifetime, "BotTwo") { - this.InitializeExtensions(); + this.PreConnectAsync().Wait(); } public string GiveMeAResponse() => "I'm working"; } 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)); IHostBuilder Create(string filename) => Host.CreateDefaultBuilder() .ConfigureServices(services => services.AddSingleton()) .ConfigureHostConfiguration(builder => builder.AddJsonFile(filename)); IHostBuilder Create(string filename) where TInterface : class, IDiscordHostedService where TBot : class, TInterface, IDiscordHostedService => Host.CreateDefaultBuilder() .ConfigureServices(services => services.AddSingleton()) .ConfigureHostConfiguration(builder => builder.AddJsonFile(filename)); [Fact] public void TestBotCustomInterface() { IHost? host = null; try { host = this.Create("BotTwo.json").Build(); var service = host.Services.GetRequiredService(); Assert.NotNull(service); var response = service.GiveMeAResponse(); Assert.Equal("I'm working", response); } finally { host?.Dispose(); } } [Fact] public void TestDifferentSection_InteractivityOnly() { IHost? host = null; try { host = this.Create("interactivity-different-section.json").Build(); var service = host.Services.GetRequiredService(); Assert.NotNull(service); Assert.NotNull(service.Client); Assert.Null(service.Client.GetExtension()); var intents = DiscordIntents.GuildEmojisAndStickers | DiscordIntents.GuildMembers | DiscordIntents.Guilds; Assert.Equal(intents, service.Client.Intents); var interactivity = service.Client.GetExtension(); Assert.NotNull(interactivity); } finally { host?.Dispose(); } } [Fact] public void TestDifferentSection_LavalinkOnly() { IHost? host = null; try { host = this.Create("lavalink-different-section.json").Build(); var service = host.Services.GetRequiredService(); Assert.NotNull(service); Assert.NotNull(service.Client); Assert.NotNull(service.Client.GetExtension()); Assert.Null(service.Client.GetExtension()); var intents = DiscordIntents.Guilds; Assert.Equal(intents, service.Client.Intents); } finally { host?.Dispose(); } } [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()); } 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()); } finally { host?.Dispose(); } } } } diff --git a/DisCatSharp.Hosting/DiscordHostedService.cs b/DisCatSharp.Hosting/BaseHostedService.cs similarity index 62% copy from DisCatSharp.Hosting/DiscordHostedService.cs copy to DisCatSharp.Hosting/BaseHostedService.cs index 7d1758b3b..78cdc2de3 100644 --- a/DisCatSharp.Hosting/DiscordHostedService.cs +++ b/DisCatSharp.Hosting/BaseHostedService.cs @@ -1,206 +1,182 @@ // 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.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 + /// Contains the common logic between having a or + /// as a Hosted Service /// - public abstract class DiscordHostedService : BackgroundService, IDiscordHostedService + public abstract class BaseHostedService : BackgroundService { - /// - public DiscordClient Client { get; private set; } - - protected readonly ILogger Logger; + protected readonly ILogger Logger; protected readonly IHostApplicationLifetime ApplicationLifetime; protected readonly IConfiguration Configuration; protected readonly IServiceProvider ServiceProvider; - private readonly string _botSection; + protected readonly string BotSection; - #pragma warning disable 8618 - /// - /// Initializes a new instance of the class. - /// - /// The config. - /// The logger. - /// The provider. - /// Current hosting environment. This will be used for shutting down the application on error - /// Name within the configuration which contains the config info for our bot. Default is DisCatSharp - protected DiscordHostedService(IConfiguration config, ILogger logger, IServiceProvider provider, IHostApplicationLifetime applicationLifetime, string configBotSection = DisCatSharp.Configuration.ConfigurationExtensions.DefaultRootLib) + internal BaseHostedService(IConfiguration config, + ILogger logger, + IServiceProvider serviceProvider, + IHostApplicationLifetime applicationLifetime, + string configBotSection = DisCatSharp.Configuration.ConfigurationExtensions.DefaultRootLib) { + this.Configuration = config; this.Logger = logger; this.ApplicationLifetime = applicationLifetime; - this.Configuration = config; - this._botSection = configBotSection; - this.ServiceProvider = provider; - this.Initialize(); + this.ServiceProvider = serviceProvider; + this.BotSection = configBotSection; } - #pragma warning restore 8618 + /// + /// 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(); /// - /// When the bot fails to start, this method will be invoked. (Default behavior is to shutdown) + /// Connect your client(s) to Discord /// - /// The exception/reason the bot couldn't start - protected virtual void OnInitializationError(Exception ex) - { - this.ApplicationLifetime.StopApplication(); - } + /// Task + protected abstract Task ConnectAsync(); /// - /// Dynamically loads extensions by using , and + /// Default behavior is to dynamically load extensions by using and /// /// - protected virtual void InitializeExtensions() + /// Client to add extension method(s) to + /// Task + protected virtual Task InitializeExtensions(DiscordClient client) { - var typeMap = this.Configuration.FindImplementedExtensions(this._botSection); + 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 - this.Client.AddExtension((BaseExtension)instance); + 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; } /// - /// Automatically search for and configure + /// Runs just prior to . Should initialize your + /// or here /// - private void Initialize() - { - try - { - this.Client = this.Configuration.BuildClient(this._botSection); - this.Client.ServiceProvider = this.ServiceProvider; - } - catch (Exception ex) - { - this.Logger.LogError($"Was unable to build {nameof(DiscordClient)} for {this.GetType().Name}"); - this.OnInitializationError(ex); - } - } + /// + protected abstract Task PreConnectAsync(); /// - /// Executes the bot. + /// Runs immediately after . + /// Recommended to initialize your extensions here /// - /// The stopping token. - /// A Task. + /// Task + protected abstract Task PostConnectAsync(); + protected override async Task ExecuteAsync(CancellationToken stoppingToken) { try { - if (this.Client == null) - throw new NullReferenceException("Discord Client cannot be null"); - - await this.PreConnect(); - await this.Client.ConnectAsync(); - this.InitializeExtensions(); - await this.PostConnect(); + await this.PreConnectAsync(); + await this.ConnectAsync(); + 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 happened and manually exit + * 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")); - 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); } - - /// - /// 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/DiscordHostedService.cs b/DisCatSharp.Hosting/DiscordHostedService.cs index 7d1758b3b..7201ce17b 100644 --- a/DisCatSharp.Hosting/DiscordHostedService.cs +++ b/DisCatSharp.Hosting/DiscordHostedService.cs @@ -1,206 +1,80 @@ // 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.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 abstract class DiscordHostedService : BackgroundService, IDiscordHostedService + public abstract class DiscordHostedService : BaseHostedService, IDiscordHostedService { /// - public DiscordClient Client { get; private set; } - - protected readonly ILogger Logger; - protected readonly IHostApplicationLifetime ApplicationLifetime; - protected readonly IConfiguration Configuration; - protected readonly IServiceProvider ServiceProvider; - private readonly string _botSection; + public DiscordClient Client { get; protected set; } #pragma warning disable 8618 - /// - /// Initializes a new instance of the class. - /// - /// The config. - /// The logger. - /// The provider. - /// Current hosting environment. This will be used for shutting down the application on error - /// Name within the configuration which contains the config info for our bot. Default is DisCatSharp - protected DiscordHostedService(IConfiguration config, ILogger logger, IServiceProvider provider, IHostApplicationLifetime applicationLifetime, string configBotSection = DisCatSharp.Configuration.ConfigurationExtensions.DefaultRootLib) - { - this.Logger = logger; - this.ApplicationLifetime = applicationLifetime; - this.Configuration = config; - this._botSection = configBotSection; - this.ServiceProvider = provider; - this.Initialize(); - } - - #pragma warning restore 8618 - - /// - /// When the bot fails to start, this method will be invoked. (Default behavior is to shutdown) - /// - /// The exception/reason the bot couldn't start - protected virtual void OnInitializationError(Exception ex) + /// 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 + protected DiscordHostedService(IConfiguration config, + ILogger logger, + IServiceProvider serviceProvider, + IHostApplicationLifetime applicationLifetime, + string configBotSection = DisCatSharp.Configuration.ConfigurationExtensions.DefaultRootLib) + : base(config, logger, serviceProvider, applicationLifetime, configBotSection) { - this.ApplicationLifetime.StopApplication(); - } - - /// - /// Dynamically loads extensions by using , and - /// - /// - protected virtual void InitializeExtensions() - { - 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 - this.Client.AddExtension((BaseExtension)instance); - } - catch (Exception ex) - { - this.Logger.LogError($"Unable to register '{typePair.Value.ImplementationType.Name}': \n\t{ex.Message}"); - this.OnInitializationError(ex); - } } - /// - /// Automatically search for and configure - /// - private void Initialize() + protected override Task PreConnectAsync() { try { - this.Client = this.Configuration.BuildClient(this._botSection); + this.Client = this.Configuration.BuildClient(this.BotSection); this.Client.ServiceProvider = this.ServiceProvider; } catch (Exception ex) { this.Logger.LogError($"Was unable to build {nameof(DiscordClient)} for {this.GetType().Name}"); this.OnInitializationError(ex); } - } - /// - /// Executes the bot. - /// - /// The stopping token. - /// A Task. - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - try - { - if (this.Client == null) - throw new NullReferenceException("Discord Client cannot be null"); - - await this.PreConnect(); - await this.Client.ConnectAsync(); - this.InitializeExtensions(); - await this.PostConnect(); - } - 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 happened and manually exit - */ - - this.Logger.LogError($"Was unable to start {this.GetType().Name} Bot as a hosted service."); - this.OnInitializationError(ex); - } - - // Wait indefinitely -- but use stopping token so we can properly cancel if needed - await Task.Delay(-1, stoppingToken); + return Task.CompletedTask; } - /// - /// 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; + protected override async Task ConnectAsync() => await this.Client.ConnectAsync(); + protected override Task PostConnectAsync() + { + this.InitializeExtensions(this.Client); + return Task.CompletedTask; + } } } diff --git a/DisCatSharp.Hosting/DiscordSharedHostedService.cs b/DisCatSharp.Hosting/DiscordSharedHostedService.cs new file mode 100644 index 000000000..e5fa0665b --- /dev/null +++ b/DisCatSharp.Hosting/DiscordSharedHostedService.cs @@ -0,0 +1,84 @@ +// 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.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 + protected DiscordShardedHostedService(IConfiguration config, + ILogger logger, + IServiceProvider serviceProvider, + IHostApplicationLifetime applicationLifetime, + string configBotSection = DisCatSharp.Configuration.ConfigurationExtensions.DefaultRootLib) + : base(config, logger, serviceProvider, applicationLifetime, configBotSection) + { + + } +#pragma warning restore 8618 + + protected override Task PreConnectAsync() + { + try + { + var config = this.Configuration.ExtractConfig("Discord", this.BotSection); + 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 override async Task ConnectAsync() + { + await this.ShardedClient.InitializeShardsAsync(); + await this.ShardedClient.StartAsync(); + } + + protected override Task PostConnectAsync() + { + foreach (var client in this.ShardedClient.ShardClients.Values) + { + client.ServiceProvider = this.ServiceProvider; + this.InitializeExtensions(client); + } + + return Task.CompletedTask; + } + } +} diff --git a/DisCatSharp.Hosting/IDiscordHostedService.cs b/DisCatSharp.Hosting/IDiscordHostedService.cs index 22c5ee59f..826f276bc 100644 --- a/DisCatSharp.Hosting/IDiscordHostedService.cs +++ b/DisCatSharp.Hosting/IDiscordHostedService.cs @@ -1,36 +1,44 @@ // 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. 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; } } + + /// + /// Contract required for to work in a web hosting environment + /// + public interface IDiscordHostedShardService : Microsoft.Extensions.Hosting.IHostedService + { + DiscordShardedClient ShardedClient { get; } + } }