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

@@ -3,10 +3,8 @@ using Discord.WebSocket;
public class Program public class Program
{ {
private DiscordSocketClient _client; private static DiscordSocketClient _client;
static void Main(string[] args) => new Program().MainAsync().GetAwaiter().GetResult(); public static async Task MainAsync()
public async Task MainAsync()
{ {
// When working with events that have Cacheable<IMessage, ulong> parameters, // When working with events that have Cacheable<IMessage, ulong> parameters,
// you must enable the message cache in your config settings if you plan to // you must enable the message cache in your config settings if you plan to
@@ -27,7 +25,7 @@ public class Program
await Task.Delay(-1); await Task.Delay(-1);
} }
private async Task MessageUpdated(Cacheable<IMessage, ulong> before, SocketMessage after, ISocketMessageChannel channel) private static async Task MessageUpdated(Cacheable<IMessage, ulong> before, SocketMessage after, ISocketMessageChannel channel)
{ {
// If the message was not in the cache, downloading it will result in getting a copy of `after`. // If the message was not in the cache, downloading it will result in getting a copy of `after`.
var message = await before.GetOrDownloadAsync(); var message = await before.GetOrDownloadAsync();

View File

@@ -1,14 +1,6 @@
public class Program public class Program
{ {
private readonly IServiceProvider _serviceProvider; private static IServiceProvider _serviceProvider;
public Program()
{
_serviceProvider = CreateProvider();
}
static void Main(string[] args)
=> new Program().RunAsync(args).GetAwaiter().GetResult();
static IServiceProvider CreateProvider() static IServiceProvider CreateProvider()
{ {
@@ -17,8 +9,8 @@ public class Program
return collection.BuildServiceProvider(); return collection.BuildServiceProvider();
} }
async Task RunAsync(string[] args) static async Task Main(string[] args)
{ {
//... _serviceProvider = CreateProvider();
} }
} }

View File

@@ -1,8 +1,6 @@
public class Program public class Program
{ {
public static Task Main(string[] args) => new Program().MainAsync(); public static async Task Main()
public async Task MainAsync()
{ {
} }
} }

View File

@@ -1,6 +1,6 @@
private DiscordSocketClient _client; private static DiscordSocketClient _client;
public async Task MainAsync() public static async Task Main()
{ {
_client = new DiscordSocketClient(); _client = new DiscordSocketClient();

View File

@@ -1,10 +1,8 @@
public class Program public class Program
{ {
private DiscordSocketClient _client; private static DiscordSocketClient _client;
public static Task Main(string[] args) => new Program().MainAsync(); public async Task Main()
public async Task MainAsync()
{ {
_client = new DiscordSocketClient(); _client = new DiscordSocketClient();
_client.Log += Log; _client.Log += Log;

View File

@@ -1,4 +1,4 @@
private Task Log(LogMessage msg) private static Task Log(LogMessage msg)
{ {
Console.WriteLine(msg.ToString()); Console.WriteLine(msg.ToString());
return Task.CompletedTask; return Task.CompletedTask;

View File

@@ -1,11 +1,11 @@
public async Task MainAsync() public static async Task Main()
{ {
// ... // ...
_client.MessageReceived += MessageReceived; _client.MessageReceived += MessageReceived;
// ... // ...
} }
private async Task MessageReceived(SocketMessage message) private static async Task MessageReceived(SocketMessage message)
{ {
if (message.Content == "!ping") if (message.Content == "!ping")
{ {

View File

@@ -10,21 +10,7 @@ using Discord.WebSocket;
class Program class Program
{ {
// Program entry point // Program entry point
static Task Main(string[] args) static async Task Main(string[] args)
{
// Call the Program constructor, followed by the
// MainAsync method and wait until it finishes (which should be never).
return new Program().MainAsync();
}
private readonly DiscordSocketClient _client;
// Keep the CommandService and DI container around for use with commands.
// These two types require you install the Discord.Net.Commands package.
private readonly CommandService _commands;
private readonly IServiceProvider _services;
private Program()
{ {
_client = new DiscordSocketClient(new DiscordSocketConfig _client = new DiscordSocketClient(new DiscordSocketConfig
{ {
@@ -58,9 +44,15 @@ class Program
// Setup your DI container. // Setup your DI container.
_services = ConfigureServices(); _services = ConfigureServices();
} }
private static DiscordSocketClient _client;
// Keep the CommandService and DI container around for use with commands.
// These two types require you install the Discord.Net.Commands package.
private static CommandService _commands;
private static IServiceProvider _services;
// If any services require the client, or the CommandService, or something else you keep on hand, // If any services require the client, or the CommandService, or something else you keep on hand,
// pass them as parameters into this method as needed. // pass them as parameters into this method as needed.
// If this method is getting pretty long, you can seperate it out into another file using partials. // If this method is getting pretty long, you can seperate it out into another file using partials.
@@ -110,7 +102,7 @@ class Program
return Task.CompletedTask; return Task.CompletedTask;
} }
private async Task MainAsync() private static async Task MainAsync()
{ {
// Centralize the logic for commands into a separate method. // Centralize the logic for commands into a separate method.
await InitCommands(); await InitCommands();
@@ -125,7 +117,7 @@ class Program
await Task.Delay(Timeout.Infinite); await Task.Delay(Timeout.Infinite);
} }
private async Task InitCommands() private static async Task InitCommands()
{ {
// Either search the program and add all Module classes that can be found. // Either search the program and add all Module classes that can be found.
// Module classes MUST be marked 'public' or they will be ignored. // Module classes MUST be marked 'public' or they will be ignored.
@@ -140,7 +132,7 @@ class Program
_client.MessageReceived += HandleCommandAsync; _client.MessageReceived += HandleCommandAsync;
} }
private async Task HandleCommandAsync(SocketMessage arg) private static async Task HandleCommandAsync(SocketMessage arg)
{ {
// Bail out if it's a System Message. // Bail out if it's a System Message.
var msg = arg as SocketUserMessage; var msg = arg as SocketUserMessage;

View File

@@ -7,11 +7,11 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework> <TargetFramework>net6.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Discord.Net" Version="2.0.0" /> <PackageReference Include="Discord.Net" Version="3.13.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -4,117 +4,109 @@ using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace BasicBot namespace BasicBot;
// This is a minimal, bare-bones example of using Discord.Net.
//
// If writing a bot with commands/interactions, we recommend using the Discord.Net.Commands/Discord.Net.Interactions
// framework, rather than handling them yourself, like we do in this sample.
//
// You can find samples of using the command framework:
// - Here, under the TextCommandFramework sample
// - At the guides: https://discordnet.dev/guides/text_commands/intro.html
//
// You can find samples of using the interaction framework:
// - Here, under the InteractionFramework sample
// - At the guides: https://discordnet.dev/guides/int_framework/intro.html
class Program
{ {
// This is a minimal, bare-bones example of using Discord.Net. // Non-static readonly fields can only be assigned in a constructor.
// // If you want to assign it elsewhere, consider removing the readonly keyword.
// If writing a bot with commands/interactions, we recommend using the Discord.Net.Commands/Discord.Net.Interactions private static DiscordSocketClient _client;
// framework, rather than handling them yourself, like we do in this sample.
// // Discord.Net heavily utilizes TAP for async, so we create
// You can find samples of using the command framework: // an asynchronous context from the beginning.
// - Here, under the TextCommandFramework sample public static async Task Main(string[] args)
// - At the guides: https://discordnet.dev/guides/text_commands/intro.html
//
// You can find samples of using the interaction framework:
// - Here, under the InteractionFramework sample
// - At the guides: https://discordnet.dev/guides/int_framework/intro.html
class Program
{ {
// Non-static readonly fields can only be assigned in a constructor. // Config used by DiscordSocketClient
// If you want to assign it elsewhere, consider removing the readonly keyword. // Define intents for the client
private readonly DiscordSocketClient _client; // Note that GatewayIntents.MessageContent is a privileged intent, and requires extra setup in the developer portal.
var config = new DiscordSocketConfig
// Discord.Net heavily utilizes TAP for async, so we create
// an asynchronous context from the beginning.
static void Main(string[] args)
=> new Program()
.MainAsync()
.GetAwaiter()
.GetResult();
public Program()
{ {
// Config used by DiscordSocketClient GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.MessageContent
// Define intents for the client };
var config = new DiscordSocketConfig
{
GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.MessageContent
};
// It is recommended to Dispose of a client when you are finished // It is recommended to Dispose of a client when you are finished
// using it, at the end of your app's lifetime. // using it, at the end of your app's lifetime.
_client = new DiscordSocketClient(config); _client = new DiscordSocketClient(config);
// Subscribing to client events, so that we may receive them whenever they're invoked. // Subscribing to client events, so that we may receive them whenever they're invoked.
_client.Log += LogAsync; _client.Log += LogAsync;
_client.Ready += ReadyAsync; _client.Ready += ReadyAsync;
_client.MessageReceived += MessageReceivedAsync; _client.MessageReceived += MessageReceivedAsync;
_client.InteractionCreated += InteractionCreatedAsync; _client.InteractionCreated += InteractionCreatedAsync;
// Tokens should be considered secret data, and never hard-coded.
await _client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token"));
// Different approaches to making your token a secret is by putting them in local .json, .yaml, .xml or .txt files, then reading them on startup.
await _client.StartAsync();
// Block the program until it is closed.
await Task.Delay(Timeout.Infinite);
}
private static Task LogAsync(LogMessage log)
{
Console.WriteLine(log.ToString());
return Task.CompletedTask;
}
// The Ready event indicates that the client has opened a
// connection and it is now safe to access the cache.
private static Task ReadyAsync()
{
Console.WriteLine($"{_client.CurrentUser} is connected!");
return Task.CompletedTask;
}
// This is not the recommended way to write a bot - consider
// reading over the Commands Framework sample.
private static async Task MessageReceivedAsync(SocketMessage message)
{
// The bot should never respond to itself.
if (message.Author.Id == _client.CurrentUser.Id)
return;
if (message.Content == "!ping")
{
// Create a new ComponentBuilder, in which dropdowns & buttons can be created.
var cb = new ComponentBuilder()
.WithButton("Click me!", "unique-id", ButtonStyle.Primary);
// Send a message with content 'pong', including a button.
// This button needs to be build by calling .Build() before being passed into the call.
await message.Channel.SendMessageAsync("pong!", components: cb.Build());
} }
}
public async Task MainAsync() // For better functionality & a more developer-friendly approach to handling any kind of interaction, refer to:
// https://discordnet.dev/guides/int_framework/intro.html
private static async Task InteractionCreatedAsync(SocketInteraction interaction)
{
// safety-casting is the best way to prevent something being cast from being null.
// If this check does not pass, it could not be cast to said type.
if (interaction is SocketMessageComponent component)
{ {
// Tokens should be considered secret data, and never hard-coded. // Check for the ID created in the button mentioned above.
await _client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token")); if (component.Data.CustomId == "unique-id")
// Different approaches to making your token a secret is by putting them in local .json, .yaml, .xml or .txt files, then reading them on startup. await interaction.RespondAsync("Thank you for clicking my button!");
await _client.StartAsync(); else
Console.WriteLine("An ID has been received that has no handler!");
// Block the program until it is closed.
await Task.Delay(Timeout.Infinite);
}
private Task LogAsync(LogMessage log)
{
Console.WriteLine(log.ToString());
return Task.CompletedTask;
}
// The Ready event indicates that the client has opened a
// connection and it is now safe to access the cache.
private Task ReadyAsync()
{
Console.WriteLine($"{_client.CurrentUser} is connected!");
return Task.CompletedTask;
}
// This is not the recommended way to write a bot - consider
// reading over the Commands Framework sample.
private async Task MessageReceivedAsync(SocketMessage message)
{
// The bot should never respond to itself.
if (message.Author.Id == _client.CurrentUser.Id)
return;
if (message.Content == "!ping")
{
// Create a new ComponentBuilder, in which dropdowns & buttons can be created.
var cb = new ComponentBuilder()
.WithButton("Click me!", "unique-id", ButtonStyle.Primary);
// Send a message with content 'pong', including a button.
// This button needs to be build by calling .Build() before being passed into the call.
await message.Channel.SendMessageAsync("pong!", components: cb.Build());
}
}
// For better functionality & a more developer-friendly approach to handling any kind of interaction, refer to:
// https://discordnet.dev/guides/int_framework/intro.html
private async Task InteractionCreatedAsync(SocketInteraction interaction)
{
// safety-casting is the best way to prevent something being cast from being null.
// If this check does not pass, it could not be cast to said type.
if (interaction is SocketMessageComponent component)
{
// Check for the ID created in the button mentioned above.
if (component.Data.CustomId == "unique-id")
await interaction.RespondAsync("Thank you for clicking my button!");
else
Console.WriteLine("An ID has been received that has no handler!");
}
} }
} }
} }

View File

@@ -6,7 +6,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Discord.Net.WebSocket" Version="3.10.0"/> <PackageReference Include="Discord.Net.WebSocket" Version="3.13.0"/>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -4,35 +4,30 @@ using Discord.WebSocket;
using System; using System;
using System.Threading.Tasks; 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)
// Check if the component matches the target properly. return Task.FromResult(PreconditionResult.FromError("Context unrecognized as component context."));
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 ':' // here the name and wildcards are split by ':'
var param = componentContext.Data.CustomId.Split(':'); var param = componentContext.Data.CustomId.Split(':');
// here we determine that we should always check for the first ',' present. // 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. // 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)) if (param.Length > 1 && ulong.TryParse(param[1].Split(',')[0], out ulong id))
return (context.User.Id == id) return (context.User.Id == id)
// If the user ID // If the user ID
? Task.FromResult(PreconditionResult.FromSuccess()) ? Task.FromResult(PreconditionResult.FromSuccess())
: Task.FromResult(PreconditionResult.FromError("User ID does not match component ID!")); : 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;
using Discord.Interactions; using Discord.Interactions;
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks; 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);
case TokenType.Bot: return context.User.Id != application.Owner.Id
var application = await context.Client.GetApplicationInfoAsync().ConfigureAwait(false); ? PreconditionResult.FromError(ErrorMessage ?? "Command can only be run by the owner of the bot.")
if (context.User.Id != application.Owner.Id) : PreconditionResult.FromSuccess();
return PreconditionResult.FromError(ErrorMessage ?? "Command can only be run by the owner of the bot."); default:
return PreconditionResult.FromSuccess(); return PreconditionResult.FromError($"{nameof(RequireOwnerAttribute)} is not supported by this {nameof(TokenType)}.");
default:
return PreconditionResult.FromError($"{nameof(RequireOwnerAttribute)} is not supported by this {nameof(TokenType)}.");
}
} }
} }
} }

View File

@@ -1,20 +1,13 @@
using Discord.Interactions; 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,
First, Third,
Second, Fourth,
Third, [ChoiceDisplay("Twenty First")]
Fourth, TwentyFirst
[ChoiceDisplay("Twenty First")]
TwentyFirst
}
} }

View File

@@ -6,76 +6,90 @@ using System;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; 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; _client = client;
private readonly InteractionService _handler; _handler = handler;
private readonly IServiceProvider _services; _services = services;
private readonly IConfiguration _configuration; _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; // Create an execution context that matches the generic type parameter of your InteractionModuleBase<T> modules.
_handler = handler; var context = new SocketInteractionContext(_client, interaction);
_services = services;
_configuration = config; // 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;
}
} }
catch
public async Task InitializeAsync()
{ {
// Process when the client is ready, so we can register our commands. // 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
_client.Ready += ReadyAsync; // response, or at least let the user know that something went wrong during the command execution.
_handler.Log += LogAsync; if (interaction.Type is InteractionType.ApplicationCommand)
await interaction.GetOriginalResponseAsync().ContinueWith(async (msg) => await msg.Result.DeleteAsync());
// 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());
}
} }
} }
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;
using System.Threading.Tasks; 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 // Dependencies can be accessed through Property injection, public properties with public setters will be set by the service provider
public class ExampleModule : InteractionModuleBase<SocketInteractionContext> 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 _handler = handler;
public InteractionService Commands { get; set; } }
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 // [Summary] lets you customize the name and the description of a parameter
public ExampleModule(InteractionHandler handler) [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; await userMessage.PinAsync();
} await RespondAsync(":white_check_mark: Successfully pinned message!");
// 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!");
}
} }
} }
} }

