Skip to content

Creating slash commands

Slash commands are the new way of defining commands, even though there are limitations with them, we do have some advantages such as being easier to fill in, choices and auto-completion.

If you wish to handle application commands yourself, you can disable them by disabling BApplicationConfigBuilder.enable.

Defining the command method

Tip

Make sure you read the common command requirements first!

In addition to the common requirements, the first parameter must be GlobalSlashEvent for global commands or GuildSlashEvent for guild commands, or guild-only global commands (default).

Annotated commands

Annotated command methods must be annotated with @JDASlashCommand, where you can set the scope, name, description, etc..., while the declaring class must extend ApplicationCommand.

Why do I need to extend ApplicationCommand?

As a limitation of annotated commands, you are required to extend this class as it allows the framework to ask your commands for stuff, like what guilds a command should be pushed to, getting a value generator for one of their options, and also getting choices.

Example

@Command
class SlashPingKotlin : ApplicationCommand() {
    // Default scope is global, guild-only (GUILD_NO_DM)
    @JDASlashCommand(name = "ping", description = "Pong!")
    suspend fun onSlashPing(event: GuildSlashEvent) {
        event.deferReply(true).queue()

        val ping = event.jda.getRestPing().await()
        event.hook.editOriginal("Pong! $ping ms").await()
    }
}
@Command
public class SlashPing extends ApplicationCommand {
    // Default scope is global, guild-only (GUILD_NO_DM)
    @JDASlashCommand(name = "ping", description = "Pong!")
    public void onSlashPing(GuildSlashEvent event) {
        event.deferReply(true).queue();

        event.getJDA().getRestPing().queue(ping -> {
            event.getHook().editOriginal("Pong! " + ping + " ms").queue();
        });
    }
}

Top level configuration

You can configure properties which only applies to top-level commands, using @TopLevelSlashCommandData, this includes elements such as the scope (guild, global or global without DMs), description and whether the command is locked to admins by default.

Subcommands

To make a subcommand, set the name and subcommand on the annotation.

You will also need to add a @TopLevelSlashCommandData, it must only be used once per top-level command, this allows you to set top-level attributes.

Example

@Command
class SlashTag : ApplicationCommand() {
    // Data for /tag create
    @JDASlashCommand(name = "tag", subcommand = "create", description = "Creates a tag")
    // Data for /tag
    @TopLevelSlashCommandData(description = "Manage tags")
    fun onSlashTagCreate(event: GuildSlashEvent) {
        // ...
    }

    // Data for /tag delete
    @JDASlashCommand(name = "tag", subcommand = "delete", description = "Deletes a tag")
    fun onSlashTagDelete(event: GuildSlashEvent) {
        // ...
    }
}
@Command
public class SlashTag extends ApplicationCommand {
    // Data for /tag create
    @JDASlashCommand(name = "tag", subcommand = "create", description = "Creates a tag")
    // Data for /tag
    @TopLevelSlashCommandData(description = "Manage tags")
    public void onSlashTagCreate(GuildSlashEvent event) {
        // ...
    }

    // Data for /tag delete
    @JDASlashCommand(name = "tag", subcommand = "delete", description = "Deletes a tag")
    public void onSlashTagDelete(GuildSlashEvent event) {
        // ...
    }
}

Note

You cannot have both subcommands and top-level commands (i.e., an annotation with only name set).

However, you can have both subcommand groups and subcommands groups containing subcommands.

Adding options

Options can be added with a parameter annotated with @SlashOption.

All supported types are documented under ParameterResolver, and other types can be added.

Example

@Command
class SlashSayKotlin : ApplicationCommand() {
    @JDASlashCommand(name = "say", description = "Says something")
    suspend fun onSlashSay(event: GuildSlashEvent, @SlashOption(description = "What to say") content: String) {
        event.reply(content).await()
    }
}
@Command
public class SlashSay extends ApplicationCommand {
    @JDASlashCommand(name = "say", description = "Says something")
    public void onSlashSay(GuildSlashEvent event, @SlashOption(description = "What to say") String content) {
        event.reply(content).queue();
    }
}

Inferred option names

