Commands

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) {});

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
);

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) => 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.

main.dart
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 or json 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!'));

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!'));
});

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!'));
});
});

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.

Define 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.

Definition
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 obligatoire
  • name: Nom de l'option
  • description: Description de l'option
final file = File('config/test_commands.yaml');
final definition = CommandDefinition()
..using(file)
..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 obligatoire
  • name: Nom de l'option
  • description: Description de l'option
final file = File('config/test_commands.yaml');
final definition = CommandDefinition()
..using(file)
..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 obligatoire
  • name: Nom de l'option
  • description: Description de l'option
  • choices: Liste des choix possibles
  • choices.name: Nom du choix
  • choices.value: Valeur du choix
final file = File('config/test_commands.yaml');
final definition = CommandDefinition()
..using(file)
..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

main.dart
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)
]));
}
}

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);
}
}