Slash is a library written 100% with Kotlin that works with JDA 5.0.0-beta.3 (Java Discord API) for an advanced implementation of Application Commands for Discord.
This library does not synchronize the commands created with the commands published on Discord.
- Implement handler for default command.
- Implement handlers for sub-commands.
- Implement handlers for sub-commands groups.
- Add support for Auto Complete command interactions.
- Add support for Modals command interactions.
- Add support for non-guild commands (DM commands).
- Synchronize discord published commands with create commands.
- Useful docs.
- Be a nice package :).
- Generate commands at build time.
Package Name | Required Version |
---|---|
Kotlinx Coroutines | 1.7.1 |
Java JDK | 11 |
JDA | 5.0.0-beta.19 |
Kotlin | 1.9.10 |
Sentry | 7.1.0 (optional) |
Create a command inside the package net.example.commands
called PingCommand.kt
:
class PingCommand : BaseSlashCommand("ping") {
// This command can be used on guilds and direct messages.
// SlashCommandContext is used on DM and ALL targets.
@OnSlashCommand(target = InteractionTarget.ALL)
suspend fun default(ctx: SlashCommandContext) {
ctx.acknowledge(true)
val restPing = ctx.jda.restPing.await()
val gatewayPing = ctx.jda.gatewayPing
ctx.embed {
setTitle("Pong!")
addField("Rest", "${restPing}ms", true)
addField("Gateway", "${gatewayPing}ms", true)
}.queue()
}
}
class Whois : BaseSlashCommand("whois") {
// This command only can be used on guilds.
// If you try to use it with SlashCommandContext instead of GuildSlashCommandContext
// the library will report warms about this.
@OnSlashCommand(target = InteractionTarget.GUILD)
suspend fun default(ctx: GuildSlashCommandContext, member: Member) {
ctx.embed {
setAuthor(/* ... */)
setTitle("Whois ${member.asTag}")
setDescription(/* ... */)
}.queue()
// When using queue on a ContextAction will automatically select between
// reply() and send()
}
}
Create a command inside package net.example.commands
called RoleCommand.kt
:
class RoleCommand : BaseSlashCommand("role") {
// The parsed path is role/add
// This handler required MANAGE_ROLES permission fot both, bot and user who execute the command.
@OnSlashCommand("add", target = InteractionTarget.GUILD)
@Permissions(bot = [Permission.MANAGE_ROLES], user = [Permission.MANAGE_ROLES])
suspend fun addRole(ctx: GuildSlashCommandContext, member: Member) {
// This handler will add a role to the member.
}
// The parsed path is role/remove
// This handler required MANAGE_ROLES permission fot both, bot and user who execute the command.
@OnSlashCommand("remove", target = InteractionTarget.GUILD)
@Permissions(bot = [Permission.MANAGE_ROLES], user = [Permission.MANAGE_ROLES])
suspend fun removeRole(ctx: GuildSlashCommandContext, member: Member) {
// This handler will remove a role to a member if the member have the role.
}
// The parsed path is role/list
@OnSlashCommand("list", target = InteractionTarget.GUILD)
suspend fun listRoles(ctx: GuildSlashCommandContext, member: Member?) {
// This handler has a nullable param, that means the option on the command event
// can be null.
}
// The parsed path is role/compare
@OnSlashCommand("compare", target = InteractionTarget.GUILD)
suspend fun compareRoles(ctx: GuildSlashCommandContext, member1: Member, member2: Member) {
// This handler will compare the roles between two members from the guild.
}
}
This command will create 4 handlers with the following user representation:
- /role add: (member)
- /role remove: (member)
- /role list: (member?)
- /role compare: (member) (member)
Create a command inside package net.example.commands
called TwitchCommand.kt
:
class TwitchCommand : BaseSlashCommand("twitch") {
// The parsed path is twitch/clips/top
@OnSlashCommand(group = "clips", name = "top", target = InteractionTarget.ALL)
@Permissions(bot = [Permission.MESSAGE_EMBED_LINKS])
suspend fun clipTop(ctx: SlashCommandContext, channel: String?) {
}
// The parsed path is twitch/clips/random
@OnSlashCommand(group = "clips", name = "random", target = InteractionTarget.ALL)
@Permissions(bot = [Permission.MESSAGE_EMBED_LINKS])
suspend fun clipRandom(ctx: SlashCommandContext, channel: String?) {
}
}
This command will create 2 handlers with the following user representation:
- /twitch clips top (channel?)
- /twitch clips random (channel?)
Register the handler using SlashCommandClient.default(packageName)
with the package name where the commands are located, and register
the event listener in your JDA or ShardManager builder.
val shardManager = DefaultShardManagerBuilder().apply { /* ... */ }.build(false)
val commandClient = SlashCommandClient.default("com.example.commands")
.contextCreator(object : ContextCreator {
// You can override the default ContextCreator
override suspend fun createContext(event: SlashCommandInteractionEvent): SlashCommandContext {
return SlashCommandContext.impl(event)
}
// SlashCommandContext and GuildSlashCommandContext contains an extra object
// that is a AtomicReference<Any?> so you can set any object here on the context creation
// and retrieve it when you handle the command.
override suspend fun createGuildContext(event: SlashCommandInteractionEvent): GuildSlashCommandContext {
val context = SlashCommandContext.guild(event)
context.extra.set(Utils.getGuildConfig(event))
return context
}
})
.addCheck { ctx ->
// Imagine you have an ignored channels filter, you can add the global check here.
if (!ctx.isFromGuild || ctx.guild == null) return true
val cannotExecute = Utils.checkIgnoredChannels(ctx.guild)
return !cannotExecute
}
.buildWith(shardManager)
commandClient
will register PingCommand
, RoleCommand
and TwitchCommand
.
You can build context actions inside SlashCommands so easy.
@OnSlashCommand(target = InteractionTarget.ALL)
suspend fun contextActions(ctx: SlashCommandContext) {
// This is a context action
val embedAction: EmbedContextAction = ctx.embed {
setTitle("Embed Title")
}
val messageAction: MessageContextAction = ctx.message {
append("Message content")
}
// To execute an action use send() or reply()
// Only use this if you know if the interaction was acknowledged or
// you need the response from discord.
val messageResult: ReplyAction = messageAction.reply().await()
val embedResult: WebhookMessageAction<Message> = embedAction.send().await()
// You can queue the request
// This will check if the interaction was acknowledged previusly and use the correct behaviour
ctx.embed {
setDescription("This is the third message but i dont need to use reply() or send()")
}.queue()
// When using queue() you can set if the message hast the ephemeral flag or not (by default is set to false)
ctx.message("This message will be ephemeral").queue(true)
// Ephemeral messages only are ephemeral when the first reply is ephemeral.
// You can get the generated message use 'original'.
val embed = embedAction.original
val message = messageAction.original
}
You have 3 seconds to respond or acknowledge an interaction, you can handle this so easy with the following code.
@OnSlashCommand(target = InteractionTarget.ALL)
suspend fun someCommand(ctx: SlashCommandContext) {
// If your need to wait before continue the code execution, you can use
ctx.tryAcknowledge().await()
// But if the interaction is already acknowledged this will throw an IllegalStateException.
// If you don't know if your interaction was acknowledged and don't need to wait use
ctx.acknowledge()
// If you want to the interaction follow-up messages to be ephemeral, you need to set true when using the function.
ctx.acknowledge(true)
}
You can use the annotation @OptionName the set a custom name for an option.
@OnSlashCommand(target = InteractionTarget.ALL)
suspend fun customName(ctx: SlashCommandContext, @OptionName("query") option1: String) {
// the variable option1 will get the content of ctx.getOption("query")!
}
Since version 0.6.3 you can rate limit the execution of slash commands based on 3 different targets:
- User
- Channel
- Guild
Configure the rate limited (is not necessary):
val commandHandler = SlashCommandClient.default("com.example.commands")
.configureRateLimit {
purgeUnit = TimeUnit.MINUTES
purgeDelay = 5
onRateLimitHit = { ctx, rateLimit ->
// Override default with your implementation.
}
}
AutoCompleteContext extends CommandAutoCompleteInteraction.
@OnAutoComplete("commands", "search", optionName = "query")
suspend fun commandSearchAutocomplete(ctx: AutoCompleteContext) {
}
ModalContext extends ModalInteraction
@OnModal supports RegEx.
@OnModal("feature-request:(.+?)")
suspend fun testModal(ctx: ModalContext) {
}
@RateLimit(quota = 5, duration = 20, unit = TimeUnit.SECONDS, target = RateLimit.Target.GUILD)
@OnSlashCommand(target = InteractionTarget.GUILD)
fun rateLimitedCommand(ctx: GuildSlashCommand) {
ctx.message {
append("This is an example of a rate limited Slash Command, ")
append("If you hit the limit you will be notified.")
}.queue()
}
@RateLimit annotation documentation
repositories {
mavenCentral()
}
dependencies {
implementation("tv.blademaker:slash:x.y.z")
}
<dependency>
<groupId>tv.blademaker</groupId>
<artifactId>slash</artifactId>
<version>x.y.z</version>
</dependency>