Display names of options can be set on the annotation, but can also be deduced from the parameter name, this is natively supported in Kotlin, but for Java, you will need to enable parameter names on the Java compiler.

Using choices

You must override getOptionChoices in order to return a list of choices, be careful to check against the command path as well as the option's display name.

Example

@Command
class SlashConvertKotlin : ApplicationCommand() {
    override fun getOptionChoices(guild: Guild?, commandPath: CommandPath, optionName: String): List<Choice> {
        if (commandPath.name == "convert") {
            if (optionName == "from" || optionName == "to") {
                return listOf(TimeUnit.SECONDS, TimeUnit.MINUTES, TimeUnit.HOURS, TimeUnit.DAYS)
                    // The Resolvers class helps us by providing resolvers for any enum type.
                    // We're just using the helper method to change an enum value to a more natural name.
                    .map { Choice(Resolvers.toHumanName(it), it.name) }
            }
        }

        return super.getOptionChoices(guild, commandPath, optionName)
    }

    @JDASlashCommand(name = "convert", description = "Convert time to another unit")
    suspend fun onSlashConvert(
        event: GuildSlashEvent,
        @SlashOption(description = "The time to convert") time: Long,
        @SlashOption(description = "The unit to convert from") from: TimeUnit,
        @SlashOption(description = "The unit to convert to") to: TimeUnit
    ) {
        event.reply("${to.convert(time, from)} ${to.name.lowercase()}").await()
    }
}
@Command
public class SlashConvert extends ApplicationCommand {
    @NotNull
    @Override
    public List<Choice> getOptionChoices(@Nullable Guild guild, @NotNull CommandPath commandPath, @NotNull String optionName) {
        if (commandPath.getName().equals("convert")) {
            if (optionName.equals("from") || optionName.equals("to")) {
                return Stream.of(TimeUnit.SECONDS, TimeUnit.MINUTES, TimeUnit.HOURS, TimeUnit.DAYS)
                        // The Resolvers class helps us by providing resolvers for any enum type.
                        // We're just using the helper method to change an enum value to a more natural name.
                        .map(u -> new Choice(Resolvers.toHumanName(u), u.name()))
                        .toList();
            }
        }

        return super.getOptionChoices(guild, commandPath, optionName);
    }

    @JDASlashCommand(name = "convert", description = "Convert time to another unit")
    public void onSlashConvert(
            GuildSlashEvent event,
            @SlashOption(description = "The time to convert") long time,
            @SlashOption(description = "The unit to convert from") TimeUnit from,
            @SlashOption(description = "The unit to convert to") TimeUnit to
    ) {
        event.reply(to.convert(time, from) + " " + to.name().toLowerCase()).queue();
    }
}

As you can see, despite the short choice list, the method is quite lengthy and causes duplications with multiple commands. This issue is solved with predefined choices.

Using autocomplete

Learn how to create an autocomplete handler here

Enabling autocompletion for an option is done by referencing an existing handler, in the autocomplete property of your @SlashOption.

Example

Using the autocomplete handler we made "Creating autocomplete handlers":

@Command
class SlashWord : ApplicationCommand() {
    @JDASlashCommand(name = "word", description = "Autocompletes a word")
    suspend fun onSlashWord(
        event: GuildSlashEvent,
        @SlashOption(description = "The word", autocomplete = SlashWordAutocomplete.WORD_AUTOCOMPLETE_NAME) word: String,
    ) {
        event.reply_("Your word was $word", ephemeral = true).await()
    }
}
@Command
public class SlashWord extends ApplicationCommand {
    @JDASlashCommand(name = "word", description = "Autocompletes a word")
    public void onSlashWord(GuildSlashEvent event,
                            @SlashOption(description = "The word", autocomplete = SlashWordAutocomplete.WORD_AUTOCOMPLETE_NAME) String word) {
        event.reply("Your word was " + word).setEphemeral(true).queue();
    }
}

Generated values

Generated values are parameters that get their values from a lambda everytime a command is run.

You must give one by overriding ApplicationCommand.getGeneratedValueSupplier, similarly to adding choices.

As always, make sure to check against the command path as well as the option's display name.

Example