View File

@@ -7,68 +7,50 @@ using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; 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; GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.GuildMembers,
private readonly IServiceProvider _services; AlwaysDownloadUsers = true,
};
private readonly DiscordSocketConfig _socketConfig = new() public static async Task Main(string[] args)
{ {
GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.GuildMembers, _configuration = new ConfigurationBuilder()
AlwaysDownloadUsers = true, .AddEnvironmentVariables(prefix: "DC_")
}; .AddJsonFile("appsettings.json", optional: true)
.Build();
public Program() _services = new ServiceCollection()
{ .AddSingleton(_configuration)
_configuration = new ConfigurationBuilder() .AddSingleton(_socketConfig)
.AddEnvironmentVariables(prefix: "DC_") .AddSingleton<DiscordSocketClient>()
.AddJsonFile("appsettings.json", optional: true) .AddSingleton(x => new InteractionService(x.GetRequiredService<DiscordSocketClient>()))
.Build(); .AddSingleton<InteractionHandler>()
.BuildServiceProvider();
_services = new ServiceCollection() var client = _services.GetRequiredService<DiscordSocketClient>();
.AddSingleton(_configuration)
.AddSingleton(_socketConfig)
.AddSingleton<DiscordSocketClient>()
.AddSingleton(x => new InteractionService(x.GetRequiredService<DiscordSocketClient>()))
.AddSingleton<InteractionHandler>()
.BuildServiceProvider();
}
static void Main(string[] args) client.Log += LogAsync;
=> new Program().RunAsync()
.GetAwaiter()
.GetResult();
public async Task RunAsync() // Here we can initialize the service that will register and execute our commands
{ await _services.GetRequiredService<InteractionHandler>()
var client = _services.GetRequiredService<DiscordSocketClient>(); .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 // Never quit the program until manually forced to.
await _services.GetRequiredService<InteractionHandler>() await Task.Delay(Timeout.Infinite);
.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
}
} }
private static async Task LogAsync(LogMessage message)
=> Console.WriteLine(message.ToString());
} }

