diff --git a/DisCatSharp.ApplicationCommands/Context/ApplicationCommandsPermissionContext.cs b/DisCatSharp.ApplicationCommands/Context/ApplicationCommandsPermissionContext.cs index fff507b7e..d1bbd782f 100644 --- a/DisCatSharp.ApplicationCommands/Context/ApplicationCommandsPermissionContext.cs +++ b/DisCatSharp.ApplicationCommands/Context/ApplicationCommandsPermissionContext.cs @@ -1,82 +1,82 @@ // 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.Entities; namespace DisCatSharp.ApplicationCommands { /// /// The application commands permission context. /// public class ApplicationCommandsPermissionContext { /// /// Gets the type. /// public Type Type { get; } /// /// Gets the name. /// public string Name { get; } /// /// Gets the permissions. /// public IReadOnlyCollection Permissions => _permissions; private readonly List _permissions = new(); /// /// Initializes a new instance of the class. /// /// The type. /// The name. internal ApplicationCommandsPermissionContext(Type type, string name) { this.Type = type; this.Name = name; } /// /// Adds a user to the permission system. /// /// The Id of the user to give this permission. /// The permission for the application command. If set to true, they can use the command. If set to false, they can't use the command. public void AddUser(ulong userId, bool permission) => _permissions.Add(new DiscordApplicationCommandPermission(userId, ApplicationCommandPermissionType.User, permission)); /// /// Adds a user to the permission system. /// /// The Id of the role to give this permission. /// The permission for the application command. If set to true, they can use the command. If set to false, they can't use the command. public void AddRole(ulong roleId, bool permission) => _permissions.Add(new DiscordApplicationCommandPermission(roleId, ApplicationCommandPermissionType.Role, permission)); /// /// Adds a channel to the permission system. /// - /// The Id of the channel to give this permission. + /// The Id of the channel to give this permission. /// The permission for the application command. If set to true, they can use the command. If set to false, they can't use the command. public void AddChannel(ulong channelId, bool permission) => _permissions.Add(new DiscordApplicationCommandPermission(channelId, ApplicationCommandPermissionType.Channel, permission)); } } diff --git a/DisCatSharp.Hosting.DependencyInjection/ServiceCollectionExtensions.cs b/DisCatSharp.Hosting.DependencyInjection/ServiceCollectionExtensions.cs index b6e309496..c7cec0eb6 100644 --- a/DisCatSharp.Hosting.DependencyInjection/ServiceCollectionExtensions.cs +++ b/DisCatSharp.Hosting.DependencyInjection/ServiceCollectionExtensions.cs @@ -1,46 +1,49 @@ using Microsoft.Extensions.DependencyInjection; namespace DisCatSharp.Hosting.DependencyInjection { /// /// The service collection extensions. /// public static class ServiceCollectionExtensions { /// - /// Add as a background service + /// Adds your bot as a BackgroundService, registered in Dependency Injection as /// /// /// is scoped to ServiceLifetime.Singleton.
/// Maps to Implementation of ///
/// /// /// public static IServiceCollection AddDiscordHostedService(this IServiceCollection services) where TService : class, IDiscordHostedService { - services.AddSingleton(); - services.AddHostedService(provider => provider.GetRequiredService()); + 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 /// 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; } } } diff --git a/DisCatSharp.Hosting.Tests/HostTests.cs b/DisCatSharp.Hosting.Tests/HostTests.cs index 804419ac8..163a23893 100644 --- a/DisCatSharp.Hosting.Tests/HostTests.cs +++ b/DisCatSharp.Hosting.Tests/HostTests.cs @@ -1,251 +1,251 @@ // 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 class Bot : DiscordHostedService { - public Bot(IConfiguration config, ILogger logger, IServiceProvider provider) : base(config, logger, provider) + public Bot(IConfiguration config, ILogger logger, IServiceProvider provider, IHostApplicationLifetime lifetime) : base(config, logger, provider, lifetime) { } } public class MyCustomBot : DiscordHostedService { - public MyCustomBot(IConfiguration config, ILogger logger, IServiceProvider provider) : base(config, logger, provider, "MyCustomBot") + public MyCustomBot(IConfiguration config, ILogger logger, IServiceProvider provider, IHostApplicationLifetime lifetime) : base(config, logger, provider, lifetime, "MyCustomBot") { } } public interface IBotTwoService : IDiscordHostedService { string GiveMeAResponse(); } public class BotTwoService : DiscordHostedService, IBotTwoService { - public BotTwoService(IConfiguration config, ILogger logger, IServiceProvider provider) : base(config, logger, provider, "BotTwo") + public BotTwoService(IConfiguration config, ILogger logger, IServiceProvider provider, IHostApplicationLifetime lifetime) : base(config, logger, provider, lifetime, "BotTwo") { } 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/DiscordHostedService.cs index 5cecc59c9..7f8b448ef 100644 --- a/DisCatSharp.Hosting/DiscordHostedService.cs +++ b/DisCatSharp.Hosting/DiscordHostedService.cs @@ -1,159 +1,194 @@ // 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 DiscordClient Client { get; private set; } protected readonly ILogger Logger; + protected readonly IHostApplicationLifetime ApplicationLifetime; #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, string configBotSection = Configuration.ConfigurationExtensions.DefaultRootLib) + protected DiscordHostedService(IConfiguration config, ILogger logger, IServiceProvider provider, IHostApplicationLifetime applicationLifetime, string configBotSection = Configuration.ConfigurationExtensions.DefaultRootLib) { this.Logger = logger; + this.ApplicationLifetime = applicationLifetime; this.Initialize(config, provider, configBotSection); } #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) + { + this.ApplicationLifetime.StopApplication(); + } + /// /// Automatically search for and configure /// /// /// /// Name within the configuration which contains the config info for our bot private void Initialize(IConfiguration config, IServiceProvider provider, string configBotSection) { var typeMap = config.FindImplementedExtensions(configBotSection); this.Logger.LogDebug($"Found the following config types: {string.Join("\n\t", typeMap.Keys)}"); - this.Client = config.BuildClient(configBotSection); + try + { + this.Client = config.BuildClient(configBotSection); + } + catch (Exception ex) + { + this.Logger.LogError($"Was unable to build {nameof(DiscordClient)} for {this.GetType().Name}"); + this.OnInitializationError(ex); + } 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(provider, typePair.Value.ConfigType)) : ActivatorUtilities.CreateInstance(provider, 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); } } /// /// Executes the bot. /// /// The stopping token. /// A Task. 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(); + try + { + if (this.Client == null) + throw new NullReferenceException("Discord Client cannot be null"); + + await this.PreConnect(); + await this.Client.ConnectAsync(); + 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); } /// /// 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; } }