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 without subcommands
├── command
Basic command with subcommands
As soon as you define a subcommand, the level 0 command can no longer have any behaviour (handler, options)
├── command
│ └── subcommand
│ └── subcommand
Basic command with subcommands under many groups
├── command
│ ├── groups
│ │ └── subcommand
│ │ └── subcommand
│ └── groups
│ └── subcommand
│ └── subcommand
Basic command with 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) => ctx.interaction.reply('Hello, world!'));
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) {});
final commandBuilder = CommandBuilder()
.setContext(CommandContextType.server)
.setHandler((ServerCommandContext ctx) {});
final commandBuilder = CommandBuilder()
.setContext(CommandContextType.global)
.setHandler((GlobalCommandContext 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.
final commandBuilder = CommandBuilder()
.setHandler((ctx, {required String str}) {
print(str);
})
.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
);
Option.integer(
name: 'int',
description: 'Your number',
required: true
);
Option.double(
name: 'int',
description: 'Your number',
required: true
);
Option.boolean(
name: 'bool',
description: 'Your boolean',
required: true
);
Mentionable types are available to define options for your commands.
Option.user(
name: 'user',
description: 'Your user',
required :true
);
Option.channel(
name: 'channel',
description: 'Your channel',
required :true
);
Option.role(
name: 'role',
description: 'Your role',
required: true
);
Option.mentionable(
name: 'mentionable',
description: 'Your mentionable',
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) => ctx.interaction.reply('Hello, world!'));
.addSubCommand((command) {
command
.setName('bar')
.setDescription('This is a subcommand description')
.setHandler((ctx) => ctx.interaction.reply('Hello, world!'));
});
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) => ctx.interaction.reply('Hello, world!'));
.createGroup((group) {
group
.setName('group')
.setDescription('This is a group description')
.addSubCommand((command) {
command
.setName('bar')
.setDescription('This is a subcommand description')
.setHandler((ctx) => ctx.interaction.reply('Hello, world!'));
});
});
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 = Client()
.setCache((e) => MemoryProvider())
.build();
client.commands.declare((command) {
command
..setName('foo')
..setDescription('This is a command description')
..setHandler((ctx) => ctx.interaction.reply('Hello, world!'));
});
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
yaml
orjson
file
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) => ctx.interaction.reply('Hello, world!'));
final file = File('src/commands.yaml');
final translation = Translation.file(file: file, key: 'test');
final commandBuilder = CommandBuilder()
.setName('foo', translation: translation)
.setDescription('This is a test command', translation: translation)
.setHandler((ctx) => ctx.interaction.reply('Hello, world!'));
commands:
test:
name:
fr: bonjour
en-GB: hello
description:
fr: bonjour le monde description
en-GB: hello world description
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) => ctx.interaction.reply('Hello, world!'));
});
final file = File('src/commands.yaml');
final translation = Translation.file(file: file, key: 'test');
final subTranslation = Translation.file(file: file, key: 'test.sub1');
final commandBuilder = CommandBuilder()
.setName('foo', translation: translation)
.setDescription('This is a test command', translation: translation)
.addSubCommand((command) {
command
.setName('sub1', translation: subTranslation)
.setDescription('This is a subcommand description', translation: subTranslation)
.setHandler((ctx) => ctx.interaction.reply('Hello, world!'));
});
commands:
test:
name:
fr: bonjour
en-GB: hello
description:
fr: bonjour le monde description
en-GB: hello world description
test.sub1:
name:
fr: sub1
en-GB: sub1
description:
fr: Ceci est une sous-commande de test
en-GB: This is a subcommand
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) => ctx.interaction.reply('Hello, world!'));
});
});
final file = File('src/commands.yaml');
final translation = Translation.file(file: file, key: 'test');
final subTranslation = Translation.file(file: file, key: 'test.sub1');
final groupTranslation = Translation.file(file: file, key: 'test.group');
final commandBuilder = CommandBuilder()
.setName('foo', translation: translation)
.setDescription('This is a test command', translation: translation)
.createGroup((group) {
group
.setName('group', translation: groupTranslation)
.setDescription('This is a group command', translation: groupTranslation)
.addSubCommand((command) {
command
.setName('sub1', translation: subTranslation)
.setDescription('This is a subcommand description', translation: subTranslation)
.setHandler((ctx) => ctx.interaction.reply('Hello, world!'));
})
);
commands:
test.group:
name:
fr: groupe
en-GB: group
description:
fr: groupe description
en-GB: group description
test:
name:
fr: bonjour
en-GB: hello
description:
fr: bonjour le monde description
en-GB: hello world description
test.sub1:
name:
fr: sub1
en-GB: sub1
description:
fr: Ceci est une sous-commande de test
en-GB: This is a subcommand
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);
commands:
test:
name:
_default: role
fr: role
en-GB: role
description:
_default: Role manager
fr: Management des rôles
en-GB: Role manager
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.
Define basic command handlers
final file = File('config/test_commands.yaml');
final definition = CommandDefinition()
..using(file)
..setHandler('test', (ctx) {
print('Hello, world!');
});
commands:
test: 👈 # Named command key
name:
_default: role
fr: role
en-GB: role
description:
_default: Role manager
fr: Management des rôles
en-GB: Role manager
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!');
});
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:
name:
_default: add
fr: ajout
en-GB: add
description:
_default: Add given role
fr: Ajoute un rôle donné
en-GB: Add given role
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 role
Define 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, {required String? str}) {
print(str);
});
options:
- type: string
required: false
name:
_default: str
fr: mot
en-GB: str
description:
_default: str description
fr: Chaine de caractère
en-GB: String sentence
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, {required Role role}) {
print(str);
});
options:
- type: role
required: true
name:
_default: role
fr: role
en-GB: role
description:
_default: Target role
fr: Role ciblé
en-GB: Target role
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, {required int language}) {
print(str);
});
options:
- type: choice.int
required: true
name:
_default: language
fr: language
en-GB: language
description:
_default: Choose the best language
fr: Choisissez la meilleure langue
en-GB: Choose the best language
choices:
- name: Dart
value: 1
- name: Python
value: 2
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)
]));
});
commands:
test:
name:
_default: role
fr: role
en-GB: role
description:
_default: Role manager
fr: Management des rôles
en-GB: Role manager
test.getValue:
name:
_default: getValue
fr: get-value
en-GB: get-value
description:
_default: Get
fr: Obtenir une valeur
en-GB: Get value
Registering definition
final file = File('config/test_commands.yaml');
final client = Client()
.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}) {
ctx.interaction.reply('Selected value: $value');
}
@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)
]));
}
}
abstract interface class CommandDeclaration implements CommandContract<CommandDeclarationBuilder>{
CommandDeclarationBuilder build();
}
final client = Client()
.setCache((e) => MemoryProvider())
.registerCommand(MyCommand.new) // 👈 Put your command
.build();
await client.init();
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}) {
ctx.interaction.reply('Role $role has been selected');
}
@override
CommandDefinitionBuilder build() {
return CommandDefinitionBuilder()
..using(File('config/test_commands.yaml'))
..setHandler('role.add', addRole);
}
}
commands:
role:
name:
_default: role
fr: role
en-GB: role
description:
_default: Role manager
fr: Management des rôles
en-GB: Role manager
role.add:
name:
_default: add
fr: ajout
en-GB: add
description:
_default: Add given role
fr: Ajoute un rôle donné
en-GB: Add given role
options:
- type: role
required: true
name:
_default: role
fr: role
en-GB: role
description:
_default: Target role
fr: Role ciblé
en-GB: Target role
abstract interface class CommandDefinition implements CommandContract<CommandDeclarationBuilder>{
CommandDefinitionBuilder build();
}
final client = Client()
.setCache((e) => MemoryProvider())
.registerCommand(MyCommand.new) // 👈 Put your command
.build();
await client.init();