View File

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

View File

@@ -1,18 +1,16 @@
using Discord.Interactions; using Discord.Interactions;
using Discord.WebSocket;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace ShardedClient.Modules namespace ShardedClient.Modules;
// A display of portability, which shows how minimal the difference between the 2 frameworks is.
public class InteractionModule : InteractionModuleBase<ShardedInteractionContext>
{ {
// A display of portability, which shows how minimal the difference between the 2 frameworks is. [SlashCommand("info", "Information about this shard.")]
public class InteractionModule : InteractionModuleBase<ShardedInteractionContext> public async Task InfoAsync()
{ {
[SlashCommand("info", "Information about this shard.")] var msg = $@"Hi {Context.User}! There are currently {Context.Client.Shards.Count} shards!
public async Task InfoAsync()
{
var msg = $@"Hi {Context.User}! There are currently {Context.Client.Shards.Count} shards!
This guild is being served by shard number {Context.Client.GetShardFor(Context.Guild).ShardId}"; This guild is being served by shard number {Context.Client.GetShardFor(Context.Guild).ShardId}";
await RespondAsync(msg); await RespondAsync(msg);
}
} }
} }

View File

@@ -1,17 +1,16 @@
using Discord.Commands; using Discord.Commands;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace ShardedClient.Modules namespace ShardedClient.Modules;
// Remember to make your module reference the ShardedCommandContext
public class PublicModule : ModuleBase<ShardedCommandContext>
{ {
// Remember to make your module reference the ShardedCommandContext [Command("info")]
public class PublicModule : ModuleBase<ShardedCommandContext> public async Task InfoAsync()
{ {
[Command("info")] var msg = $@"Hi {Context.User}! There are currently {Context.Client.Shards.Count} shards!
public async Task InfoAsync()
{
var msg = $@"Hi {Context.User}! There are currently {Context.Client.Shards.Count} shards!
This guild is being served by shard number {Context.Client.GetShardFor(Context.Guild).ShardId}"; This guild is being served by shard number {Context.Client.GetShardFor(Context.Guild).ShardId}";
await ReplyAsync(msg); await ReplyAsync(msg);
}
} }
} }

