Skip to main content

Application Commands

note

This page is a continuation of the Interactions (Reference) which dives into what Interactions are.

note

This page uses a chat-input command as the example for all sections. But this code will work with all types of Application Commands.

Lifecycle

Creating a Command

Creating application commands are very easy, all you need to do is build the command's structure using the ApplicationCommandRequest.Builder. Then use the RestClient to make the request to discord.

caution

Global commands have a TTL of 1 hour. This means it will take up to 1 hour for a global command's changes to take effect in the discord client.

We recommend using guild commands while developing and testing, and switching to global commands when ready to deploy.

info

These endpoints are idempotent, meaning commands that have not changed (and are included in the request) will not change.

Global Command

// Get our application's ID
long applicationId = client.getRestClient().getApplicationId().block();

// Build our command's definition
ApplicationCommandRequest greetCmdRequest = ApplicationCommandRequest.builder()
.name("greet")
.description("Greets You")
.addOption(ApplicationCommandOptionData.builder()
.name("name")
.description("Your name")
.type(ApplicationCommandOption.Type.STRING.getValue())
.required(true)
.build()
).build();

// Create the command with Discord
client.getRestClient().getApplicationService()
.createGlobalApplicationCommand(applicationId, greetCmdRequest)
.subscribe();

Guild Command

Creating a guild command is just as easy as creating a global command, the only extra info you need is the Guild's ID.

// application ID and command definition are the same as the global command

long guildId = 208023865127862272L; //Discord4J's server ID.

client.getRestClient().getApplicationService()
.createGuildApplicationCommand(applicationId, guildId, greetCmdRequest)
.subscribe();

Editing a Command

To edit a command, we will use the same ApplicationCommandRequest we built in the last section. We also need the ApplicationCommandData that represents what discord knows about our commands.

Global Command

// Get the commands from discord as a Map
Map<String, ApplicationCommandData> discordCommands = client.getRestClient()
.getApplicationService()
.getGlobalApplicationCommands(applicationId)
.collectMap(ApplicationCommandData::name)
.block();

// Pull out the copy of the greet command
ApplicationCommandData discordGreetCmd = discordCommands.get(greetCmdRequest.name());
long discordGreetCmdId = Long.parseLong(discordGreetCmd.id())

// Modify command
client.getRestClient().getApplicationService()
.modifyGlobalApplicationCommand(applicationId, discordGreetCmdId, greetCmdRequest)
.subscribe();

Guild Command

long guildId = 208023865127862272L; //Discord4J's server ID.

// Get the commands from discord as a Map
Map<String, ApplicationCommandData> discordCommands = client.getRestClient()
.getApplicationService()
.getGuildApplicationCommands(applicationId, guildId)
.collectMap(ApplicationCommandData::name)
.block();

// Pull out the copy of the greet command
ApplicationCommandData discordGreetCmd = discordCommands.get(greetCmdRequest.name());

// Modify command
client.getRestClient().getApplicationService()
.modifyGuildApplicationCommand(applicationId, guildId, Long.parseLong(discordGreetCmd.id()), greetCmdRequest)
.subscribe();

Bulk Overwrite

warning

This does not simply edit commands, but replaces them outright!

info

These endpoints are idempotent, meaning commands that have not changed (and are included in the request) will not change.

You may have made large changes to your commands and would rather replace them instead of updating your existing commands.

To do so, all you need is a list of ApplicationCommandRequest and use the following method:

Global Command
client.getRestClient().getApplicationService()
.bulkOverwriteGlobalApplicationCommand(applicationId, commandRequestList)
.subscribe();
Guild Command
long guildId = 208023865127862272L; //Discord4J's server ID.

client.getRestClient().getApplicationService()
.bulkOverwriteGuildApplicationCommand(applicationId, guildId, commandRequestList)
.subscribe();

Deleting a Command

Sometimes you just don't want a command anymore, in that case we can easily delete the commands we don't want.

Global Command

// Get the commands from discord as a Map
Map<String, ApplicationCommandData> discordCommands = client.getRestClient()
.getApplicationService()
.getGlobalApplicationCommands(applicationId)
.collectMap(ApplicationCommandData::name)
.block();

// Get the ID of our greet command
long commandId = Long.parseLong(discordCommands.get("greet").id());

// Delete it
client.getRestClient().getApplicationService()
.deleteGlobalApplicationCommand(applicationId, commandId)
.subscribe();

Guild Command

long guildId = 208023865127862272L; //Discord4J's server ID.

// Get the commands from discord as a Map
Map<String, ApplicationCommandData> discordCommands = client.getRestClient()
.getApplicationService()
.getGuildApplicationCommands(applicationId, guildId)
.collectMap(ApplicationCommandData::name)
.block();

// Get the ID of our greet command
long commandId = Long.parseLong(discordCommands.get("greet").id());

// Delete it
client.getRestClient().getApplicationService()
.deleteGuildApplicationCommand(applicationId, guildId, commandId)
.subscribe();

Simplifying the Lifecycle

note

This section only applies to global commands currently. Feel free to suggestion changes/additions to include simplifying guild commands.

Hardcoding commands can come with downsides. It's much harder to read and maintain the command structure and handle changes made to them. However, this can all be simplified easily.

Instead of hardcoding the commands, we will put commands in resources/commands as their raw json form, and have a class dedicated to reading these files and handling all discord requests from above.

