Commands
Slash commands were implemented with the aim of replacing messages considered as commands with a start-of-line
character, often defined as a message prefix like !.
Introduction
Slash commands allow users to interact with bots in a more intuitive and structured way, using predefined syntax.
They make it easier to execute specific commands without having to remember prefixes or complex message formats.
They were created to replace message-based commands with prefixes, making the user experience smoother and reducing syntax errors.
When you change the structure of your commands, please restart your entire application process so that the changes take
effect even if the hmr is active.
Valid structure
In accordance with the Discord API, it is possible to define commands in several formats explained here.
Basic command
├── command
Subcommands
As soon as you define a subcommand, the level 0 command can no longer have any behaviour (handler, options)
├── command
│ └── subcommand
│ └── subcommand
Subcommands under many groups
├── command
│ ├── groups
│ │ └── subcommand
│ │ └── subcommand
│ └── groups
│ └── subcommand
│ └── subcommand
Subcommands and groups
├── command
│ ├── groups
│ │ └── subcommand
│ ├── groups
│ │ └── subcommand
│ │ └── subcommand
│ └── subcommand
Basic command
To simplify the creation and configuration of commands, we have decided to provide a builder that allows us to reduce errors linked to the data structure requested by Discord’s HTTP API.
The name and description properties are mandatory for each command, group or subcommand.
In the case of a command or subcommand, it is necessary to define a handler that will be used later to respond to the interaction.
final commandBuilder = CommandBuilder()
.setName('foo')
.setDescription('This is a command description')
.setHandler((ctx) {
final builder = MessageComponentBuilder()
..text('Hello from World');
ctx.interaction.reply(builder);
});
Context
If we follow Discord’s data structure, we can see that each interaction has a context which
can be server or global.
When we develop a command, we prefer to use the context of the guild in which the interaction has been
executed, as this does not have a cache, unlike a global command.
So, by default, each command uses the guild’s context and is pushed to Discord when the
ServerCreate event.
You can change the context of your command using the builder’s setContext method.
final commandBuilder = CommandBuilder()
.setContext(CommandContextType.server)
.setContext(CommandContextType.global);
Depending on your choice, you will need to adapt your handler to find out the context in which it is being executed.
final commandBuilder = CommandBuilder()
.setHandler((CommandContext ctx) {});Assign options
Commands can have options that allow you to define arguments for the command.
The order in which the options are declared is important because it will affect how your users use the command users.
final commandBuilder = CommandBuilder()
.addOption(
Option.string(
name: 'str',
description: 'Your sentence',
required: true
)
);
Now that the options have been defined for your order, you need to modify your handler to take them into account in its parameters.
Each option defined must be added to named parameters (the order of declaration is not important)
The name of your parameter declared in your handler must be the same as that defined in the name key of your option.
void handle(ctx, {required String str}) {
print(str);
}
final commandBuilder = CommandBuilder()
.setHandler(handle)
.addOption(
Option.string(
name: 'str',
description: 'Your sentence',
required: true
)
);
If your parameter is optional, remember to indicate this in the declaration of your handler using the language’s
null safety feature.
final commandBuilder = CommandBuilder()
.setHandler((ctx, {required String? str}) {});
Options types
Basic types are available to define options for your commands.
Option.string(
name: 'str',
description: 'Your sentence',
required: true
);Mentionable types are available to define options for your commands.
Option.user(
name: 'user',
description: 'Your user',
required :true
);Defining subcommand
Subcommands are commands nested within a top-level command.
We’ll use the same builder to define our level 0 command, to which we’ll add subcommands
using the .addSubCommand method.
When you declare subcommands, you can no longer define a handler for the top-level command.
final commandBuilder = CommandBuilder()
.setName('foo')
.setDescription('This is a command description')
.setHandler((ctx) async {
final builder = MessageComponentBuilder()
..text('Hello from World');
await ctx.interaction.reply(builder);
});
.addSubCommand((command) {
command
.setName('bar')
.setDescription('This is a subcommand description')
.setHandler((ctx) async {
final builder = MessageComponentBuilder()
..text('Hello from World');
await ctx.interaction.reply(builder);
});
});
As with a basic command, you can define options for your subcommands in the same way as explained above.
Command group
Groups are used to group several subcommands together by function. When you define a group, you must add one or more subcommands to it.
When you define a group, you can no longer define a handler for the top-level command.
A group cannot have a handler, so a handler must be defined for each subcommand.
final commandBuilder = CommandBuilder()
.setName('foo')
.setDescription('This is a command description')
.setHandler((ctx) async {
final builder = MessageComponentBuilder()
..text('Hello from World');
await ctx.interaction.reply(builder);
});
.createGroup((group) {
group
.setName('group')
.setDescription('This is a group description')
.addSubCommand((command) {
command
.setName('bar')
.setDescription('This is a subcommand description')
.setHandler((ctx) async {
final builder = MessageComponentBuilder()
..text('Hello from World');
await ctx.interaction.reply(builder);
});
});
});
Registering commands
Once you have defined your order, you need to declare it in your client for it to be taken into account at Discord.
final client = ClientBuilder()
.setCache(MemoryProvider.new)
.build();
client.commands.declare((command) {
command
..setName('foo')
..setDescription('This is a command description')
..setHandler((ctx) async {
final builder = MessageComponentBuilder()
..text('Hello from World');
await ctx.interaction.reply(builder);
});
});
await client.init();Translations
Translations were introduced to allow developers to define text content in several languages. In the case of our commands, they allow messages and arguments to be displayed in the user’s language.
Translations can only be used for name and description fields.
We can apply a translation in two different ways:
- From a
Map<String, String>. - From a
yamlorjsonfile
The main advantage of using a Map<String, String> is the simplicity of defining translations
directly in the code.
However, when your application has to manage a multitude of languages, it becomes difficult to define everything in a single in a single file without losing readability.
This is where the ‘configuration file’ approach comes into play, enabling you to define translations in an external file, thereby separating the ‘logic’ aspect from the ‘configuration’ aspect.
Basic command with translations
final commandBuilder = CommandBuilder()
.setName('foo', translation: Translation({
'fr': 'foo',
'en': 'foo'
}))
.setDescription('This is a test command', translation: Translation({
'fr': 'Ceci est une commande de test',
'en': 'This is a test command'
}))
.setHandler((ctx) {
final builder = MessageComponentBuilder()
..text('Hello from World');
ctx.interaction.reply(builder);
});Command with subcommands and translations
final commandBuilder = CommandBuilder()
.setName('foo', translation: Translation({
'fr': 'foo',
'en': 'foo'
}))
.setDescription('This is a test command', translation: Translation({
'fr': 'Ceci est une commande de test',
'en': 'This is a test command'
}))
.addSubCommand((command) {
command
.setName('sub1', translation: Translation({
'fr': 'sub1',
'en': 'sub1'
}))
.setDescription('This is a sub1 command', translation: Translation({
'fr': 'Ceci est une sous-commande de test',
'en': 'This is a subcommand'
}))
.setHandler((ctx) async {
final builder = MessageComponentBuilder()
..text('Hello from World');
await ctx.interaction.reply(builder);
});
});Command with subcommands, groups and translations
Declaring translations for a group of subcommands is identical to what we have done so far.
final commandBuilder = CommandBuilder()
.setName('foo', translation: Translation({
'fr': 'foo',
'en': 'foo'
}))
.setDescription('This is a test command', translation: Translation({
'fr': 'Ceci est une commande de test',
'en': 'This is a test command'
}))
.createGroup((group) {
group
.setName('group', translation: Translation({
'fr': 'group',
'en': 'group'
}))
.setDescription('This is a group command', translation: Translation({
'fr': 'Ceci est un groupe de commande de test',
'en': 'This is a group test command'
}))
.addSubCommand((command) {
command
.setName('sub1', translation: Translation({
'fr': 'sub1',
'en': 'sub1'
}))
.setDescription('This is a sub1 command', translation: Translation({
'fr': 'Ceci est une sous-commande de test',
'en': 'This is a subcommand'
}))
.setHandler((ctx) async {
final builder = MessageComponentBuilder()
..text('Hello from World');
await ctx.interaction.reply(builder);
});
});
});Command definition
As we saw earlier, the declaration of a command is simple at first, but becomes very complex as soon as you add several subcommands or groups of subcommands.
To simplify the declaration of your commands, we’ve decided to offer you a more modular approach
thanks to command definition.
The principle of command definition is to define a data structure in a so-called ‘configuration’ file and then to use this structure to define your commands. file and then use it as the source to build the command (the opposite of what we’ve been talking about so far). discussed so far).
The command is therefore built from a configuration file and can then be overloaded by the builder.
The using instruction is used to load the configuration file and build the command.
This command must be called before any other instruction.
final file = File('config/test_commands.yaml');
final definition = CommandDefinition()
..using(file);Define handlers
When you use the command definition approach, you must define a handler for commands or sub-commands.
The association between your command and its handler is made using the key used to declare a command in the definition file.
Basic command handlers
final file = File('config/test_commands.yaml');
final definition = CommandDefinition()
..using(file)
..setHandler('test', (ctx) {
print('Hello, world!');
});The key named _default is used to define a default value for a given field.
It is comparable to the name or description of a command that has no translation.
Define subcommand handlers
final file = File('config/test_commands.yaml');
final definition = CommandDefinition()
..using(file)
..setHandler('test', (ctx) => print('Hello, world!'))
..setHandler('test.sub1', (ctx) {
print('Hello, world!');
});Define groups
Assigning a subcommand to a group simply requires the group to be declared in the definition file and then
associated with the subcommand using the group key. and then associate it with the subcommand using the group key.
Only one group can be associated with any one subcommand.
groups:
myGroup:
name:
_default: myGroup
fr: mon-group
en-GB: my-group
description:
_default: Description of the first group
fr: Description de mon groupe
en-GB: Description of my group
commands:
test:
name:
_default: role
fr: role
en-GB: role
description:
_default: Role manager
fr: Management des rôles
en-GB: Role manager
test.add:
group: myGroup
name:
_default: add
fr: ajout
en-GB: add
description:
_default: Add given role
fr: Ajoute un rôle donné
en-GB: Add given roleDefine options
As with the command structure, options are no exception and must be defined in the definition file.
Options are declared in the same way as commands, using a named key.
Basically, an option is constructed as follows.
Basic types
type: Type de l’option (string,integer,double,boolean)required: Indique si l’option est obligatoirename: Nom de l’optiondescription: Description de l’option
final file = File('config/test_commands.yaml');
final definition = CommandDefinition()
..using(file)
..setHandler('test', (ctx) {
..setHandler('test', (ctx, {required String? str}) {
print(str);
});Mentionable types
type: Type de l’option (user,channel,role,mentionable)required: Indique si l’option est obligatoirename: Nom de l’optiondescription: Description de l’option
final file = File('config/test_commands.yaml');
final definition = CommandDefinition()
..using(file)
..setHandler('test', (ctx) {
..setHandler('test', (ctx, {required Role role}) {
print(str);
});Choice types
type: Type de l’option (choice.string,choice.integer,choice.double)required: Indique si l’option est obligatoirename: Nom de l’optiondescription: Description de l’optionchoices: Liste des choix possibleschoices.name: Nom du choixchoices.value: Valeur du choix
final file = File('config/test_commands.yaml');
final definition = CommandDefinition()
..using(file)
..setHandler('test', (ctx) {
..setHandler('test', (ctx, {required int language}) {
print(str);
});Override command context
If you want to use a context as a base and then override it according to your use cases, you can
retrieve the command’s context in order to access its builder and modify it.
final file = File('config/test_commands.yaml');
final definition = CommandDefinition()
..using(file)
..setHandler('test.getValue', (ctx, {required int value}) => print(str))
..context('test.getValue', (command) {
command
..setDescription('Get value')
..addOption(
ChoiceOption.integer(
name: 'value',
description: 'This is a value option',
required: true,
choices: [
Choice('First value', 1),
Choice('Second value', 2)
]));
});Registering definition
final file = File('config/test_commands.yaml');
final client = ClientBuilder()
.setCache((e) => MemoryProvider())
.build();
client.commands.define((command) {
command
..using(file)
..setHandler('test.getValue', (ctx, {required int value}) {
print(str);
});
}));
await client.init();Command with class approach
As mentioned above, there are two ways of creating a command. So far, each example has been written using a functional approach.
It is possible to define a command using an object-oriented approach by using a contract.
abstract interface class CommandContract<T extends CommandBuilder> {
T build();
}
Command with declaration
In this example, we will use the command definition approach to define our command with CommandDeclaration contract.
final class MyCommand implements CommandDeclaration {
FutureOr<void> handle(CommandContext ctx, {required int value}) async {
final builder = MessageComponentBuilder()
..text('Selected value: $value');
await ctx.interaction.reply(builder);
}
@override
CommandDeclarationBuilder build() {
return CommandDeclarationBuilder()
..setName('foo')
..setDescription('This is a command description')
..setHandler(handle)
..addOption(
ChoiceOption.integer(
name: 'value',
description: 'This is a value option',
required: true,
choices: [
Choice('First value', 1),
Choice('Second value', 2)
]));
}
}Command with definition
In this example, we will use the command definition approach to define our command with CommandDefinition contract.
final class MyCommand implements CommandDefinition {
FutureOr<void> addRole(CommandContext ctx, {required Role role}) async {
final builder = MessageComponentBuilder()
..text('Role $role has been selected');
await ctx.interaction.reply(builder);
}
@override
CommandDefinitionBuilder build() {
return CommandDefinitionBuilder()
..using(File('config/test_commands.yaml'))
..setHandler('role.add', addRole);
}
}Registering
To register a command, you need to call the register method on your client and pass your command as a parameter.
Future<void> main() async {
final client = ClientBuilder()
.build();
client.register(MyCommand.new) // 👈 Put your command
await client.init();
}