View File

@@ -8,78 +8,70 @@ using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace ShardedClient namespace ShardedClient;
// This is a minimal example of using Discord.Net's Sharded Client
// The provided DiscordShardedClient class simplifies having multiple
// DiscordSocketClient instances (or shards) to serve a large number of guilds.
class Program
{ {
// This is a minimal example of using Discord.Net's Sharded Client public static async Task Main(string[] args)
// The provided DiscordShardedClient class simplifies having multiple
// DiscordSocketClient instances (or shards) to serve a large number of guilds.
class Program
{ {
static void Main(string[] args) // You specify the amount of shards you'd like to have with the
=> new Program() // DiscordSocketConfig. Generally, it's recommended to
.MainAsync() // have 1 shard per 1500-2000 guilds your bot is in.
.GetAwaiter() var config = new DiscordSocketConfig
.GetResult();
public async Task MainAsync()
{ {
// You specify the amount of shards you'd like to have with the TotalShards = 2,
// DiscordSocketConfig. Generally, it's recommended to GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.MessageContent
// have 1 shard per 1500-2000 guilds your bot is in. };
var config = new DiscordSocketConfig
{
TotalShards = 2,
GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.MessageContent
};
// You should dispose a service provider created using ASP.NET // You should dispose a service provider created using ASP.NET
// when you are finished using it, at the end of your app's lifetime. // when you are finished using it, at the end of your app's lifetime.
// If you use another dependency injection framework, you should inspect // If you use another dependency injection framework, you should inspect
// its documentation for the best way to do this. // its documentation for the best way to do this.
using (var services = ConfigureServices(config)) await using var services = ConfigureServices(config);
{
var client = services.GetRequiredService<DiscordShardedClient>();
// The Sharded Client does not have a Ready event. var client = services.GetRequiredService<DiscordShardedClient>();
// The ShardReady event is used instead, allowing for individual
// control per shard.
client.ShardReady += ReadyAsync;
client.Log += LogAsync;
await services.GetRequiredService<InteractionHandlingService>() // The Sharded Client does not have a Ready event.
.InitializeAsync(); // The ShardReady event is used instead, allowing for individual
// control per shard.
client.ShardReady += ReadyAsync;
client.Log += LogAsync;
await services.GetRequiredService<CommandHandlingService>() await services.GetRequiredService<InteractionHandlingService>()
.InitializeAsync(); .InitializeAsync();
// Tokens should be considered secret data, and never hard-coded. await services.GetRequiredService<CommandHandlingService>()
await client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token")); .InitializeAsync();
await client.StartAsync();
await Task.Delay(Timeout.Infinite); // Tokens should be considered secret data, and never hard-coded.
} await client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token"));
} await client.StartAsync();
private ServiceProvider ConfigureServices(DiscordSocketConfig config) await Task.Delay(Timeout.Infinite);
=> new ServiceCollection() }
.AddSingleton(new DiscordShardedClient(config))
.AddSingleton<CommandService>() private static ServiceProvider ConfigureServices(DiscordSocketConfig config)
.AddSingleton(x => new InteractionService(x.GetRequiredService<DiscordShardedClient>())) => new ServiceCollection()
.AddSingleton<CommandHandlingService>() .AddSingleton(new DiscordShardedClient(config))
.AddSingleton<InteractionHandlingService>() .AddSingleton<CommandService>()
.BuildServiceProvider(); .AddSingleton(x => new InteractionService(x.GetRequiredService<DiscordShardedClient>()))
.AddSingleton<CommandHandlingService>()
.AddSingleton<InteractionHandlingService>()
.BuildServiceProvider();
private Task ReadyAsync(DiscordSocketClient shard) private static Task ReadyAsync(DiscordSocketClient shard)
{ {
Console.WriteLine($"Shard Number {shard.ShardId} is connected and ready!"); Console.WriteLine($"Shard Number {shard.ShardId} is connected and ready!");
return Task.CompletedTask; return Task.CompletedTask;
} }
private Task LogAsync(LogMessage log) private static Task LogAsync(LogMessage log)
{ {
Console.WriteLine(log.ToString()); Console.WriteLine(log.ToString());
return Task.CompletedTask; return Task.CompletedTask;
}
} }
} }

View File

@@ -6,67 +6,66 @@ using System;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace ShardedClient.Services namespace ShardedClient.Services;
public class CommandHandlingService
{ {
public class CommandHandlingService private readonly CommandService _commands;
private readonly DiscordShardedClient _discord;
private readonly IServiceProvider _services;
public CommandHandlingService(IServiceProvider services)
{ {
private readonly CommandService _commands; _commands = services.GetRequiredService<CommandService>();
private readonly DiscordShardedClient _discord; _discord = services.GetRequiredService<DiscordShardedClient>();
private readonly IServiceProvider _services; _services = services;
public CommandHandlingService(IServiceProvider services) _commands.CommandExecuted += CommandExecutedAsync;
{ _commands.Log += LogAsync;
_commands = services.GetRequiredService<CommandService>(); _discord.MessageReceived += MessageReceivedAsync;
_discord = services.GetRequiredService<DiscordShardedClient>(); }
_services = services;
_commands.CommandExecuted += CommandExecutedAsync; public async Task InitializeAsync()
_commands.Log += LogAsync; {
_discord.MessageReceived += MessageReceivedAsync; await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services);
} }
public async Task InitializeAsync() public async Task MessageReceivedAsync(SocketMessage rawMessage)
{ {
await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); // Ignore system messages, or messages from other bots
} if (rawMessage is not SocketUserMessage message)
return;
if (message.Source != MessageSource.User)
return;
public async Task MessageReceivedAsync(SocketMessage rawMessage) // This value holds the offset where the prefix ends
{ var argPos = 0;
// Ignore system messages, or messages from other bots if (!message.HasMentionPrefix(_discord.CurrentUser, ref argPos))
if (rawMessage is not SocketUserMessage message) return;
return;
if (message.Source != MessageSource.User)
return;
// This value holds the offset where the prefix ends // A new kind of command context, ShardedCommandContext can be utilized with the commands framework
var argPos = 0; var context = new ShardedCommandContext(_discord, message);
if (!message.HasMentionPrefix(_discord.CurrentUser, ref argPos)) await _commands.ExecuteAsync(context, argPos, _services);
return; }
// A new kind of command context, ShardedCommandContext can be utilized with the commands framework public async Task CommandExecutedAsync(Optional<CommandInfo> command, ICommandContext context, IResult result)
var context = new ShardedCommandContext(_discord, message); {
await _commands.ExecuteAsync(context, argPos, _services); // command is unspecified when there was a search failure (command not found); we don't care about these errors
} if (!command.IsSpecified)
return;
public async Task CommandExecutedAsync(Optional<CommandInfo> command, ICommandContext context, IResult result) // the command was successful, we don't care about this result, unless we want to log that a command succeeded.
{ if (result.IsSuccess)
// command is unspecified when there was a search failure (command not found); we don't care about these errors return;
if (!command.IsSpecified)
return;
// the command was successful, we don't care about this result, unless we want to log that a command succeeded. // the command failed, let's notify the user that something happened.
if (result.IsSuccess) await context.Channel.SendMessageAsync($"error: {result}");
return; }
// the command failed, let's notify the user that something happened. private Task LogAsync(LogMessage log)
await context.Channel.SendMessageAsync($"error: {result}"); {
} Console.WriteLine(log.ToString());
private Task LogAsync(LogMessage log) return Task.CompletedTask;
{
Console.WriteLine(log.ToString());
return Task.CompletedTask;
}
} }
} }

