diff --git a/.github/workflows/Manual_Nuget_Push.yml b/.github/workflows/Manual_Nuget_Push.yml new file mode 100644 index 0000000..799fe86 --- /dev/null +++ b/.github/workflows/Manual_Nuget_Push.yml @@ -0,0 +1,37 @@ +# ------------------------------------------------------------------------------ +# +# +# This code was generated. +# +# - To turn off auto-generation set: +# +# [GitHubActions (AutoGenerate = false)] +# +# - To trigger manual generation invoke: +# +# nuke --generate-configuration GitHubActions_Manual_Nuget_Push --host GitHubActions +# +# +# ------------------------------------------------------------------------------ + +name: Manual_Nuget_Push + +on: [workflow_dispatch] + +jobs: + ubuntu-latest: + name: ubuntu-latest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: 'Cache: .nuke/temp, ~/.nuget/packages' + uses: actions/cache@v3 + with: + path: | + .nuke/temp + ~/.nuget/packages + key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props') }} + - name: 'Run: NugetPush' + run: ./build.cmd NugetPush + env: + NugetApiKey: ${{ secrets.NUGET_API_KEY }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2adc60b..f038057 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,23 +1,41 @@ -name: .NET Core Build +# ------------------------------------------------------------------------------ +# +# +# This code was generated. +# +# - To turn off auto-generation set: +# +# [GitHubActions (AutoGenerate = false)] +# +# - To trigger manual generation invoke: +# +# nuke --generate-configuration GitHubActions_Build --host GitHubActions +# +# +# ------------------------------------------------------------------------------ + +name: Build on: push: - branches: [ master ] + branches: + - master pull_request: - branches: [ master ] + branches: + - master jobs: - build: - + ubuntu-latest: + name: ubuntu-latest runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Setup .NET Core - uses: actions/setup-dotnet@v1 - with: - dotnet-version: '6.0.x' - - name: Install dependencies - run: dotnet restore - - name: Build - run: dotnet build --configuration Release --no-restore + - uses: actions/checkout@v3 + - name: 'Cache: .nuke/temp, ~/.nuget/packages' + uses: actions/cache@v3 + with: + path: | + .nuke/temp + ~/.nuget/packages + key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props') }} + - name: 'Run: Compile' + run: ./build.cmd Compile diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json new file mode 100644 index 0000000..4f72a31 --- /dev/null +++ b/.nuke/build.schema.json @@ -0,0 +1,111 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/build", + "title": "Build Schema", + "definitions": { + "build": { + "type": "object", + "properties": { + "Continue": { + "type": "boolean", + "description": "Indicates to continue a previously failed build attempt" + }, + "Help": { + "type": "boolean", + "description": "Shows the help text for this build assembly" + }, + "Host": { + "type": "string", + "description": "Host for execution. Default is 'automatic'", + "enum": [ + "AppVeyor", + "AzurePipelines", + "Bamboo", + "Bitbucket", + "Bitrise", + "GitHubActions", + "GitLab", + "Jenkins", + "Rider", + "SpaceAutomation", + "TeamCity", + "Terminal", + "TravisCI", + "VisualStudio", + "VSCode" + ] + }, + "NoLogo": { + "type": "boolean", + "description": "Disables displaying the NUKE logo" + }, + "NugetApiKey": { + "type": "string", + "description": "Nuget Api Key", + "default": "Secrets must be entered via 'nuke :secrets [profile]'" + }, + "Partition": { + "type": "string", + "description": "Partition to use on CI" + }, + "Plan": { + "type": "boolean", + "description": "Shows the execution plan (HTML)" + }, + "Profile": { + "type": "array", + "description": "Defines the profiles to load", + "items": { + "type": "string" + } + }, + "Root": { + "type": "string", + "description": "Root directory during build execution" + }, + "Skip": { + "type": "array", + "description": "List of targets to be skipped. Empty list skips all dependencies", + "items": { + "type": "string", + "enum": [ + "Clean", + "Compile", + "NugetPack", + "NugetPush", + "Restore" + ] + } + }, + "Solution": { + "type": "string", + "description": "Path to a solution file that is automatically loaded" + }, + "Target": { + "type": "array", + "description": "List of targets to be invoked. Default is '{default_target}'", + "items": { + "type": "string", + "enum": [ + "Clean", + "Compile", + "NugetPack", + "NugetPush", + "Restore" + ] + } + }, + "Verbosity": { + "type": "string", + "description": "Logging verbosity during build execution. Default is 'Normal'", + "enum": [ + "Minimal", + "Normal", + "Quiet", + "Verbose" + ] + } + } + } + } +} diff --git a/.nuke/parameters.json b/.nuke/parameters.json new file mode 100644 index 0000000..b9bef29 --- /dev/null +++ b/.nuke/parameters.json @@ -0,0 +1,4 @@ +{ + "$schema": "./build.schema.json", + "Solution": "Discord.Addons.Hosting.sln" +} diff --git a/Discord.Addons.Hosting.sln b/Discord.Addons.Hosting.sln index bbafa9a..2829839 100644 --- a/Discord.Addons.Hosting.sln +++ b/Discord.Addons.Hosting.sln @@ -11,12 +11,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Simple", "Samples\Sa EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.ShardedClient", "Samples\Sample.ShardedClient\Sample.ShardedClient.csproj", "{540A37B5-E304-49A4-B68E-08941C0D33F1}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "build\_build.csproj", "{5F0002A4-BACE-4097-A1B6-4350CEB9283F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5F0002A4-BACE-4097-A1B6-4350CEB9283F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5F0002A4-BACE-4097-A1B6-4350CEB9283F}.Release|Any CPU.ActiveCfg = Release|Any CPU {03ED5619-5F9E-4CC6-8B8F-7D610C3169EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {03ED5619-5F9E-4CC6-8B8F-7D610C3169EB}.Debug|Any CPU.Build.0 = Debug|Any CPU {03ED5619-5F9E-4CC6-8B8F-7D610C3169EB}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/Discord.Addons.Hosting/Discord.Addons.Hosting.csproj b/Discord.Addons.Hosting/Discord.Addons.Hosting.csproj index 42e509d..2e06711 100644 --- a/Discord.Addons.Hosting/Discord.Addons.Hosting.csproj +++ b/Discord.Addons.Hosting/Discord.Addons.Hosting.csproj @@ -1,9 +1,9 @@  - net6.0 + net6.0;net8.0 Discord.Addons.Hosting - 5.2.0 + 6.0.0 Hawxy Simplifying Discord.Net hosting with .NET Generic Host (Microsoft.Extensions.Hosting) true @@ -12,7 +12,7 @@ https://github.com/Hawxy/Discord.Addons.Hosting git icon.png - Hawxy 2018-2023 + Hawxy 2018-2024 true discord,discord.net,addon,hosting,microsoft.extensions.hosting Enable @@ -21,9 +21,17 @@ - - - + + + + + + + + + + + diff --git a/Discord.Addons.Hosting/DiscordClientService.cs b/Discord.Addons.Hosting/DiscordClientService.cs index 9db82b7..cb82ab3 100644 --- a/Discord.Addons.Hosting/DiscordClientService.cs +++ b/Discord.Addons.Hosting/DiscordClientService.cs @@ -1,7 +1,7 @@ #region License /* - Copyright 2019-2022 Hawxy + Copyright 2019-2024 Hawxy Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Discord.Addons.Hosting/DiscordHostBuilderExtensions.cs b/Discord.Addons.Hosting/DiscordHostBuilderExtensions.cs index 78a4cef..9ab2d0c 100644 --- a/Discord.Addons.Hosting/DiscordHostBuilderExtensions.cs +++ b/Discord.Addons.Hosting/DiscordHostBuilderExtensions.cs @@ -1,6 +1,6 @@ #region License /* - Copyright 2019-2022 Hawxy + Copyright 2019-2024 Hawxy Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,19 +16,15 @@ limitations under the License. */ #endregion -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; namespace Discord.Addons.Hosting; /// -/// Extends with Discord.Net configuration methods. +/// Extensions with Discord.Net configuration options. /// public static class DiscordHostBuilderExtensions { @@ -42,16 +38,13 @@ public static class DiscordHostBuilderExtensions /// 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) + [Obsolete("This extension is obsolete and will be removed in a future version. Replace with builder.Services.AddDiscordShardedHost. See the Discord.Addons.Hosting repository for more information.")] + public static IHostBuilder ConfigureDiscordShardedHost(this IHostBuilder builder, Action config) { - builder.ConfigureDiscordHostInternal(config); - - return builder.ConfigureServices((_, collection) => + ArgumentNullException.ThrowIfNull(config); + return builder.ConfigureServices((context, 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.AddDiscordShardedHost((hostConfig, _) => config(context, hostConfig)); }); } @@ -65,44 +58,14 @@ public static IHostBuilder ConfigureDiscordShardedHost(this IHostBuilder builder /// 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) + [Obsolete("This extension is obsolete and will be removed in a future version. Replace with builder.Services.AddDiscordHost. See the Discord.Addons.Hosting repository for more information.")] + public static IHostBuilder ConfigureDiscordHost(this IHostBuilder builder, Action config) { - 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"); - - collection.AddSingleton(); - }); - } - - private static void ConfigureDiscordHostInternal(this IHostBuilder builder, Action? config = null) where T: BaseSocketClient - { - builder.ConfigureServices((context, collection) => + ArgumentNullException.ThrowIfNull(config); + return builder.ConfigureServices((context, collection) => { - collection.AddOptions().Validate(x => ValidateToken(x.Token)); - - if (config != null) - collection.Configure(x => config(context, x)); - - collection.AddSingleton(typeof(LogAdapter<>)); - collection.AddHostedService>(); + collection.AddDiscordHost((hostConfig, _) => config(context, hostConfig)); }); - - static bool ValidateToken(string token) - { - try - { - TokenUtils.ValidateToken(TokenType.Bot, token); - return true; - } - catch (Exception e) when (e is ArgumentNullException or ArgumentException) - { - return false; - } - } } /// @@ -111,7 +74,9 @@ static bool ValidateToken(string token) /// 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) => { }); + [Obsolete("This extension is obsolete and will be removed in a future version. Replace with builder.Services.AddCommandService. See the Discord.Addons.Hosting repository for more information.")] + public static IHostBuilder UseCommandService(this IHostBuilder builder) => + builder.ConfigureServices((_, collection) => collection.AddCommandService()); /// /// Adds a instance to the host for use with a Discord.NET client. /> @@ -124,19 +89,13 @@ static bool ValidateToken(string token) /// The (generic) host builder. /// Thrown if config is null /// Thrown if is already added to the collection + [Obsolete("This extension is obsolete and will be removed in a future version. Replace with builder.Services.AddCommandService. See the Discord.Addons.Hosting repository for more information.")] public static IHostBuilder UseCommandService(this IHostBuilder builder, Action config) { ArgumentNullException.ThrowIfNull(config); - 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.AddSingleton(); - collection.AddHostedService(); + collection.AddCommandService((commandServiceConfig, _) => config(context, commandServiceConfig)); }); return builder; @@ -148,7 +107,12 @@ public static IHostBuilder UseCommandService(this IHostBuilder builder, ActionThe 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) => { }); + [Obsolete("This extension is obsolete and will be removed in a future version. Replace with builder.Services.AddInteractionService. See the Discord.Addons.Hosting repository for more information.")] + public static IHostBuilder UseInteractionService(this IHostBuilder builder) => builder.ConfigureServices( + (_, collection) => + { + collection.AddInteractionService(); + }); /// @@ -162,19 +126,14 @@ public static IHostBuilder UseCommandService(this IHostBuilder builder, ActionThe (generic) host builder. /// Thrown if config is null /// Thrown if is already added to the collection + [Obsolete("This extension is obsolete and will be removed in a future version. Replace with builder.Services.AddInteractionService. See the Discord.Addons.Hosting repository for more information.")] 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(); + collection.AddInteractionService((interactionConfig, _) => config(context, interactionConfig)); }); return builder; diff --git a/Discord.Addons.Hosting/DiscordHostConfiguration.cs b/Discord.Addons.Hosting/DiscordHostConfiguration.cs index 9eb7a73..fb4c443 100644 --- a/Discord.Addons.Hosting/DiscordHostConfiguration.cs +++ b/Discord.Addons.Hosting/DiscordHostConfiguration.cs @@ -1,6 +1,6 @@ #region License /* - Copyright 2019-2022 Hawxy + Copyright 2019-2024 Hawxy Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Discord.Addons.Hosting/Injectables/InjectableCommandService.cs b/Discord.Addons.Hosting/Injectables/InjectableCommandService.cs index 4bb7418..47ce9cf 100644 --- a/Discord.Addons.Hosting/Injectables/InjectableCommandService.cs +++ b/Discord.Addons.Hosting/Injectables/InjectableCommandService.cs @@ -1,6 +1,6 @@ #region License /* - Copyright 2019-2022 Hawxy + Copyright 2019-2024 Hawxy Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Discord.Addons.Hosting/Injectables/InjectableDiscordClient.cs b/Discord.Addons.Hosting/Injectables/InjectableDiscordClient.cs index 3a304e9..d624d59 100644 --- a/Discord.Addons.Hosting/Injectables/InjectableDiscordClient.cs +++ b/Discord.Addons.Hosting/Injectables/InjectableDiscordClient.cs @@ -1,6 +1,6 @@ #region License /* - Copyright 2019-2022 Hawxy + Copyright 2019-2024 Hawxy Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Discord.Addons.Hosting/Injectables/InjectableInteractionService.cs b/Discord.Addons.Hosting/Injectables/InjectableInteractionService.cs index f41f2a3..c95172b 100644 --- a/Discord.Addons.Hosting/Injectables/InjectableInteractionService.cs +++ b/Discord.Addons.Hosting/Injectables/InjectableInteractionService.cs @@ -1,6 +1,6 @@ #region License /* - Copyright 2019-2022 Hawxy + Copyright 2019-2024 Hawxy Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Discord.Addons.Hosting/ServiceCollectionExtensions.cs b/Discord.Addons.Hosting/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..01a7e48 --- /dev/null +++ b/Discord.Addons.Hosting/ServiceCollectionExtensions.cs @@ -0,0 +1,155 @@ +#region License +/* + Copyright 2019-2024 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.Injectables; +using Discord.Addons.Hosting.Services; +using Discord.Addons.Hosting.Util; +using Discord.Commands; +using Discord.Interactions; +using Discord.WebSocket; +using Microsoft.Extensions.DependencyInjection; + +namespace Discord.Addons.Hosting; + +/// +/// Extensions for registering Discord.NET services with +/// +public static class ServiceCollectionExtensions +{ + /// + /// Adds and optionally configures a along with the required services. + /// + /// + /// A is supplied so you can pull data from additional services if required. + /// + /// The service collection to configure. + /// The delegate for the that will be used to configure the host. + /// Thrown if client is already added to the service collection + public static void AddDiscordShardedHost(this IServiceCollection collection, Action config) + { + collection.AddDiscordHostInternal(config); + + if (collection.Any(x => x.ServiceType.BaseType == typeof(BaseSocketClient))) + throw new InvalidOperationException("Cannot add more than one Discord Client to host"); + + collection.AddSingleton(); + } + + /// + /// Adds and optionally configures a along with the required services. + /// + /// + /// A is supplied so you can pull data from additional services if required. + /// + /// The host builder to configure. + /// The delegate for the that will be used to configure the host. + /// Thrown if client is already added to the service collection + public static void AddDiscordHost(this IServiceCollection builder, Action config) + { + builder.AddDiscordHostInternal(config); + + if (builder.Any(x => x.ServiceType.BaseType == typeof(BaseSocketClient))) + throw new InvalidOperationException("Cannot add more than one Discord Client to host"); + + builder.AddSingleton(); + } + + private static void AddDiscordHostInternal(this IServiceCollection collection, Action config) where T: BaseSocketClient + { + collection.AddOptions() + .Configure(config) + .Validate(x => ValidateToken(x.Token), "Provided bot token is invalid or missing"); + + collection.AddSingleton(typeof(LogAdapter<>)); + collection.AddHostedService>(); + + static bool ValidateToken(string token) + { + try + { + 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 service collection to configure. + /// Thrown if is already added to the collection + public static void AddCommandService(this IServiceCollection collection) => collection.AddCommandService((context, config) => { }); + + /// + /// Adds a instance to the host for use with a Discord.NET client. /> + /// + /// + /// A is supplied so you can pull data from additional services if required. + /// + /// The service collection to configure. + /// The delegate for configuring the that will be used to initialise the service. + /// Thrown if config is null + /// Thrown if is already added to the collection + public static void AddCommandService(this IServiceCollection collection, Action config) + { + ArgumentNullException.ThrowIfNull(config); + + if (collection.Any(x => x.ServiceType == typeof(CommandService))) + throw new InvalidOperationException("Cannot add more than one CommandService to host"); + + collection.AddOptions().Configure(config); + + collection.AddSingleton(); + collection.AddHostedService(); + } + + /// + /// Adds a instance to the host for use with a Discord.NET client. /> + /// + /// The service collection to configure. + /// Thrown if is already added to the collection + public static void AddInteractionService(this IServiceCollection collection) => collection.AddInteractionService((_, _) => { }); + + + /// + /// Adds a instance to the host for use with a Discord.NET client. /> + /// + /// + /// A is supplied so you can pull data from additional services if required. + /// + /// The service collection to configure. + /// The delegate for configuring the that will be used to initialise the service. + /// Thrown if config is null + /// Thrown if is already added to the collection + public static void AddInteractionService(this IServiceCollection collection, Action config) + { + ArgumentNullException.ThrowIfNull(config); + + if (collection.Any(x => x.ServiceType == typeof(InteractionService))) + throw new InvalidOperationException("Cannot add more than one InteractionService to host"); + + collection.AddOptions().Configure(config); + + collection.AddSingleton(); + collection.AddHostedService(); + + } +} \ No newline at end of file diff --git a/Discord.Addons.Hosting/Services/CommandServiceRegistrationHost.cs b/Discord.Addons.Hosting/Services/CommandServiceRegistrationHost.cs index 4a394a5..e687302 100644 --- a/Discord.Addons.Hosting/Services/CommandServiceRegistrationHost.cs +++ b/Discord.Addons.Hosting/Services/CommandServiceRegistrationHost.cs @@ -1,6 +1,6 @@ #region License /* - Copyright 2019-2022 Hawxy + Copyright 2019-2024 Hawxy Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Discord.Addons.Hosting/Services/DiscordHostedService.cs b/Discord.Addons.Hosting/Services/DiscordHostedService.cs index 51b34cd..bcbd9f9 100644 --- a/Discord.Addons.Hosting/Services/DiscordHostedService.cs +++ b/Discord.Addons.Hosting/Services/DiscordHostedService.cs @@ -1,6 +1,6 @@ #region License /* - Copyright 2019-2022 Hawxy + Copyright 2019-2024 Hawxy Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Discord.Addons.Hosting/Services/InteractionServiceRegistrationHost.cs b/Discord.Addons.Hosting/Services/InteractionServiceRegistrationHost.cs index 4dbc7fa..62d143d 100644 --- a/Discord.Addons.Hosting/Services/InteractionServiceRegistrationHost.cs +++ b/Discord.Addons.Hosting/Services/InteractionServiceRegistrationHost.cs @@ -1,6 +1,6 @@ #region License /* - Copyright 2019-2022 Hawxy + Copyright 2019-2024 Hawxy Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Discord.Addons.Hosting/Util/LogAdapter.cs b/Discord.Addons.Hosting/Util/LogAdapter.cs index 130d8a1..ff2950b 100644 --- a/Discord.Addons.Hosting/Util/LogAdapter.cs +++ b/Discord.Addons.Hosting/Util/LogAdapter.cs @@ -1,6 +1,6 @@ #region License /* - Copyright 2019-2022 Hawxy + Copyright 2019-2024 Hawxy Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Discord.Addons.Hosting/Util/ShardedClientExtensions.cs b/Discord.Addons.Hosting/Util/ShardedClientExtensions.cs index a1d9e54..ca3c79c 100644 --- a/Discord.Addons.Hosting/Util/ShardedClientExtensions.cs +++ b/Discord.Addons.Hosting/Util/ShardedClientExtensions.cs @@ -1,6 +1,6 @@ #region License /* - Copyright 2019-2022 Hawxy + Copyright 2019-2024 Hawxy Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Discord.Addons.Hosting/Util/SocketClientExtensions.cs b/Discord.Addons.Hosting/Util/SocketClientExtensions.cs index 9952a8c..c62d03f 100644 --- a/Discord.Addons.Hosting/Util/SocketClientExtensions.cs +++ b/Discord.Addons.Hosting/Util/SocketClientExtensions.cs @@ -1,6 +1,6 @@ #region License /* - Copyright 2019-2022 Hawxy + Copyright 2019-2024 Hawxy Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -50,7 +50,7 @@ public static Task WaitForReadyAsync(this DiscordSocketClient client, Cancellati internal static void RegisterSocketClientReady(this DiscordSocketClient client) { _socketTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - client.Ready += ClientReady; + client.Ready += async () => await ClientReady(); Task ClientReady() { diff --git a/README.md b/README.md index 6b15920..726f0b2 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,9 @@ # Discord.Addons.Hosting -![.NET Core Build](https://github.com/Hawxy/Discord.Addons.Hosting/workflows/.NET%20Core%20Build/badge.svg) [![NuGet](https://img.shields.io/nuget/v/Discord.Addons.Hosting.svg?style=flat-square)](https://www.nuget.org/packages/Discord.Addons.Hosting) ![Nuget](https://img.shields.io/nuget/dt/Discord.Addons.Hosting?style=flat-square) [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 an `IHostedService`, featuring: +This package provides extensions that will run a Discord.NET socket/sharded client as an `IHostedService`, featuring: ✅ Simplified, best practice bot creation with a reduction in boilerplate. @@ -17,52 +16,57 @@ This package provides extensions to a .NET Generic Host (`IHostBuilder`) that wi .NET 6.0+ is required. ```csharp -// CreateDefaultBuilder configures a lot of stuff for us automatically +// CreateApplicationBuilder configures a lot of stuff for us automatically // See: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host -var host = Host.CreateDefaultBuilder() - .ConfigureDiscordHost((context, config) => - { - config.SocketConfig = new DiscordSocketConfig - { - LogLevel = LogSeverity.Verbose, - AlwaysDownloadUsers = true, - MessageCacheSize = 200 - }; - - 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) => +var builder = Host.CreateApplicationBuilder(args); + +// Configure Discord.NET +builder.Services.AddDiscordHost((config, _) => +{ + config.SocketConfig = new DiscordSocketConfig { - //Add any other services here - services.AddHostedService(); - services.AddHostedService(); - services.AddHostedService(); - }).Build(); - + LogLevel = LogSeverity.Verbose, + AlwaysDownloadUsers = true, + MessageCacheSize = 200, + GatewayIntents = GatewayIntents.All + }; + + config.Token = builder.Configuration["Token"]!; +}); + +// Optionally wire up the command service +builder.Services.AddCommandService((config, _) => +{ + config.DefaultRunMode = RunMode.Async; + config.CaseSensitiveCommands = false; +}); + +// Optionally wire up the interaction service +builder.Services.AddInteractionService((config, _) => +{ + config.LogLevel = LogSeverity.Info; + config.UseCompiledLambda = true; +}); +// Add any other services here +builder.Services.AddHostedService(); +builder.Services.AddHostedService(); +builder.Services.AddHostedService(); +builder.Services.AddHostedService(); + +var host = builder.Build(); + await host.RunAsync(); ``` ## Getting Started -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. +1. Create a [.NET 8 Worker Service](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-8.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-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. +7. Create and start your application using a HostBuilder as shown above and in the examples linked below. ## Examples @@ -72,12 +76,12 @@ Fully working examples are available [here](https://github.com/Hawxy/Discord.Add To use the sharded client instead of the socket client, simply replace `ConfigureDiscordHost` with `ConfigureDiscordShardedHost`: ```csharp -.ConfigureDiscordShardedHost((context, config) => +.AddDiscordShardedHost((config, _) => { config.SocketConfig = new DiscordSocketConfig { // Manually set the required shards, or leave empty for the recommended count - TotalShards = 4 + TotalShards = 4 }; config.Token = context.Configuration["token"]; @@ -126,12 +130,7 @@ public class CommandHandler : DiscordClientService } ``` ```csharp - .ConfigureServices((context, services) => -{ - services.AddHostedService(); - //.... -}); - + builder.Services.AddHostedService(); ``` The `WaitForReadyAsync` extension method is also available for both client types to await execution of your service until the client has reached a Ready state: @@ -163,7 +162,7 @@ public class BotStatusService : DiscordClientService When shutdown is requested, the host will wait a maximum of 5 seconds for services to stop before timing out. -If you're finding that this isn't enough time, you can modify the shutdown timeout via the [ShutdownTimeout host setting](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-5.0#shutdowntimeout). +If you're finding that this isn't enough time, you can modify the shutdown timeout via the [ShutdownTimeout host setting](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-8.0#shutdowntimeout). ### IOptions diff --git a/Samples/Sample.ShardedClient/InteractionHandler.cs b/Samples/Sample.ShardedClient/InteractionHandler.cs index a25053c..a49b2fb 100644 --- a/Samples/Sample.ShardedClient/InteractionHandler.cs +++ b/Samples/Sample.ShardedClient/InteractionHandler.cs @@ -7,147 +7,77 @@ namespace Sample.ShardedClient; -// NOTE: This command handler is specifically for using InteractionService-based commands -internal class InteractionHandler : DiscordShardedClientService +public class InteractionHandler : DiscordShardedClientService { + private readonly InteractionService _handler; 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) + public InteractionHandler(DiscordShardedClient client, ILogger logger, InteractionService handler, IServiceProvider provider) : base(client, logger) { + _handler = handler; _provider = provider; - _interactionService = interactionService; - _environment = environment; - _configuration = configuration; } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) { + // Add the public modules that inherit InteractionModuleBase to the InteractionService + await _handler.AddModulesAsync(Assembly.GetEntryAssembly(), _provider); + // 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; + // Also process the result of the command execution. + _handler.InteractionExecuted += HandleInteractionExecute; - await _interactionService.AddModulesAsync(Assembly.GetEntryAssembly(), _provider); - await Client.WaitForReadyAsync(stoppingToken); + await Client.WaitForReadyAsync(cancellationToken); - // 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(); + // Register the commands globally. + // alternatively you can use _handler.RegisterCommandsToGuildAsync() to register commands to a specific guild. + await _handler.RegisterCommandsGloballyAsync(); } - private Task ComponentCommandExecuted(ComponentCommandInfo commandInfo, IInteractionContext context, IResult result) + + private async Task HandleInteraction(SocketInteraction interaction) { - if (!result.IsSuccess) + try { - switch (result.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; - } - } + // Create an execution context that matches the generic type parameter of your InteractionModuleBase modules. + var context = new ShardedInteractionContext(Client, interaction); - return Task.CompletedTask; - } + // Execute the incoming command. + var result = await _handler.ExecuteCommandAsync(context, _provider); - private Task ContextCommandExecuted(ContextCommandInfo context, IInteractionContext arg2, IResult result) - { - if (!result.IsSuccess) + // Due to async nature of InteractionFramework, the result here may always be success. + // That's why we also need to handle the InteractionExecuted event. + if (!result.IsSuccess) + switch (result.Error) + { + case InteractionCommandError.UnmetPrecondition: + // implement + break; + default: + break; + } + } + catch { - switch (result.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; - } + // If Slash Command execution fails it is most likely that the original interaction acknowledgement will persist. It is a good idea to delete the original + // response, or at least let the user know that something went wrong during the command execution. + if (interaction.Type is InteractionType.ApplicationCommand) + await interaction.GetOriginalResponseAsync().ContinueWith(async (msg) => await msg.Result.DeleteAsync()); } - - return Task.CompletedTask; } - private Task SlashCommandExecuted(SlashCommandInfo commandInfo, IInteractionContext context, IResult result) + private async Task HandleInteractionExecute(ICommandInfo commandInfo, IInteractionContext context, IResult result) { if (!result.IsSuccess) - { switch (result.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/Program.cs b/Samples/Sample.ShardedClient/Program.cs index a0dc5a1..70fce50 100644 --- a/Samples/Sample.ShardedClient/Program.cs +++ b/Samples/Sample.ShardedClient/Program.cs @@ -4,37 +4,39 @@ using Discord.WebSocket; using Sample.ShardedClient; -var host = 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.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) => +var builder = Host.CreateApplicationBuilder(args); + +builder.Services.AddDiscordShardedHost((config, _) => +{ + config.SocketConfig = new DiscordSocketConfig { - //Add any other services here - services.AddHostedService(); - services.AddHostedService(); - services.AddHostedService(); - }).Build(); + LogLevel = LogSeverity.Verbose, + AlwaysDownloadUsers = true, + MessageCacheSize = 200, + }; + + config.Token = builder.Configuration["Token"]!; +}); + +builder.Services.AddCommandService((config, _) => +{ + config.DefaultRunMode = RunMode.Async; + config.CaseSensitiveCommands = false; +}); + +builder.Services.AddInteractionService((config, _) => +{ + config.LogLevel = LogSeverity.Info; + config.UseCompiledLambda = true; +}); + + +builder.Services.AddHostedService(); +builder.Services.AddHostedService(); +builder.Services.AddHostedService(); + +var host = builder.Build(); + +await host.RunAsync(); await host.RunAsync(); \ No newline at end of file diff --git a/Samples/Sample.ShardedClient/Sample.ShardedClient.csproj b/Samples/Sample.ShardedClient/Sample.ShardedClient.csproj index cc6c79e..ea9555c 100644 --- a/Samples/Sample.ShardedClient/Sample.ShardedClient.csproj +++ b/Samples/Sample.ShardedClient/Sample.ShardedClient.csproj @@ -1,16 +1,12 @@  - net6.0 + net8.0 enable enable 5751e5fb-2beb-463c-bdb7-03e8e997481b - - - - - + diff --git a/Samples/SampleBotSerilog/InteractionHandler.cs b/Samples/SampleBotSerilog/InteractionHandler.cs index e6b968d..1ac8497 100644 --- a/Samples/SampleBotSerilog/InteractionHandler.cs +++ b/Samples/SampleBotSerilog/InteractionHandler.cs @@ -7,148 +7,77 @@ namespace Sample.Serilog; -// NOTE: This command handler is specifically for using InteractionService-based commands -internal class InteractionHandler : DiscordClientService +public class InteractionHandler : DiscordClientService { + private readonly InteractionService _handler; 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) + public InteractionHandler(DiscordSocketClient client, ILogger logger, InteractionService handler, IServiceProvider provider) : base(client, logger) { + _handler = handler; _provider = provider; - _interactionService = interactionService; - _environment = environment; - _configuration = configuration; } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) { + // Add the public modules that inherit InteractionModuleBase to the InteractionService + await _handler.AddModulesAsync(Assembly.GetEntryAssembly(), _provider); + // 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; + // Also process the result of the command execution. + _handler.InteractionExecuted += HandleInteractionExecute; - await _interactionService.AddModulesAsync(Assembly.GetEntryAssembly(), _provider); - await Client.WaitForReadyAsync(stoppingToken); + await Client.WaitForReadyAsync(cancellationToken); - // 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(); + // Register the commands globally. + // alternatively you can use _handler.RegisterCommandsToGuildAsync() to register commands to a specific guild. + await _handler.RegisterCommandsGloballyAsync(); } - private Task ComponentCommandExecuted(ComponentCommandInfo commandInfo, IInteractionContext context, IResult result) + + private async Task HandleInteraction(SocketInteraction interaction) { - if (!result.IsSuccess) + try { - switch (result.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; - } + // Create an execution context that matches the generic type parameter of your InteractionModuleBase modules. + var context = new SocketInteractionContext(Client, interaction); + + // Execute the incoming command. + var result = await _handler.ExecuteCommandAsync(context, _provider); + + // Due to async nature of InteractionFramework, the result here may always be success. + // That's why we also need to handle the InteractionExecuted event. + if (!result.IsSuccess) + switch (result.Error) + { + case InteractionCommandError.UnmetPrecondition: + // implement + break; + default: + break; + } } - - return Task.CompletedTask; - } - - private Task ContextCommandExecuted(ContextCommandInfo context, IInteractionContext arg2, IResult result) - { - if (!result.IsSuccess) + catch { - switch (result.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; - } + // If Slash Command execution fails it is most likely that the original interaction acknowledgement will persist. It is a good idea to delete the original + // response, or at least let the user know that something went wrong during the command execution. + if (interaction.Type is InteractionType.ApplicationCommand) + await interaction.GetOriginalResponseAsync().ContinueWith(async (msg) => await msg.Result.DeleteAsync()); } - - return Task.CompletedTask; } - private Task SlashCommandExecuted(SlashCommandInfo commandInfo, IInteractionContext context, IResult result) + private async Task HandleInteractionExecute(ICommandInfo commandInfo, IInteractionContext context, IResult result) { if (!result.IsSuccess) - { switch (result.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/Program.cs b/Samples/SampleBotSerilog/Program.cs index ee4902e..5aa71bb 100644 --- a/Samples/SampleBotSerilog/Program.cs +++ b/Samples/SampleBotSerilog/Program.cs @@ -2,11 +2,9 @@ using Discord.Addons.Hosting; using Discord.Commands; using Discord.WebSocket; -using Sample.Serilog; using Serilog; using Serilog.Events; - Log.Logger = new LoggerConfiguration() .MinimumLevel.Information() .MinimumLevel.Override("Microsoft", LogEventLevel.Error) @@ -15,49 +13,46 @@ try { - Log.Information("Starting host"); + var builder = Host.CreateApplicationBuilder(args); - var host = Host.CreateDefaultBuilder(args) - // Serilog.Extensions.Hosting is required. - .UseSerilog() - .ConfigureDiscordHost((context, config) => + // requires Serilog.Extensions.Hosting + builder.Services.AddSerilog(); + + builder.Services.AddDiscordHost((config, _) => + { + config.SocketConfig = new DiscordSocketConfig { - config.SocketConfig = new DiscordSocketConfig - { - LogLevel = LogSeverity.Verbose, - AlwaysDownloadUsers = true, - MessageCacheSize = 200 - }; + LogLevel = LogSeverity.Verbose, + AlwaysDownloadUsers = true, + MessageCacheSize = 200, + GatewayIntents = GatewayIntents.All + }; - config.Token = context.Configuration["Token"]; + config.Token = builder.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}"; + }); - //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(); + builder.Services.AddCommandService((config, _) => + { + config.DefaultRunMode = RunMode.Async; + config.CaseSensitiveCommands = false; + }); + + builder.Services.AddInteractionService((config, _) => + { + config.LogLevel = LogSeverity.Info; + config.UseCompiledLambda = true; + }); + + var host = builder.Build(); await host.RunAsync(); - return 0; } catch (Exception ex) { Log.Fatal(ex, "Host terminated unexpectedly"); - return 1; } finally { diff --git a/Samples/SampleBotSerilog/Sample.Serilog.csproj b/Samples/SampleBotSerilog/Sample.Serilog.csproj index c8265d6..f9ea478 100644 --- a/Samples/SampleBotSerilog/Sample.Serilog.csproj +++ b/Samples/SampleBotSerilog/Sample.Serilog.csproj @@ -1,17 +1,16 @@  - net6.0 + net8.0 enable enable 5751e5fb-2beb-463c-bdb7-03e8e997481b - - - - + + + diff --git a/Samples/SampleBotSimple/BotStatusService.cs b/Samples/SampleBotSimple/BotStatusService.cs index 9cd632a..a8d4626 100644 --- a/Samples/SampleBotSimple/BotStatusService.cs +++ b/Samples/SampleBotSimple/BotStatusService.cs @@ -5,7 +5,7 @@ namespace Sample.Simple; -public class BotStatusService : DiscordClientService +public sealed class BotStatusService : DiscordClientService { public BotStatusService(DiscordSocketClient client, ILogger logger) : base(client, logger) { diff --git a/Samples/SampleBotSimple/InteractionHandler.cs b/Samples/SampleBotSimple/InteractionHandler.cs index accd4fc..4e0090c 100644 --- a/Samples/SampleBotSimple/InteractionHandler.cs +++ b/Samples/SampleBotSimple/InteractionHandler.cs @@ -1,153 +1,83 @@ -using System.Reflection; -using Discord; -using Discord.Addons.Hosting; -using Discord.Addons.Hosting.Util; +using Discord; using Discord.Interactions; using Discord.WebSocket; +using System.Reflection; +using Discord.Addons.Hosting; +using Discord.Addons.Hosting.Util; namespace Sample.Simple; -// NOTE: This command handler is specifically for using InteractionService-based commands -internal class InteractionHandler : DiscordClientService +public class InteractionHandler : DiscordClientService { + private readonly InteractionService _handler; 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) + public InteractionHandler(DiscordSocketClient client, ILogger logger, InteractionService handler, IServiceProvider provider) : base(client, logger) { + _handler = handler; _provider = provider; - _interactionService = interactionService; - _environment = environment; - _configuration = configuration; } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) { + // Add the public modules that inherit InteractionModuleBase to the InteractionService + await _handler.AddModulesAsync(Assembly.GetEntryAssembly(), _provider); + // 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); + // Also process the result of the command execution. + _handler.InteractionExecuted += HandleInteractionExecute; - // 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(); + await Client.WaitForReadyAsync(cancellationToken); + // Register the commands globally. + // alternatively you can use _handler.RegisterCommandsToGuildAsync() to register commands to a specific guild. + await _handler.RegisterCommandsGloballyAsync(); } - private Task ComponentCommandExecuted(ComponentCommandInfo commandInfo, IInteractionContext context, IResult result) + + private async Task HandleInteraction(SocketInteraction interaction) { - if (!result.IsSuccess) + try { - switch (result.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; - } + // Create an execution context that matches the generic type parameter of your InteractionModuleBase modules. + var context = new SocketInteractionContext(Client, interaction); + + // Execute the incoming command. + var result = await _handler.ExecuteCommandAsync(context, _provider); + + // Due to async nature of InteractionFramework, the result here may always be success. + // That's why we also need to handle the InteractionExecuted event. + if (!result.IsSuccess) + switch (result.Error) + { + case InteractionCommandError.UnmetPrecondition: + // implement + break; + default: + break; + } } - - return Task.CompletedTask; - } - - private Task ContextCommandExecuted(ContextCommandInfo context, IInteractionContext arg2, IResult result) - { - if (!result.IsSuccess) + catch { - switch (result.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; - } + // If Slash Command execution fails it is most likely that the original interaction acknowledgement will persist. It is a good idea to delete the original + // response, or at least let the user know that something went wrong during the command execution. + if (interaction.Type is InteractionType.ApplicationCommand) + await interaction.GetOriginalResponseAsync().ContinueWith(async (msg) => await msg.Result.DeleteAsync()); } - - return Task.CompletedTask; } - private Task SlashCommandExecuted(SlashCommandInfo commandInfo, IInteractionContext context, IResult result) + private async Task HandleInteractionExecute(ICommandInfo commandInfo, IInteractionContext context, IResult result) { if (!result.IsSuccess) - { switch (result.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/Program.cs b/Samples/SampleBotSimple/Program.cs index f176011..349ab47 100644 --- a/Samples/SampleBotSimple/Program.cs +++ b/Samples/SampleBotSimple/Program.cs @@ -4,36 +4,40 @@ using Discord.WebSocket; using Sample.Simple; -var host = 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) => +var builder = Host.CreateApplicationBuilder(args); + +builder.Services.AddDiscordHost((config, _) => +{ + config.SocketConfig = new DiscordSocketConfig { - //Add any other services here - services.AddHostedService(); - services.AddHostedService(); - services.AddHostedService(); - services.AddHostedService(); - }).Build(); + LogLevel = LogSeverity.Verbose, + AlwaysDownloadUsers = true, + MessageCacheSize = 200, + GatewayIntents = GatewayIntents.All + }; + + config.Token = builder.Configuration["Token"]!; +}); + + +builder.Services.AddCommandService((config, _) => +{ + config.DefaultRunMode = RunMode.Async; + config.CaseSensitiveCommands = false; +}); + +builder.Services.AddInteractionService((config, _) => +{ + config.LogLevel = LogSeverity.Info; + config.UseCompiledLambda = true; +}); + + +builder.Services.AddHostedService(); +builder.Services.AddHostedService(); +builder.Services.AddHostedService(); +builder.Services.AddHostedService(); + +var host = builder.Build(); await host.RunAsync(); \ No newline at end of file diff --git a/Samples/SampleBotSimple/Sample.Simple.csproj b/Samples/SampleBotSimple/Sample.Simple.csproj index a371282..6557146 100644 --- a/Samples/SampleBotSimple/Sample.Simple.csproj +++ b/Samples/SampleBotSimple/Sample.Simple.csproj @@ -1,17 +1,12 @@  - net6.0 + net8.0 enable enable 5751e5fb-2beb-463c-bdb7-03e8e997481b - - - - - diff --git a/build.cmd b/build.cmd new file mode 100755 index 0000000..b08cc59 --- /dev/null +++ b/build.cmd @@ -0,0 +1,7 @@ +:; set -eo pipefail +:; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) +:; ${SCRIPT_DIR}/build.sh "$@" +:; exit $? + +@ECHO OFF +powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %* diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..c0c0e61 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,74 @@ +[CmdletBinding()] +Param( + [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] + [string[]]$BuildArguments +) + +Write-Output "PowerShell $($PSVersionTable.PSEdition) version $($PSVersionTable.PSVersion)" + +Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { Write-Error $_ -ErrorAction Continue; exit 1 } +$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent + +########################################################################### +# CONFIGURATION +########################################################################### + +$BuildProjectFile = "$PSScriptRoot\build\_build.csproj" +$TempDirectory = "$PSScriptRoot\\.nuke\temp" + +$DotNetGlobalFile = "$PSScriptRoot\\global.json" +$DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1" +$DotNetChannel = "STS" + +$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1 +$env:DOTNET_CLI_TELEMETRY_OPTOUT = 1 +$env:DOTNET_MULTILEVEL_LOOKUP = 0 + +########################################################################### +# EXECUTION +########################################################################### + +function ExecSafe([scriptblock] $cmd) { + & $cmd + if ($LASTEXITCODE) { exit $LASTEXITCODE } +} + +# If dotnet CLI is installed globally and it matches requested version, use for execution +if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and ` + $(dotnet --version) -and $LASTEXITCODE -eq 0) { + $env:DOTNET_EXE = (Get-Command "dotnet").Path +} +else { + # Download install script + $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1" + New-Item -ItemType Directory -Path $TempDirectory -Force | Out-Null + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + (New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile) + + # If global.json exists, load expected version + if (Test-Path $DotNetGlobalFile) { + $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json) + if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) { + $DotNetVersion = $DotNetGlobal.sdk.version + } + } + + # Install by channel or version + $DotNetDirectory = "$TempDirectory\dotnet-win" + if (!(Test-Path variable:DotNetVersion)) { + ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath } + } else { + ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } + } + $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" +} + +Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)" + +if (Test-Path env:NUKE_ENTERPRISE_TOKEN) { + & $env:DOTNET_EXE nuget remove source "nuke-enterprise" > $null + & $env:DOTNET_EXE nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password $env:NUKE_ENTERPRISE_TOKEN > $null +} + +ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet } +ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments } diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..2f10dcb --- /dev/null +++ b/build.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +bash --version 2>&1 | head -n 1 + +set -eo pipefail +SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) + +########################################################################### +# CONFIGURATION +########################################################################### + +BUILD_PROJECT_FILE="$SCRIPT_DIR/build/_build.csproj" +TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp" + +DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json" +DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh" +DOTNET_CHANNEL="STS" + +export DOTNET_CLI_TELEMETRY_OPTOUT=1 +export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 +export DOTNET_MULTILEVEL_LOOKUP=0 + +########################################################################### +# EXECUTION +########################################################################### + +function FirstJsonValue { + perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}" +} + +# If dotnet CLI is installed globally and it matches requested version, use for execution +if [ -x "$(command -v dotnet)" ] && dotnet --version &>/dev/null; then + export DOTNET_EXE="$(command -v dotnet)" +else + # Download install script + DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh" + mkdir -p "$TEMP_DIRECTORY" + curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL" + chmod +x "$DOTNET_INSTALL_FILE" + + # If global.json exists, load expected version + if [[ -f "$DOTNET_GLOBAL_FILE" ]]; then + DOTNET_VERSION=$(FirstJsonValue "version" "$(cat "$DOTNET_GLOBAL_FILE")") + if [[ "$DOTNET_VERSION" == "" ]]; then + unset DOTNET_VERSION + fi + fi + + # Install by channel or version + DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix" + if [[ -z ${DOTNET_VERSION+x} ]]; then + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path + else + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path + fi + export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" +fi + +echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)" + +if [[ ! -z ${NUKE_ENTERPRISE_TOKEN+x} && "$NUKE_ENTERPRISE_TOKEN" != "" ]]; then + "$DOTNET_EXE" nuget remove source "nuke-enterprise" &>/dev/null || true + "$DOTNET_EXE" nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password "$NUKE_ENTERPRISE_TOKEN" --store-password-in-clear-text &>/dev/null || true +fi + +"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet +"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" diff --git a/build/.editorconfig b/build/.editorconfig new file mode 100644 index 0000000..31e43dc --- /dev/null +++ b/build/.editorconfig @@ -0,0 +1,11 @@ +[*.cs] +dotnet_style_qualification_for_field = false:warning +dotnet_style_qualification_for_property = false:warning +dotnet_style_qualification_for_method = false:warning +dotnet_style_qualification_for_event = false:warning +dotnet_style_require_accessibility_modifiers = never:warning + +csharp_style_expression_bodied_methods = true:silent +csharp_style_expression_bodied_properties = true:warning +csharp_style_expression_bodied_indexers = true:warning +csharp_style_expression_bodied_accessors = true:warning diff --git a/build/Build.cs b/build/Build.cs new file mode 100644 index 0000000..f404edc --- /dev/null +++ b/build/Build.cs @@ -0,0 +1,84 @@ +using Nuke.Common; +using Nuke.Common.CI.GitHubActions; +using Nuke.Common.IO; +using Nuke.Common.ProjectModel; +using Nuke.Common.Tools.DotNet; +using static Nuke.Common.Tools.DotNet.DotNetTasks; + +[GitHubActions( + "Build", + GitHubActionsImage.UbuntuLatest, + OnPushBranches = new []{ "master" }, + OnPullRequestBranches = new []{ "master" }, + InvokedTargets = new[] { nameof(Compile) })] +[GitHubActions( + "Manual Nuget Push", + GitHubActionsImage.UbuntuLatest, + On = new[] { GitHubActionsTrigger.WorkflowDispatch }, + InvokedTargets = new[] { nameof(NugetPush) }, + ImportSecrets = new[] { nameof(NugetApiKey) })] +class Build : NukeBuild +{ + /// Support plugins are available for: + /// - JetBrains ReSharper https://nuke.build/resharper + /// - JetBrains Rider https://nuke.build/rider + /// - Microsoft VisualStudio https://nuke.build/visualstudio + /// - Microsoft VSCode https://nuke.build/vscode + + public static int Main () => Execute(x => x.Compile); + + [Solution(GenerateProjects = true)] readonly Solution Solution; + + AbsolutePath ArtifactsDirectory => RootDirectory / "artifacts"; + + Target Clean => _ => _ + .Before(Restore) + .Executes(() => + { + ArtifactsDirectory.CreateOrCleanDirectory(); + }); + + Target Restore => _ => _ + .Executes(() => + { + DotNetRestore(s => s + .SetProjectFile(Solution)); + }); + + Target Compile => _ => _ + .DependsOn(Restore) + .Executes(() => + { + DotNetBuild(s => s + .SetProjectFile(Solution) + .SetConfiguration("Release") + .EnableNoRestore()); + }); + + + Target NugetPack => _ => _ + .DependsOn(Compile) + .Executes(() => + { + DotNetPack(_ => _ + .SetProject(Solution.Discord_Addons_Hosting) + .SetConfiguration("Release") + .EnableContinuousIntegrationBuild() + .SetOutputDirectory(ArtifactsDirectory)); + }); + + [Parameter("Nuget Api Key")] [Secret] readonly string NugetApiKey; + + Target NugetPush => _ => _ + .DependsOn(NugetPack) + .Requires(() => !string.IsNullOrEmpty(NugetApiKey)) + .Executes(() => + { + DotNetNuGetPush(_ => _ + .SetSource("https://api.nuget.org/v3/index.json") + .SetTargetPath(ArtifactsDirectory / "*.nupkg") + .EnableSkipDuplicate() + .EnableNoSymbols() + .SetApiKey(NugetApiKey)); + }); +} diff --git a/build/Directory.Build.props b/build/Directory.Build.props new file mode 100644 index 0000000..e147d63 --- /dev/null +++ b/build/Directory.Build.props @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/build/Directory.Build.targets b/build/Directory.Build.targets new file mode 100644 index 0000000..2532609 --- /dev/null +++ b/build/Directory.Build.targets @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/build/_build.csproj b/build/_build.csproj new file mode 100644 index 0000000..c97b50f --- /dev/null +++ b/build/_build.csproj @@ -0,0 +1,18 @@ + + + + Exe + net6.0 + + CS0649;CS0169;CA1050;CA1822;CA2211;IDE1006 + .. + .. + 1 + false + + + + + + + diff --git a/build/_build.csproj.DotSettings b/build/_build.csproj.DotSettings new file mode 100644 index 0000000..eb3f4c2 --- /dev/null +++ b/build/_build.csproj.DotSettings @@ -0,0 +1,28 @@ + + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + Implicit + Implicit + ExpressionBody + 0 + NEXT_LINE + True + False + 120 + IF_OWNER_IS_SINGLE_LINE + WRAP_IF_LONG + False + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + True + True + True + True + True + True + True + True + True