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.

Note

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.

Important

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.

Note

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)

Note

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.

Note

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.

Note

When you define a group, you can no longer define a handler for the top-level command.

Note

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.

Important

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

Note

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

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.

Note

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