From 249493c017f01beaf01106b6eb04216b178f6b91 Mon Sep 17 00:00:00 2001 From: JT Date: Sun, 19 Dec 2021 21:33:42 +0800 Subject: [PATCH] v5.0.0 (#22) * InteractionService support + .NET 6 pass * .NET 6 styling pass * Update samples * Fix github build * Update README * Minor typo * Alpha release * Improve InteractionHandler example * Update README.md * Fix wire-up typo * Support passing shardids to the sharded client * Version bump * v3.0.0 Release * Fix csproj formatting --- .github/workflows/build.yml | 2 +- Discord.Addons.Hosting.sln | 6 +- .../CommandServiceRegistrationHost.cs | 49 ---- .../Discord.Addons.Hosting.csproj | 25 +- .../DiscordClientService.cs | 112 ++++----- .../DiscordHostBuilderExtensions.cs | 232 ++++++++++-------- .../DiscordHostConfiguration.cs | 47 ++-- .../DiscordHostedService.cs | 72 ------ Discord.Addons.Hosting/InitializedService.cs | 51 ---- .../Injectables/InjectableCommandService.cs | 27 ++ .../Injectables/InjectableDiscordClient.cs | 27 +- .../InjectableInteractionService.cs | 33 +++ .../CommandServiceRegistrationHost.cs | 47 ++++ .../Services/DiscordHostedService.cs | 68 +++++ .../InteractionServiceRegistrationHost.cs | 48 ++++ Discord.Addons.Hosting/Util/LogAdapter.cs | 57 ++--- .../Util/ShardedClientExtensions.cs | 94 +++---- .../Util/SocketClientExtensions.cs | 69 +++--- Discord.Addons.Hosting/Util/TaskExtensions.cs | 44 ---- README.md | 27 +- .../Sample.ShardedClient/BotStatusService.cs | 28 +-- .../Sample.ShardedClient/CommandHandler.cs | 88 ++++--- .../InteractionHandler.cs | 154 ++++++++++++ .../Modules/InteractionModule.cs | 12 + .../Modules/PublicModule.cs | 51 ++++ Samples/Sample.ShardedClient/Program.cs | 65 +++-- .../Properties/launchSettings.json | 2 +- Samples/Sample.ShardedClient/PublicModule.cs | 56 ----- .../Sample.ShardedClient.csproj | 8 +- Samples/Sample.ShardedClient/appsettings.json | 1 + Samples/SampleBotSerilog/BotStatusService.cs | 28 +-- Samples/SampleBotSerilog/CommandHandler.cs | 92 ++++--- .../SampleBotSerilog/InteractionHandler.cs | 154 ++++++++++++ .../Modules/InteractionModule.cs | 12 + .../SampleBotSerilog/Modules/PublicModule.cs | 51 ++++ Samples/SampleBotSerilog/Program.cs | 112 ++++----- .../Properties/launchSettings.json | 2 +- Samples/SampleBotSerilog/PublicModule.cs | 56 ----- .../SampleBotSerilog/Sample.Serilog.csproj | 31 ++- Samples/SampleBotSerilog/appsettings.json | 3 +- Samples/SampleBotSimple/BotStatusService.cs | 28 +-- Samples/SampleBotSimple/CommandHandler.cs | 86 +++---- Samples/SampleBotSimple/InteractionHandler.cs | 155 ++++++++++++ Samples/SampleBotSimple/LongRunningService.cs | 40 ++- .../Modules/InteractionModule.cs | 12 + .../SampleBotSimple/Modules/PublicModule.cs | 51 ++++ Samples/SampleBotSimple/Program.cs | 66 +++-- .../Properties/launchSettings.json | 2 +- Samples/SampleBotSimple/PublicModule.cs | 97 ++++---- Samples/SampleBotSimple/Sample.Simple.csproj | 26 +- Samples/SampleBotSimple/appsettings.json | 1 + 51 files changed, 1619 insertions(+), 1088 deletions(-) delete mode 100644 Discord.Addons.Hosting/CommandServiceRegistrationHost.cs delete mode 100644 Discord.Addons.Hosting/DiscordHostedService.cs delete mode 100644 Discord.Addons.Hosting/InitializedService.cs create mode 100644 Discord.Addons.Hosting/Injectables/InjectableCommandService.cs create mode 100644 Discord.Addons.Hosting/Injectables/InjectableInteractionService.cs create mode 100644 Discord.Addons.Hosting/Services/CommandServiceRegistrationHost.cs create mode 100644 Discord.Addons.Hosting/Services/DiscordHostedService.cs create mode 100644 Discord.Addons.Hosting/Services/InteractionServiceRegistrationHost.cs delete mode 100644 Discord.Addons.Hosting/Util/TaskExtensions.cs create mode 100644 Samples/Sample.ShardedClient/InteractionHandler.cs create mode 100644 Samples/Sample.ShardedClient/Modules/InteractionModule.cs create mode 100644 Samples/Sample.ShardedClient/Modules/PublicModule.cs delete mode 100644 Samples/Sample.ShardedClient/PublicModule.cs create mode 100644 Samples/SampleBotSerilog/InteractionHandler.cs create mode 100644 Samples/SampleBotSerilog/Modules/InteractionModule.cs create mode 100644 Samples/SampleBotSerilog/Modules/PublicModule.cs delete mode 100644 Samples/SampleBotSerilog/PublicModule.cs create mode 100644 Samples/SampleBotSimple/InteractionHandler.cs create mode 100644 Samples/SampleBotSimple/Modules/InteractionModule.cs create mode 100644 Samples/SampleBotSimple/Modules/PublicModule.cs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 26fe1f5..2adc60b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: '5.0.x' + dotnet-version: '6.0.x' - name: Install dependencies run: dotnet restore - name: Build diff --git a/Discord.Addons.Hosting.sln b/Discord.Addons.Hosting.sln index 5c39d7d..bbafa9a 100644 --- a/Discord.Addons.Hosting.sln +++ b/Discord.Addons.Hosting.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28729.10 +# Visual Studio Version 17 +VisualStudioVersion = 17.1.31911.260 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Addons.Hosting", "Discord.Addons.Hosting\Discord.Addons.Hosting.csproj", "{03ED5619-5F9E-4CC6-8B8F-7D610C3169EB}" EndProject @@ -9,7 +9,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Serilog", "Samples\S EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Simple", "Samples\SampleBotSimple\Sample.Simple.csproj", "{E65C387F-9D99-4257-A458-1F6D5A4D80F7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.ShardedClient", "Samples\Sample.ShardedClient\Sample.ShardedClient.csproj", "{540A37B5-E304-49A4-B68E-08941C0D33F1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.ShardedClient", "Samples\Sample.ShardedClient\Sample.ShardedClient.csproj", "{540A37B5-E304-49A4-B68E-08941C0D33F1}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/Discord.Addons.Hosting/CommandServiceRegistrationHost.cs b/Discord.Addons.Hosting/CommandServiceRegistrationHost.cs deleted file mode 100644 index fa59dea..0000000 --- a/Discord.Addons.Hosting/CommandServiceRegistrationHost.cs +++ /dev/null @@ -1,49 +0,0 @@ -#region License -/* - Copyright 2021 Hawxy - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ -#endregion -using System.Threading; -using System.Threading.Tasks; -using Discord.Addons.Hosting.Util; -using Discord.Commands; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace Discord.Addons.Hosting -{ - internal class CommandServiceRegistrationHost : IHostedService - { - private readonly CommandService _commandService; - private readonly ILogger _logger; - private readonly LogAdapter _adapter; - - public CommandServiceRegistrationHost(CommandService commandService, ILogger logger, LogAdapter adapter) - { - _commandService = commandService; - _logger = logger; - _adapter = adapter; - } - - public Task StartAsync(CancellationToken cancellationToken) - { - _commandService.Log += _adapter.Log; - _logger.LogDebug("Registered logger for CommandService"); - return Task.CompletedTask; - } - - public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; - } -} diff --git a/Discord.Addons.Hosting/Discord.Addons.Hosting.csproj b/Discord.Addons.Hosting/Discord.Addons.Hosting.csproj index 2144ca6..edc28c8 100644 --- a/Discord.Addons.Hosting/Discord.Addons.Hosting.csproj +++ b/Discord.Addons.Hosting/Discord.Addons.Hosting.csproj @@ -1,9 +1,9 @@  - netstandard2.1;net5.0 + net6.0 Discord.Addons.Hosting - 4.0.2 + 5.0.0 Hawxy Simplifying Discord.Net hosting with .NET Generic Host (Microsoft.Extensions.Hosting) true @@ -12,27 +12,22 @@ https://github.com/Hawxy/Discord.Addons.Hosting git icon.png - Hawxy 2018-2021 + Hawxy 2018-2022 true discord,discord.net,addon,hosting,microsoft.extensions.hosting Enable + Enable - - + + + + - - - - - - - - - - + + diff --git a/Discord.Addons.Hosting/DiscordClientService.cs b/Discord.Addons.Hosting/DiscordClientService.cs index 3ef4027..9db82b7 100644 --- a/Discord.Addons.Hosting/DiscordClientService.cs +++ b/Discord.Addons.Hosting/DiscordClientService.cs @@ -1,7 +1,7 @@ #region License /* - Copyright 2021 Hawxy + Copyright 2019-2022 Hawxy Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,80 +19,76 @@ limitations under the License. #endregion using Microsoft.Extensions.Hosting; -using System.Threading; -using System.Threading.Tasks; using Discord.WebSocket; using Microsoft.Extensions.Logging; -namespace Discord.Addons.Hosting -{ +namespace Discord.Addons.Hosting; +/// +/// Base class for implementing an with startup execution requirements. This class implements +/// +public abstract class DiscordServiceBase : BackgroundService where T : BaseSocketClient +{ /// - /// Base class for implementing an with startup execution requirements. This class implements + /// The running Discord.NET Client /// - public abstract class DiscordServiceBase : BackgroundService where T : BaseSocketClient - { - /// - /// The running Discord.NET Client - /// - protected T Client { get; } - - /// - /// This service's logger - /// - protected ILogger Logger { get; } + protected T Client { get; } - /// - /// Creates a new - /// - /// The logger for this service - /// The discord client - protected DiscordServiceBase(T client, ILogger logger) - { - Client = client; - Logger = logger; - } + /// + /// This service's logger + /// + protected ILogger Logger { get; } - /// - /// This method is called when the starts. If the implementation is long-running, it should return a task that represents - /// the lifetime of the operation(s) being performed. - /// For more information, see - /// - /// Triggered when is called. - /// A that represents the long running operations. - protected abstract override Task ExecuteAsync(CancellationToken stoppingToken); + /// + /// Creates a new + /// + /// The logger for this service + /// The discord client + protected DiscordServiceBase(T client, ILogger logger) + { + Client = client; + Logger = logger; } /// - /// Base class for implementing an for a with startup execution requirements. - /// This class implements and should be registered in your service collection with `AddHostedService` + /// This method is called when the starts. If the implementation is long-running, it should return a task that represents + /// the lifetime of the operation(s) being performed. + /// For more information, see /// - public abstract class DiscordClientService : DiscordServiceBase - { - /// - /// Creates a new - /// - /// The logger for this service - /// The discord client - protected DiscordClientService(DiscordSocketClient client, ILogger logger) : base(client, logger) - { - } + /// Triggered when is called. + /// A that represents the long running operations. + protected abstract override Task ExecuteAsync(CancellationToken stoppingToken); +} +/// +/// Base class for implementing an for a with startup execution requirements. +/// This class implements and should be registered in your service collection with `AddHostedService` +/// +public abstract class DiscordClientService : DiscordServiceBase +{ + /// + /// Creates a new + /// + /// The logger for this service + /// The discord client + protected DiscordClientService(DiscordSocketClient client, ILogger logger) : base(client, logger) + { } +} + +/// +/// Base class for implementing an for a with startup execution requirements. +/// This class implements and should be registered in your service collection with `AddHostedService` +/// +public abstract class DiscordShardedClientService : DiscordServiceBase +{ /// - /// Base class for implementing an for a with startup execution requirements. - /// This class implements and should be registered in your service collection with `AddHostedService` + /// Creates a new /// - public abstract class DiscordShardedClientService : DiscordServiceBase + /// The logger for this service + /// The discord client + protected DiscordShardedClientService(DiscordShardedClient client, ILogger logger) : base(client, logger) { - /// - /// Creates a new - /// - /// The logger for this service - /// The discord client - protected DiscordShardedClientService(DiscordShardedClient client, ILogger logger) : base(client, logger) - { - } } } \ No newline at end of file diff --git a/Discord.Addons.Hosting/DiscordHostBuilderExtensions.cs b/Discord.Addons.Hosting/DiscordHostBuilderExtensions.cs index 81732b9..78a4cef 100644 --- a/Discord.Addons.Hosting/DiscordHostBuilderExtensions.cs +++ b/Discord.Addons.Hosting/DiscordHostBuilderExtensions.cs @@ -1,6 +1,6 @@ #region License /* - Copyright 2021 Hawxy + Copyright 2019-2022 Hawxy Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,132 +15,168 @@ You may obtain a copy of the License at limitations under the License. */ #endregion -using System; -using System.Linq; + using Discord.Addons.Hosting.Injectables; +using Discord.Addons.Hosting.Services; using Discord.Addons.Hosting.Util; using Discord.Commands; +using Discord.Interactions; using Discord.WebSocket; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Options; -namespace Discord.Addons.Hosting +namespace Discord.Addons.Hosting; + +/// +/// Extends with Discord.Net configuration methods. +/// +public static class DiscordHostBuilderExtensions { /// - /// Extends with Discord.Net configuration methods. + /// Adds and optionally configures a along with the required services. /// - public static class DiscordHostBuilderExtensions + /// + /// A is supplied so that the configuration and service provider can be used. + /// + /// The host builder to configure. + /// The delegate for the that will be used to configure the host. + /// The generic host builder. + /// Thrown if client is already added to the service collection + public static IHostBuilder ConfigureDiscordShardedHost(this IHostBuilder builder, Action? config = null) { - /// - /// Adds and optionally configures a along with the required services. - /// - /// - /// A is supplied so that the configuration and service provider can be used. - /// - /// The host builder to configure. - /// The delegate for the that will be used to configure the host. - /// The generic host builder. - /// Thrown if client is already added to the service collection - public static IHostBuilder ConfigureDiscordShardedHost(this IHostBuilder builder, Action? config = null) + builder.ConfigureDiscordHostInternal(config); + + return builder.ConfigureServices((_, collection) => { - builder.ConfigureDiscordHostInternal(config); + if (collection.Any(x => x.ServiceType.BaseType == typeof(BaseSocketClient))) + throw new InvalidOperationException("Cannot add more than one Discord Client to host"); - return builder.ConfigureServices((_, collection) => - { - if (collection.Any(x => x.ServiceType.BaseType == typeof(BaseSocketClient))) - throw new InvalidOperationException("Cannot add more than one Discord Client to host"); + collection.AddSingleton(); + }); + } - collection.AddSingleton(); - }); - } + /// + /// Adds and optionally configures a along with the required services. + /// + /// + /// A is supplied so that the configuration and service provider can be used. + /// + /// The host builder to configure. + /// The delegate for the that will be used to configure the host. + /// The generic host builder. + /// Thrown if client is already added to the service collection + public static IHostBuilder ConfigureDiscordHost(this IHostBuilder builder, Action? config = null) + { + builder.ConfigureDiscordHostInternal(config); - /// - /// Adds and optionally configures a along with the required services. - /// - /// - /// A is supplied so that the configuration and service provider can be used. - /// - /// The host builder to configure. - /// The delegate for the that will be used to configure the host. - /// The generic host builder. - /// Thrown if client is already added to the service collection - public static IHostBuilder ConfigureDiscordHost(this IHostBuilder builder, Action? config = null) + return builder.ConfigureServices((_, collection) => { - builder.ConfigureDiscordHostInternal(config); - - return builder.ConfigureServices((_, collection) => - { - if (collection.Any(x => x.ServiceType.BaseType == typeof(BaseSocketClient))) - throw new InvalidOperationException("Cannot add more than one Discord Client to host"); + if (collection.Any(x => x.ServiceType.BaseType == typeof(BaseSocketClient))) + throw new InvalidOperationException("Cannot add more than one Discord Client to host"); - collection.AddSingleton(); - }); - } + collection.AddSingleton(); + }); + } - private static IHostBuilder ConfigureDiscordHostInternal(this IHostBuilder builder, Action? config = null) where T: BaseSocketClient + private static void ConfigureDiscordHostInternal(this IHostBuilder builder, Action? config = null) where T: BaseSocketClient + { + builder.ConfigureServices((context, collection) => { - return builder.ConfigureServices((context, collection) => - { - collection.AddOptions().Validate(x => ValidateToken(x.Token)); + collection.AddOptions().Validate(x => ValidateToken(x.Token)); - if (config != null) - collection.Configure(x => config(context, x)); + if (config != null) + collection.Configure(x => config(context, x)); - collection.AddSingleton(typeof(LogAdapter<>)); - collection.AddHostedService>(); - }); + collection.AddSingleton(typeof(LogAdapter<>)); + collection.AddHostedService>(); + }); - static bool ValidateToken(string token) + static bool ValidateToken(string token) + { + try { - try - { - TokenUtils.ValidateToken(TokenType.Bot, token); - return true; - } - catch (Exception e) when (e is ArgumentNullException || e is ArgumentException) - { - return false; - } + TokenUtils.ValidateToken(TokenType.Bot, token); + return true; + } + catch (Exception e) when (e is ArgumentNullException or ArgumentException) + { + return false; } } + } - /// - /// Adds a instance to the host for use with a Discord.Net client. /> - /// - /// The host builder to configure. - /// The (generic) host builder. - /// Thrown if is already added to the collection - public static IHostBuilder UseCommandService(this IHostBuilder builder) => builder.UseCommandService((context, config) => { }); - - /// - /// Adds a instance to the host for use with a Discord.Net client. /> - /// - /// - /// A is supplied so that the configuration and service provider can be used. - /// - /// The host builder to configure. - /// The delegate for configuring the that will be used to initialise the service. - /// The (generic) host builder. - /// Thrown if config is null - /// Thrown if is already added to the collection - public static IHostBuilder UseCommandService(this IHostBuilder builder, Action config) + /// + /// Adds a instance to the host for use with a Discord.NET client. /> + /// + /// The host builder to configure. + /// The (generic) host builder. + /// Thrown if is already added to the collection + public static IHostBuilder UseCommandService(this IHostBuilder builder) => builder.UseCommandService((context, config) => { }); + + /// + /// Adds a instance to the host for use with a Discord.NET client. /> + /// + /// + /// A is supplied so that the configuration and service provider can be used. + /// + /// The host builder to configure. + /// The delegate for configuring the that will be used to initialise the service. + /// The (generic) host builder. + /// Thrown if config is null + /// Thrown if is already added to the collection + public static IHostBuilder UseCommandService(this IHostBuilder builder, Action config) + { + ArgumentNullException.ThrowIfNull(config); + + builder.ConfigureServices((context, collection) => { - if (config == null) - throw new ArgumentNullException(nameof(config)); + if (collection.Any(x => x.ServiceType == typeof(CommandService))) + throw new InvalidOperationException("Cannot add more than one CommandService to host"); - builder.ConfigureServices((context, collection) => - { - if (collection.Any(x => x.ServiceType == typeof(CommandService))) - throw new InvalidOperationException("Cannot add more than one CommandService to host"); + collection.Configure(x => config(context, x)); - collection.Configure(x => config(context, x)); + collection.AddSingleton(); + collection.AddHostedService(); + }); - collection.AddSingleton(x=> new CommandService(x.GetRequiredService>().Value)); - collection.AddHostedService(); - }); + return builder; + } - return builder; - } + /// + /// Adds a instance to the host for use with a Discord.NET client. /> + /// + /// The host builder to configure. + /// The (generic) host builder. + /// Thrown if is already added to the collection + public static IHostBuilder UseInteractionService(this IHostBuilder builder) => builder.UseInteractionService((context, config) => { }); + + + /// + /// Adds a instance to the host for use with a Discord.NET client. /> + /// + /// + /// A is supplied so that the configuration and service provider can be used. + /// + /// The host builder to configure. + /// The delegate for configuring the that will be used to initialise the service. + /// The (generic) host builder. + /// Thrown if config is null + /// Thrown if is already added to the collection + public static IHostBuilder UseInteractionService(this IHostBuilder builder, Action config) + { + ArgumentNullException.ThrowIfNull(config); + + builder.ConfigureServices((context, collection) => + { + if (collection.Any(x => x.ServiceType == typeof(InteractionService))) + throw new InvalidOperationException("Cannot add more than one InteractionService to host"); + + collection.Configure(x => config(context, x)); + + collection.AddSingleton(); + collection.AddHostedService(); + }); + + return builder; } -} +} \ No newline at end of file diff --git a/Discord.Addons.Hosting/DiscordHostConfiguration.cs b/Discord.Addons.Hosting/DiscordHostConfiguration.cs index 049e7aa..9eb7a73 100644 --- a/Discord.Addons.Hosting/DiscordHostConfiguration.cs +++ b/Discord.Addons.Hosting/DiscordHostConfiguration.cs @@ -1,6 +1,6 @@ #region License /* - Copyright 2021 Hawxy + Copyright 2019-2022 Hawxy Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,31 +15,34 @@ You may obtain a copy of the License at limitations under the License. */ #endregion -using System; + using Discord.WebSocket; -namespace Discord.Addons.Hosting +namespace Discord.Addons.Hosting; + +/// +/// Configuration passed to the hosted service +/// +public class DiscordHostConfiguration { /// - /// Configuration passed to the hosted service + /// The bots token. + /// + public string Token { get; set; } = string.Empty; + /// + /// Sets a custom output format for logs coming from Discord.NET's integrated logger. + /// + /// + /// The default simply concatenates the message source with the log message. + /// + public Func LogFormat { get; set; } = (message, _) => $"{message.Source}: {message.Message}"; + + /// + public DiscordSocketConfig SocketConfig { get; set; } = new(); + + /// + /// Optional explicit shard ID(s) for the /// - public class DiscordHostConfiguration - { - - /// - /// The bots token. - /// - public string Token { get; set; } = string.Empty; - /// - /// Sets a custom output format for logs coming from Discord.NET's integrated logger. - /// - /// - /// The default simply concatenates the message source with the log message. - /// - public Func LogFormat { get; set; } = (message, exception) => $"{message.Source}: {message.Message}"; - - /// - public DiscordSocketConfig SocketConfig { get; set; } = new DiscordSocketConfig(); - } + public int[]? ShardIds { get; set; } } \ No newline at end of file diff --git a/Discord.Addons.Hosting/DiscordHostedService.cs b/Discord.Addons.Hosting/DiscordHostedService.cs deleted file mode 100644 index 38d77c5..0000000 --- a/Discord.Addons.Hosting/DiscordHostedService.cs +++ /dev/null @@ -1,72 +0,0 @@ -#region License -/* - Copyright 2021 Hawxy - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ -#endregion - -using System; -using System.Threading; -using System.Threading.Tasks; -using Discord.Addons.Hosting.Util; -using Discord.WebSocket; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace Discord.Addons.Hosting -{ - internal class DiscordHostedService : IHostedService where T: BaseSocketClient - { - private readonly ILogger> _logger; - private readonly T _client; - private readonly DiscordHostConfiguration _config; - - public DiscordHostedService(ILogger> logger, IOptions options, LogAdapter adapter, T client) - { - _logger = logger; - _config = options.Value; - _client = client; - _client.Log += adapter.Log; - } - - public async Task StartAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("Discord.NET hosted service is starting"); - - try - { - await _client.LoginAsync(TokenType.Bot, _config.Token).WithCancellation(cancellationToken); - await _client.StartAsync().WithCancellation(cancellationToken); - } - catch (OperationCanceledException) - { - _logger.LogWarning("Startup has been aborted, exiting..."); - } - } - - public async Task StopAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("Discord.NET hosted service is stopping"); - try - { - await _client.StopAsync().WithCancellation(cancellationToken); - } - catch (OperationCanceledException) - { - _logger.LogCritical("Discord.NET client could not be stopped within the given timeout and may have permanently deadlocked"); - } - } - } -} diff --git a/Discord.Addons.Hosting/InitializedService.cs b/Discord.Addons.Hosting/InitializedService.cs deleted file mode 100644 index ac8d2d6..0000000 --- a/Discord.Addons.Hosting/InitializedService.cs +++ /dev/null @@ -1,51 +0,0 @@ -#region License -/* - Copyright 2021 Hawxy - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ -#endregion -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Hosting; - -namespace Discord.Addons.Hosting -{ - /// - /// Base class for implementing an with one-time setup requirements. - /// -#if NET - [Obsolete("Replace with DiscordClientService. See the Discord.Addons.Hosting github repository for more information.", DiagnosticId - = "DAH001", UrlFormat = "https://github.com/Hawxy/Discord.Addons.Hosting/releases/")] -#else - [Obsolete("Replace with DiscordClientService. See the Discord.Addons.Hosting github repository for more information.")] -#endif - public abstract class InitializedService : IHostedService - { - /// - /// This method is called when the starts for the first time. - /// - /// Triggered when is stopped during startup. - public abstract Task InitializeAsync(CancellationToken cancellationToken); - - /// - public async Task StartAsync(CancellationToken cancellationToken) - { - await InitializeAsync(cancellationToken); - } - - /// - public virtual Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; - } -} diff --git a/Discord.Addons.Hosting/Injectables/InjectableCommandService.cs b/Discord.Addons.Hosting/Injectables/InjectableCommandService.cs new file mode 100644 index 0000000..4bb7418 --- /dev/null +++ b/Discord.Addons.Hosting/Injectables/InjectableCommandService.cs @@ -0,0 +1,27 @@ +#region License +/* + Copyright 2019-2022 Hawxy + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +#endregion +using Discord.Commands; +using Microsoft.Extensions.Options; + +namespace Discord.Addons.Hosting.Injectables; + +internal class InjectableCommandService : CommandService +{ + public InjectableCommandService(IOptions config) : base(config.Value) {} + +} \ No newline at end of file diff --git a/Discord.Addons.Hosting/Injectables/InjectableDiscordClient.cs b/Discord.Addons.Hosting/Injectables/InjectableDiscordClient.cs index 6acdc26..3a304e9 100644 --- a/Discord.Addons.Hosting/Injectables/InjectableDiscordClient.cs +++ b/Discord.Addons.Hosting/Injectables/InjectableDiscordClient.cs @@ -1,6 +1,6 @@ -#region License +#region License /* - Copyright 2021 Hawxy + Copyright 2019-2022 Hawxy Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,21 +20,20 @@ limitations under the License. using Discord.WebSocket; using Microsoft.Extensions.Options; -namespace Discord.Addons.Hosting.Injectables +namespace Discord.Addons.Hosting.Injectables; + +internal class InjectableDiscordSocketClient : DiscordSocketClient { - internal class InjectableDiscordSocketClient : DiscordSocketClient + public InjectableDiscordSocketClient(IOptions config) : base(config.Value.SocketConfig) { - public InjectableDiscordSocketClient(IOptions config) : base(config.Value.SocketConfig) - { - this.RegisterSocketClientReady(); - } + this.RegisterSocketClientReady(); } +} - internal class InjectableDiscordShardedClient : DiscordShardedClient +internal class InjectableDiscordShardedClient : DiscordShardedClient +{ + public InjectableDiscordShardedClient(IOptions config) : base(config.Value.ShardIds, config.Value.SocketConfig) { - public InjectableDiscordShardedClient(IOptions config) : base(config.Value.SocketConfig) - { - this.RegisterShardedClientReady(); - } + this.RegisterShardedClientReady(); } -} +} \ No newline at end of file diff --git a/Discord.Addons.Hosting/Injectables/InjectableInteractionService.cs b/Discord.Addons.Hosting/Injectables/InjectableInteractionService.cs new file mode 100644 index 0000000..f41f2a3 --- /dev/null +++ b/Discord.Addons.Hosting/Injectables/InjectableInteractionService.cs @@ -0,0 +1,33 @@ +#region License +/* + Copyright 2019-2022 Hawxy + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +#endregion +using Discord.Interactions; +using Discord.WebSocket; +using Microsoft.Extensions.Options; + +namespace Discord.Addons.Hosting.Injectables; + +internal class InjectableInteractionService : InteractionService +{ + public InjectableInteractionService(DiscordSocketClient discord, IOptions config) : base(discord, config.Value) + { + } + + public InjectableInteractionService(DiscordShardedClient discord, IOptions config) : base(discord, config.Value) + { + } +} \ No newline at end of file diff --git a/Discord.Addons.Hosting/Services/CommandServiceRegistrationHost.cs b/Discord.Addons.Hosting/Services/CommandServiceRegistrationHost.cs new file mode 100644 index 0000000..4a394a5 --- /dev/null +++ b/Discord.Addons.Hosting/Services/CommandServiceRegistrationHost.cs @@ -0,0 +1,47 @@ +#region License +/* + Copyright 2019-2022 Hawxy + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +#endregion + +using Discord.Addons.Hosting.Util; +using Discord.Commands; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Discord.Addons.Hosting.Services; + +internal class CommandServiceRegistrationHost : IHostedService +{ + private readonly CommandService _commandService; + private readonly ILogger _logger; + private readonly LogAdapter _adapter; + + public CommandServiceRegistrationHost(CommandService commandService, ILogger logger, LogAdapter adapter) + { + _commandService = commandService; + _logger = logger; + _adapter = adapter; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _commandService.Log += _adapter.Log; + _logger.LogInformation($"Registered logger for {nameof(CommandService)}"); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; +} \ No newline at end of file diff --git a/Discord.Addons.Hosting/Services/DiscordHostedService.cs b/Discord.Addons.Hosting/Services/DiscordHostedService.cs new file mode 100644 index 0000000..51b34cd --- /dev/null +++ b/Discord.Addons.Hosting/Services/DiscordHostedService.cs @@ -0,0 +1,68 @@ +#region License +/* + Copyright 2019-2022 Hawxy + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +#endregion + +using Discord.Addons.Hosting.Util; +using Discord.WebSocket; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Discord.Addons.Hosting.Services; + +internal class DiscordHostedService : IHostedService where T: BaseSocketClient +{ + private readonly ILogger> _logger; + private readonly T _client; + private readonly DiscordHostConfiguration _config; + + public DiscordHostedService(ILogger> logger, IOptions options, LogAdapter adapter, T client) + { + _logger = logger; + _config = options.Value; + _client = client; + _client.Log += adapter.Log; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Discord.NET hosted service is starting"); + + try + { + await _client.LoginAsync(TokenType.Bot, _config.Token).WaitAsync(cancellationToken); + await _client.StartAsync().WaitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + _logger.LogWarning("Startup has been aborted, exiting..."); + } + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Discord.NET hosted service is stopping"); + try + { + await _client.StopAsync().WaitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + _logger.LogCritical("Discord.NET client could not be stopped within the given timeout and may have permanently deadlocked"); + } + } +} \ No newline at end of file diff --git a/Discord.Addons.Hosting/Services/InteractionServiceRegistrationHost.cs b/Discord.Addons.Hosting/Services/InteractionServiceRegistrationHost.cs new file mode 100644 index 0000000..4dbc7fa --- /dev/null +++ b/Discord.Addons.Hosting/Services/InteractionServiceRegistrationHost.cs @@ -0,0 +1,48 @@ +#region License +/* + Copyright 2019-2022 Hawxy + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +#endregion + +using Discord.Addons.Hosting.Util; +using Discord.Interactions; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Discord.Addons.Hosting.Services; + +internal class InteractionServiceRegistrationHost : IHostedService +{ + private readonly InteractionService _interactionService; + private readonly ILogger _logger; + private readonly LogAdapter _adapter; + + public InteractionServiceRegistrationHost(InteractionService interactionService, ILogger logger, LogAdapter adapter) + { + _interactionService = interactionService; + _logger = logger; + _adapter = adapter; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _interactionService.Log += _adapter.Log; + _logger.LogInformation($"Registered logger for {nameof(InteractionService)}"); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + +} \ No newline at end of file diff --git a/Discord.Addons.Hosting/Util/LogAdapter.cs b/Discord.Addons.Hosting/Util/LogAdapter.cs index df4bac4..130d8a1 100644 --- a/Discord.Addons.Hosting/Util/LogAdapter.cs +++ b/Discord.Addons.Hosting/Util/LogAdapter.cs @@ -1,6 +1,6 @@ #region License /* - Copyright 2021 Hawxy + Copyright 2019-2022 Hawxy Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,40 +16,37 @@ limitations under the License. */ #endregion -using System; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace Discord.Addons.Hosting.Util +namespace Discord.Addons.Hosting.Util; + +internal class LogAdapter where T: class { - internal class LogAdapter where T: class - { - private readonly ILogger _logger; - private readonly Func _formatter; + private readonly ILogger _logger; + private readonly Func _formatter; - public LogAdapter(ILogger logger, IOptions options) - { - _logger = logger; - _formatter = options.Value.LogFormat; - } + public LogAdapter(ILogger logger, IOptions options) + { + _logger = logger; + _formatter = options.Value.LogFormat; + } - public Task Log(LogMessage message) - { - _logger.Log(GetLogLevel(message.Severity), default, message, message.Exception, _formatter); - return Task.CompletedTask; - } - - private static LogLevel GetLogLevel(LogSeverity severity) - => severity switch - { - LogSeverity.Critical => LogLevel.Critical, - LogSeverity.Error => LogLevel.Error, - LogSeverity.Warning => LogLevel.Warning, - LogSeverity.Info => LogLevel.Information, - LogSeverity.Verbose => LogLevel.Debug, - LogSeverity.Debug => LogLevel.Trace, - _ => throw new ArgumentOutOfRangeException(nameof(severity), severity, null) - }; + public Task Log(LogMessage message) + { + _logger.Log(GetLogLevel(message.Severity), default, message, message.Exception, _formatter); + return Task.CompletedTask; } + + private static LogLevel GetLogLevel(LogSeverity severity) + => severity switch + { + LogSeverity.Critical => LogLevel.Critical, + LogSeverity.Error => LogLevel.Error, + LogSeverity.Warning => LogLevel.Warning, + LogSeverity.Info => LogLevel.Information, + LogSeverity.Verbose => LogLevel.Debug, + LogSeverity.Debug => LogLevel.Trace, + _ => throw new ArgumentOutOfRangeException(nameof(severity), severity, null) + }; } \ No newline at end of file diff --git a/Discord.Addons.Hosting/Util/ShardedClientExtensions.cs b/Discord.Addons.Hosting/Util/ShardedClientExtensions.cs index a684a72..a1d9e54 100644 --- a/Discord.Addons.Hosting/Util/ShardedClientExtensions.cs +++ b/Discord.Addons.Hosting/Util/ShardedClientExtensions.cs @@ -1,58 +1,70 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading; -using System.Threading.Tasks; +#region License +/* + Copyright 2019-2022 Hawxy + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +#endregion + using Discord.WebSocket; -namespace Discord.Addons.Hosting.Util +namespace Discord.Addons.Hosting.Util; + +/// +/// Utilities for the Discord.NET sharded client +/// +public static class ShardedClientExtensions { /// - /// Utilities for the Discord.NET sharded client + /// Asynchronously waits for all shards to report ready. /// - public static class ShardedClientExtensions + /// The Discord.NET sharded client. + /// A cancellation token that will cause an early exit if cancelled. + /// + public static Task WaitForReadyAsync(this DiscordShardedClient client, CancellationToken cancellationToken) { - /// - /// Asynchronously waits for all shards to report ready. - /// - /// The Discord.NET sharded client. - /// A cancellation token that will cause an early exit if cancelled. - /// - public static Task WaitForReadyAsync(this DiscordShardedClient client, CancellationToken cancellationToken) - { - if (_shardedTcs is null) - throw new InvalidOperationException("The sharded client has not been registered correctly. Did you use ConfigureDiscordShardedHost on your HostBuilder?"); + if (_shardedTcs is null) + throw new InvalidOperationException("The sharded client has not been registered correctly. Did you use ConfigureDiscordShardedHost on your HostBuilder?"); - if (_shardedTcs.Task.IsCompleted) - return _shardedTcs.Task; + if (_shardedTcs.Task.IsCompleted) + return _shardedTcs.Task; - var registration = cancellationToken.Register( - state => { ((TaskCompletionSource)state!).TrySetResult(null!); }, - _shardedTcs); + var registration = cancellationToken.Register( + state => { ((TaskCompletionSource)state!).TrySetResult(null!); }, + _shardedTcs); - return _shardedTcs.Task.ContinueWith(_ => registration.DisposeAsync()); - } + return _shardedTcs.Task.ContinueWith(_ => registration.DisposeAsync()); + } - private static TaskCompletionSource? _shardedTcs; + private static TaskCompletionSource? _shardedTcs; - internal static void RegisterShardedClientReady(this DiscordShardedClient client) - { - _shardedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var shardReadyCount = 0; + internal static void RegisterShardedClientReady(this DiscordShardedClient client) + { + _shardedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var shardReadyCount = 0; - client.ShardReady += ShardReady; + client.ShardReady += ShardReady; - Task ShardReady(DiscordSocketClient _) + Task ShardReady(DiscordSocketClient _) + { + shardReadyCount++; + if (shardReadyCount == client.Shards.Count) { - shardReadyCount++; - if (shardReadyCount == client.Shards.Count) - { - _shardedTcs!.TrySetResult(null!); - client.ShardReady -= ShardReady; - } - return Task.CompletedTask; + _shardedTcs!.TrySetResult(null!); + client.ShardReady -= ShardReady; } - + return Task.CompletedTask; } + } -} +} \ No newline at end of file diff --git a/Discord.Addons.Hosting/Util/SocketClientExtensions.cs b/Discord.Addons.Hosting/Util/SocketClientExtensions.cs index e6dffef..9952a8c 100644 --- a/Discord.Addons.Hosting/Util/SocketClientExtensions.cs +++ b/Discord.Addons.Hosting/Util/SocketClientExtensions.cs @@ -1,6 +1,6 @@ #region License /* - Copyright 2021 Hawxy + Copyright 2019-2022 Hawxy Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,51 +15,48 @@ You may obtain a copy of the License at limitations under the License. */ #endregion -using System; -using System.Threading; -using System.Threading.Tasks; + using Discord.WebSocket; -namespace Discord.Addons.Hosting.Util +namespace Discord.Addons.Hosting.Util; + +/// +/// Utilities for the Discord.NET socket client +/// +public static class SocketClientExtensions { /// - /// Utilities for the Discord.NET socket client + /// Asynchronously waits for the socket client's ready event to fire. /// - public static class SocketClientExtensions + /// The Discord.NET socket client + /// The cancellation + /// + public static Task WaitForReadyAsync(this DiscordSocketClient client, CancellationToken cancellationToken) { - /// - /// Asynchronously waits for the socket client's ready event to fire. - /// - /// The Discord.NET socket client - /// The cancellation - /// - public static Task WaitForReadyAsync(this DiscordSocketClient client, CancellationToken cancellationToken) - { - if (_socketTcs is null) - throw new InvalidOperationException("The socket client has not been registered correctly. Did you use ConfigureDiscordHost on your HostBuilder?"); + if (_socketTcs is null) + throw new InvalidOperationException("The socket client has not been registered correctly. Did you use ConfigureDiscordHost on your HostBuilder?"); - if(_socketTcs.Task.IsCompleted) - return _socketTcs.Task; + if(_socketTcs.Task.IsCompleted) + return _socketTcs.Task; - var registration = cancellationToken.Register( - state => { ((TaskCompletionSource)state!).TrySetResult(null!); }, - _socketTcs); + var registration = cancellationToken.Register( + state => { ((TaskCompletionSource)state!).TrySetResult(null!); }, + _socketTcs); - return _socketTcs.Task.ContinueWith(_=> registration.DisposeAsync()); - } + return _socketTcs.Task.ContinueWith(_=> registration.DisposeAsync()); + } - private static TaskCompletionSource? _socketTcs; - internal static void RegisterSocketClientReady(this DiscordSocketClient client) - { - _socketTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - client.Ready += ClientReady; + private static TaskCompletionSource? _socketTcs; + internal static void RegisterSocketClientReady(this DiscordSocketClient client) + { + _socketTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + client.Ready += ClientReady; - Task ClientReady() - { - _socketTcs!.TrySetResult(null!); - client.Ready -= ClientReady; - return Task.CompletedTask; - } + Task ClientReady() + { + _socketTcs!.TrySetResult(null!); + client.Ready -= ClientReady; + return Task.CompletedTask; } } -} +} \ No newline at end of file diff --git a/Discord.Addons.Hosting/Util/TaskExtensions.cs b/Discord.Addons.Hosting/Util/TaskExtensions.cs deleted file mode 100644 index 91a23fa..0000000 --- a/Discord.Addons.Hosting/Util/TaskExtensions.cs +++ /dev/null @@ -1,44 +0,0 @@ -#region License -/* - Copyright 2021 Hawxy - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ -#endregion -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Discord.Addons.Hosting.Util -{ - internal static class TaskExtensions - { - public static async Task WithCancellation(this Task task, CancellationToken cancellationToken) - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - await using (cancellationToken.Register(state => - { - ((TaskCompletionSource)state!).TrySetResult(null!); - }, - tcs)) - { - var resultTask = await Task.WhenAny(task, tcs.Task); - if (resultTask == tcs.Task) - { - throw new OperationCanceledException(cancellationToken); - } - } - } - } -} diff --git a/README.md b/README.md index e60ada6..b28c606 100644 --- a/README.md +++ b/README.md @@ -4,19 +4,18 @@ ![Nuget](https://img.shields.io/nuget/dt/Discord.Addons.Hosting?style=flat-square) | Discord.NET Fork | [Original](https://github.com/discord-net/Discord.Net) | [Labs](https://github.com/Discord-Net-Labs/Discord.Net-Labs) |---------------------|----------|-------------| -| Nuget Version | `4.0.2` | `4.0.3-labs` | +| Nuget Version | `5.0.0` | `5.0.0-labs` | -[Discord.Net](https://github.com/RogueException/Discord.Net) hosting with [Microsoft.Extensions.Hosting](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host). -This package provides extensions to a .NET Generic Host (`IHostBuilder`) that will run a Discord.Net socket/sharded client as a controllable `IHostedService`. This simplifies initial bot creation and moves the usual boilerplate to a convenient builder pattern. +[Discord.NET](https://github.com/RogueException/Discord.Net) hosting with [Microsoft.Extensions.Hosting](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host). +This package provides extensions to a .NET Generic Host (`IHostBuilder`) that will run a Discord.NET socket/sharded client as a controllable `IHostedService`. This simplifies initial bot creation and moves the usual boilerplate to a convenient builder pattern. - -NET Core 3.1+ is required. +.NET 6.0+ is required. ```csharp // CreateDefaultBuilder configures a lot of stuff for us automatically // See: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host -var hostBuilder = Host.CreateDefaultBuilder() +var host = Host.CreateDefaultBuilder() .ConfigureDiscordHost((context, config) => { config.SocketConfig = new DiscordSocketConfig @@ -28,29 +27,37 @@ var hostBuilder = Host.CreateDefaultBuilder() config.Token = context.Configuration["token"]; }) + // Optionally wire up the command service .UseCommandService((context, config) => { config.DefaultRunMode = RunMode.Async; config.CaseSensitiveCommands = false; }) + // Optionally wire up the interactions service + .UseInteractionService((context, config) => + { + config.LogLevel = LogSeverity.Info; + config.UseCompiledLambda = true; + }) .ConfigureServices((context, services) => { //Add any other services here services.AddHostedService(); services.AddHostedService(); services.AddHostedService(); - }); + }).Build(); -await hostBuilder.RunConsoleAsync(); +await host.RunAsync(); ``` ## Getting Started -1. Create a [.NET 5 Worker Service](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0&tabs=visual-studio#worker-service-template) using Visual Studio or via the dotnet cli (`dotnet new worker -o MyWorkerService`) +1. Create a [.NET 6 Worker Service](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-6.0&tabs=visual-studio#worker-service-template) using Visual Studio or via the dotnet cli (`dotnet new worker -o MyWorkerService`) 2. Add ```Discord.Addons.Hosting``` to your project. -3. Set your bot token via the [dotnet secrets manager](https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-5.0&tabs=windows#set-a-secret): `dotnet user-secrets set "token" "your-token-here"` +3. Set your bot token via the [dotnet secrets manager](https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-6.0&tabs=windows#set-a-secret): `dotnet user-secrets set "token" "your-token-here"` 4. Add your bot prefix to `appsettings.json` 5. Configure your Discord client with `ConfigureDiscordHost`. +6. Enable the `CommandService` and/or the `InteractionService` with `UseCommandService` and `UseInteractionService` 6. Create and start your application using a HostBuilder as shown above and in the examples linked below. ## Examples diff --git a/Samples/Sample.ShardedClient/BotStatusService.cs b/Samples/Sample.ShardedClient/BotStatusService.cs index 9e64cb5..c32706e 100644 --- a/Samples/Sample.ShardedClient/BotStatusService.cs +++ b/Samples/Sample.ShardedClient/BotStatusService.cs @@ -1,25 +1,21 @@ -using System.Threading; -using System.Threading.Tasks; -using Discord; +using Discord; using Discord.Addons.Hosting; using Discord.Addons.Hosting.Util; using Discord.WebSocket; -using Microsoft.Extensions.Logging; -namespace Sample.ShardedClient +namespace Sample.ShardedClient; + +public class BotStatusService : DiscordShardedClientService { - public class BotStatusService : DiscordShardedClientService + public BotStatusService(DiscordShardedClient client, ILogger logger) : base(client, logger) { - public BotStatusService(DiscordShardedClient client, ILogger logger) : base(client, logger) - { - } + } - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - await Client.WaitForReadyAsync(stoppingToken); - Logger.LogInformation("Client is ready!"); + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await Client.WaitForReadyAsync(stoppingToken); + Logger.LogInformation("Client is ready!"); - await Client.SetActivityAsync(new Game("Set my status!")); - } + await Client.SetActivityAsync(new Game("Set my status!")); } -} +} \ No newline at end of file diff --git a/Samples/Sample.ShardedClient/CommandHandler.cs b/Samples/Sample.ShardedClient/CommandHandler.cs index e55237e..37811c9 100644 --- a/Samples/Sample.ShardedClient/CommandHandler.cs +++ b/Samples/Sample.ShardedClient/CommandHandler.cs @@ -1,56 +1,50 @@ -using System; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; +using System.Reflection; using Discord; using Discord.Addons.Hosting; using Discord.Commands; using Discord.WebSocket; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -namespace Sample.ShardedClient +namespace Sample.ShardedClient; + +public class CommandHandler : DiscordShardedClientService { - public class CommandHandler : DiscordShardedClientService + private readonly IServiceProvider _provider; + private readonly CommandService _commandService; + private readonly IConfiguration _config; + + public CommandHandler(DiscordShardedClient client, ILogger logger, IServiceProvider provider, CommandService commandService, IConfiguration config) : base(client, logger) + { + _provider = provider; + _commandService = commandService; + _config = config; + } + protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - private readonly IServiceProvider _provider; - private readonly CommandService _commandService; - private readonly IConfiguration _config; - - public CommandHandler(DiscordShardedClient client, ILogger logger, IServiceProvider provider, CommandService commandService, IConfiguration config) : base(client, logger) - { - _provider = provider; - _commandService = commandService; - _config = config; - } - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - Client.MessageReceived += HandleMessage; - _commandService.CommandExecuted += CommandExecutedAsync; - await _commandService.AddModulesAsync(Assembly.GetEntryAssembly(), _provider); - } - - private async Task HandleMessage(SocketMessage incomingMessage) - { - if (incomingMessage is not SocketUserMessage message) return; - if (message.Source != MessageSource.User) return; - - int argPos = 0; - if (!message.HasStringPrefix(_config["Prefix"], ref argPos) && !message.HasMentionPrefix(Client.CurrentUser, ref argPos)) return; - - var context = new ShardedCommandContext(Client, message); - - await _commandService.ExecuteAsync(context, argPos, _provider); - } - - public async Task CommandExecutedAsync(Optional command, ICommandContext context, IResult result) - { - Logger.LogInformation("User {user} attempted to use command {command}", context.User, command.Value.Name); - - if (!command.IsSpecified || result.IsSuccess) - return; - - await context.Channel.SendMessageAsync($"Error: {result}"); - } + Client.MessageReceived += HandleMessage; + _commandService.CommandExecuted += CommandExecutedAsync; + await _commandService.AddModulesAsync(Assembly.GetEntryAssembly(), _provider); + } + + private async Task HandleMessage(SocketMessage incomingMessage) + { + if (incomingMessage is not SocketUserMessage message) return; + if (message.Source != MessageSource.User) return; + + int argPos = 0; + if (!message.HasStringPrefix(_config["Prefix"], ref argPos) && !message.HasMentionPrefix(Client.CurrentUser, ref argPos)) return; + + var context = new ShardedCommandContext(Client, message); + + await _commandService.ExecuteAsync(context, argPos, _provider); + } + + public async Task CommandExecutedAsync(Optional command, ICommandContext context, IResult result) + { + Logger.LogInformation("User {user} attempted to use command {command}", context.User, command.Value.Name); + + if (!command.IsSpecified || result.IsSuccess) + return; + + await context.Channel.SendMessageAsync($"Error: {result}"); } } \ No newline at end of file diff --git a/Samples/Sample.ShardedClient/InteractionHandler.cs b/Samples/Sample.ShardedClient/InteractionHandler.cs new file mode 100644 index 0000000..e7d8b4d --- /dev/null +++ b/Samples/Sample.ShardedClient/InteractionHandler.cs @@ -0,0 +1,154 @@ +using System.Reflection; +using Discord; +using Discord.Addons.Hosting; +using Discord.Addons.Hosting.Util; +using Discord.Interactions; +using Discord.WebSocket; + +namespace Sample.ShardedClient; + +// NOTE: This command handler is specifically for using InteractionService-based commands +internal class InteractionHandler : DiscordShardedClientService +{ + private readonly IServiceProvider _provider; + private readonly InteractionService _interactionService; + private readonly IHostEnvironment _environment; + private readonly IConfiguration _configuration; + + public InteractionHandler(DiscordShardedClient client, ILogger logger, IServiceProvider provider, InteractionService interactionService, IHostEnvironment environment, IConfiguration configuration) : base(client, logger) + { + _provider = provider; + _interactionService = interactionService; + _environment = environment; + _configuration = configuration; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + // Process the InteractionCreated payloads to execute Interactions commands + Client.InteractionCreated += HandleInteraction; + + // Process the command execution results + _interactionService.SlashCommandExecuted += SlashCommandExecuted; + _interactionService.ContextCommandExecuted += ContextCommandExecuted; + _interactionService.ComponentCommandExecuted += ComponentCommandExecuted; + + await _interactionService.AddModulesAsync(Assembly.GetEntryAssembly(), _provider); + await Client.WaitForReadyAsync(stoppingToken); + + // If DOTNET_ENVIRONMENT is set to development, only register the commands to a single guild + if (_environment.IsDevelopment()) + await _interactionService.RegisterCommandsToGuildAsync(_configuration.GetValue("DevGuild")); + else + await _interactionService.RegisterCommandsGloballyAsync(); + } + + private Task ComponentCommandExecuted(ComponentCommandInfo arg1, Discord.IInteractionContext arg2, IResult arg3) + { + if (!arg3.IsSuccess) + { + switch (arg3.Error) + { + case InteractionCommandError.UnmetPrecondition: + // implement + break; + case InteractionCommandError.UnknownCommand: + // implement + break; + case InteractionCommandError.BadArgs: + // implement + break; + case InteractionCommandError.Exception: + // implement + break; + case InteractionCommandError.Unsuccessful: + // implement + break; + default: + break; + } + } + + return Task.CompletedTask; + } + + private Task ContextCommandExecuted(ContextCommandInfo arg1, Discord.IInteractionContext arg2, IResult arg3) + { + if (!arg3.IsSuccess) + { + switch (arg3.Error) + { + case InteractionCommandError.UnmetPrecondition: + // implement + break; + case InteractionCommandError.UnknownCommand: + // implement + break; + case InteractionCommandError.BadArgs: + // implement + break; + case InteractionCommandError.Exception: + // implement + break; + case InteractionCommandError.Unsuccessful: + // implement + break; + default: + break; + } + } + + return Task.CompletedTask; + } + + private Task SlashCommandExecuted(SlashCommandInfo arg1, Discord.IInteractionContext arg2, IResult arg3) + { + if (!arg3.IsSuccess) + { + switch (arg3.Error) + { + case InteractionCommandError.UnmetPrecondition: + // implement + break; + case InteractionCommandError.UnknownCommand: + // implement + break; + case InteractionCommandError.BadArgs: + // implement + break; + case InteractionCommandError.Exception: + // implement + break; + case InteractionCommandError.Unsuccessful: + // implement + break; + default: + break; + } + } + + return Task.CompletedTask; + } + + + private async Task HandleInteraction(SocketInteraction arg) + { + try + { + // Create an execution context that matches the generic type parameter of your InteractionModuleBase modules + var ctx = new ShardedInteractionContext(Client, arg); + await _interactionService.ExecuteCommandAsync(ctx, _provider); + } + catch (Exception ex) + { + Logger.LogError(ex, "Exception occurred whilst attempting to handle interaction."); + + if (arg.Type == InteractionType.ApplicationCommand) + { + var msg = await arg.GetOriginalResponseAsync(); + await msg.DeleteAsync(); + } + + } + } +} \ No newline at end of file diff --git a/Samples/Sample.ShardedClient/Modules/InteractionModule.cs b/Samples/Sample.ShardedClient/Modules/InteractionModule.cs new file mode 100644 index 0000000..d596fbb --- /dev/null +++ b/Samples/Sample.ShardedClient/Modules/InteractionModule.cs @@ -0,0 +1,12 @@ +using Discord.Interactions; + +namespace Sample.ShardedClient.Modules; + +public class InteractionModule : InteractionModuleBase +{ + [SlashCommand("echo", "Echo an input")] + public async Task Echo(string input) + { + await RespondAsync(input); + } +} \ No newline at end of file diff --git a/Samples/Sample.ShardedClient/Modules/PublicModule.cs b/Samples/Sample.ShardedClient/Modules/PublicModule.cs new file mode 100644 index 0000000..41cba65 --- /dev/null +++ b/Samples/Sample.ShardedClient/Modules/PublicModule.cs @@ -0,0 +1,51 @@ +using Discord; +using Discord.Commands; + +namespace Sample.ShardedClient.Modules; + +public class PublicModule : ModuleBase +{ + private readonly ILogger _logger; + //You can inject the host. This is useful if you want to shutdown the host via a command, but be careful with it. + private readonly IHost _host; + + public PublicModule(IHost host, ILogger logger) + { + _host = host; + _logger = logger; + } + + [Command("ping")] + [Alias("pong", "hello")] + public async Task PingAsync() + { + _logger.LogInformation("User {user} used the ping command!", Context.User.Username); + await ReplyAsync("pong!"); + } + + [Command("shutdown")] + public Task Stop() + { + _ = _host.StopAsync(); + return Task.CompletedTask; + } + + [Command("log")] + public Task TestLogs() + { + _logger.LogTrace("This is a trace log"); + _logger.LogDebug("This is a debug log"); + _logger.LogInformation("This is an information log"); + _logger.LogWarning("This is a warning log"); + _logger.LogError(new InvalidOperationException("Invalid Operation"), "This is a error log with exception"); + _logger.LogCritical(new InvalidOperationException("Invalid Operation"), "This is a critical load with exception"); + + _logger.Log(GetLogLevel(LogSeverity.Error), "Error logged from a Discord LogSeverity.Error"); + _logger.Log(GetLogLevel(LogSeverity.Info), "Information logged from Discord LogSeverity.Info "); + + return Task.CompletedTask; + } + + private static LogLevel GetLogLevel(LogSeverity severity) + => (LogLevel)Math.Abs((int)severity - 5); +} \ No newline at end of file diff --git a/Samples/Sample.ShardedClient/Program.cs b/Samples/Sample.ShardedClient/Program.cs index 2f4f907..a0dc5a1 100644 --- a/Samples/Sample.ShardedClient/Program.cs +++ b/Samples/Sample.ShardedClient/Program.cs @@ -2,44 +2,39 @@ using Discord.Addons.Hosting; using Discord.Commands; using Discord.WebSocket; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Sample.ShardedClient; -namespace Sample.Simple -{ - class Program +var host = Host.CreateDefaultBuilder(args) + .ConfigureDiscordShardedHost((context, config) => { - public static void Main(string[] args) + config.SocketConfig = new DiscordSocketConfig { - CreateHostBuilder(args).Build().Run(); - } + LogLevel = LogSeverity.Verbose, + AlwaysDownloadUsers = true, + MessageCacheSize = 200, + TotalShards = 4 + }; - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureDiscordShardedHost((context, config) => - { - config.SocketConfig = new DiscordSocketConfig - { - LogLevel = LogSeverity.Verbose, - AlwaysDownloadUsers = true, - MessageCacheSize = 200, - TotalShards = 4 - }; + config.Token = context.Configuration["Token"]; - config.Token = context.Configuration["Token"]; - }) - //Omit this if you don't use the command service - .UseCommandService((context, config) => - { - config.DefaultRunMode = RunMode.Async; - config.CaseSensitiveCommands = false; - }) - .ConfigureServices((context, services) => - { - //Add any other services here - services.AddHostedService(); - services.AddHostedService(); - }); - } -} + config.ShardIds = new[] { 1 }; + }) + .UseCommandService((context, config) => + { + config.DefaultRunMode = RunMode.Async; + config.CaseSensitiveCommands = false; + }) + .UseInteractionService((context, config) => + { + config.LogLevel = LogSeverity.Info; + config.UseCompiledLambda = true; + }) + .ConfigureServices((context, services) => + { + //Add any other services here + services.AddHostedService(); + services.AddHostedService(); + services.AddHostedService(); + }).Build(); + +await host.RunAsync(); \ No newline at end of file diff --git a/Samples/Sample.ShardedClient/Properties/launchSettings.json b/Samples/Sample.ShardedClient/Properties/launchSettings.json index ed22452..11f3369 100644 --- a/Samples/Sample.ShardedClient/Properties/launchSettings.json +++ b/Samples/Sample.ShardedClient/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "Sample.ShardedClient": { "commandName": "Project", - "dotnetRunMessages": "true", + "dotnetRunMessages": true, "environmentVariables": { "DOTNET_ENVIRONMENT": "Development" } diff --git a/Samples/Sample.ShardedClient/PublicModule.cs b/Samples/Sample.ShardedClient/PublicModule.cs deleted file mode 100644 index ccfd863..0000000 --- a/Samples/Sample.ShardedClient/PublicModule.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Threading.Tasks; -using Discord; -using Discord.Commands; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace Sample.ShardedClient -{ - public class PublicModule : ModuleBase - { - private readonly ILogger _logger; - //You can inject the host. This is useful if you want to shutdown the host via a command, but be careful with it. - private readonly IHost _host; - - public PublicModule(IHost host, ILogger logger) - { - _host = host; - _logger = logger; - } - - [Command("ping")] - [Alias("pong", "hello")] - public async Task PingAsync() - { - _logger.LogInformation($"User {Context.User.Username} used the ping command!"); - await ReplyAsync("pong!"); - } - - [Command("shutdown")] - public Task Stop() - { - _ = _host.StopAsync(); - return Task.CompletedTask; - } - - [Command("log")] - public Task TestLogs() - { - _logger.LogTrace("This is a trace log"); - _logger.LogDebug("This is a debug log"); - _logger.LogInformation("This is an information log"); - _logger.LogWarning("This is a warning log"); - _logger.LogError(new InvalidOperationException("Invalid Operation"), "This is a error log with exception"); - _logger.LogCritical(new InvalidOperationException("Invalid Operation"), "This is a critical load with exception"); - - _logger.Log(GetLogLevel(LogSeverity.Error), "Error logged from a Discord LogSeverity.Error"); - _logger.Log(GetLogLevel(LogSeverity.Info), "Information logged from Discord LogSeverity.Info "); - - return Task.CompletedTask; - } - - private static LogLevel GetLogLevel(LogSeverity severity) - => (LogLevel)Math.Abs((int)severity - 5); - } -} diff --git a/Samples/Sample.ShardedClient/Sample.ShardedClient.csproj b/Samples/Sample.ShardedClient/Sample.ShardedClient.csproj index 0b26d8f..495c03f 100644 --- a/Samples/Sample.ShardedClient/Sample.ShardedClient.csproj +++ b/Samples/Sample.ShardedClient/Sample.ShardedClient.csproj @@ -1,12 +1,14 @@ - net5.0 - 5751e5fb-2beb-463c-bdb7-03e8e997481b + net6.0 + enable + enable + 5751e5fb-2beb-463c-bdb7-03e8e997481b - + diff --git a/Samples/Sample.ShardedClient/appsettings.json b/Samples/Sample.ShardedClient/appsettings.json index ebc6a67..01078c5 100644 --- a/Samples/Sample.ShardedClient/appsettings.json +++ b/Samples/Sample.ShardedClient/appsettings.json @@ -1,6 +1,7 @@ { "Prefix": "*", "Token": "", + "DevGuild": 0, "Logging": { "LogLevel": { "Default": "Information", diff --git a/Samples/SampleBotSerilog/BotStatusService.cs b/Samples/SampleBotSerilog/BotStatusService.cs index e74b8ba..fe5b691 100644 --- a/Samples/SampleBotSerilog/BotStatusService.cs +++ b/Samples/SampleBotSerilog/BotStatusService.cs @@ -1,25 +1,21 @@ -using System.Threading; -using System.Threading.Tasks; -using Discord; +using Discord; using Discord.Addons.Hosting; using Discord.Addons.Hosting.Util; using Discord.WebSocket; -using Microsoft.Extensions.Logging; -namespace Sample.Serilog +namespace Sample.Serilog; + +public class BotStatusService : DiscordClientService { - public class BotStatusService : DiscordClientService + public BotStatusService(DiscordSocketClient client, ILogger logger) : base(client, logger) { - public BotStatusService(DiscordSocketClient client, ILogger logger) : base(client, logger) - { - } + } - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - await Client.WaitForReadyAsync(stoppingToken); - Logger.LogInformation("Client is ready!"); + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await Client.WaitForReadyAsync(stoppingToken); + Logger.LogInformation("Client is ready!"); - await Client.SetActivityAsync(new Game("Set my status!")); - } + await Client.SetActivityAsync(new Game("Set my status!")); } -} +} \ No newline at end of file diff --git a/Samples/SampleBotSerilog/CommandHandler.cs b/Samples/SampleBotSerilog/CommandHandler.cs index 53b151b..ba480f6 100644 --- a/Samples/SampleBotSerilog/CommandHandler.cs +++ b/Samples/SampleBotSerilog/CommandHandler.cs @@ -1,58 +1,52 @@ -using System; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; +using System.Reflection; using Discord; using Discord.Addons.Hosting; -using Discord.Addons.Hosting.Util; using Discord.Commands; using Discord.WebSocket; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using Serilog; +using IResult = Discord.Commands.IResult; -namespace Sample.Serilog +namespace Sample.Serilog; + +// NOTE: This command handler is specifically for using traditional bot commands. +public class CommandHandler : DiscordClientService { - public class CommandHandler : DiscordClientService + private readonly IServiceProvider _provider; + private readonly CommandService _commandService; + private readonly IConfiguration _config; + + public CommandHandler(DiscordSocketClient client, ILogger logger, IServiceProvider provider, CommandService commandService, IConfiguration config) : base(client, logger) + { + _provider = provider; + _commandService = commandService; + _config = config; + } + protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - private readonly IServiceProvider _provider; - private readonly CommandService _commandService; - private readonly IConfiguration _config; - - public CommandHandler(DiscordSocketClient client, ILogger logger, IServiceProvider provider, CommandService commandService, IConfiguration config) : base(client, logger) - { - _provider = provider; - _commandService = commandService; - _config = config; - } - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - Client.MessageReceived += HandleMessage; - _commandService.CommandExecuted += CommandExecutedAsync; - await _commandService.AddModulesAsync(Assembly.GetEntryAssembly(), _provider); - } - - private async Task HandleMessage(SocketMessage incomingMessage) - { - if (incomingMessage is not SocketUserMessage message) return; - if (message.Source != MessageSource.User) return; - - int argPos = 0; - if (!message.HasStringPrefix(_config["Prefix"], ref argPos) && !message.HasMentionPrefix(Client.CurrentUser, ref argPos)) return; - - var context = new SocketCommandContext(Client, message); - - await _commandService.ExecuteAsync(context, argPos, _provider); - } - - public async Task CommandExecutedAsync(Optional command, ICommandContext context, IResult result) - { - Logger.LogInformation("User {user} attempted to use command {command}", context.User, command.Value.Name); - - if (!command.IsSpecified || result.IsSuccess) - return; - - await context.Channel.SendMessageAsync($"Error: {result}"); - } + Client.MessageReceived += HandleMessage; + _commandService.CommandExecuted += CommandExecutedAsync; + await _commandService.AddModulesAsync(Assembly.GetEntryAssembly(), _provider); + } + + private async Task HandleMessage(SocketMessage incomingMessage) + { + if (incomingMessage is not SocketUserMessage message) return; + if (message.Source != MessageSource.User) return; + + int argPos = 0; + if (!message.HasStringPrefix(_config["Prefix"], ref argPos) && !message.HasMentionPrefix(Client.CurrentUser, ref argPos)) return; + + var context = new SocketCommandContext(Client, message); + + await _commandService.ExecuteAsync(context, argPos, _provider); + } + + public async Task CommandExecutedAsync(Optional command, ICommandContext context, IResult result) + { + Logger.LogInformation("User {user} attempted to use command {command}", context.User, command.Value.Name); + + if (!command.IsSpecified || result.IsSuccess) + return; + + await context.Channel.SendMessageAsync($"Error: {result}"); } } \ No newline at end of file diff --git a/Samples/SampleBotSerilog/InteractionHandler.cs b/Samples/SampleBotSerilog/InteractionHandler.cs new file mode 100644 index 0000000..d05b222 --- /dev/null +++ b/Samples/SampleBotSerilog/InteractionHandler.cs @@ -0,0 +1,154 @@ +using System.Reflection; +using Discord; +using Discord.Addons.Hosting; +using Discord.Addons.Hosting.Util; +using Discord.Interactions; +using Discord.WebSocket; + +namespace Sample.Serilog; + +// NOTE: This command handler is specifically for using InteractionService-based commands +internal class InteractionHandler : DiscordClientService +{ + private readonly IServiceProvider _provider; + private readonly InteractionService _interactionService; + private readonly IHostEnvironment _environment; + private readonly IConfiguration _configuration; + + public InteractionHandler(DiscordSocketClient client, ILogger logger, IServiceProvider provider, InteractionService interactionService, IHostEnvironment environment, IConfiguration configuration) : base(client, logger) + { + _provider = provider; + _interactionService = interactionService; + _environment = environment; + _configuration = configuration; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + // Process the InteractionCreated payloads to execute Interactions commands + Client.InteractionCreated += HandleInteraction; + + // Process the command execution results + _interactionService.SlashCommandExecuted += SlashCommandExecuted; + _interactionService.ContextCommandExecuted += ContextCommandExecuted; + _interactionService.ComponentCommandExecuted += ComponentCommandExecuted; + + await _interactionService.AddModulesAsync(Assembly.GetEntryAssembly(), _provider); + await Client.WaitForReadyAsync(stoppingToken); + + // If DOTNET_ENVIRONMENT is set to development, only register the commands to a single guild + if (_environment.IsDevelopment()) + await _interactionService.RegisterCommandsToGuildAsync(_configuration.GetValue("DevGuild")); + else + await _interactionService.RegisterCommandsGloballyAsync(); + } + + private Task ComponentCommandExecuted(ComponentCommandInfo arg1, Discord.IInteractionContext arg2, IResult arg3) + { + if (!arg3.IsSuccess) + { + switch (arg3.Error) + { + case InteractionCommandError.UnmetPrecondition: + // implement + break; + case InteractionCommandError.UnknownCommand: + // implement + break; + case InteractionCommandError.BadArgs: + // implement + break; + case InteractionCommandError.Exception: + // implement + break; + case InteractionCommandError.Unsuccessful: + // implement + break; + default: + break; + } + } + + return Task.CompletedTask; + } + + private Task ContextCommandExecuted(ContextCommandInfo arg1, Discord.IInteractionContext arg2, IResult arg3) + { + if (!arg3.IsSuccess) + { + switch (arg3.Error) + { + case InteractionCommandError.UnmetPrecondition: + // implement + break; + case InteractionCommandError.UnknownCommand: + // implement + break; + case InteractionCommandError.BadArgs: + // implement + break; + case InteractionCommandError.Exception: + // implement + break; + case InteractionCommandError.Unsuccessful: + // implement + break; + default: + break; + } + } + + return Task.CompletedTask; + } + + private Task SlashCommandExecuted(SlashCommandInfo arg1, Discord.IInteractionContext arg2, IResult arg3) + { + if (!arg3.IsSuccess) + { + switch (arg3.Error) + { + case InteractionCommandError.UnmetPrecondition: + // implement + break; + case InteractionCommandError.UnknownCommand: + // implement + break; + case InteractionCommandError.BadArgs: + // implement + break; + case InteractionCommandError.Exception: + // implement + break; + case InteractionCommandError.Unsuccessful: + // implement + break; + default: + break; + } + } + + return Task.CompletedTask; + } + + + private async Task HandleInteraction(SocketInteraction arg) + { + try + { + // Create an execution context that matches the generic type parameter of your InteractionModuleBase modules + var ctx = new SocketInteractionContext(Client, arg); + await _interactionService.ExecuteCommandAsync(ctx, _provider); + } + catch (Exception ex) + { + Logger.LogError(ex, "Exception occurred whilst attempting to handle interaction."); + + if (arg.Type == InteractionType.ApplicationCommand) + { + var msg = await arg.GetOriginalResponseAsync(); + await msg.DeleteAsync(); + } + + } + } +} \ No newline at end of file diff --git a/Samples/SampleBotSerilog/Modules/InteractionModule.cs b/Samples/SampleBotSerilog/Modules/InteractionModule.cs new file mode 100644 index 0000000..c6ee078 --- /dev/null +++ b/Samples/SampleBotSerilog/Modules/InteractionModule.cs @@ -0,0 +1,12 @@ +using Discord.Interactions; + +namespace Sample.Serilog.Modules; + +public class InteractionModule : InteractionModuleBase +{ + [SlashCommand("echo", "Echo an input")] + public async Task Echo(string input) + { + await RespondAsync(input); + } +} \ No newline at end of file diff --git a/Samples/SampleBotSerilog/Modules/PublicModule.cs b/Samples/SampleBotSerilog/Modules/PublicModule.cs new file mode 100644 index 0000000..5eb800e --- /dev/null +++ b/Samples/SampleBotSerilog/Modules/PublicModule.cs @@ -0,0 +1,51 @@ +using Discord; +using Discord.Commands; + +namespace Sample.Serilog.Modules; + +public class PublicModule : ModuleBase +{ + private readonly ILogger _logger; + //You can inject the host. This is useful if you want to shutdown the host via a command, but be careful with it. + private readonly IHost _host; + + public PublicModule(IHost host, ILogger logger) + { + _host = host; + _logger = logger; + } + + [Command("ping")] + [Alias("pong", "hello")] + public async Task PingAsync() + { + _logger.LogInformation("User {user} used the ping command!", Context.User.Username); + await ReplyAsync("pong!"); + } + + [Command("shutdown")] + public Task Stop() + { + _ = _host.StopAsync(); + return Task.CompletedTask; + } + + [Command("log")] + public Task TestLogs() + { + _logger.LogTrace("This is a trace log"); + _logger.LogDebug("This is a debug log"); + _logger.LogInformation("This is an information log"); + _logger.LogWarning("This is a warning log"); + _logger.LogError(new InvalidOperationException("Invalid Operation"), "This is a error log with exception"); + _logger.LogCritical(new InvalidOperationException("Invalid Operation"), "This is a critical load with exception"); + + _logger.Log(GetLogLevel(LogSeverity.Error), "Error logged from a Discord LogSeverity.Error"); + _logger.Log(GetLogLevel(LogSeverity.Info), "Information logged from Discord LogSeverity.Info "); + + return Task.CompletedTask; + } + + private static LogLevel GetLogLevel(LogSeverity severity) + => (LogLevel)Math.Abs((int)severity - 5); +} \ No newline at end of file diff --git a/Samples/SampleBotSerilog/Program.cs b/Samples/SampleBotSerilog/Program.cs index bb4509b..ee4902e 100644 --- a/Samples/SampleBotSerilog/Program.cs +++ b/Samples/SampleBotSerilog/Program.cs @@ -1,73 +1,65 @@ -using System; -using System.Threading.Tasks; -using Discord; +using Discord; using Discord.Addons.Hosting; using Discord.Commands; using Discord.WebSocket; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; +using Sample.Serilog; using Serilog; using Serilog.Events; -namespace Sample.Serilog + +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .MinimumLevel.Override("Microsoft", LogEventLevel.Error) + .WriteTo.Console() + .CreateLogger(); + +try { - class Program - { - public static int Main(string[] args) - { - //Log is available everywhere, useful for places where it isn't practical to use ILogger injection - Log.Logger = new LoggerConfiguration() - .MinimumLevel.Information() - .MinimumLevel.Override("Microsoft", LogEventLevel.Error) - .WriteTo.Console() - .CreateLogger(); + Log.Information("Starting host"); - try - { - Log.Information("Starting host"); - CreateHostBuilder(args).Build().Run(); - return 0; - } - catch (Exception ex) - { - Log.Fatal(ex, "Host terminated unexpectedly"); - return 1; - } - finally + var host = Host.CreateDefaultBuilder(args) + // Serilog.Extensions.Hosting is required. + .UseSerilog() + .ConfigureDiscordHost((context, config) => + { + config.SocketConfig = new DiscordSocketConfig { - Log.CloseAndFlush(); - } - } + LogLevel = LogSeverity.Verbose, + AlwaysDownloadUsers = true, + MessageCacheSize = 200 + }; - public static IHostBuilder CreateHostBuilder(string[] args) => - //CreateDefaultBuilder configures a lot of stuff for us automatically. - //See: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-5.0#default-builder-settings - Host.CreateDefaultBuilder(args) - //Serilog.Extensions.Hosting is required. - .UseSerilog() - .ConfigureDiscordHost((context, config) => - { - config.SocketConfig = new DiscordSocketConfig - { - LogLevel = LogSeverity.Verbose, - AlwaysDownloadUsers = true, - MessageCacheSize = 200 - }; + config.Token = context.Configuration["Token"]; - config.Token = context.Configuration["Token"]; + //Use this to configure a custom format for Client/CommandService logging if needed. The default is below and should be suitable for Serilog usage + config.LogFormat = (message, exception) => $"{message.Source}: {message.Message}"; + }) + .UseCommandService((context, config) => + { + config.LogLevel = LogSeverity.Info; + config.DefaultRunMode = RunMode.Async; + }) + .UseInteractionService((context, config) => + { + config.LogLevel = LogSeverity.Info; + config.UseCompiledLambda = true; + }) + .ConfigureServices((context, services) => + { + services.AddHostedService(); + services.AddHostedService(); + services.AddHostedService(); + }).Build(); - //Use this to configure a custom format for Client/CommandService logging if needed. The default is below and should be suitable for Serilog usage - config.LogFormat = (message, exception) => $"{message.Source}: {message.Message}"; - }) - .UseCommandService((context, config) => - { - config.LogLevel = LogSeverity.Verbose; - config.DefaultRunMode = RunMode.Async; - }) - .ConfigureServices((context, services) => - { - services.AddHostedService(); - services.AddHostedService(); - }); - } + await host.RunAsync(); + return 0; } +catch (Exception ex) +{ + Log.Fatal(ex, "Host terminated unexpectedly"); + return 1; +} +finally +{ + Log.CloseAndFlush(); +} \ No newline at end of file diff --git a/Samples/SampleBotSerilog/Properties/launchSettings.json b/Samples/SampleBotSerilog/Properties/launchSettings.json index bacafea..541763a 100644 --- a/Samples/SampleBotSerilog/Properties/launchSettings.json +++ b/Samples/SampleBotSerilog/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "Sample.Serilog": { "commandName": "Project", - "dotnetRunMessages": "true", + "dotnetRunMessages": true, "environmentVariables": { "DOTNET_ENVIRONMENT": "Development" } diff --git a/Samples/SampleBotSerilog/PublicModule.cs b/Samples/SampleBotSerilog/PublicModule.cs deleted file mode 100644 index c0538d5..0000000 --- a/Samples/SampleBotSerilog/PublicModule.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Threading.Tasks; -using Discord; -using Discord.Commands; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace Sample.Serilog -{ - public class PublicModule : ModuleBase - { - private readonly ILogger _logger; - //You can inject the host. This is useful if you want to shutdown the host via a command, but be careful with it. - private readonly IHost _host; - - public PublicModule(IHost host, ILogger logger) - { - _host = host; - _logger = logger; - } - - [Command("ping")] - [Alias("pong", "hello")] - public async Task PingAsync() - { - _logger.LogInformation($"User {Context.User.Username} used the ping command!"); - await ReplyAsync("pong!"); - } - - [Command("shutdown")] - public Task Stop() - { - _ = _host.StopAsync(); - return Task.CompletedTask; - } - - [Command("log")] - public Task TestLogs() - { - _logger.LogTrace("This is a trace log"); - _logger.LogDebug("This is a debug log"); - _logger.LogInformation("This is an information log"); - _logger.LogWarning("This is a warning log"); - _logger.LogError(new InvalidOperationException("Invalid Operation"), "This is a error log with exception"); - _logger.LogCritical(new InvalidOperationException("Invalid Operation"), "This is a critical load with exception"); - - _logger.Log(GetLogLevel(LogSeverity.Error), "Error logged from a Discord LogSeverity.Error"); - _logger.Log(GetLogLevel(LogSeverity.Info), "Information logged from Discord LogSeverity.Info "); - - return Task.CompletedTask; - } - - private static LogLevel GetLogLevel(LogSeverity severity) - => (LogLevel)Math.Abs((int)severity - 5); - } -} diff --git a/Samples/SampleBotSerilog/Sample.Serilog.csproj b/Samples/SampleBotSerilog/Sample.Serilog.csproj index 0d6b29a..3b5ab52 100644 --- a/Samples/SampleBotSerilog/Sample.Serilog.csproj +++ b/Samples/SampleBotSerilog/Sample.Serilog.csproj @@ -1,22 +1,21 @@  - - net5.0 - 5751e5fb-2beb-463c-bdb7-03e8e997481b - + + net6.0 + enable + enable + 5751e5fb-2beb-463c-bdb7-03e8e997481b + - - - - - - - - - + + + + + + - - - + + + \ No newline at end of file diff --git a/Samples/SampleBotSerilog/appsettings.json b/Samples/SampleBotSerilog/appsettings.json index 169fdb7..20a37db 100644 --- a/Samples/SampleBotSerilog/appsettings.json +++ b/Samples/SampleBotSerilog/appsettings.json @@ -1,4 +1,5 @@ { "Prefix": "!", - "Token": "" + "Token": "", + "DevGuild": 0 } diff --git a/Samples/SampleBotSimple/BotStatusService.cs b/Samples/SampleBotSimple/BotStatusService.cs index e74b8ba..9cd632a 100644 --- a/Samples/SampleBotSimple/BotStatusService.cs +++ b/Samples/SampleBotSimple/BotStatusService.cs @@ -1,25 +1,21 @@ -using System.Threading; -using System.Threading.Tasks; -using Discord; +using Discord; using Discord.Addons.Hosting; using Discord.Addons.Hosting.Util; using Discord.WebSocket; -using Microsoft.Extensions.Logging; -namespace Sample.Serilog +namespace Sample.Simple; + +public class BotStatusService : DiscordClientService { - public class BotStatusService : DiscordClientService + public BotStatusService(DiscordSocketClient client, ILogger logger) : base(client, logger) { - public BotStatusService(DiscordSocketClient client, ILogger logger) : base(client, logger) - { - } + } - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - await Client.WaitForReadyAsync(stoppingToken); - Logger.LogInformation("Client is ready!"); + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await Client.WaitForReadyAsync(stoppingToken); + Logger.LogInformation("Client is ready!"); - await Client.SetActivityAsync(new Game("Set my status!")); - } + await Client.SetActivityAsync(new Game("Set my status!")); } -} +} \ No newline at end of file diff --git a/Samples/SampleBotSimple/CommandHandler.cs b/Samples/SampleBotSimple/CommandHandler.cs index 2492101..8fdb5e2 100644 --- a/Samples/SampleBotSimple/CommandHandler.cs +++ b/Samples/SampleBotSimple/CommandHandler.cs @@ -1,55 +1,49 @@ -using System; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; +using System.Reflection; using Discord; using Discord.Addons.Hosting; using Discord.Commands; using Discord.WebSocket; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -namespace Sample.Simple +namespace Sample.Simple; + +public class CommandHandler : DiscordClientService { - public class CommandHandler : DiscordClientService + private readonly IServiceProvider _provider; + private readonly CommandService _commandService; + private readonly IConfiguration _config; + + public CommandHandler(DiscordSocketClient client, ILogger logger, IServiceProvider provider, CommandService commandService, IConfiguration config) : base(client, logger) + { + _provider = provider; + _commandService = commandService; + _config = config; + } + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + Client.MessageReceived += HandleMessage; + _commandService.CommandExecuted += CommandExecutedAsync; + await _commandService.AddModulesAsync(Assembly.GetEntryAssembly(), _provider); + } + + private async Task HandleMessage(SocketMessage incomingMessage) { - private readonly IServiceProvider _provider; - private readonly CommandService _commandService; - private readonly IConfiguration _config; - - public CommandHandler(DiscordSocketClient client, ILogger logger, IServiceProvider provider, CommandService commandService, IConfiguration config) : base(client, logger) - { - _provider = provider; - _commandService = commandService; - _config = config; - } - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - Client.MessageReceived += HandleMessage; - _commandService.CommandExecuted += CommandExecutedAsync; - await _commandService.AddModulesAsync(Assembly.GetEntryAssembly(), _provider); - } - - private async Task HandleMessage(SocketMessage incomingMessage) - { - if (incomingMessage is not SocketUserMessage message) return; - if (message.Source != MessageSource.User) return; - - int argPos = 0; - if (!message.HasStringPrefix(_config["Prefix"], ref argPos) && !message.HasMentionPrefix(Client.CurrentUser, ref argPos)) return; - - var context = new SocketCommandContext(Client, message); - await _commandService.ExecuteAsync(context, argPos, _provider); - } - - public async Task CommandExecutedAsync(Optional command, ICommandContext context, IResult result) - { - Logger.LogInformation("User {user} attempted to use command {command}", context.User, command.Value.Name); - - if (!command.IsSpecified || result.IsSuccess) - return; - - await context.Channel.SendMessageAsync($"Error: {result}"); - } + if (incomingMessage is not SocketUserMessage message) return; + if (message.Source != MessageSource.User) return; + + int argPos = 0; + if (!message.HasStringPrefix(_config["Prefix"], ref argPos) && !message.HasMentionPrefix(Client.CurrentUser, ref argPos)) return; + + var context = new SocketCommandContext(Client, message); + await _commandService.ExecuteAsync(context, argPos, _provider); + } + + public async Task CommandExecutedAsync(Optional command, ICommandContext context, IResult result) + { + Logger.LogInformation("User {user} attempted to use command {command}", context.User, command.Value.Name); + + if (!command.IsSpecified || result.IsSuccess) + return; + + await context.Channel.SendMessageAsync($"Error: {result}"); } } \ No newline at end of file diff --git a/Samples/SampleBotSimple/InteractionHandler.cs b/Samples/SampleBotSimple/InteractionHandler.cs new file mode 100644 index 0000000..e643f2d --- /dev/null +++ b/Samples/SampleBotSimple/InteractionHandler.cs @@ -0,0 +1,155 @@ +using System.Reflection; +using Discord; +using Discord.Addons.Hosting; +using Discord.Addons.Hosting.Util; +using Discord.Interactions; +using Discord.WebSocket; + +namespace Sample.Simple; + +// NOTE: This command handler is specifically for using InteractionService-based commands +internal class InteractionHandler : DiscordClientService +{ + private readonly IServiceProvider _provider; + private readonly InteractionService _interactionService; + private readonly IHostEnvironment _environment; + private readonly IConfiguration _configuration; + + public InteractionHandler(DiscordSocketClient client, ILogger logger, IServiceProvider provider, InteractionService interactionService, IHostEnvironment environment, IConfiguration configuration) : base(client, logger) + { + _provider = provider; + _interactionService = interactionService; + _environment = environment; + _configuration = configuration; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + // Process the InteractionCreated payloads to execute Interactions commands + Client.InteractionCreated += HandleInteraction; + + // Process the command execution results + _interactionService.SlashCommandExecuted += SlashCommandExecuted; + _interactionService.ContextCommandExecuted += ContextCommandExecuted; + _interactionService.ComponentCommandExecuted += ComponentCommandExecuted; + + await _interactionService.AddModulesAsync(Assembly.GetEntryAssembly(), _provider); + await Client.WaitForReadyAsync(stoppingToken); + + // If DOTNET_ENVIRONMENT is set to development, only register the commands to a single guild + if (_environment.IsDevelopment()) + await _interactionService.RegisterCommandsToGuildAsync(_configuration.GetValue("DevGuild")); + else + await _interactionService.RegisterCommandsGloballyAsync(); + + } + + private Task ComponentCommandExecuted(ComponentCommandInfo arg1, Discord.IInteractionContext arg2, IResult arg3) + { + if (!arg3.IsSuccess) + { + switch (arg3.Error) + { + case InteractionCommandError.UnmetPrecondition: + // implement + break; + case InteractionCommandError.UnknownCommand: + // implement + break; + case InteractionCommandError.BadArgs: + // implement + break; + case InteractionCommandError.Exception: + // implement + break; + case InteractionCommandError.Unsuccessful: + // implement + break; + default: + break; + } + } + + return Task.CompletedTask; + } + + private Task ContextCommandExecuted(ContextCommandInfo arg1, Discord.IInteractionContext arg2, IResult arg3) + { + if (!arg3.IsSuccess) + { + switch (arg3.Error) + { + case InteractionCommandError.UnmetPrecondition: + // implement + break; + case InteractionCommandError.UnknownCommand: + // implement + break; + case InteractionCommandError.BadArgs: + // implement + break; + case InteractionCommandError.Exception: + // implement + break; + case InteractionCommandError.Unsuccessful: + // implement + break; + default: + break; + } + } + + return Task.CompletedTask; + } + + private Task SlashCommandExecuted(SlashCommandInfo arg1, Discord.IInteractionContext arg2, IResult arg3) + { + if (!arg3.IsSuccess) + { + switch (arg3.Error) + { + case InteractionCommandError.UnmetPrecondition: + // implement + break; + case InteractionCommandError.UnknownCommand: + // implement + break; + case InteractionCommandError.BadArgs: + // implement + break; + case InteractionCommandError.Exception: + // implement + break; + case InteractionCommandError.Unsuccessful: + // implement + break; + default: + break; + } + } + + return Task.CompletedTask; + } + + + private async Task HandleInteraction(SocketInteraction arg) + { + try + { + // Create an execution context that matches the generic type parameter of your InteractionModuleBase modules + var ctx = new SocketInteractionContext(Client, arg); + await _interactionService.ExecuteCommandAsync(ctx, _provider); + } + catch (Exception ex) + { + Logger.LogError(ex, "Exception occurred whilst attempting to handle interaction."); + + if (arg.Type == InteractionType.ApplicationCommand) + { + var msg = await arg.GetOriginalResponseAsync(); + await msg.DeleteAsync(); + } + + } + } +} \ No newline at end of file diff --git a/Samples/SampleBotSimple/LongRunningService.cs b/Samples/SampleBotSimple/LongRunningService.cs index 9c52c24..34ea917 100644 --- a/Samples/SampleBotSimple/LongRunningService.cs +++ b/Samples/SampleBotSimple/LongRunningService.cs @@ -1,33 +1,25 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Discord.Addons.Hosting; +using Discord.Addons.Hosting; using Discord.Addons.Hosting.Util; using Discord.WebSocket; -using Microsoft.Extensions.Logging; -namespace Sample.Serilog +namespace Sample.Simple; + +public class LongRunningService : DiscordClientService { - public class LongRunningService : DiscordClientService + public LongRunningService(DiscordSocketClient client, ILogger logger) : base(client, logger) { - public LongRunningService(DiscordSocketClient client, ILogger logger) : base(client, logger) - { - } + } - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - // Wait for the client to be ready - await Client.WaitForReadyAsync(stoppingToken); - //Start a pumping background service that lasts for the length of host's existence + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + // Wait for the client to be ready + await Client.WaitForReadyAsync(stoppingToken); + //Start a pumping background service that lasts for the length of host's existence - while (!stoppingToken.IsCancellationRequested) - { - Logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); - await Task.Delay(1000, stoppingToken); - } + while (!stoppingToken.IsCancellationRequested) + { + Logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); + await Task.Delay(1000, stoppingToken); } } -} +} \ No newline at end of file diff --git a/Samples/SampleBotSimple/Modules/InteractionModule.cs b/Samples/SampleBotSimple/Modules/InteractionModule.cs new file mode 100644 index 0000000..3788471 --- /dev/null +++ b/Samples/SampleBotSimple/Modules/InteractionModule.cs @@ -0,0 +1,12 @@ +using Discord.Interactions; + +namespace Sample.Simple.Modules; + +public class InteractionModule : InteractionModuleBase +{ + [SlashCommand("echo", "Echo an input")] + public async Task Echo(string input) + { + await RespondAsync(input); + } +} \ No newline at end of file diff --git a/Samples/SampleBotSimple/Modules/PublicModule.cs b/Samples/SampleBotSimple/Modules/PublicModule.cs new file mode 100644 index 0000000..f3cb403 --- /dev/null +++ b/Samples/SampleBotSimple/Modules/PublicModule.cs @@ -0,0 +1,51 @@ +using Discord; +using Discord.Commands; + +namespace Sample.Simple.Modules; + +public class PublicModule : ModuleBase +{ + private readonly ILogger _logger; + //You can inject the host. This is useful if you want to shutdown the host via a command, but be careful with it. + private readonly IHost _host; + + public PublicModule(IHost host, ILogger logger) + { + _host = host; + _logger = logger; + } + + [Command("ping")] + [Alias("pong", "hello")] + public async Task PingAsync() + { + _logger.LogInformation("User {user} used the ping command!", Context.User.Username); + await ReplyAsync("pong!"); + } + + [Command("shutdown")] + public Task Stop() + { + _ = _host.StopAsync(); + return Task.CompletedTask; + } + + [Command("log")] + public Task TestLogs() + { + _logger.LogTrace("This is a trace log"); + _logger.LogDebug("This is a debug log"); + _logger.LogInformation("This is an information log"); + _logger.LogWarning("This is a warning log"); + _logger.LogError(new InvalidOperationException("Invalid Operation"), "This is a error log with exception"); + _logger.LogCritical(new InvalidOperationException("Invalid Operation"), "This is a critical load with exception"); + + _logger.Log(GetLogLevel(LogSeverity.Error), "Error logged from a Discord LogSeverity.Error"); + _logger.Log(GetLogLevel(LogSeverity.Info), "Information logged from Discord LogSeverity.Info "); + + return Task.CompletedTask; + } + + private static LogLevel GetLogLevel(LogSeverity severity) + => (LogLevel)Math.Abs((int)severity - 5); +} \ No newline at end of file diff --git a/Samples/SampleBotSimple/Program.cs b/Samples/SampleBotSimple/Program.cs index cee9ef0..f176011 100644 --- a/Samples/SampleBotSimple/Program.cs +++ b/Samples/SampleBotSimple/Program.cs @@ -2,44 +2,38 @@ using Discord.Addons.Hosting; using Discord.Commands; using Discord.WebSocket; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Sample.Serilog; +using Sample.Simple; -namespace Sample.Simple -{ - class Program +var host = Host.CreateDefaultBuilder(args) + .ConfigureDiscordHost((context, config) => { - public static void Main(string[] args) + config.SocketConfig = new DiscordSocketConfig { - CreateHostBuilder(args).Build().Run(); - } + LogLevel = LogSeverity.Verbose, + AlwaysDownloadUsers = true, + MessageCacheSize = 200 + }; - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureDiscordHost((context, config) => - { - config.SocketConfig = new DiscordSocketConfig - { - LogLevel = LogSeverity.Verbose, - AlwaysDownloadUsers = true, - MessageCacheSize = 200 - }; + config.Token = context.Configuration["Token"]; + }) + //Omit this if you don't use the command service + .UseCommandService((context, config) => + { + config.DefaultRunMode = RunMode.Async; + config.CaseSensitiveCommands = false; + }) + .UseInteractionService((context, config) => + { + config.LogLevel = LogSeverity.Info; + config.UseCompiledLambda = true; + }) + .ConfigureServices((context, services) => + { + //Add any other services here + services.AddHostedService(); + services.AddHostedService(); + services.AddHostedService(); + services.AddHostedService(); + }).Build(); - config.Token = context.Configuration["Token"]; - }) - //Omit this if you don't use the command service - .UseCommandService((context, config) => - { - config.DefaultRunMode = RunMode.Async; - config.CaseSensitiveCommands = false; - }) - .ConfigureServices((context, services) => - { - //Add any other services here - services.AddHostedService(); - services.AddHostedService(); - services.AddHostedService(); - }); - } -} +await host.RunAsync(); \ No newline at end of file diff --git a/Samples/SampleBotSimple/Properties/launchSettings.json b/Samples/SampleBotSimple/Properties/launchSettings.json index ec046e3..a89f495 100644 --- a/Samples/SampleBotSimple/Properties/launchSettings.json +++ b/Samples/SampleBotSimple/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "Sample.Simple": { "commandName": "Project", - "dotnetRunMessages": "true", + "dotnetRunMessages": true, "environmentVariables": { "DOTNET_ENVIRONMENT": "Development" } diff --git a/Samples/SampleBotSimple/PublicModule.cs b/Samples/SampleBotSimple/PublicModule.cs index 184239a..8a37f1b 100644 --- a/Samples/SampleBotSimple/PublicModule.cs +++ b/Samples/SampleBotSimple/PublicModule.cs @@ -1,56 +1,51 @@ -using System; -using System.Threading.Tasks; -using Discord; +using Discord; using Discord.Commands; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -namespace Sample.Simple +namespace Sample.Simple; + +public class PublicModule : ModuleBase { - public class PublicModule : ModuleBase + private readonly ILogger _logger; + //You can inject the host. This is useful if you want to shutdown the host via a command, but be careful with it. + private readonly IHost _host; + + public PublicModule(IHost host, ILogger logger) + { + _host = host; + _logger = logger; + } + + [Command("ping")] + [Alias("pong", "hello")] + public async Task PingAsync() + { + _logger.LogInformation("User {user} used the ping command!", Context.User.Username); + await ReplyAsync("pong!"); + } + + [Command("shutdown")] + public Task Stop() { - private readonly ILogger _logger; - //You can inject the host. This is useful if you want to shutdown the host via a command, but be careful with it. - private readonly IHost _host; - - public PublicModule(IHost host, ILogger logger) - { - _host = host; - _logger = logger; - } - - [Command("ping")] - [Alias("pong", "hello")] - public async Task PingAsync() - { - _logger.LogInformation($"User {Context.User.Username} used the ping command!"); - await ReplyAsync("pong!"); - } - - [Command("shutdown")] - public Task Stop() - { - _ = _host.StopAsync(); - return Task.CompletedTask; - } - - [Command("log")] - public Task TestLogs() - { - _logger.LogTrace("This is a trace log"); - _logger.LogDebug("This is a debug log"); - _logger.LogInformation("This is an information log"); - _logger.LogWarning("This is a warning log"); - _logger.LogError(new InvalidOperationException("Invalid Operation"), "This is a error log with exception"); - _logger.LogCritical(new InvalidOperationException("Invalid Operation"), "This is a critical load with exception"); - - _logger.Log(GetLogLevel(LogSeverity.Error), "Error logged from a Discord LogSeverity.Error"); - _logger.Log(GetLogLevel(LogSeverity.Info), "Information logged from Discord LogSeverity.Info "); - - return Task.CompletedTask; - } - - private static LogLevel GetLogLevel(LogSeverity severity) - => (LogLevel)Math.Abs((int)severity - 5); + _ = _host.StopAsync(); + return Task.CompletedTask; } -} + + [Command("log")] + public Task TestLogs() + { + _logger.LogTrace("This is a trace log"); + _logger.LogDebug("This is a debug log"); + _logger.LogInformation("This is an information log"); + _logger.LogWarning("This is a warning log"); + _logger.LogError(new InvalidOperationException("Invalid Operation"), "This is a error log with exception"); + _logger.LogCritical(new InvalidOperationException("Invalid Operation"), "This is a critical load with exception"); + + _logger.Log(GetLogLevel(LogSeverity.Error), "Error logged from a Discord LogSeverity.Error"); + _logger.Log(GetLogLevel(LogSeverity.Info), "Information logged from Discord LogSeverity.Info "); + + return Task.CompletedTask; + } + + private static LogLevel GetLogLevel(LogSeverity severity) + => (LogLevel)Math.Abs((int)severity - 5); +} \ No newline at end of file diff --git a/Samples/SampleBotSimple/Sample.Simple.csproj b/Samples/SampleBotSimple/Sample.Simple.csproj index 1b7bf57..57085ea 100644 --- a/Samples/SampleBotSimple/Sample.Simple.csproj +++ b/Samples/SampleBotSimple/Sample.Simple.csproj @@ -1,19 +1,19 @@  - - net5.0 - + + net6.0 + enable + enable + 5751e5fb-2beb-463c-bdb7-03e8e997481b + - - - - - - - + + + + - - - + + + \ No newline at end of file diff --git a/Samples/SampleBotSimple/appsettings.json b/Samples/SampleBotSimple/appsettings.json index 9eec8cb..5d07b87 100644 --- a/Samples/SampleBotSimple/appsettings.json +++ b/Samples/SampleBotSimple/appsettings.json @@ -1,6 +1,7 @@ { "Prefix": "!", "Token": "", + "DevGuild": 0, "Logging": { "LogLevel": { "Default": "Information",