Browse Source

Greatly reduce code complexity & make IF samples functional (#2205)

* Greatly reduce code complexity

* Fixes sharded client IF implementation
tags/3.5.0
Armano den Boef GitHub 3 years ago
parent
commit
47de5a2fb4
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 170 additions and 351 deletions
  1. +0
    -152
      samples/InteractionFramework/CommandHandler.cs
  2. +0
    -0
      samples/InteractionFramework/Enums/ExampleEnum.cs
  3. +81
    -0
      samples/InteractionFramework/InteractionHandler.cs
  4. +0
    -18
      samples/InteractionFramework/Modules/ComponentModule.cs
  5. +48
    -36
      samples/InteractionFramework/Modules/ExampleModule.cs
  6. +0
    -30
      samples/InteractionFramework/Modules/MessageCommandModule.cs
  7. +0
    -51
      samples/InteractionFramework/Modules/SlashCommandModule.cs
  8. +0
    -17
      samples/InteractionFramework/Modules/UserCommandModule.cs
  9. +33
    -42
      samples/InteractionFramework/Program.cs
  10. +1
    -1
      samples/ShardedClient/Modules/InteractionModule.cs
  11. +5
    -2
      samples/ShardedClient/Program.cs
  12. +2
    -2
      samples/ShardedClient/Services/InteractionHandlingService.cs

+ 0
- 152
samples/InteractionFramework/CommandHandler.cs View File

@@ -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<T> 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<T> 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
}
}

samples/InteractionFramework/ExampleEnum.cs → samples/InteractionFramework/Enums/ExampleEnum.cs View File


+ 81
- 0
samples/InteractionFramework/InteractionHandler.cs View File

@@ -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<T> 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<ulong>("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<T> 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());
}
}
}
}

+ 0
- 18
samples/InteractionFramework/Modules/ComponentModule.cs View File

@@ -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<SocketInteractionContext<SocketMessageComponent>>
{
// 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!");
}
}

samples/InteractionFramework/Modules/GeneralModule.cs → samples/InteractionFramework/Modules/ExampleModule.cs View File

@@ -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<SocketInteractionContext>
public class ExampleModule : InteractionModuleBase<SocketInteractionContext>
{
// 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!");
}
}
}
}

+ 0
- 30
samples/InteractionFramework/Modules/MessageCommandModule.cs View File

@@ -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<SocketInteractionContext<SocketMessageCommand>>
{
// 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!");
}
}
}
}

+ 0
- 51
samples/InteractionFramework/Modules/SlashCommandModule.cs View File

@@ -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<SocketInteractionContext<SocketSlashCommand>>
{
// 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}");
}
}
}

+ 0
- 17
samples/InteractionFramework/Modules/UserCommandModule.cs View File

@@ -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<SocketInteractionContext<SocketUserCommand>>
{
// 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}>!");
}
}


+ 33
- 42
samples/InteractionFramework/Program.cs View File

@@ -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<DiscordSocketClient>()
.AddSingleton(x => new InteractionService(x.GetRequiredService<DiscordSocketClient>()))
.AddSingleton<InteractionHandler>()
.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<DiscordSocketClient>();
var commands = services.GetRequiredService<InteractionService>();
public async Task RunAsync()
{
var client = _services.GetRequiredService<DiscordSocketClient>();

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<ulong>("testGuild"), true);
else
await commands.RegisterCommandsGloballyAsync(true);
};

// Here we can initialize the service that will register and execute our commands
await services.GetRequiredService<CommandHandler>().InitializeAsync();
await _services.GetRequiredService<InteractionHandler>()
.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<DiscordSocketClient>()
.AddSingleton(x => new InteractionService(x.GetRequiredService<DiscordSocketClient>()))
.AddSingleton<CommandHandler>()
.BuildServiceProvider();
private async Task LogAsync(LogMessage message)
=> Console.WriteLine(message.ToString());

static bool IsDebug ( )
public static bool IsDebug()
{
#if DEBUG
return true;


+ 1
- 1
samples/ShardedClient/Modules/InteractionModule.cs View File

@@ -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<ShardedInteractionContext<SocketSlashCommand>>
public class InteractionModule : InteractionModuleBase<ShardedInteractionContext>
{
[SlashCommand("info", "Information about this shard.")]
public async Task InfoAsync()


+ 5
- 2
samples/ShardedClient/Program.cs View File

@@ -45,8 +45,11 @@ namespace ShardedClient
client.ShardReady += ReadyAsync;
client.Log += LogAsync;

await services.GetRequiredService<InteractionHandlingService>().InitializeAsync();
await services.GetRequiredService<CommandHandlingService>().InitializeAsync();
await services.GetRequiredService<InteractionHandlingService>()
.InitializeAsync();

await services.GetRequiredService<CommandHandlingService>()
.InitializeAsync();

// Tokens should be considered secret data, and never hard-coded.
await client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token"));


+ 2
- 2
samples/ShardedClient/Services/InteractionHandlingService.cs View File

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



Loading…
Cancel
Save