Adding Entity guides, flowcharts, better sample system. (#2054)

* initial

* Interaction glossary entry

* Sharded Interaction sample

* Renames into solution

* Debugging samples

* Modify target location for webhookclient

* Finalizing docs work, resolving docfx errors.

* Adding threaduser to user chart

* Add branch info to readme.

* Edits to user chart

* Resolve format for glossary entries

* Patch sln target

* Issue with file naming fixed

* Patch 1/x for builds

* Appending suggestions
This commit is contained in:
Armano den Boef
2022-01-27 14:50:49 +01:00
committed by GitHub
parent b0f59e3eb9
commit b14af1c008
57 changed files with 1059 additions and 344 deletions

View File

@@ -0,0 +1,37 @@
using Discord;
using Discord.Interactions;
using Discord.WebSocket;
using System;
using System.Threading.Tasks;
namespace InteractionFramework.Attributes
{
internal class DoUserCheck : PreconditionAttribute
{
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."));
else
{
// 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 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."));
}
}
}
}

View File

@@ -0,0 +1,27 @@
using Discord;
using Discord.Interactions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace InteractionFramework.Attributes
{
public class RequireOwnerAttribute : PreconditionAttribute
{
public override async Task<PreconditionResult> CheckRequirementsAsync (IInteractionContext context, ICommandInfo commandInfo, IServiceProvider services)
{
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)}.");
}
}
}
}

View File

@@ -0,0 +1,152 @@
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
}
}

View File

@@ -0,0 +1,10 @@
namespace InteractionFramework
{
public enum ExampleEnum
{
First,
Second,
Third,
Fourth
}
}

View File

@@ -0,0 +1,18 @@
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!");
}
}

View File

@@ -0,0 +1,88 @@
using Discord;
using Discord.Interactions;
using System.Threading.Tasks;
namespace InteractionFramework.Modules
{
// Interation modules must be public and inherit from an IInterationModuleBase
public class GeneralModule : 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;
// Constructor injection is also a valid way to access the dependecies
public GeneralModule(CommandHandler 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.
// [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));
}
// [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());
}
}
// 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.");
}
// 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(params string[] selections)
{
// implement
}
}
}

View File

@@ -0,0 +1,30 @@
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!");
}
}
}
}

View File

@@ -0,0 +1,51 @@
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}");
}
}
}

View File

@@ -0,0 +1,17 @@
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}>!");
}
}

View File

@@ -0,0 +1,83 @@
using Discord;
using Discord.Interactions;
using Discord.WebSocket;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace InteractionFramework
{
class Program
{
// Entry point of the program.
static void Main ( string[] args )
{
// 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()
.AddEnvironmentVariables(prefix: "DC_")
.AddJsonFile("appsettings.json", optional: true)
.Build();
RunAsync(config).GetAwaiter().GetResult();
}
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);
var client = services.GetRequiredService<DiscordSocketClient>();
var commands = services.GetRequiredService<InteractionService>();
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();
// Bot token can be provided from the Configuration object we set up earlier
await client.LoginAsync(TokenType.Bot, configuration["token"]);
await client.StartAsync();
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();
static bool IsDebug ( )
{
#if DEBUG
return true;
#else
return false;
#endif
}
}
}

View File

@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<RootNamespace>InteractionFramework</RootNamespace>
<StartupObject></StartupObject>
</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" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Discord.Net.Core\Discord.Net.Core.csproj" />
<ProjectReference Include="..\..\src\Discord.Net.Interactions\Discord.Net.Interactions.csproj" />
<ProjectReference Include="..\..\src\Discord.Net.Rest\Discord.Net.Rest.csproj" />
<ProjectReference Include="..\..\src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" />
</ItemGroup>
</Project>