@Command
class SlashCreateTimeKotlin : ApplicationCommand() {
    override fun getGeneratedValueSupplier(
        guild: Guild?,
        commandId: String?,
        commandPath: CommandPath,
        optionName: String,
        parameterType: ParameterType
    ): ApplicationGeneratedValueSupplier {
        if (commandPath.name == "create_time") {
            if (optionName == "timestamp") {
                // Create a snapshot of the instant the command was created
                val now = Instant.now()
                // Give back the instant snapshot, as this will be called every time the command runs
                return ApplicationGeneratedValueSupplier { now }
            }
        }

        return super.getGeneratedValueSupplier(guild, commandId, commandPath, optionName, parameterType)
    }

    @JDASlashCommand(name = "create_time", description = "Shows the creation time of this command")
    suspend fun onSlashCreateTime(
        event: GuildSlashEvent,
        @GeneratedOption timestamp: Instant
    ) {
        event.reply("I was created on ${TimeFormat.DATE_TIME_SHORT.format(timestamp)}").await()
    }
}
@Command
public class SlashCreateTime extends ApplicationCommand {
    @NotNull
    @Override
    public ApplicationGeneratedValueSupplier getGeneratedValueSupplier(
            @Nullable Guild guild,
            @Nullable String commandId,
            @NotNull CommandPath commandPath,
            @NotNull String optionName,
            @NotNull ParameterType parameterType
    ) {
        if (commandPath.getName().equals("create_time")) {
            if (optionName.equals("timestamp")) {
                // Create a snapshot of the instant the command was created
                final Instant now = Instant.now();
                // Give back the instant snapshot, as this will be called every time the command runs
                return event -> now;
            }
        }

        return super.getGeneratedValueSupplier(guild, commandId, commandPath, optionName, parameterType);
    }

    @JDASlashCommand(name = "create_time", description = "Shows the creation time of this command")
    public void onSlashTimeIn(
            GuildSlashEvent event,
            @GeneratedOption Instant timestamp
    ) {
        event.reply("I was created on " + TimeFormat.DATE_TIME_SHORT.format(timestamp)).queue();
    }
}

Rate limiting

This lets you reject application commands if the user tries to use them too often.

Using an (anonymous) rate limiter

Use @RateLimit or @Cooldown to define one on an application command.

@Command
class SlashRateLimit : ApplicationCommand() {

    // A rate limit for this slash command only
    @RateLimit(
        // Apply to each user, regardless of channel/guild
        scope = RateLimitScope.USER,
        // Delete the message telling the user about the remaining rate limit after it expires
        deleteOnRefill = true,
        // At least one of those needs to be empty for the command to be rejected
        bandwidths = [
            // 5 uses, 5 tokens gets added gradually over an hour (so, 1 token every 12 minutes)
            Bandwidth(
                capacity = 5, // 5 uses
                Refill(
                    type = RefillType.GREEDY,       // Gradually
                    tokens = 5,                     // gives 5 tokens
                    period = 1,                     // every 1
                    periodUnit = ChronoUnit.HOURS   // hour
                )
            ),
            // 2 uses, 2 tokens gets added at once after 2 minutes
            // This is to prevent the user from spamming the command
            Bandwidth(
                capacity = 2, // 2 uses
                Refill(
                    type = RefillType.INTERVAL,     // At once,
                    tokens = 2,                     // give 2 tokens
                    period = 2,                     // every 2
                    periodUnit = ChronoUnit.MINUTES // minutes
                )
            ),
        ]
    )
    @JDASlashCommand(name = "rate_limit")
    suspend fun onSlashRateLimit(event: GuildSlashEvent) {
        event.reply("Hello world!").await()
    }
}
@Command
public class SlashRateLimit extends ApplicationCommand {