View File

@@ -1,62 +1,65 @@
using Discord; using Discord;
using Discord.Interactions; using Discord.Interactions;
using Discord.WebSocket; using Discord.WebSocket;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using System; using System;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace ShardedClient.Services namespace ShardedClient.Services;
public class InteractionHandlingService
{ {
public class InteractionHandlingService private readonly InteractionService _service;
private readonly DiscordShardedClient _client;
private readonly IServiceProvider _provider;
public InteractionHandlingService(IServiceProvider services)
{ {
private readonly InteractionService _service; _service = services.GetRequiredService<InteractionService>();
private readonly DiscordShardedClient _client; _client = services.GetRequiredService<DiscordShardedClient>();
private readonly IServiceProvider _provider; _provider = services;
public InteractionHandlingService(IServiceProvider services) _service.Log += LogAsync;
_client.InteractionCreated += OnInteractionAsync;
_client.ShardReady += ReadyAsync;
// For examples on how to handle post execution,
// see the InteractionFramework samples.
}
// Register all modules, and add the commands from these modules to either guild or globally depending on the build state.
public async Task InitializeAsync()
{
await _service.AddModulesAsync(typeof(InteractionHandlingService).Assembly, _provider);
}
private async Task OnInteractionAsync(SocketInteraction interaction)
{
_ = Task.Run(async () =>
{ {
_service = services.GetRequiredService<InteractionService>(); var context = new ShardedInteractionContext(_client, interaction);
_client = services.GetRequiredService<DiscordShardedClient>(); await _service.ExecuteCommandAsync(context, _provider);
_provider = services; });
await Task.CompletedTask;
}
_service.Log += LogAsync; private Task LogAsync(LogMessage log)
_client.InteractionCreated += OnInteractionAsync; {
_client.ShardReady += ReadyAsync; Console.WriteLine(log.ToString());
// For examples on how to handle post execution,
// see the InteractionFramework samples.
}
// Register all modules, and add the commands from these modules to either guild or globally depending on the build state. return Task.CompletedTask;
public async Task InitializeAsync() }
private bool _hasRegistered = false;
private async Task ReadyAsync(DiscordSocketClient _)
{
// ShardReady is called for each shard; to avoid getting ratelimited we only want to register commands once.
if (!_hasRegistered)
{ {
await _service.AddModulesAsync(typeof(InteractionHandlingService).Assembly, _provider);
}
private async Task OnInteractionAsync(SocketInteraction interaction)
{
_ = Task.Run(async () =>
{
var context = new ShardedInteractionContext(_client, interaction);
await _service.ExecuteCommandAsync(context, _provider);
});
await Task.CompletedTask;
}
private Task LogAsync(LogMessage log)
{
Console.WriteLine(log.ToString());
return Task.CompletedTask;
}
private async Task ReadyAsync(DiscordSocketClient _)
{
#if DEBUG
await _service.RegisterCommandsToGuildAsync(1 /* implement */);
#else
await _service.RegisterCommandsGloballyAsync(); await _service.RegisterCommandsGloballyAsync();
#endif _hasRegistered = true;
} }
} }
} }

