From 47de5a2fb450a846a805f5185e8bf7bf46c533ca Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Sat, 26 Mar 2022 13:37:30 +0100 Subject: [PATCH] Greatly reduce code complexity & make IF samples functional (#2205) * Greatly reduce code complexity * Fixes sharded client IF implementation --- .../InteractionFramework/CommandHandler.cs | 152 ------------------ .../{ => Enums}/ExampleEnum.cs | 0 .../InteractionHandler.cs | 81 ++++++++++ .../Modules/ComponentModule.cs | 18 --- .../{GeneralModule.cs => ExampleModule.cs} | 84 +++++----- .../Modules/MessageCommandModule.cs | 30 ---- .../Modules/SlashCommandModule.cs | 51 ------ .../Modules/UserCommandModule.cs | 17 -- samples/InteractionFramework/Program.cs | 75 ++++----- .../Modules/InteractionModule.cs | 2 +- samples/ShardedClient/Program.cs | 7 +- .../Services/InteractionHandlingService.cs | 4 +- 12 files changed, 170 insertions(+), 351 deletions(-) delete mode 100644 samples/InteractionFramework/CommandHandler.cs rename samples/InteractionFramework/{ => Enums}/ExampleEnum.cs (100%) create mode 100644 samples/InteractionFramework/InteractionHandler.cs delete mode 100644 samples/InteractionFramework/Modules/ComponentModule.cs rename samples/InteractionFramework/Modules/{GeneralModule.cs => ExampleModule.cs} (54%) delete mode 100644 samples/InteractionFramework/Modules/MessageCommandModule.cs delete mode 100644 samples/InteractionFramework/Modules/SlashCommandModule.cs delete mode 100644 samples/InteractionFramework/Modules/UserCommandModule.cs diff --git a/samples/InteractionFramework/CommandHandler.cs b/samples/InteractionFramework/CommandHandler.cs deleted file mode 100644 index 9a505246f..000000000 --- a/samples/InteractionFramework/CommandHandler.cs +++ /dev/null @@ -1,152 +0,0 @@ -using Discord; -using Discord.Interactions; -using Discord.WebSocket; -using System; -using System.Reflection; -using System.Threading.Tasks; - -namespace InteractionFramework -{ - public class CommandHandler - { - private readonly DiscordSocketClient _client; - private readonly InteractionService _commands; - private readonly IServiceProvider _services; - - public CommandHandler(DiscordSocketClient client, InteractionService commands, IServiceProvider services) - { - _client = client; - _commands = commands; - _services = services; - } - - public async Task InitializeAsync ( ) - { - // Add the public modules that inherit InteractionModuleBase to the InteractionService - await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); - // Another approach to get the assembly of a specific type is: - // typeof(CommandHandler).Assembly - - - // Process the InteractionCreated payloads to execute Interactions commands - _client.InteractionCreated += HandleInteraction; - - // Process the command execution results - _commands.SlashCommandExecuted += SlashCommandExecuted; - _commands.ContextCommandExecuted += ContextCommandExecuted; - _commands.ComponentCommandExecuted += ComponentCommandExecuted; - } - - # region Error Handling - - 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; - } - # endregion - - # region Execution - - 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 _commands.ExecuteCommandAsync(ctx, _services); - } - catch (Exception ex) - { - Console.WriteLine(ex); - - // If a 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(arg.Type == InteractionType.ApplicationCommand) - await arg.GetOriginalResponseAsync().ContinueWith(async (msg) => await msg.Result.DeleteAsync()); - } - } - # endregion - } -} diff --git a/samples/InteractionFramework/ExampleEnum.cs b/samples/InteractionFramework/Enums/ExampleEnum.cs similarity index 100% rename from samples/InteractionFramework/ExampleEnum.cs rename to samples/InteractionFramework/Enums/ExampleEnum.cs diff --git a/samples/InteractionFramework/InteractionHandler.cs b/samples/InteractionFramework/InteractionHandler.cs new file mode 100644 index 000000000..bc6f47285 --- /dev/null +++ b/samples/InteractionFramework/InteractionHandler.cs @@ -0,0 +1,81 @@ +using Discord; +using Discord.Interactions; +using Discord.WebSocket; +using Microsoft.Extensions.Configuration; +using System; +using System.Reflection; +using System.Threading.Tasks; + +namespace InteractionFramework +{ + public class InteractionHandler + { + private readonly DiscordSocketClient _client; + private readonly InteractionService _handler; + private readonly IServiceProvider _services; + private readonly IConfiguration _configuration; + + public InteractionHandler(DiscordSocketClient client, InteractionService handler, IServiceProvider services, IConfiguration config) + { + _client = client; + _handler = handler; + _services = services; + _configuration = config; + } + + public async Task InitializeAsync() + { + // Process when the client is ready, so we can register our commands. + _client.Ready += ReadyAsync; + _handler.Log += LogAsync; + + // Add the public modules that inherit InteractionModuleBase to the InteractionService + await _handler.AddModulesAsync(Assembly.GetEntryAssembly(), _services); + + // Process the InteractionCreated payloads to execute Interactions commands + _client.InteractionCreated += HandleInteraction; + } + + private async Task LogAsync(LogMessage log) + => Console.WriteLine(log); + + private async Task ReadyAsync() + { + // Context & Slash commands can be automatically registered, but this process needs to happen after the client enters the READY state. + // Since Global Commands take around 1 hour to register, we should use a test guild to instantly update and test our commands. + if (Program.IsDebug()) + await _handler.RegisterCommandsToGuildAsync(_configuration.GetValue("testGuild"), true); + else + await _handler.RegisterCommandsGloballyAsync(true); + } + + private async Task HandleInteraction(SocketInteraction interaction) + { + try + { + // 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, _services); + + if (!result.IsSuccess) + switch (result.Error) + { + case InteractionCommandError.UnmetPrecondition: + // implement + break; + default: + break; + } + } + catch + { + // 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()); + } + } + } +} diff --git a/samples/InteractionFramework/Modules/ComponentModule.cs b/samples/InteractionFramework/Modules/ComponentModule.cs deleted file mode 100644 index 643004ded..000000000 --- a/samples/InteractionFramework/Modules/ComponentModule.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Discord.Interactions; -using Discord.WebSocket; -using InteractionFramework.Attributes; -using System.Threading.Tasks; - -namespace InteractionFramework -{ - // As with all other modules, we create the context by defining what type of interaction this module is supposed to target. - internal class ComponentModule : InteractionModuleBase> - { - // With the Attribute DoUserCheck you can make sure that only the user this button targets can click it. This is defined by the first wildcard: *. - // See Attributes/DoUserCheckAttribute.cs for elaboration. - [DoUserCheck] - [ComponentInteraction("myButton:*")] - public async Task ClickButtonAsync(string userId) - => await RespondAsync(text: ":thumbsup: Clicked!"); - } -} diff --git a/samples/InteractionFramework/Modules/GeneralModule.cs b/samples/InteractionFramework/Modules/ExampleModule.cs similarity index 54% rename from samples/InteractionFramework/Modules/GeneralModule.cs rename to samples/InteractionFramework/Modules/ExampleModule.cs index 78740a960..1c0a6c8a2 100644 --- a/samples/InteractionFramework/Modules/GeneralModule.cs +++ b/samples/InteractionFramework/Modules/ExampleModule.cs @@ -1,32 +1,25 @@ using Discord; using Discord.Interactions; +using InteractionFramework.Attributes; +using System; using System.Threading.Tasks; namespace InteractionFramework.Modules { // Interation modules must be public and inherit from an IInterationModuleBase - public class GeneralModule : InteractionModuleBase + public class ExampleModule : InteractionModuleBase { // Dependencies can be accessed through Property injection, public properties with public setters will be set by the service provider public InteractionService Commands { get; set; } - private CommandHandler _handler; + private InteractionHandler _handler; // Constructor injection is also a valid way to access the dependecies - public GeneralModule(CommandHandler handler) + public ExampleModule(InteractionHandler handler) { _handler = handler; } - // Slash Commands are declared using the [SlashCommand], you need to provide a name and a description, both following the Discord guidelines - [SlashCommand("ping", "Recieve a pong")] - // By setting the DefaultPermission to false, you can disable the command by default. No one can use the command until you give them permission - [DefaultPermission(false)] - public async Task Ping ( ) - { - await RespondAsync("pong"); - } - // You can use a number of parameter types in you Slash Command handlers (string, int, double, bool, IUser, IChannel, IMentionable, IRole, Enums) by default. Optionally, // you can implement your own TypeConverters to support a wider range of parameter types. For more information, refer to the library documentation. // Optional method parameters(parameters with a default value) also will be displayed as optional on Discord. @@ -34,9 +27,15 @@ namespace InteractionFramework.Modules // [Summary] lets you customize the name and the description of a parameter [SlashCommand("echo", "Repeat the input")] public async Task Echo(string echo, [Summary(description: "mention the user")]bool mention = false) - { - await RespondAsync(echo + (mention ? Context.User.Mention : string.Empty)); - } + => await RespondAsync(echo + (mention ? Context.User.Mention : string.Empty)); + + [SlashCommand("ping", "Pings the bot and returns its latency.")] + public async Task GreetUserAsync() + => await RespondAsync(text: $":ping_pong: It took me {Context.Client.Latency}ms to respond to you!", ephemeral: true); + + [SlashCommand("bitrate", "Gets the bitrate of a specific voice channel.")] + public async Task GetBitrateAsync([ChannelTypes(ChannelType.Voice, ChannelType.Stage)] IChannel channel) + => await RespondAsync(text: $"This voice channel has a bitrate of {(channel as IVoiceChannel).Bitrate}"); // [Group] will create a command group. [SlashCommand]s and [ComponentInteraction]s will be registered with the group prefix [Group("test_group", "This is a command group")] @@ -46,25 +45,7 @@ namespace InteractionFramework.Modules // choice option [SlashCommand("choice_example", "Enums create choices")] public async Task ChoiceExample(ExampleEnum input) - { - await RespondAsync(input.ToString()); - } - } - - // User Commands can only have one parameter, which must be a type of SocketUser - [UserCommand("SayHello")] - public async Task SayHello(IUser user) - { - await RespondAsync($"Hello, {user.Mention}"); - } - - // Message Commands can only have one parameter, which must be a type of SocketMessage - [MessageCommand("Delete")] - [Attributes.RequireOwner] - public async Task DeleteMesage(IMessage message) - { - await message.DeleteAsync(); - await RespondAsync("Deleted message."); + => await RespondAsync(input.ToString()); } // Use [ComponentInteraction] to handle message component interactions. Message component interaction with the matching customId will be executed. @@ -80,9 +61,40 @@ namespace InteractionFramework.Modules // Select Menu interactions, contain ids of the menu options that were selected by the user. You can access the option ids from the method parameters. // You can also use the wild card pattern with Select Menus, in that case, the wild card captures will be passed on to the method first, followed by the option ids. [ComponentInteraction("roleSelect")] - public async Task RoleSelect(params string[] selections) + public async Task RoleSelect(string[] selections) + { + throw new NotImplementedException(); + } + + // With the Attribute DoUserCheck you can make sure that only the user this button targets can click it. This is defined by the first wildcard: *. + // See Attributes/DoUserCheckAttribute.cs for elaboration. + [DoUserCheck] + [ComponentInteraction("myButton:*")] + public async Task ClickButtonAsync(string userId) + => await RespondAsync(text: ":thumbsup: Clicked!"); + + // This command will greet target user in the channel this was executed in. + [UserCommand("greet")] + public async Task GreetUserAsync(IUser user) + => await RespondAsync(text: $":wave: {Context.User} said hi to you, <@{user.Id}>!"); + + // Pins a message in the channel it is in. + [MessageCommand("pin")] + public async Task PinMessageAsync(IMessage message) { - // implement + // make a safety cast to check if the message is ISystem- or IUserMessage + if (message is not IUserMessage userMessage) + await RespondAsync(text: ":x: You cant pin system messages!"); + + // if the pins in this channel are equal to or above 50, no more messages can be pinned. + else if ((await Context.Channel.GetPinnedMessagesAsync()).Count >= 50) + await RespondAsync(text: ":x: You cant pin any more messages, the max has already been reached in this channel!"); + + else + { + await userMessage.PinAsync(); + await RespondAsync(":white_check_mark: Successfully pinned message!"); + } } } } diff --git a/samples/InteractionFramework/Modules/MessageCommandModule.cs b/samples/InteractionFramework/Modules/MessageCommandModule.cs deleted file mode 100644 index d07d276f5..000000000 --- a/samples/InteractionFramework/Modules/MessageCommandModule.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Discord; -using Discord.Interactions; -using Discord.WebSocket; -using System.Threading.Tasks; - -namespace InteractionFramework.Modules -{ - // A transient module for executing commands. This module will NOT keep any information after the command is executed. - internal class MessageCommandModule : InteractionModuleBase> - { - // Pins a message in the channel it is in. - [MessageCommand("pin")] - public async Task PinMessageAsync(IMessage message) - { - // make a safety cast to check if the message is ISystem- or IUserMessage - if (message is not IUserMessage userMessage) - await RespondAsync(text: ":x: You cant pin system messages!"); - - // if the pins in this channel are equal to or above 50, no more messages can be pinned. - else if ((await Context.Channel.GetPinnedMessagesAsync()).Count >= 50) - await RespondAsync(text: ":x: You cant pin any more messages, the max has already been reached in this channel!"); - - else - { - await userMessage.PinAsync(); - await RespondAsync(":white_check_mark: Successfully pinned message!"); - } - } - } -} diff --git a/samples/InteractionFramework/Modules/SlashCommandModule.cs b/samples/InteractionFramework/Modules/SlashCommandModule.cs deleted file mode 100644 index a066ea18c..000000000 --- a/samples/InteractionFramework/Modules/SlashCommandModule.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Discord; -using Discord.Interactions; -using Discord.WebSocket; -using System; -using System.Threading.Tasks; - -namespace InteractionFramework.Modules -{ - public enum Hobby - { - Gaming, - - Art, - - Reading - } - - // A transient module for executing commands. This module will NOT keep any information after the command is executed. - class SlashCommandModule : InteractionModuleBase> - { - // Will be called before execution. Here you can populate several entities you may want to retrieve before executing a command. - // I.E. database objects - public override void BeforeExecute(ICommandInfo command) - { - // Anything - throw new NotImplementedException(); - } - - // Will be called after execution - public override void AfterExecute(ICommandInfo command) - { - // Anything - throw new NotImplementedException(); - } - - [SlashCommand("ping", "Pings the bot and returns its latency.")] - public async Task GreetUserAsync() - => await RespondAsync(text: $":ping_pong: It took me {Context.Client.Latency}ms to respond to you!", ephemeral: true); - - [SlashCommand("hobby", "Choose your hobby from the list!")] - public async Task ChooseAsync(Hobby hobby) - => await RespondAsync(text: $":thumbsup: Your hobby is: {hobby}."); - - [SlashCommand("bitrate", "Gets the bitrate of a specific voice channel.")] - public async Task GetBitrateAsync([ChannelTypes(ChannelType.Voice, ChannelType.Stage)] IChannel channel) - { - var voiceChannel = channel as IVoiceChannel; - await RespondAsync(text: $"This voice channel has a bitrate of {voiceChannel.Bitrate}"); - } - } -} diff --git a/samples/InteractionFramework/Modules/UserCommandModule.cs b/samples/InteractionFramework/Modules/UserCommandModule.cs deleted file mode 100644 index 60c5246ce..000000000 --- a/samples/InteractionFramework/Modules/UserCommandModule.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Discord; -using Discord.Interactions; -using Discord.WebSocket; -using System.Threading.Tasks; - -namespace InteractionFramework.Modules -{ - // A transient module for executing commands. This module will NOT keep any information after the command is executed. - class UserCommandModule : InteractionModuleBase> - { - // This command will greet target user in the channel this was executed in. - [UserCommand("greet")] - public async Task GreetUserAsync(IUser user) - => await RespondAsync(text: $":wave: {Context.User} said hi to you, <@{user.Id}>!"); - } -} - diff --git a/samples/InteractionFramework/Program.cs b/samples/InteractionFramework/Program.cs index 49db29714..b9c4697af 100644 --- a/samples/InteractionFramework/Program.cs +++ b/samples/InteractionFramework/Program.cs @@ -9,69 +9,60 @@ using System.Threading.Tasks; namespace InteractionFramework { - class Program + public class Program { - // Entry point of the program. - static void Main ( string[] args ) + private readonly IConfiguration _configuration; + private readonly IServiceProvider _services; + + private readonly DiscordSocketConfig _socketConfig = new() + { + GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.GuildMembers, + AlwaysDownloadUsers = true, + }; + + public Program() { - // One of the more flexable ways to access the configuration data is to use the Microsoft's Configuration model, - // this way we can avoid hard coding the environment secrets. I opted to use the Json and environment variable providers here. - IConfiguration config = new ConfigurationBuilder() + _configuration = new ConfigurationBuilder() .AddEnvironmentVariables(prefix: "DC_") .AddJsonFile("appsettings.json", optional: true) .Build(); - RunAsync(config).GetAwaiter().GetResult(); + _services = new ServiceCollection() + .AddSingleton(_configuration) + .AddSingleton(_socketConfig) + .AddSingleton() + .AddSingleton(x => new InteractionService(x.GetRequiredService())) + .AddSingleton() + .BuildServiceProvider(); } - static async Task RunAsync (IConfiguration configuration) - { - // Dependency injection is a key part of the Interactions framework but it needs to be disposed at the end of the app's lifetime. - using var services = ConfigureServices(configuration); + static void Main(string[] args) + => new Program().RunAsync() + .GetAwaiter() + .GetResult(); - var client = services.GetRequiredService(); - var commands = services.GetRequiredService(); + public async Task RunAsync() + { + var client = _services.GetRequiredService(); client.Log += LogAsync; - commands.Log += LogAsync; - - // Slash Commands and Context Commands are can be automatically registered, but this process needs to happen after the client enters the READY state. - // Since Global Commands take around 1 hour to register, we should use a test guild to instantly update and test our commands. To determine the method we should - // register the commands with, we can check whether we are in a DEBUG environment and if we are, we can register the commands to a predetermined test guild. - client.Ready += async ( ) => - { - if (IsDebug()) - // Id of the test guild can be provided from the Configuration object - await commands.RegisterCommandsToGuildAsync(configuration.GetValue("testGuild"), true); - else - await commands.RegisterCommandsGloballyAsync(true); - }; // Here we can initialize the service that will register and execute our commands - await services.GetRequiredService().InitializeAsync(); + await _services.GetRequiredService() + .InitializeAsync(); // Bot token can be provided from the Configuration object we set up earlier - await client.LoginAsync(TokenType.Bot, configuration["token"]); + await client.LoginAsync(TokenType.Bot, _configuration["token"]); await client.StartAsync(); + // Never quit the program until manually forced to. await Task.Delay(Timeout.Infinite); } - static Task LogAsync(LogMessage message) - { - Console.WriteLine(message.ToString()); - return Task.CompletedTask; - } - - static ServiceProvider ConfigureServices ( IConfiguration configuration ) - => new ServiceCollection() - .AddSingleton(configuration) - .AddSingleton() - .AddSingleton(x => new InteractionService(x.GetRequiredService())) - .AddSingleton() - .BuildServiceProvider(); + private async Task LogAsync(LogMessage message) + => Console.WriteLine(message.ToString()); - static bool IsDebug ( ) + public static bool IsDebug() { #if DEBUG return true; diff --git a/samples/ShardedClient/Modules/InteractionModule.cs b/samples/ShardedClient/Modules/InteractionModule.cs index 089328e7d..6c2f0e940 100644 --- a/samples/ShardedClient/Modules/InteractionModule.cs +++ b/samples/ShardedClient/Modules/InteractionModule.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; namespace ShardedClient.Modules { // A display of portability, which shows how minimal the difference between the 2 frameworks is. - public class InteractionModule : InteractionModuleBase> + public class InteractionModule : InteractionModuleBase { [SlashCommand("info", "Information about this shard.")] public async Task InfoAsync() diff --git a/samples/ShardedClient/Program.cs b/samples/ShardedClient/Program.cs index 717ce1d80..2b8f49edb 100644 --- a/samples/ShardedClient/Program.cs +++ b/samples/ShardedClient/Program.cs @@ -45,8 +45,11 @@ namespace ShardedClient client.ShardReady += ReadyAsync; client.Log += LogAsync; - await services.GetRequiredService().InitializeAsync(); - await services.GetRequiredService().InitializeAsync(); + await services.GetRequiredService() + .InitializeAsync(); + + await services.GetRequiredService() + .InitializeAsync(); // Tokens should be considered secret data, and never hard-coded. await client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token")); diff --git a/samples/ShardedClient/Services/InteractionHandlingService.cs b/samples/ShardedClient/Services/InteractionHandlingService.cs index 59b479361..3c41d7f33 100644 --- a/samples/ShardedClient/Services/InteractionHandlingService.cs +++ b/samples/ShardedClient/Services/InteractionHandlingService.cs @@ -31,9 +31,9 @@ namespace ShardedClient.Services { await _service.AddModulesAsync(typeof(InteractionHandlingService).Assembly, _provider); #if DEBUG - await _service.AddCommandsToGuildAsync(_client.Guilds.First(x => x.Id == 1)); + await _service.RegisterCommandsToGuildAsync(1 /* implement */); #else - await _service.AddCommandsGloballyAsync(); + await _service.RegisterCommandsGloballyAsync(); #endif }