    // A rate limit for this slash command only
    @RateLimit(
            // Apply to each user, regardless of channel/guild
            scope = RateLimitScope.USER,
            // Delete the message telling the user about the remaining rate limit after it expires
            deleteOnRefill = true,
            // At least one of those needs to be empty for the command to be rejected
            bandwidths = {
                    // 5 uses, 5 tokens gets added gradually over an hour (so, 1 token every 12 minutes)
                    @Bandwidth(
                            capacity = 5, // 5 uses
                            refill = @Refill(
                                    type = RefillType.GREEDY,       // Gradually
                                    tokens = 5,                     // gives 5 tokens
                                    period = 1,                     // every 1
                                    periodUnit = ChronoUnit.HOURS   // hour
                            )
                    ),
                    // 2 uses, 2 tokens gets added at once after 2 minutes
                    // This is to prevent the user from spamming the command
                    @Bandwidth(
                            capacity = 2, // 2 uses
                            refill = @Refill(
                                    type = RefillType.INTERVAL,     // At once,
                                    tokens = 2,                     // give 2 tokens
                                    period = 2,                     // every 2
                                    periodUnit = ChronoUnit.MINUTES // minutes
                            )
                    ),
            }
    )
    @JDASlashCommand(name = "rate_limit")
    public void onSlashRateLimit(GuildSlashEvent event) {
        event.reply("Hello world!").queue();
    }
}

Using an existing rate limiter

Nothing as simple as using @RateLimitReference with the group of a rate limiter defined in a RateLimitProvider.

Learn how to create a rate limiter with "Defining a rate limit"

@BService
class WikiRateLimitProvider : RateLimitProvider {

    override fun declareRateLimit(manager: RateLimitManager) {
        // Lets the user use the command 5 times in an hour, but also 2 times in 2 minutes to prevent spam
        val bucketConfiguration = Buckets.spikeProtected(
            capacity = 5,               // 5 uses
            duration = 1.hours,         // Gives 5 tokens gradually, during an hour (1 token every 12 minutes)
            spikeCapacity = 2,          // 2 uses
            spikeDuration = 2.minutes   // Give 2 tokens every 2 minutes
        )

        val rateLimiter = RateLimiter.createDefault(
            // Apply to each user, regardless of channel/guild
            RateLimitScope.USER,
            // Delete the message telling the user about the remaining rate limit after it expires
            deleteOnRefill = true,
            // Give our constant bucket configuration
            configurationSupplier = bucketConfiguration.toSupplier()
        )

        // Register, any command can use it
        manager.rateLimit(RATE_LIMIT_GROUP, rateLimiter)
    }

    companion object {
        // The name of the rate limit, so you can reference it in your commands/components
        const val RATE_LIMIT_GROUP = "Wiki"
    }
}
@Command
class SlashRateLimitExisting : ApplicationCommand() {

    // A rate limit for this slash command only
    @RateLimitReference(WikiRateLimitProvider.RATE_LIMIT_GROUP)
    @JDASlashCommand(name = "rate_limit_existing")
    suspend fun onSlashRateLimit(event: GuildSlashEvent) {
        event.reply("Hello world!").await()
    }
}
@BService
public class WikiRateLimitProvider implements RateLimitProvider {
    // The name of the rate limit, so you can reference it in your commands/components
    public static final String RATE_LIMIT_GROUP = "Wiki";

    @Override
    public void declareRateLimit(@NotNull RateLimitManager rateLimitManager) {
        // Lets the user use the command 5 times in an hour, but also 2 times in 2 minutes to prevent spam
        final var bucketConfiguration = Buckets.createSpikeProtected(
                5,                      // 5 uses
                Duration.ofHours(1),    // Gives 5 tokens gradually, during an hour (1 token every 12 minutes)
                2,                      // 2 uses
                Duration.ofMinutes(2)   // Give 2 tokens every 2 minutes
        );

        final var rateLimiter = RateLimiter.createDefault(
                // Apply to each user, regardless of channel/guild
                RateLimitScope.USER,
                // Give our constant bucket configuration
                BucketConfigurationSupplier.constant(bucketConfiguration),
                // Delete the message telling the user about the remaining rate limit after it expires
                true
        );

        // Register
        rateLimitManager.rateLimit(RATE_LIMIT_GROUP, rateLimiter);
    }
}
@Command
public class SlashRateLimitExisting extends ApplicationCommand {

    // A rate limit for this slash command only
    @RateLimitReference(WikiRateLimitProvider.RATE_LIMIT_GROUP)
    @JDASlashCommand(name = "rate_limit_existing")
    public void onSlashRateLimit(GuildSlashEvent event) {
        event.reply("Hello world!").queue();
    }
}