View File

@@ -7,8 +7,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Discord.Net" Version="3.10.0" /> <PackageReference Include="Discord.Net" Version="3.13.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -4,66 +4,65 @@ using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using TextCommandFramework.Services; using TextCommandFramework.Services;
namespace TextCommandFramework.Modules namespace TextCommandFramework.Modules;
// Modules must be public and inherit from an IModuleBase
public class PublicModule : ModuleBase<SocketCommandContext>
{ {
// Modules must be public and inherit from an IModuleBase // Dependency Injection will fill this value in for us
public class PublicModule : ModuleBase<SocketCommandContext> public PictureService PictureService { get; set; }
[Command("ping")]
[Alias("pong", "hello")]
public Task PingAsync()
=> ReplyAsync("pong!");
[Command("cat")]
public async Task CatAsync()
{ {
// Dependency Injection will fill this value in for us // Get a stream containing an image of a cat
public PictureService PictureService { get; set; } var stream = await PictureService.GetCatPictureAsync();
// Streams must be seeked to their beginning before being uploaded!
[Command("ping")] stream.Seek(0, SeekOrigin.Begin);
[Alias("pong", "hello")] await Context.Channel.SendFileAsync(stream, "cat.png");
public Task PingAsync()
=> ReplyAsync("pong!");
[Command("cat")]
public async Task CatAsync()
{
// Get a stream containing an image of a cat
var stream = await PictureService.GetCatPictureAsync();
// Streams must be seeked to their beginning before being uploaded!
stream.Seek(0, SeekOrigin.Begin);
await Context.Channel.SendFileAsync(stream, "cat.png");
}
// Get info on a user, or the user who invoked the command if one is not specified
[Command("userinfo")]
public async Task UserInfoAsync(IUser user = null)
{
user ??= Context.User;
await ReplyAsync(user.ToString());
}
// Ban a user
[Command("ban")]
[RequireContext(ContextType.Guild)]
// make sure the user invoking the command can ban
[RequireUserPermission(GuildPermission.BanMembers)]
// make sure the bot itself can ban
[RequireBotPermission(GuildPermission.BanMembers)]
public async Task BanUserAsync(IGuildUser user, [Remainder] string reason = null)
{
await user.Guild.AddBanAsync(user, reason: reason);
await ReplyAsync("ok!");
}
// [Remainder] takes the rest of the command's arguments as one argument, rather than splitting every space
[Command("echo")]
public Task EchoAsync([Remainder] string text)
// Insert a ZWSP before the text to prevent triggering other bots!
=> ReplyAsync('\u200B' + text);
// 'params' will parse space-separated elements into a list
[Command("list")]
public Task ListAsync(params string[] objects)
=> ReplyAsync("You listed: " + string.Join("; ", objects));
// Setting a custom ErrorMessage property will help clarify the precondition error
[Command("guild_only")]
[RequireContext(ContextType.Guild, ErrorMessage = "Sorry, this command must be ran from within a server, not a DM!")]
public Task GuildOnlyCommand()
=> ReplyAsync("Nothing to see here!");
} }
// Get info on a user, or the user who invoked the command if one is not specified
[Command("userinfo")]
public async Task UserInfoAsync(IUser user = null)
{
user ??= Context.User;
await ReplyAsync(user.ToString());
}
// Ban a user
[Command("ban")]
[RequireContext(ContextType.Guild)]
// make sure the user invoking the command can ban
[RequireUserPermission(GuildPermission.BanMembers)]
// make sure the bot itself can ban
[RequireBotPermission(GuildPermission.BanMembers)]
public async Task BanUserAsync(IGuildUser user, [Remainder] string reason = null)
{
await user.Guild.AddBanAsync(user, reason: reason);
await ReplyAsync("ok!");
}
// [Remainder] takes the rest of the command's arguments as one argument, rather than splitting every space
[Command("echo")]
public Task EchoAsync([Remainder] string text)
// Insert a ZWSP before the text to prevent triggering other bots!
=> ReplyAsync('\u200B' + text);
// 'params' will parse space-separated elements into a list
[Command("list")]
public Task ListAsync(params string[] objects)
=> ReplyAsync("You listed: " + string.Join("; ", objects));
// Setting a custom ErrorMessage property will help clarify the precondition error
[Command("guild_only")]
[RequireContext(ContextType.Guild, ErrorMessage = "Sorry, this command must be ran from within a server, not a DM!")]
public Task GuildOnlyCommand()
=> ReplyAsync("Nothing to see here!");
} }