We will maintain the following structure for our json files: src/main/resources/commands/*.json

Application commands follow a simple json structure defined by Discord, our greet command's json looks like this:

{
"name": "greet",
"description": "Greets you",
"options": [
{
"name": "name",
"description": "Your name",
"type": 3,
"required": true
}
]
}
tip

"type": 3 is the option type for STRING. See Interactions (Reference) - Option Types

To read these files and run our code, we will be using the GlobalCommandRegistrar found in our Example Projects (due to its 100 line length, it cannot easily be in a code-block on this page).

Once that file is in your project, all you need to do is call it after logging in:

try {
new GlobalCommandRegistrar(client.getRestClient()).registerCommands();
} catch (Exception e) {
//Handle exception
}
tip

Using the Spring Boot Framework, we can simplify this even more and remove the explicit call after logging in. See our Spring Example Projects for more information.

Receiving and Responding

Discord provides 2 ways to receive commands. Either through the gateway, or via a webhook. In this section, we will cover the former.

Receiving Commands

Every application command can be received with this simple code:

client.on(ApplicationCommandInteractionEvent.class, event -> {
// logic
}).subscribe();

However, you may not want all application commands, and might just want a slash command:

client.on(ChatInputInteractionEvent.class, event -> {
// logic
}).subscribe();

For message commands and user commands use MessageInteractionEvent and UserInteractionEvent respectively

Responding to Commands

There are multiple ways of responding to a command, for a list of available methods see this section. The simplest would be reply():

client.on(ChatInputInteractionEvent.class, event -> {
if (event.getCommandName().equals("ping")) {
return event.reply("Pong!");
}
}).subscribe();
caution

When you receive and interaction, you have 3 seconds to respond before it times out.

If you can return something immediately, we use reply. If it takes longer than 3 seconds to gather the data and respond, use deferReply and when you are ready to respond with data, use editReply or createFollowup

When you use defer, you have up to 15 minutes before the interaction is no longer valid.

public static void main(String[] args) {
GatewayDiscordClient client = DiscordClientBuilder.create("token").build()
.login()
.block();

client.on(ChatInputInteractionEvent.class, event -> {
if (event.getCommandName().equals("ping")) {
return event.deferReply().then(methodThatTakesALongTime(event));
}
}).subscribe();
}

private static Mono<Message> methodThatTakesALongTime(ChatInputInteractionEvent event) {
// Do logic that takes awhile, then return
return event.createFollowup("This took awhile!");
}

Ephemeral Responses

When using reply/deferReply/createFollowup everyone in the channel can see this, not just the user that issued the command.

Since you may not always want that, Discord provides ephemeral versions of these response types. ephemeral means that only the user who issued the command will see the response.

To respond ephemerally, we add an extra line:

client.on(ChatInputInteractionEvent.class, event -> {
if (event.getCommandName().equals("ping")) {
return event
.reply("Pong!")
.withEphemeral(true);
}
}).subscribe();

And to deferReply ephemerally:

client.on(ChatInputInteractionEvent.class, event -> {
if (event.getCommandName().equals("ping")) {
return event.deferReply().withEphemeral(true);
}
}).subscribe();
note

If you deferReply ephemerally, you must use an ephemeral followup.

The same goes in reverse, if you deferReply normally, you must also follow up normally. Discord does not support mixing these response modifiers.

Ephemeral messages technically do not exist (like the clyde bot messages you've likely seen). When the user's client refreshes, or they switch to a different client, the ephemeral message will not be visible to them anymore.

Editing Responses

After the initial response is sent, you may want to edit that response.

client.on(ChatInputInteractionEvent.class, event -> {
if (event.getCommandName().equals("ping")) {
event.reply("Pong!").subscribe();

// Do something

event.editReply(InteractionReplyEditSpec.builder()
.build()
.withContentOrNull("Pong! Pong!")
).subscribe();
}
}).subscribe();

Followups

Sometimes, you might not want to send one response, but might want to send multiple. Fear not, Discord lets you send multiple followups as long as the interaction is valid for.

client.on(ChatInputInteractionEvent.class, event -> {
if (event.getCommandName().equals("ping")) {
event.reply("Pong!").subscribe();

event.createFollowup("PingPing!").subscribe();
event.createFollowup("PongPong!").subscribe();
}
}).subscribe();

Handling Options

note

This only applies to Chat Input commands, Message and User commands do not support options.

Unlike a ping command, our greet command from earlier accepts arguments, or options.

To get an option and assign a default value, we can use something like this to do it reactively:

String name = event.getOption("name")
.flatMap(ApplicationCommandInteractionOption::getValue)
.map(ApplicationCommandInteractionOptionValue::asString)
.orElse("Your default value");

To see what that looks like in practice:

client.on(ChatInputInteractionEvent.class, event -> {
if (event.getCommandName().equals("greet")) {
/*
Since slash command options are optional according to discord, we will wrap it into the following function
that gets the value of our option as a String without chaining several .get() on all the optional values
In this case, there is no fear it will return empty/null as this is marked "required: true" in our json.
*/
String name = event.getOption("name")
.flatMap(ApplicationCommandInteractionOption::getValue)
.map(ApplicationCommandInteractionOptionValue::asString)
.get(); //This is warning us that we didn't check if its present, we can ignore this on required options

//Reply to the slash command, with the name the user supplied
return event.reply()
.withEphemeral(true)
.withContent("Hello, " + name);
}
}).subscribe();

Getting and Deleting Responses

warning

You cannot get nor delete ephemeral messages.

In addition to editing the initial response, you can also delete it.

event.reply("pong!").subscribe();
event.deleteReply().subscribe();

Finally, you may want to get the initial reply in another part of your code:

event.reply("pong!").subscribe();
Message message = event.getReply().block();

Permissions

caution

Discord is making major changes to the permission system in the near future. Permissions will be documented then.

Autocomplete

Autocompletion is handled through a secondary interaction event type. Please refer to How-to Guides - Auto Complete information on how to use this feature.

Further Reading