Cooldown

A cooldown is a rate limit, but with fewer parameters, it can be used as @Cooldown(5, ChronoUnit.SECONDS /* also scope and deleteOnRefill */).

Filtering commands

You can use @DeclarationFilter if you wish to declare a guild command conditionally.

Note

There is no equivalent for DSL commands as you can check and return early with your own code.

Creating the filter

Create a service implementing CommandDeclarationFilter, and make the method return true if the command can be declared. All filters must return true for the command to be declared.

Example

@BService
class BotOwnerIsGuildOwnerDeclarationFilter(
    private val botOwners: BotOwners, // Provided by the framework
) : CommandDeclarationFilter {

    override fun filter(guild: Guild, path: CommandPath, commandId: String?): Boolean {
        // Only allow this command to be in guilds owned by the bot owner
        return guild.ownerIdLong in botOwners.ownerIds
    }
}
@BService
public class BotOwnerIsGuildOwnerDeclarationFilter implements CommandDeclarationFilter {
    private final BotOwners botOwners; // Provided by the framework

    public BotOwnerIsGuildOwnerDeclarationFilter(BotOwners botOwners) {
        this.botOwners = botOwners;
    }

    @Override
    public boolean filter(@NotNull Guild guild, @NotNull CommandPath commandPath, @Nullable String s) {
        // Only allow this command to be in guilds owned by the bot owner
        return botOwners.getOwnerIds().contains(guild.getOwnerIdLong());
    }
}

Using the filter

Add a @DeclarationFilter on your command and reference your filter inside it.

Example

@Command
class SlashBotOwnerIsGuildOwner : ApplicationCommand() {

    // All filters must return 'true' for the command to be declared
    @DeclarationFilter(BotOwnerIsGuildOwnerDeclarationFilter::class)
    // The command needs to be registered on guilds for it to be filtered
    @TopLevelSlashCommandData(scope = CommandScope.GUILD)
    @JDASlashCommand(name = "bot_owner_is_guild_owner")
    suspend fun onSlashBotOwnerIsGuildOwner(event: GuildSlashEvent) {
        event.reply_("You are the owner of this bot and guild!", ephemeral = true).await()
    }
}
@Command
public class SlashBotOwnerIsGuildOwner extends ApplicationCommand {

    // All filters must return 'true' for the command to be declared
    @DeclarationFilter(BotOwnerIsGuildOwnerDeclarationFilter.class)
    // The command needs to be registered on guilds for it to be filtered
    @TopLevelSlashCommandData(scope = CommandScope.GUILD)
    @JDASlashCommand(name = "bot_owner_is_guild_owner")
    public void onSlashBotOwnerIsGuildOwner(GuildSlashEvent event) {
        event.reply("You are the owner of this bot and guild!")
                .setEphemeral(true)
                .queue();
    }
}

DSL commands (Kotlin)

Commands can be DSL-declared by either implementing:

You can then use the slashCommand method on the manager, give it the command name, the command method, and then configure your command.

Tip

You are allowed to not add any command at all, for example, if the guild in GuildApplicationCommandManager isn't a guild you want the command to appear in.

Example

@Command
class SlashPingKotlinDsl : GlobalApplicationCommandProvider {
    suspend fun onSlashPing(event: GuildSlashEvent) {
        event.deferReply(true).queue()

        val ping = event.jda.getRestPing().await()
        event.hook.editOriginal("Pong! $ping ms").await()
    }

    override fun declareGlobalApplicationCommands(manager: GlobalApplicationCommandManager) {
        // Default scope is global, guild-only (GUILD_NO_DM)
        manager.slashCommand("ping", function = ::onSlashPing) {
            description = "Pong!"
        }
    }
}

Subcommands

As top-level commands cannot be made alongside subcommands, the top-level function must be null.

You can then add a subcommand by using subcommand, where each subcommand is its own function.

Example

@Command
class SlashTagDsl : GlobalApplicationCommandProvider {
    fun onSlashTagCreate(event: GuildSlashEvent) {
        // ...
    }

