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;
}
}