Update sample projects & samples in docs (#2823)

* update them all

* more docs

* moar docs
This commit is contained in:
Mihail Gribkov
2024-01-11 18:25:56 +03:00
committed by GitHub
parent 8227d70b86
commit e2e8c0fd6a
31 changed files with 732 additions and 806 deletions

View File

@@ -4,35 +4,30 @@ using Discord.WebSocket;
using System;
using System.Threading.Tasks;
namespace InteractionFramework.Attributes
namespace InteractionFramework.Attributes;
internal class DoUserCheck : PreconditionAttribute
{
internal class DoUserCheck : PreconditionAttribute
public override Task<PreconditionResult> CheckRequirementsAsync(IInteractionContext context, ICommandInfo commandInfo, IServiceProvider services)
{
public override Task<PreconditionResult> CheckRequirementsAsync(IInteractionContext context, ICommandInfo commandInfo, IServiceProvider services)
{
// Check if the component matches the target properly.
if (context.Interaction is not SocketMessageComponent componentContext)
return Task.FromResult(PreconditionResult.FromError("Context unrecognized as component context."));
// Check if the component matches the target properly.
if (context.Interaction is not SocketMessageComponent componentContext)
return Task.FromResult(PreconditionResult.FromError("Context unrecognized as component context."));
else
{
// The approach here entirely depends on how you construct your custom ID. In this case, the format is:
// unique-name:*,*
// The approach here entirely depends on how you construct your custom ID. In this case, the format is:
// unique-name:*,*
// here the name and wildcards are split by ':'
var param = componentContext.Data.CustomId.Split(':');
// here the name and wildcards are split by ':'
var param = componentContext.Data.CustomId.Split(':');
// here we determine that we should always check for the first ',' present.
// This will deal with additional wildcards by always selecting the first wildcard present.
if (param.Length > 1 && ulong.TryParse(param[1].Split(',')[0], out ulong id))
return (context.User.Id == id)
// If the user ID
? Task.FromResult(PreconditionResult.FromSuccess())
: Task.FromResult(PreconditionResult.FromError("User ID does not match component ID!"));
// here we determine that we should always check for the first ',' present.
// This will deal with additional wildcards by always selecting the first wildcard present.
if (param.Length > 1 && ulong.TryParse(param[1].Split(',')[0], out ulong id))
return (context.User.Id == id)
// If the user ID
? Task.FromResult(PreconditionResult.FromSuccess())
: Task.FromResult(PreconditionResult.FromError("User ID does not match component ID!"));
else
return Task.FromResult(PreconditionResult.FromError("Parse cannot be done if no userID exists."));
}
}
return Task.FromResult(PreconditionResult.FromError("Parse cannot be done if no userID exists."));
}
}

View File

@@ -1,27 +1,23 @@
using Discord;
using Discord.Interactions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace InteractionFramework.Attributes
namespace InteractionFramework.Attributes;
public class RequireOwnerAttribute : PreconditionAttribute
{
public class RequireOwnerAttribute : PreconditionAttribute
public override async Task<PreconditionResult> CheckRequirementsAsync(IInteractionContext context, ICommandInfo commandInfo, IServiceProvider services)
{
public override async Task<PreconditionResult> CheckRequirementsAsync(IInteractionContext context, ICommandInfo commandInfo, IServiceProvider services)
switch (context.Client.TokenType)
{
switch (context.Client.TokenType)
{
case TokenType.Bot:
var application = await context.Client.GetApplicationInfoAsync().ConfigureAwait(false);
if (context.User.Id != application.Owner.Id)
return PreconditionResult.FromError(ErrorMessage ?? "Command can only be run by the owner of the bot.");
return PreconditionResult.FromSuccess();
default:
return PreconditionResult.FromError($"{nameof(RequireOwnerAttribute)} is not supported by this {nameof(TokenType)}.");
}
case TokenType.Bot:
var application = await context.Client.GetApplicationInfoAsync().ConfigureAwait(false);
return context.User.Id != application.Owner.Id
? PreconditionResult.FromError(ErrorMessage ?? "Command can only be run by the owner of the bot.")
: PreconditionResult.FromSuccess();
default:
return PreconditionResult.FromError($"{nameof(RequireOwnerAttribute)} is not supported by this {nameof(TokenType)}.");
}
}
}

View File

@@ -1,20 +1,13 @@
using Discord.Interactions;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace InteractionFramework
namespace InteractionFramework;
public enum ExampleEnum
{
public enum ExampleEnum
{
First,
Second,
Third,
Fourth,
[ChoiceDisplay("Twenty First")]
TwentyFirst
}
First,
Second,
Third,
Fourth,
[ChoiceDisplay("Twenty First")]
TwentyFirst
}

View File

@@ -6,76 +6,90 @@ using System;
using System.Reflection;
using System.Threading.Tasks;
namespace InteractionFramework
namespace InteractionFramework;
public class InteractionHandler
{
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)
{
private readonly DiscordSocketClient _client;
private readonly InteractionService _handler;
private readonly IServiceProvider _services;
private readonly IConfiguration _configuration;
_client = client;
_handler = handler;
_services = services;
_configuration = config;
}
public InteractionHandler(DiscordSocketClient client, InteractionService handler, IServiceProvider services, IConfiguration 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;
// Also process the result of the command execution.
_handler.InteractionExecuted += HandleInteractionExecute;
}
private async Task LogAsync(LogMessage log)
=> Console.WriteLine(log);
private async Task ReadyAsync()
{
// Register the commands globally.
// alternatively you can use _handler.RegisterCommandsGloballyAsync() to register commands to a specific guild.
await _handler.RegisterCommandsGloballyAsync();
}
private async Task HandleInteraction(SocketInteraction interaction)
{
try
{
_client = client;
_handler = handler;
_services = services;
_configuration = config;
// 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);
// 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;
}
}
public async Task InitializeAsync()
catch
{
// 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());
}
// 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());
}
}
private async Task HandleInteractionExecute(ICommandInfo commandInfo, IInteractionContext context, IResult result)
{
if (!result.IsSuccess)
switch (result.Error)
{
case InteractionCommandError.UnmetPrecondition:
// implement
break;
default:
break;
}
}
}