    fun onSlashTagDelete(event: GuildSlashEvent) {
        // ...
    }

    override fun declareGlobalApplicationCommands(manager: GlobalApplicationCommandManager) {
        // Pass a null function as this is not a top-level command
        manager.slashCommand("tag", function = null) {
            description = "Manage tags"

            subcommand("create", ::onSlashTagCreate) {
                description = "Creates a tag"
            }

            subcommand("delete", ::onSlashTagDelete) {
                description = "Deletes a tag"
            }
        }
    }
}

Info

You can still create both subcommands, and subcommand groups containg subcommands.

Adding options

Options can be added with a parameter and declaring it using option in your command builder, where the declaredName is the name of your parameter, the block will let you change the description, choices, etc.

All supported types are documented under ParameterResolver, and other types can be added.

Example

@Command
class SlashSayKotlinDsl : GlobalApplicationCommandProvider {
    suspend fun onSlashSay(event: GuildSlashEvent, content: String) {
        event.reply(content).await()
    }

    override fun declareGlobalApplicationCommands(manager: GlobalApplicationCommandManager) {
        manager.slashCommand("say", function = ::onSlashSay) {
            description = "Says something"

            option("content") {
                description = "What to say"
            }
        }
    }
}

Tip

You can override the option name by setting optionName in the option declaration:

option("content", optionName = "sentence") {
    ...
}

Using choices

Adding choices is very straight forward, you only have to give a list of choices to the choice property.

Example

@Command
class SlashConvertKotlinDsl : GlobalApplicationCommandProvider {
    suspend fun onSlashConvert(event: GuildSlashEvent, time: Long, from: TimeUnit, to: TimeUnit) {
        event.reply("${to.convert(time, from)} ${to.name.lowercase()}").await()
    }

    override fun declareGlobalApplicationCommands(manager: GlobalApplicationCommandManager) {
        manager.slashCommand("convert", function = ::onSlashConvert) {
            description = "Convert time to another unit"

            option("time") {
                description = "The time to convert"
            }

            option("from") {
                description = "The unit to convert from"

                choices = listOf(TimeUnit.SECONDS, TimeUnit.MINUTES, TimeUnit.HOURS, TimeUnit.DAYS)
                    // The Resolvers class helps us by providing resolvers for any enum type.
                    // We're just using the helper method to change an enum value to a more natural name.
                    .map { Choice(Resolvers.toHumanName(it), it.name) }
            }

            option("to") {
                description = "The unit to convert to"

                choices = listOf(TimeUnit.SECONDS, TimeUnit.MINUTES, TimeUnit.HOURS, TimeUnit.DAYS)
                    // The Resolvers class helps us by providing resolvers for any enum type.
                    // We're just using the helper method to change an enum value to a more natural name.
                    .map { Choice(Resolvers.toHumanName(it), it.name) }
            }
        }
    }
}

As you can see, despite the short choice list, this causes duplications with multiple commands. This issue is solved with predefined choices.

Using autocomplete

Learn how to create an autocomplete handler here

Enabling autocompletion for an option is done by referencing an existing handler, either using autocompleteByFunction or autocompleteByName.

Tip

I recommend using autocompleteByFunction as it avoids typing the name twice.

Example

Using the autocomplete handler we made "Creating autocomplete handlers":

@Command
class SlashWordDsl : GlobalApplicationCommandProvider {
    suspend fun onSlashWord(event: GuildSlashEvent, word: String) {
        event.reply_("Your word was $word", ephemeral = true).await()
    }

    override fun declareGlobalApplicationCommands(manager: GlobalApplicationCommandManager) {
        manager.slashCommand("word", function = ::onSlashWord) {
            description = "Autocompletes a word"

            option("word") {
                description = "The word"

                // Use an existing autocomplete declaration
                autocompleteByFunction(SlashWordAutocompleteDsl::onWordAutocomplete)
            }
        }
    }
}

Rate limiting

This lets you reject application commands if the user tries to use them too often.

Learn how to create a rate limiter with "Defining a rate limit"

Using an (anonymous) rate limiter

@Command
class SlashRateLimitDsl : GlobalApplicationCommandProvider {

