Creating commands
Commands represent the main form of interaction that Discord bots have with users. Creating commands is a simple and straightforward process, with the ability to customize different aspects of them.
Chat input (aka "slash") commands
Basic command
A slash command is a command that is triggered when the user sends /command-name
in chat. In the library, they are
called "chat input commands".
Here is an example of a /ping
command that makes the bot reply with "Pong!":
package testbot1;
import botrino.interaction.annotation.ChatInputCommand;
import botrino.interaction.listener.ChatInputInteractionListener;
import botrino.interaction.context.ChatInputInteractionContext;
import org.reactivestreams.Publisher;
@ChatInputCommand(name = "ping", description = "Pings the bot to check if it is alive.")
public final class PingCommand implements ChatInputInteractionListener {
@Override
public Publisher<?> run(ChatInputInteractionContext ctx) {
return ctx.event().createFollowup("Pong!");
}
}
- A chat input command must have a
@ChatInputCommand
annotation that contains the meta-information required by Discord (name of the command, description, defaultPermission, etc), and must implement theChatInputInteractionListener
interface. - The
run
method accepts aChatInputInteractionContext
that holds contextual information on the command being executed, such as the originalChatInputInteractionEvent
, theMessageChannel
where the interaction happened, theUser
who initiated the interaction, and aLocale
that may have been adapted to the target user ( see Filtering and adapting events). - Events are automatically acknowledged by default, so you can directly call
createFollowup()
without usingdeferReply()
first (reply()
will not work unless you disable automatic acknowledgment, see Acknowledging Interactions)
If you are using the Botrino framework, you have nothing else to do, the command will be automatically recognized and
registered. Otherwise, you need to manually register it into the InteractionService
like this:
interactionService.registerChatInputCommand(new PingCommand());
Command options
A command may accept one or many options, whether they are required or optional. The library
provides ChatInputCommandGrammar
that allows to inject the option values into a record class that is going to
be instantiated when the command is executed. Here is an example of a command using options:
package testbot1;
import botrino.interaction.annotation.ChatInputCommand;
import botrino.interaction.context.ChatInputInteractionContext;
import botrino.interaction.grammar.ChatInputCommandGrammar;
import botrino.interaction.listener.ChatInputInteractionListener;
import discord4j.core.object.command.ApplicationCommandOption;
import discord4j.discordjson.json.ApplicationCommandOptionData;
import org.reactivestreams.Publisher;
import java.util.List;
@ChatInputCommand(name = "options", description = "Option testing")
public class OptionsCommand implements ChatInputInteractionListener {
private final ChatInputCommandGrammar<Options> grammar = ChatInputCommandGrammar.of(Options.class);
@Override
public Publisher<?> run(ChatInputInteractionContext ctx) {
return grammar.resolve(ctx.event()).flatMap(options -> ctx.event()
.createFollowup("Value of `my-string`: " + options.myString));
}
@Override
public List<ApplicationCommandOptionData> options() {
return grammar.toOptions();
}
private record Options(
@ChatInputCommandGrammar.Option(
type = ApplicationCommandOption.Type.STRING,
name = "my-string",
description = "The string argument",
required = true,
choices = {
@ChatInputCommandGrammar.Choice(name = "Choice 1", stringValue = "1"),
@ChatInputCommandGrammar.Choice(name = "Choice 2", stringValue = "2"),
@ChatInputCommandGrammar.Choice(name = "Choice 3", stringValue = "3")
}
)
String myString) {}
}
- Create a record class that declares the fields in which you want to inject the option values. It is recommended to use an internal private record for better code readability, unless you are re-using the same record for several commands.
- Use the annotation
@ChatInputCommandGrammar.Option
on each record parameter to declare the properties of the option (the type, the name, the description, whether they are required or not, and the array of value choices, if any). - Create a new
ChatInputCommandGrammar
and pass the class to the.of()
method. You only need to instantiate once, rather than on each command execution. - In the
run(ChatInputInteractionContext)
method, call theresolve(ChatInputInteractionEvent)
method which will read the options, instantiate the record and inject the values in the annotated fields. You can then use the record object to conveniently access the values, as shown in the example above. - Override the
options()
method fromChatInputInteractionListener
and make it returnChatInputCommandGrammar#toOptions()
.
For reference, here is a table associating each ApplicationCommandOption.Type
with the type of the field carrying the
annotation:
Option type | Type of annotated field |
---|---|
STRING | java.lang.String |
INTEGER | java.lang.Long (primitive long may be used only if required = true ) |
NUMBER | java.lang.Double (primitive double may be used only if required = true ) |
BOOLEAN | java.lang.Boolean (primitive boolean may be used only if required = true ) |
USER | discord4j.core.object.entity.User (or discord4j.core.object.entity.Member if in a guild) |
CHANNEL | discord4j.core.object.entity.channel.Channel |
ROLE | discord4j.core.object.entity.Role |
MENTIONABLE | discord4j.common.util.Snowflake |
ATTACHMENT | discord4j.core.object.entity.Attachment |
Non-required options will be filled with null
if not specified by the user, which means you cannot use primitive types
for INTEGER
, NUMBER
and BOOLEAN
if required = false
, otherwise you will get NullPointerException
s.
For legacy purposes, you can use a normal class instead of a record. In that case, the class must have a no-arg
constructor, and the @ChatInputCommandGrammar.Option
annotations should be on fields declared in the class. Using
records is preferred as they are immutable.
Subcommands and subcommand groups
Discord allows to create subcommands and subcommand groups to help in organizing the logic of a complex command. Here is an example of a command using subcommands and subcommand groups:
@ChatInputCommand(
name = "nest",
description = "Subcommand testing",
subcommands = {
@Subcommand(name = "sub1", description = "Subcommand 1", listener = NestCommand.Sub1.class),
@Subcommand(name = "sub2", description = "Subcommand 2", listener = NestCommand.Sub2.class)
},
subcommandGroups = {
@SubcommandGroup(name = "group1", description = "Group 1", subcommands = {
@Subcommand(name = "sub", description = "Subcommand", listener = NestCommand.G1Sub.class)
})
}
)
public final class NestCommand {
public static final class Sub1 implements ChatInputInteractionListener {
@Override
public Publisher<?> run(ChatInputInteractionContext ctx) {
return ctx.event().createFollowup("sub1: pong!");
}
}
public static final class Sub2 implements ChatInputInteractionListener {
@Override
public Publisher<?> run(ChatInputInteractionContext ctx) {
return ctx.event().createFollowup("sub2: pong!");
}
}
public static final class G1Sub implements ChatInputInteractionListener {
@Override
public Publisher<?> run(ChatInputInteractionContext ctx) {
return ctx.event().createFollowup("group1 sub: pong!");
}
}
}
Here are the notable differences:
- The class carrying the
@ChatInputCommand
annotation no longer implementsChatInputInteractionListener
. Indeed, as per Discord's documentation a base command becomes unusable if subcommands are present. - The
@ChatInputCommand
specifies an array of@Subcommand
and@SubcommandGroup
with their own name and description. - Subcommands specify the class implementing
ChatInputInteractionListener
that is going to handle them. In this example they are internal classes, but they can as well be external.
Here is how you manually register a command containing subcommands when you control the instance
of InteractionService
:
interactionService.registerChatInputCommand(new NestCommand(), List.of(
new NestCommand.Sub1(),
new NestCommand.Sub2(),
new NestCommand.G1Sub()
));
If you are using the Botrino framework, the subcommand classes must either have a public no-arg constructor or be
declared as a service. If the classes are internal, they must be static
.
Context menu commands
Discord currently support two types of context menu commands, one on messages and one on users. It works the same as
chat input commands, but you need to use the @MessageCommand
and @UserCommand
annotations with
the MessageInteractionListener
and UserInteractionListener
interfaces, respectively.
Context menu commands are actually less complex than chat input ones, since there is no description, no options, no subcommands... Only a name and a run method:
package testbot1;
import botrino.interaction.annotation.UserCommand;
import botrino.interaction.context.UserInteractionContext;
import botrino.interaction.listener.UserInteractionListener;
import org.reactivestreams.Publisher;
@UserCommand("Fight")
public final class FightCommand implements UserInteractionListener {
@Override
public Publisher<?> run(UserInteractionContext ctx) {
return ctx.event().createFollowup("You are fighting <@" +
ctx.event().getTargetId().asString() + ">");
}
}
If you need to do manual registration, it happens
via InteractionService#registerMessageCommand(MessageInteractionListener)
and InteractionService#registerUserCommand(MessageInteractionListener)
:
interactionService.registerUserCommand(new FightCommand());
Private commands
A command may be marked "private" so it won't be deployed globally, but in a specific server instead. This is particularly useful when creating commands that are not intended to the public but for the bot owner or the team managing it.
All you have to do is to add @PrivateCommand
on top of @ChatInputCommand
, @MessageCommand
or @UserCommand
. The guild where the private command will be deployed can be defined in the config.
package testbot1;
import botrino.interaction.annotation.UserCommand;
import botrino.interaction.annotation.PrivateCommand;
import botrino.interaction.context.UserInteractionContext;
import botrino.interaction.listener.UserInteractionListener;
import org.reactivestreams.Publisher;
@PrivateCommand
@UserCommand("Blacklist")
public final class BlacklistCommand implements UserInteractionListener {
@Override
public Publisher<?> run(UserInteractionContext ctx) {
return ctx.event().createFollowup("User has been blacklisted.");
}
}
Commands as a service
The following is only applicable if you are using the Botrino framework. See Working with services.
Classes implementing commands can themselves be declared as services without any issues. For example if you need to
access the ConfigContainer
in your command, you can do this:
package testbot1;
import botrino.api.config.ConfigContainer;
import botrino.api.config.object.BotConfig;
import botrino.interaction.annotation.ChatInputCommand;
import botrino.interaction.context.ChatInputInteractionContext;
import botrino.interaction.listener.ChatInputInteractionListener;
import com.github.alex1304.rdi.finder.annotation.RdiFactory;
import com.github.alex1304.rdi.finder.annotation.RdiService;
import discord4j.gateway.intent.IntentSet;
import org.reactivestreams.Publisher;
@RdiService
@ChatInputCommand(name = "intents", description = "Displays the intents enabled for this bot.")
public final class IntentsCommand implements ChatInputInteractionListener {
private final long intents;
@RdiFactory
public IntentsCommand(ConfigContainer configContainer) {
this.intents = configContainer.get(BotConfig.class)
.enabledIntents()
.orElse(IntentSet.nonPrivileged().getRawValue());
}
@Override
public Publisher<?> run(ChatInputInteractionContext ctx) {
return ctx.event().createFollowup("Intents enabled: " + intents);
}
}
The command above accesses the values in the config.json
to get the gateway intents enabled for the bot. You can
notice the use of @RdiService
on top of @ChatInputCommand
, this works totally fine! Don't forget the @RdiFactory
to inject the configuration container, and you're ready to run the bot and try out this command.
If you declare a command as a service this way, you are allowed to do anything with it like any other service, for
example inject it in other services, or set up @RdiFactory
to be
a reactive static method in case the
command needs to perform a reactive task in order to be initialized.