View File

@@ -8,68 +8,62 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using TextCommandFramework.Services; using TextCommandFramework.Services;
namespace TextCommandFramework namespace TextCommandFramework;
// This is a minimal example of using Discord.Net's command
// framework - by no means does it show everything the framework
// is capable of.
//
// You can find samples of using the command framework:
// - Here, under the 02_commands_framework sample
// - https://github.com/foxbot/DiscordBotBase - a bare-bones bot template
// - https://github.com/foxbot/patek - a more feature-filled bot, utilizing more aspects of the library
class Program
{ {
// This is a minimal example of using Discord.Net's command // There is no need to implement IDisposable like before as we are
// framework - by no means does it show everything the framework // using dependency injection, which handles calling Dispose for us.
// is capable of. public static async Task Main(string[] args)
//
// You can find samples of using the command framework:
// - Here, under the 02_commands_framework sample
// - https://github.com/foxbot/DiscordBotBase - a bare-bones bot template
// - https://github.com/foxbot/patek - a more feature-filled bot, utilizing more aspects of the library
class Program
{ {
// There is no need to implement IDisposable like before as we are // You should dispose a service provider created using ASP.NET
// using dependency injection, which handles calling Dispose for us. // when you are finished using it, at the end of your app's lifetime.
static void Main(string[] args) // If you use another dependency injection framework, you should inspect
=> new Program().MainAsync().GetAwaiter().GetResult(); // its documentation for the best way to do this.
await using var services = ConfigureServices();
var client = services.GetRequiredService<DiscordSocketClient>();
public async Task MainAsync() client.Log += LogAsync;
{ services.GetRequiredService<CommandService>().Log += LogAsync;
// You should dispose a service provider created using ASP.NET
// when you are finished using it, at the end of your app's lifetime. // Tokens should be considered secret data and never hard-coded.
// If you use another dependency injection framework, you should inspect // We can read from the environment variable to avoid hard coding.
// its documentation for the best way to do this. await client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token"));
using (var services = ConfigureServices()) await client.StartAsync();
// Here we initialize the logic required to register our commands.
await services.GetRequiredService<CommandHandlingService>().InitializeAsync();
await Task.Delay(Timeout.Infinite);
}
private static Task LogAsync(LogMessage log)
{
Console.WriteLine(log.ToString());
return Task.CompletedTask;
}
private static ServiceProvider ConfigureServices()
{
return new ServiceCollection()
.AddSingleton(new DiscordSocketConfig
{ {
var client = services.GetRequiredService<DiscordSocketClient>(); GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.MessageContent
})
client.Log += LogAsync; .AddSingleton<DiscordSocketClient>()
services.GetRequiredService<CommandService>().Log += LogAsync; .AddSingleton<CommandService>()
.AddSingleton<CommandHandlingService>()
// Tokens should be considered secret data and never hard-coded. .AddSingleton<HttpClient>()
// We can read from the environment variable to avoid hard coding. .AddSingleton<PictureService>()
await client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token")); .BuildServiceProvider();
await client.StartAsync();
// Here we initialize the logic required to register our commands.
await services.GetRequiredService<CommandHandlingService>().InitializeAsync();
await Task.Delay(Timeout.Infinite);
}
}
private Task LogAsync(LogMessage log)
{
Console.WriteLine(log.ToString());
return Task.CompletedTask;
}
private ServiceProvider ConfigureServices()
{
return new ServiceCollection()
.AddSingleton(new DiscordSocketConfig
{
GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.MessageContent
})
.AddSingleton<DiscordSocketClient>()
.AddSingleton<CommandService>()
.AddSingleton<CommandHandlingService>()
.AddSingleton<HttpClient>()
.AddSingleton<PictureService>()
.BuildServiceProvider();
}
} }
} }

View File