    suspend fun onSlashRateLimit(event: GuildSlashEvent) {
        event.reply("Hello world!").await()
    }

    override fun declareGlobalApplicationCommands(manager: GlobalApplicationCommandManager) {
        manager.slashCommand("rate_limit_dsl", function = ::onSlashRateLimit) {
            // Lets the user use the command 5 times in an hour, but also 2 times in 2 minutes to prevent spam
            val bucketConfiguration = Buckets.spikeProtected(
                capacity = 5,               // 5 uses
                duration = 1.hours,         // Gives 5 tokens gradually, during an hour (1 token every 12 minutes)
                spikeCapacity = 2,          // 2 uses
                spikeDuration = 2.minutes   // Give 2 tokens every 2 minutes
            )

            val rateLimiter = RateLimiter.createDefault(
                // Apply to each user, regardless of channel/guild
                RateLimitScope.USER,
                // Delete the message telling the user about the remaining rate limit after it expires
                deleteOnRefill = true,
                // Give our constant bucket configuration
                configurationSupplier = bucketConfiguration.toSupplier()
            )

            rateLimit(rateLimiter)
        }
    }
}

Using an existing rate limiter

Nothing as simple as using rateLimitReference with the group of a rate limiter defined in a RateLimitProvider.

@BService
class WikiRateLimitProvider : RateLimitProvider {

    override fun declareRateLimit(manager: RateLimitManager) {
        // Lets the user use the command 5 times in an hour, but also 2 times in 2 minutes to prevent spam
        val bucketConfiguration = Buckets.spikeProtected(
            capacity = 5,               // 5 uses
            duration = 1.hours,         // Gives 5 tokens gradually, during an hour (1 token every 12 minutes)
            spikeCapacity = 2,          // 2 uses
            spikeDuration = 2.minutes   // Give 2 tokens every 2 minutes
        )

        val rateLimiter = RateLimiter.createDefault(
            // Apply to each user, regardless of channel/guild
            RateLimitScope.USER,
            // Delete the message telling the user about the remaining rate limit after it expires
            deleteOnRefill = true,
            // Give our constant bucket configuration
            configurationSupplier = bucketConfiguration.toSupplier()
        )

        // Register, any command can use it
        manager.rateLimit(RATE_LIMIT_GROUP, rateLimiter)
    }

    companion object {
        // The name of the rate limit, so you can reference it in your commands/components
        const val RATE_LIMIT_GROUP = "Wiki"
    }
}
@Command
class SlashRateLimitExistingDsl : GlobalApplicationCommandProvider {

    suspend fun onSlashRateLimit(event: GuildSlashEvent) {
        event.reply("Hello world!").await()
    }

    override fun declareGlobalApplicationCommands(manager: GlobalApplicationCommandManager) {
        manager.slashCommand("rate_limit_existing_dsl", function = ::onSlashRateLimit) {
            // Use the rate limiter we defined in [[WikiRateLimitProvider]]
            rateLimitReference(WikiRateLimitProvider.RATE_LIMIT_GROUP)
        }
    }
}

Cooldown

A cooldown is a rate limit, but with fewer parameters, it can be used as cooldown(5.seconds /* also scope and deleteOnRefill */).

Generated values

Generated values are a command parameter that gets their values computed by the given block everytime the command run.

Contrary to the annotated commands, no checks are required, as this is tied to the currently built command.

Example

@Command
class SlashCreateTimeKotlinDsl : GlobalApplicationCommandProvider {
    suspend fun onSlashCreateTime(event: GuildSlashEvent, timestamp: Instant) {
        event.reply("I was created on ${TimeFormat.DATE_TIME_SHORT.format(timestamp)}").await()
    }

    override fun declareGlobalApplicationCommands(manager: GlobalApplicationCommandManager) {
        manager.slashCommand("create_time", function = ::onSlashCreateTime) {
            description = "Shows the creation time of this command"

            // Create a snapshot of the instant the command was created
            val now = Instant.now()
            generatedOption("timestamp") {
                // Give back the instant snapshot, as this will be called every time the command is run
                return@generatedOption now
            }
        }
    }
}

Default description

