Using components¶
Components provided by the framework are your usual Discord components with additional features, they can be configured to:
- Be usable once
- Have timeouts
- Have method handlers or callbacks
- Have constraints (allow list for users/roles/permissions)
To get access to them, you can use the Buttons
and SelectMenus
factories,
as well as Components
to delete them manually and make groups.
Configuring components with Java
When configuring components, you need to use the framework's methods first, and then use the JDA methods, and finally build.
Enabling components¶
Components require a database to be set up, see this wiki page for more details.
You can then enable them with the enable
property in the components
configuration block.
BotCommands.create {
...
components {
enable = true
}
}
BotCommands.create(config -> {
...
config.components(components -> {
components.enable(true);
});
});
Components require a database to be set up, see this wiki page for more details.
You can then enable them with the botcommands.components.enable
property set to true
.
Disabling classes depending on components
You can use @RequiresComponents
if you want your class to be disabled when the components are not available.
Persistent components¶
They are components that still work after a restart,
their handlers are methods identified by their handler name,
set in @JDAButtonListener
/ @JDASelectMenuListener
.
Persistent components have a default timeout set in Components.defaultPersistentTimeout
,
which can be changed.
Info
Components which expired while the bot was offline will run their timeout handlers at startup.
Example
In Kotlin, we can use extensions to make sure we call our component handlers in a type-safe manner. This way, you will have a compiler error if the handler and the arguments don't match, it will also allow using handlers without setting a name.
This can only be used when the input argument types matches the handler parameter types.
Note
A similar timeoutWith
function exists for timeouts.
@Command
class SlashPersistentClicker(private val buttons: Buttons) : ApplicationCommand() {
@JDASlashCommand(name = "clicker", subcommand = "persistent", description = "Creates a button you can infinitely click")
suspend fun onSlashClicker(event: GuildSlashEvent) {
val button = createButton(event, count = 0)
event.replyComponents(row(button)).await()
}
// No need for a name if you use the type-safe "bindWith" extensions
@JDAButtonListener
suspend fun onCookieClick(event: ButtonEvent, @ComponentData count: Int) {
val button = createButton(event, count + 1)
event.editButton(button).await()
}
// Same thing here, no name required
@ComponentTimeoutHandler
fun onCookieTimeout(timeout: ComponentTimeoutData, @TimeoutData count: Int) {
println("User finished clicking $count cookies")
}
private suspend fun createButton(event: Interaction, @ComponentData count: Int): Button {
// Create a primary-styled button
return buttons.primary("$count cookies")
// Sets the emoji on the button,
// this can be an unicode emoji, an alias or even a custom emoji
.withEmoji("cookie")
// Create a button that can be used even after a restart
.persistent {
// Only allow the caller to use the button
constraints += event.user
// Timeout and call the method after the button hasn't been used for a day
// The timeout gets cancelled if the button is invalidated
timeoutWith(1.days, ::onCookieTimeout, count)
// When clicked, run the onCookieClick method with the count
// Extension for type-safe binding, no need to type the name
bindWith(::onCookieClick, count)
}
}
}
@Command
public class SlashClickerPersistent extends ApplicationCommand {
// Since Java doesn't have the same method references as Kotlin,
// we should use a constant name, so we don't have to type it more than once.
private static final String COOKIE_BUTTON_NAME = "SlashPersistentClicker: cookie";
private final Buttons buttons;
public SlashClickerPersistent(Buttons buttons) {
this.buttons = buttons;
}
@JDASlashCommand(name = "clicker", subcommand = "persistent", description = "Creates a button you can infinitely click")
public void onSlashClicker(GuildSlashEvent event) {
final Button button = createButton(event, 0);
event.replyComponents(ActionRow.of(button)).queue();
}
// The name should be unique,
// I recommend naming the handler "[ClassName]: [purpose]"
// And the name would be "on[purpose]Click"
@JDAButtonListener(COOKIE_BUTTON_NAME)
public void onCookieClick(ButtonEvent event, @ComponentData int count) {
final Button newButton = createButton(event, count + 1);
event.editButton(newButton).queue();
}
// Same thing here, names don't collide with other types of listener
@ComponentTimeoutHandler(COOKIE_BUTTON_NAME)
public void onCookieTimeout(ComponentTimeoutData timeout, @TimeoutData String count) {
System.out.println("User finished clicking " + count + " cookies");
}
private Button createButton(Interaction event, int count) {
// Create a primary-styled button
return buttons.primary(count + " cookies")
// Sets the emoji on the button,
// this can be an unicode emoji, an alias or even a custom emoji
.withEmoji("cookie")
// Create a button that can be used even after a restart
.persistent()
// Only allow the caller to use the button
.constraints(interactionConstraints -> {
interactionConstraints.addUsers(event.getUser());
})
// Timeout and call the method after the button hasn't been used for a day
// The timeout gets cancelled if the button is invalidated
.timeout(Duration.ofDays(1), COOKIE_BUTTON_NAME, count)
// When clicked, run the onCookieClick method with the count
.bindTo(COOKIE_BUTTON_NAME, count)
.build();
}
}
Ephemeral components¶
They are components which get invalidated after a restart, meaning they can no longer be used, their handlers are callbacks, which can also have a timeout set, and also use callbacks.
Info
"Invalidated" means that they are deleted from the database, but not necessarily from the message.
Ephemeral components have a default timeout set in Components.defaultEphemeralTimeout
, which can be changed.
Example
@Command
class SlashEphemeralClicker(private val buttons: Buttons) : ApplicationCommand() {
@JDASlashCommand(name = "clicker", subcommand = "ephemeral", description = "Creates a button you can click until the bot restarts or 15 minutes later")
suspend fun onSlashClicker(event: GuildSlashEvent) {
val button = createButton(event, count = 0)
event.replyComponents(row(button)).await()
}
private suspend fun createButton(event: IDeferrableCallback, count: Int): Button {
// Create a primary-styled button
return buttons.primary("$count cookies")
// Sets the emoji on the button,
// this can be an unicode emoji, an alias or even a custom emoji
.withEmoji("cookie")
// Create a button that can be used until the bot restarts
.ephemeral {
// Only allow the caller to use the button
constraints += event.user
// Run this callback 15 minutes after the button has been created
// The timeout gets cancelled if the button is invalidated
timeout(15.minutes) {
if (!event.hook.isExpired) {
event.hook.retrieveOriginal()
.map { it.components.asDisabled() }
.flatMap { event.hook.editOriginalComponents(it) }
.queue()
event.hook.sendMessage("You clicked $count cookies!").setEphemeral(true).queue()
} else {
println("User finished clicking $count cookies")
}
}
// When clicked, run this callback
bindTo { buttonEvent ->
val newButton = createButton(buttonEvent, count + 1)
buttonEvent.editButton(newButton).await()
}
}
}
}
You can also use components without setting a handler, and instead await the event:
@Command
class SlashEphemeralAwaitingClicker(private val buttons: Buttons) : ApplicationCommand() {
@JDASlashCommand(name = "clicker", subcommand = "ephemeral_await", description = "Creates a button you can click until the bot restarts or 15 minutes later")
suspend fun onSlashClicker(event: GuildSlashEvent) {
val button = createButton(event, count = 0)
event.replyComponents(row(button)).await()
var count = 0
// When the 15 minutes expire,
// the loop is stopped due to a TimeoutCancellationException (see 'await' on the button).
try {
while (true) {
// Wait for the button to be clicked and edit it with a new label
// you can keep the same button id as we keep awaiting the same one
val buttonEvent = button.await()
buttonEvent.editButton(button.withLabel("${++count} cookies")).await()
}
} catch (_: TimeoutCancellationException) { }
// Try to disable components if the interaction is still usable
if (!event.hook.isExpired) {
event.hook.retrieveOriginal()
.map { it.components.asDisabled() }
.flatMap { event.hook.editOriginalComponents(it) }
.queue()
event.hook.sendMessage("You clicked $count cookies!").setEphemeral(true).queue()
} else {
println("User finished clicking $count cookies")
}
}
private suspend fun createButton(event: Interaction, count: Int): Button {
// Create a primary-styled button
return buttons.primary("$count cookies")
// Sets the emoji on the button,
// this can be an unicode emoji, an alias or even a custom emoji
.withEmoji("cookie")
// Create a button that can be used until the bot restarts
.ephemeral {
// Only allow the caller to use the button
constraints += event.user
// Invalidate the button after 15 minutes, cancelling all coroutines awaiting on the button
timeout(15.minutes)
}
}
}
@Command
public class SlashClickerEphemeral extends ApplicationCommand {
private final Buttons buttons;
public SlashClickerEphemeral(Buttons buttons) {
this.buttons = buttons;
}
@JDASlashCommand(name = "clicker", subcommand = "ephemeral", description = "Creates a button you can click until the bot restarts")
public void onSlashClicker(GuildSlashEvent event) {
final Button button = createButton(event, 0);
event.replyComponents(ActionRow.of(button)).queue();
}
private Button createButton(Interaction event, int count) {
// Create a primary-styled button
return buttons.primary(count + " cookies")
// Sets the emoji on the button,
// this can be an unicode emoji, an alias or even a custom emoji
.withEmoji("cookie")
// Create a button that can be used until the bot restarts
.ephemeral()
// Only allow the caller to use the button
.constraints(interactionConstraints -> {
interactionConstraints.addUsers(event.getUser());
})
// Run this callback 15 minutes after the button has been created
// The timeout gets cancelled if the button is invalidated
.timeout(Duration.ofMinutes(15), () -> {
System.out.println("User finished clicking " + count + " cookies");
})
// When clicked, run this callback
.bindTo(buttonEvent -> {
final Button newButton = createButton(buttonEvent, count + 1);
buttonEvent.editButton(newButton).queue();
})
.build();
}
}
Component groups¶
Component groups can be created in any component factory, and allow you to configure one timeout for all components.
Also, when one of them gets invalidated (after being used with singleUse = true
),
the entire group gets invalidated.
For example, this can be useful when the user needs to use a single component, once.
Ephemeral components in groups
If you put ephemeral components in your group, you must disable the timeout with noTimeout()
.
The timeout works similarly to components, except the annotated handler is a @GroupTimeoutHandler
.
Example
@Command
class SlashClickGroup(private val buttons: Buttons) : ApplicationCommand() {
@JDASlashCommand(name = "click_group", description = "Sends two buttons and waits for any of them to be clicked")
suspend fun onSlashClickGroup(event: GuildSlashEvent) {
val firstButton = buttons.primary("1").ephemeral {
// Disable the timeout so we can use a group timeout
noTimeout()
// Make it so this button is only usable once
singleUse = true
// Only allow the caller to use the button
constraints += event.user
}
val secondButton = buttons.primary("2").ephemeral {
// Disable the timeout so we can use a group timeout
noTimeout()
// Make it so this button is only usable once
singleUse = true
// Only allow the caller to use the button
constraints += event.user
}
// Construct our group, make it expire after 1 minute
val group = buttons.group(firstButton, secondButton).ephemeral {
timeout(1.minutes)
}
event.replyComponents(row(firstButton, secondButton)).await()
// Wait for the allowed user to click one of the buttons
val buttonEvent = group.awaitAnyOrNull<ButtonEvent>()
?: return event.hook
.replaceWith("Expired!")
.awaitUnit()
// Disable the other button
buttonEvent.editButton(buttonEvent.button.asDisabled()).await()
buttonEvent.hook.editOriginal("Try clicking the other button, you can't :^)").await()
}
}
@Command
public class SlashClickGroup extends ApplicationCommand {
// Since Java doesn't have the same method references as Kotlin,
// we should use a constant name, so we don't have to type it more than once.
private static final String COOKIE_BUTTON_NAME = "SlashPersistentClicker: cookie";
private final Buttons buttons;
public SlashClickGroup(Buttons buttons) {
this.buttons = buttons;
}
@JDASlashCommand(name = "click_group", description = "Sends two buttons and waits for any of them to be clicked")
public void onSlashClicker(GuildSlashEvent event) {
final var firstButton = buttons.primary("1")
.ephemeral()
// Disable the timeout so we can use a group timeout
.noTimeout()
// Make it so this button is only usable once
.singleUse(true)
// Only allow the caller to use the button
.constraints(interactionConstraints -> {
interactionConstraints.addUsers(event.getUser());
})
// Run this method when the button is clicked
.bindTo(this::onButtonClick)
.build();
final var secondButton = buttons.primary("2")
.ephemeral()
// Disable the timeout so we can use a group timeout
.noTimeout()
// Make it so this button is only usable once
.singleUse(true)
// Only allow the caller to use the button
.constraints(interactionConstraints -> {
interactionConstraints.addUsers(event.getUser());
})
// Run this method when the button is clicked
.bindTo(this::onButtonClick)
.build();
// Construct our group, make it expire after 1 minute
buttons.group(firstButton, secondButton)
.ephemeral()
.timeout(Duration.ofMinutes(1), () -> onButtonTimeout(event))
.build();
event.replyComponents(ActionRow.of(firstButton, secondButton)).queue();
}
private void onButtonClick(ButtonEvent event) {
event.editButton(event.getButton().asDisabled()).queue();
event.getHook().editOriginal("Try clicking the other button, you can't :^)").queue();
}
private void onButtonTimeout(GuildSlashEvent event) {
event.getHook()
.editOriginal("Expired!")
.setReplace(true)
.queue();
}
}
Reset timeout on use¶
The resetTimeoutOnUse
lets you reset the timeout each time the button is clicked.
The timeout is only reset if the button was actually used, it will not be reset if unauthorized users use it.
Deleting components¶
Here are some tips on how to delete components:
- Most likely, you have the message (from a
ButtonEvent
for example) and you want to delete the buttons from the message and invalidate them, in this case you should usedeleteRows
. -
In stateful interactions, where components can be reused, you might sometimes want to store the IDs of the components, to then invalidate them when the interaction expires.
Example
The built-in paginators stores all the
int
IDs of the components used in paginators, as they cannot be deleted on each page change, as the user might reuse a component they made themselves. Storing them this way is more efficient and allows deletion when the paginator expires, usingdeleteRows
. -
In other, rare cases, you have the component instances (not the JDA ones), for which you can use
deleteComponents
Filtering¶
Components also support filtering, you can use addFilter
with either the filter type, or the filter instance directly.
Passing custom filter instances
You cannot pass filters that cannot be obtained via dependency injection,
this includes composite filters (using and
/ or
),
see ComponentInteractionFilter
for more details
Creating a filter¶
Creating a filter can be done
by implementing ComponentInteractionFilter
and registering it as a service,
they run when a component is about to be executed.
Lets create a filter that allows the component to be usable in a predefined one channel:
Note
Your filter needs to not be global in order to be used on specific components.
@BService
class GeneralChannelFilter : ComponentInteractionFilter<String/*(1)!*/> {
private val channelId = 722891685755093076
// So we can apply this filter on specific components
override val global: Boolean = false
override suspend fun checkSuspend(
event: GenericComponentInteractionCreateEvent,
handlerName: String?
): String? {
if (event.channelIdLong == channelId)
return "This button can only be used in <#$channelId>"
return null
}
}
- This is the return type of the filter, this will be passed as
userData
in your rejection handler.
@BService
public class GeneralChannelFilter implements ComponentInteractionFilter<String/*(1)!*/> {
private static final long CHANNEL_ID = 722891685755093076L;
@Override
public boolean getGlobal() {
// So we can apply this filter on specific components
return false;
}
@Nullable
@Override
public String check(@NotNull GenericComponentInteractionCreateEvent event,
@Nullable String handlerName) {
if (event.getChannelIdLong() == CHANNEL_ID)
return "This button can only be used in <#" + CHANNEL_ID + ">";
return null;
}
}
- This is the return type of the filter, this will be passed as
userData
in your rejection handler.
Creating a rejection handler¶
You must then create a single rejection handler for all your filters, it runs when one of your filters fails.
Note
All of your filters must have the same return type as the rejection handler (the generic you set on the interface).
@BService
class ComponentRejectionHandler : ComponentInteractionRejectionHandler<String/*(1)!*/> {
override suspend fun handleSuspend(
event: GenericComponentInteractionCreateEvent,
handlerName: String?,
userData: String
) {
event.reply_(userData, ephemeral = true).await()
}
}
- This is what was returned by one of your filters, this will be passed as
userData
.
@BService
public class ComponentRejectionHandler implements ComponentInteractionRejectionHandler<String/*(1)!*/> {
@Override
public void handle(@NotNull GenericComponentInteractionCreateEvent event,
@Nullable String handlerName,
@NotNull String userData) {
event.reply(userData).setEphemeral(true).queue();
}
}
- This is what was returned by one of your filters, this will be passed as
userData
.
Using an existing filter¶
Now that your filter has been created, you can reference it in your component.
buttons.primary("Can't click me").ephemeral {
filters += filter<GeneralChannelFilter>()
}
buttons.primary("Can't click me")
.ephemeral()
.addFilter(GeneralChannelFilter.class)
.build()
Rate limiting¶
Just like application commands, components can be rate limited. However, you will need to help the library differentiate components from each other (unlike commands which are differentiated by their names).
Learn how to create a rate limiter with "Defining a rate limit"
You will first need to create a ComponentRateLimitReference
,
you can do that with createRateLimitReference
,
present in any component factory (Components
, Buttons
, SelectMenus
).
The group
associated with the discriminator
will need to be unique,
as to differentiate components (referenced by discriminator
) using the same rate limiter (referenced by group
).
Example
@Command
class SlashComponentRateLimit(private val buttons: Buttons) : ApplicationCommand() {
// This is to prevent an unnecessary load on startup, emojis are slow
private val arrowUp: UnicodeEmoji by lazyUnicodeEmoji { Emojis.UP_ARROW }
private val arrowDown: UnicodeEmoji by lazyUnicodeEmoji { Emojis.DOWN_ARROW }
// The combination of the group and discriminator must be unique
private val upvoteRateLimitReference = buttons.createRateLimitReference(
// The name of the rate limiter
group = WikiRateLimitProvider.RATE_LIMIT_GROUP,
// The "discriminator" for this component,
// this is important as this differentiates a component from others
discriminator = "upvote"
)
private val downvoteRateLimitReference = buttons.createRateLimitReference(
group = WikiRateLimitProvider.RATE_LIMIT_GROUP,
discriminator = "downvote"
)
@JDASlashCommand(name = "component_rate_limit")
suspend fun onSlashComponentRateLimit(event: GuildSlashEvent) {
val upvoteButton = buttons.success("Upvote", arrowUp).ephemeral {
rateLimitReference(upvoteRateLimitReference)
bindTo { buttonEvent ->
buttonEvent.reply("Your vote has been taken into account").setEphemeral(true).queue()
}
}
val downvoteButton = buttons.danger("Downvote", arrowDown).ephemeral {
rateLimitReference(downvoteRateLimitReference)
bindTo { buttonEvent ->
buttonEvent.reply("Your anger has been taken into account").setEphemeral(true).queue()
}
}
event.reply("[Insert controversial sentence]")
.addActionRow(upvoteButton, downvoteButton)
.await()
}
}
@Command
public class SlashComponentRateLimit extends ApplicationCommand {
// This is to prevent an unnecessary load on startup, emojis are slow
private static class Emojis {
static final UnicodeEmoji arrowUp = EmojiUtils.resolveJDAEmoji("arrow_up");
static final UnicodeEmoji arrowDown = EmojiUtils.resolveJDAEmoji("arrow_down");
}
private final Buttons buttons;
private final ComponentRateLimitReference upvoteRateLimitReference;
private final ComponentRateLimitReference downvoteRateLimitReference;
public SlashComponentRateLimit(Buttons buttons) {
this.buttons = buttons;
// The combination of the group and discriminator must be unique
this.upvoteRateLimitReference = buttons.createRateLimitReference(
// The name of the rate limiter
WikiRateLimitProvider.RATE_LIMIT_GROUP,
// The "discriminator" for this component,
// this is important as this differentiates a component from others
"upvote"
);
this.downvoteRateLimitReference = buttons.createRateLimitReference(WikiRateLimitProvider.RATE_LIMIT_GROUP, "downvote");
}
@JDASlashCommand(name = "component_rate_limit")
public void onSlashComponentRateLimit(GuildSlashEvent event) {
final var upvoteButton = buttons.success("Upvote", Emojis.arrowUp)
.ephemeral()
.rateLimitReference(upvoteRateLimitReference)
.bindTo(buttonEvent -> {
buttonEvent.reply("Your vote has been taken into account").setEphemeral(true).queue();
})
.build();
final var downvoteButton = buttons.danger("Downvote", Emojis.arrowDown)
.ephemeral()
.rateLimitReference(downvoteRateLimitReference)
.bindTo(buttonEvent -> {
buttonEvent.reply("Your anger has been taken into account").setEphemeral(true).queue();
})
.build();
event.reply("[Insert controversial sentence]")
.addActionRow(upvoteButton, downvoteButton)
.queue();
}
}