View File

@@ -4,97 +4,96 @@ using InteractionFramework.Attributes;
using System;
using System.Threading.Tasks;
namespace InteractionFramework.Modules
namespace InteractionFramework.Modules;
// Interaction modules must be public and inherit from an IInteractionModuleBase
public class ExampleModule : InteractionModuleBase<SocketInteractionContext>
{
// Interaction modules must be public and inherit from an IInteractionModuleBase
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 InteractionHandler _handler;
// Constructor injection is also a valid way to access the dependencies
public ExampleModule(InteractionHandler handler)
{
// Dependencies can be accessed through Property injection, public properties with public setters will be set by the service provider
public InteractionService Commands { get; set; }
_handler = handler;
}
private InteractionHandler _handler;
// 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.
// Constructor injection is also a valid way to access the dependencies
public ExampleModule(InteractionHandler handler)
// [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));
[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")]
public class GroupExample : InteractionModuleBase<SocketInteractionContext>
{
// You can create command choices either by using the [Choice] attribute or by creating an enum. Every enum with 25 or less values will be registered as a multiple
// choice option
[SlashCommand("choice_example", "Enums create choices")]
public async Task ChoiceExample(ExampleEnum input)
=> await RespondAsync(input.ToString());
}
// Use [ComponentInteraction] to handle message component interactions. Message component interaction with the matching customId will be executed.
// Alternatively, you can create a wild card pattern using the '*' character. Interaction Service will perform a lazy regex search and capture the matching strings.
// You can then access these capture groups from the method parameters, in the order they were captured. Using the wild card pattern, you can cherry pick component interactions.
[ComponentInteraction("musicSelect:*,*")]
public async Task ButtonPress(string id, string name)
{
// ...
await RespondAsync($"Playing song: {name}/{id}");
}
// 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(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)
{
// 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
{
_handler = handler;
}
// 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.
// [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));
[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")]
public class GroupExample : InteractionModuleBase<SocketInteractionContext>
{
// You can create command choices either by using the [Choice] attribute or by creating an enum. Every enum with 25 or less values will be registered as a multiple
// choice option
[SlashCommand("choice_example", "Enums create choices")]
public async Task ChoiceExample(ExampleEnum input)
=> await RespondAsync(input.ToString());
}
// Use [ComponentInteraction] to handle message component interactions. Message component interaction with the matching customId will be executed.
// Alternatively, you can create a wild card pattern using the '*' character. Interaction Service will perform a lazy regex search and capture the matching strings.
// You can then access these capture groups from the method parameters, in the order they were captured. Using the wild card pattern, you can cherry pick component interactions.
[ComponentInteraction("musicSelect:*,*")]
public async Task ButtonPress(string id, string name)
{
// ...
await RespondAsync($"Playing song: {name}/{id}");
}
// 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(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)
{
// 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!");
}
await userMessage.PinAsync();
await RespondAsync(":white_check_mark: Successfully pinned message!");
}
}
}

View File

@@ -7,68 +7,50 @@ using System;
using System.Threading;
using System.Threading.Tasks;
namespace InteractionFramework
namespace InteractionFramework;
public class Program
{
public class Program
private static IConfiguration _configuration;
private static IServiceProvider _services;
private static readonly DiscordSocketConfig _socketConfig = new()
{
private readonly IConfiguration _configuration;
private readonly IServiceProvider _services;
GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.GuildMembers,
AlwaysDownloadUsers = true,
};
private readonly DiscordSocketConfig _socketConfig = new()
{
GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.GuildMembers,
AlwaysDownloadUsers = true,
};
public static async Task Main(string[] args)
{
_configuration = new ConfigurationBuilder()
.AddEnvironmentVariables(prefix: "DC_")
.AddJsonFile("appsettings.json", optional: true)
.Build();
public Program()
{
_configuration = new ConfigurationBuilder()
.AddEnvironmentVariables(prefix: "DC_")
.AddJsonFile("appsettings.json", optional: true)
.Build();
_services = new ServiceCollection()
.AddSingleton(_configuration)
.AddSingleton(_socketConfig)
.AddSingleton<DiscordSocketClient>()
.AddSingleton(x => new InteractionService(x.GetRequiredService<DiscordSocketClient>()))
.AddSingleton<InteractionHandler>()
.BuildServiceProvider();
_services = new ServiceCollection()
.AddSingleton(_configuration)
.AddSingleton(_socketConfig)
.AddSingleton<DiscordSocketClient>()
.AddSingleton(x => new InteractionService(x.GetRequiredService<DiscordSocketClient>()))
.AddSingleton<InteractionHandler>()
.BuildServiceProvider();
}
var client = _services.GetRequiredService<DiscordSocketClient>();
static void Main(string[] args)
=> new Program().RunAsync()
.GetAwaiter()
.GetResult();
client.Log += LogAsync;
public async Task RunAsync()
{
var client = _services.GetRequiredService<DiscordSocketClient>();
// Here we can initialize the service that will register and execute our commands
await _services.GetRequiredService<InteractionHandler>()
.InitializeAsync();
client.Log += LogAsync;
// Bot token can be provided from the Configuration object we set up earlier
await client.LoginAsync(TokenType.Bot, _configuration["token"]);
await client.StartAsync();
// Here we can initialize the service that will register and execute our commands
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.StartAsync();
// Never quit the program until manually forced to.
await Task.Delay(Timeout.Infinite);
}
private async Task LogAsync(LogMessage message)
=> Console.WriteLine(message.ToString());
public static bool IsDebug()
{
#if DEBUG
return true;
#else
return false;
#endif
}
// Never quit the program until manually forced to.
await Task.Delay(Timeout.Infinite);
}
private static async Task LogAsync(LogMessage message)
=> Console.WriteLine(message.ToString());
}

View File

@@ -8,12 +8,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
<PackageReference Include="Discord.Net.Interactions" Version="3.10.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
<PackageReference Include="Discord.Net.Interactions" Version="3.13.0" />
</ItemGroup>
</Project>