You can avoid setting the (non-localized) descriptions of your commands and options by putting them in a localization file, using the root locale (i.e., no locale suffix), and have your localization bundle registered with BApplicationConfigBuilder#addLocalizations.

The same commands as before, but without the descriptions
@Command
class SlashSayDefaultDescriptionKotlin : ApplicationCommand() {
    @JDASlashCommand(name = "say_default_description")
    suspend fun onSlashSayDefaultDescription(event: GuildSlashEvent, @SlashOption content: String) {
        event.reply(content).await()
    }
}
@Command
class SlashSayDefaultDescriptionKotlinDsl : GlobalApplicationCommandProvider {
    suspend fun onSlashSayDefaultDescription(event: GuildSlashEvent, content: String) {
        event.reply(content).await()
    }

    override fun declareGlobalApplicationCommands(manager: GlobalApplicationCommandManager) {
        manager.slashCommand("say_default_description", function = ::onSlashSayDefaultDescription) {
            option("content")
        }
    }
}
@Command
public class SlashSayDefaultDescription extends ApplicationCommand {
    @JDASlashCommand(name = "say_default_description")
    public void onSlashSayDefaultDescription(GuildSlashEvent event, @SlashOption String content) {
        event.reply(content).queue();
    }
}

Adding the root localization bundle

For the given resource bundle:

src/main/resources/bc_localization/Commands.json
{
  "say_default_description": {
    "description": "Says something",
    "options": {
      "content.description": "What to say"
    }
  }
}

You can add the bundle by calling BApplicationConfigBuilder#addLocalizations("Commands").

Using predefined choices

If your choices stay the same for every command, you can improve re-usability and avoid extra code by using choices on the resolver's level, that is, the resolver will return the choices used for every option of their type.

All you now need to do is enable usePredefinedChoices on your option.

Example

Here, the resolver for TimeUnit is already defined and will be explained in Adding option resolvers.

@Command
class SlashConvertSimplifiedKotlin : ApplicationCommand() {
    @JDASlashCommand(name = "convert_simplified", description = "Convert time to another unit")
    suspend fun onSlashConvertSimplified(
        event: GuildSlashEvent,
        @SlashOption(description = "The time to convert") time: Long,
        @SlashOption(description = "The unit to convert from", usePredefinedChoices = true) from: TimeUnit,
        @SlashOption(description = "The unit to convert to", usePredefinedChoices = true) to: TimeUnit
    ) {
        event.reply("${to.convert(time, from)} ${to.name.lowercase()}").await()
    }
}
@Command
class SlashConvertSimplifiedKotlinDsl : GlobalApplicationCommandProvider {
    suspend fun onSlashConvertSimplified(event: GuildSlashEvent, time: Long, from: TimeUnit, to: TimeUnit) {
        event.reply("${to.convert(time, from)} ${to.name.lowercase()}").await()
    }

    override fun declareGlobalApplicationCommands(manager: GlobalApplicationCommandManager) {
        manager.slashCommand("convert_simplified", function = ::onSlashConvertSimplified) {
            description = "Convert time to another unit"

            option("time") {
                description = "The time to convert"
            }

            option("from") {
                description = "The unit to convert from"

                usePredefinedChoices = true
            }

            option("to") {
                description = "The unit to convert to"

                usePredefinedChoices = true
            }
        }
    }
}
@Command
public class SlashConvertSimplified extends ApplicationCommand {
    @JDASlashCommand(name = "convert_simplified", description = "Convert time to another unit")
    public void onSlashTimeInSimplified(
            GuildSlashEvent event,
            @SlashOption(description = "The time to convert") long time,
            @SlashOption(description = "The unit to convert from", usePredefinedChoices = true) TimeUnit from,
            @SlashOption(description = "The unit to convert to", usePredefinedChoices = true) TimeUnit to
    ) {
        event.reply(to.convert(time, from) + " " + to.toString().toLowerCase()).queue();
    }
}

Update logs

You can optionally get more info on what changed in your application commands, by enabling the TRACE logs on io.github.freya022.botcommands.internal.commands.application.diff.DiffLogger, or any package it is in.

Examples

You can take a look at more examples here.