Skip to content

Declarative slash commands

Declarative commands must be declared with Kotlin, but the command itself may be in any language.

Declarative commands allow you full control over the declaration, implement at least one of them:

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 free to skip registration of any command/subcommand/group, for example, if the guild in GuildApplicationCommandManager isn't a guild you want the command to appear in.

Example

@Command
class SlashPing : 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 SlashTag : 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 SlashSay : 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 SlashConvert : 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:

Example

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

@Command
class SlashWord : 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(SlashWordAutocomplete::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 SlashRateLimit : GlobalApplicationCommandProvider {

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

    override fun declareGlobalApplicationCommands(manager: GlobalApplicationCommandManager) {
        manager.slashCommand("rate_limit", 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 SlashRateLimitExisting : GlobalApplicationCommandProvider {

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

    override fun declareGlobalApplicationCommands(manager: GlobalApplicationCommandManager) {
        manager.slashCommand("rate_limit_existing", 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 SlashCreateTime : 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
            }
        }
    }
}