@@ -6,70 +6,69 @@ using System;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace TextCommandFramework.Services namespace TextCommandFramework.Services;
public class CommandHandlingService
{ {
public class CommandHandlingService private readonly CommandService _commands;
private readonly DiscordSocketClient _discord;
private readonly IServiceProvider _services;
public CommandHandlingService(IServiceProvider services)
{ {
private readonly CommandService _commands; _commands = services.GetRequiredService<CommandService>();
private readonly DiscordSocketClient _discord; _discord = services.GetRequiredService<DiscordSocketClient>();
private readonly IServiceProvider _services; _services = services;
public CommandHandlingService(IServiceProvider services) // Hook CommandExecuted to handle post-command-execution logic.
{ _commands.CommandExecuted += CommandExecutedAsync;
_commands = services.GetRequiredService<CommandService>(); // Hook MessageReceived so we can process each message to see
_discord = services.GetRequiredService<DiscordSocketClient>(); // if it qualifies as a command.
_services = services; _discord.MessageReceived += MessageReceivedAsync;
}
// Hook CommandExecuted to handle post-command-execution logic. public async Task InitializeAsync()
_commands.CommandExecuted += CommandExecutedAsync; {
// Hook MessageReceived so we can process each message to see // Register modules that are public and inherit ModuleBase<T>.
// if it qualifies as a command. await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services);
_discord.MessageReceived += MessageReceivedAsync; }
}
public async Task InitializeAsync() public async Task MessageReceivedAsync(SocketMessage rawMessage)
{ {
// Register modules that are public and inherit ModuleBase<T>. // Ignore system messages, or messages from other bots
await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); if (!(rawMessage is SocketUserMessage message))
} return;
if (message.Source != MessageSource.User)
return;
public async Task MessageReceivedAsync(SocketMessage rawMessage) // This value holds the offset where the prefix ends
{ var argPos = 0;
// Ignore system messages, or messages from other bots // Perform prefix check. You may want to replace this with
if (!(rawMessage is SocketUserMessage message)) // (!message.HasCharPrefix('!', ref argPos))
return; // for a more traditional command format like !help.
if (message.Source != MessageSource.User) if (!message.HasMentionPrefix(_discord.CurrentUser, ref argPos))
return; return;
// This value holds the offset where the prefix ends var context = new SocketCommandContext(_discord, message);
var argPos = 0; // Perform the execution of the command. In this method,
// Perform prefix check. You may want to replace this with // the command service will perform precondition and parsing check
// (!message.HasCharPrefix('!', ref argPos)) // then execute the command if one is matched.
// for a more traditional command format like !help. await _commands.ExecuteAsync(context, argPos, _services);
if (!message.HasMentionPrefix(_discord.CurrentUser, ref argPos)) // Note that normally a result will be returned by this format, but here
return; // we will handle the result in CommandExecutedAsync,
}
var context = new SocketCommandContext(_discord, message); public async Task CommandExecutedAsync(Optional<CommandInfo> command, ICommandContext context, IResult result)
// Perform the execution of the command. In this method, {
// the command service will perform precondition and parsing check // command is unspecified when there was a search failure (command not found); we don't care about these errors
// then execute the command if one is matched. if (!command.IsSpecified)
await _commands.ExecuteAsync(context, argPos, _services); return;
// Note that normally a result will be returned by this format, but here
// we will handle the result in CommandExecutedAsync,
}
public async Task CommandExecutedAsync(Optional<CommandInfo> command, ICommandContext context, IResult result) // the command was successful, we don't care about this result, unless we want to log that a command succeeded.
{ if (result.IsSuccess)
// command is unspecified when there was a search failure (command not found); we don't care about these errors return;
if (!command.IsSpecified)
return;
// the command was successful, we don't care about this result, unless we want to log that a command succeeded. // the command failed, let's notify the user that something happened.
if (result.IsSuccess) await context.Channel.SendMessageAsync($"error: {result}");
return;
// the command failed, let's notify the user that something happened.
await context.Channel.SendMessageAsync($"error: {result}");
}
} }
} }

View File

@@ -2,19 +2,18 @@ using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace TextCommandFramework.Services namespace TextCommandFramework.Services;
public class PictureService
{ {
public class PictureService private readonly HttpClient _http;
public PictureService(HttpClient http)
=> _http = http;
public async Task<Stream> GetCatPictureAsync()
{ {
private readonly HttpClient _http; var resp = await _http.GetAsync("https://cataas.com/cat");
return await resp.Content.ReadAsStreamAsync();
public PictureService(HttpClient http)
=> _http = http;
public async Task<Stream> GetCatPictureAsync()
{
var resp = await _http.GetAsync("https://cataas.com/cat");
return await resp.Content.ReadAsStreamAsync();
}
} }
} }

View File

@@ -7,9 +7,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Discord.Net.Commands" Version="3.10.0" /> <PackageReference Include="Discord.Net.Commands" Version="3.13.0" />
<PackageReference Include="Discord.Net.Websocket" Version="3.10.0" /> <PackageReference Include="Discord.Net.Websocket" Version="3.13.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -2,33 +2,28 @@ using Discord;
using Discord.Webhook; using Discord.Webhook;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace WebHookClient namespace WebHookClient;
// This is a minimal example of using Discord.Net's Webhook Client
// Webhooks are send-only components of Discord that allow you to make a POST request
// To a channel specific URL to send a message to that channel.
class Program
{ {
// This is a minimal example of using Discord.Net's Webhook Client public static async Task Main()
// Webhooks are send-only components of Discord that allow you to make a POST request
// To a channel specific URL to send a message to that channel.
class Program
{ {
static void Main(string[] args) // The webhook url follows the format https://discord.com/api/webhooks/{id}/{token}
=> new Program().MainAsync().GetAwaiter().GetResult(); // Because anyone with the webhook URL can use your webhook
// you should NOT hard code the URL or ID + token into your application.
using var client = new DiscordWebhookClient("https://discord.com/api/webhooks/123/abc123");
public async Task MainAsync() var embed = new EmbedBuilder
{ {
// The webhook url follows the format https://discord.com/api/webhooks/{id}/{token} Title = "Test Embed",
// Because anyone with the webhook URL can use your webhook Description = "Test Description"
// you should NOT hard code the URL or ID + token into your application. };
using (var client = new DiscordWebhookClient("https://discord.com/api/webhooks/123/abc123"))
{
var embed = new EmbedBuilder
{
Title = "Test Embed",
Description = "Test Description"
};
// Webhooks are able to send multiple embeds per message // Webhooks are able to send multiple embeds per message
// As such, your embeds must be passed as a collection. // As such, your embeds must be passed as a collection.
await client.SendMessageAsync(text: "Send a message to this webhook!", embeds: new[] { embed.Build() }); await client.SendMessageAsync(text: "Send a message to this webhook!", embeds: new[] { embed.Build() });
}
}
} }
} }

View File

@@ -7,7 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Discord.Net.Webhook" Version="3.10.0" /> <PackageReference Include="Discord.Net.Webhook" Version="3.13.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>