Skip to content

Using components

This requires a database to be set up!

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.

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.

Type-safe component methods and optional handlers in Kotlin

You can bind a method to your component, enabling you to pass arguments in a type-safe way with bindTo extensions, a similarly used timeout function also exists.

You can also not use bindTo and instead use await() on the built component.

@Command
class SlashClickWaiter(private val buttons: Buttons) : ApplicationCommand() {
    @JDASlashCommand(name = "click_waiter", description = "Sends a button and waits for it to be clicked")
    suspend fun onSlashClickWaiter(event: GuildSlashEvent) {
        val button = buttons.primary("Click me").ephemeral {
            // Make it so this button is only usable once
            singleUse = true

            // Only allow the caller to use the button
            constraints += event.user
        }
        event.replyComponents(row(button)).await()

        // Wait for the allowed user to click the button
        val buttonEvent: ButtonEvent = button.awaitOrNull() // (1)!
            ?: return event.hook
                .replaceWith("Expired!")
                .awaitUnit() // (2)!

        buttonEvent.editMessage("!")
            // Replace the entire message
            .setReplace(true)
            // Delete after 5 seconds
            .deleteDelayed(5.seconds)
            .await()
    }
}
  1. awaitOrNull returns null when the component expired, useful when combined with an elvis operator, this is the equivalent of a try catch on TimeoutCancellationException.

  2. awaitUnit is an extension to await and then return Unit, which helps in common scenarios where you want to reply using an elvis operator.

Persistent components have no timeout by default, as their purpose is to be long-lived, however, you can set one using timeout, which accept a timeout handler name, set with @ComponentTimeoutHandler.

Info

Components which expired while the bot was offline will run their timeout handlers at startup.

Example

@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()
    }

    // The name should be unique,
    // I recommend naming the handler "[ClassName]: [purpose]"
    // And the name would be "on[purpose]Click"
    @JDAButtonListener("SlashPersistentClicker: cookie")
    suspend fun onCookieClick(event: ButtonEvent, @ComponentData count: Int) {
        val button = createButton(event, count + 1)
        event.editComponents(row(button)).await()
    }

    // Same thing here, names don't collide with other types of listener
    @ComponentTimeoutHandler("SlashPersistentClicker: cookie")
    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 {
                // Make it so this button is only usable once
                // this is not an issue as we recreate the button everytime.
                // If this wasn't usable only once, the timeout would run for each button.
                singleUse = true

                // 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.editComponents(ActionRow.of(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()

                // Make it so this button is only usable once
                // this is not an issue as we recreate the button everytime.
                // If this wasn't usable only once, the timeout would run for each button.
                .singleUse(true)

                // 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.defaultTimeout, 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")
    suspend fun onSlashClicker(event: GuildSlashEvent) {
        val button = createButton(event, count = 0)
        event.replyComponents(row(button)).await()
    }

    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 {
                // Make it so this button is only usable once
                // this is not an issue as we recreate the button everytime.
                // If this wasn't usable only once, the timeout would run for each button.
                singleUse = true

                // Only allow the caller to use the button
                constraints += event.user

                // Run this callback after the button hasn't been used for a day
                // The timeout gets cancelled if the button is invalidated
                timeout(1.days) {
                    println("User finished clicking $count cookies")
                }

                // When clicked, run this callback
                bindTo { buttonEvent ->
                    val newButton = createButton(buttonEvent, count + 1)
                    buttonEvent.editComponents(row(newButton)).await()
                }
            }
    }
}
@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()

                // Make it so this button is only usable once
                // this is not an issue as we recreate the button everytime.
                // If this wasn't usable only once, the timeout would run for each button.
                .singleUse(true)

                // Only allow the caller to use the button
                .constraints(interactionConstraints -> {
                    interactionConstraints.addUsers(event.getUser());
                })

                // Run this callback after the button hasn't been used for a day
                // The timeout gets cancelled if the button is invalidated
                .timeout(Duration.ofDays(1), () -> {
                    System.out.println("User finished clicking " + count + " cookies");
                })

                // When clicked, run this callback
                .bindTo(buttonEvent -> {
                    final Button newButton = createButton(buttonEvent, count + 1);
                    buttonEvent.editComponents(ActionRow.of(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();
    }
}

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.

GeneralChannelFilter.kt
@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
    }
}
  1. This is the return type of the filter, this will be passed as userData in your rejection handler.
GeneralChannelFilter.java
@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;
    }
}
  1. 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).

ComponentRejectionHandler.kt
@BService
class ComponentRejectionHandler : ComponentInteractionRejectionHandler<String/*(1)!*/> {
    override suspend fun handleSuspend(
        event: GenericComponentInteractionCreateEvent,
        handlerName: String?,
        userData: String
    ) {
        event.reply_(userData, ephemeral = true).await()
    }
}
  1. This is what was returned by one of your filters, this will be passed as userData.
ComponentRejectionHandler.java
@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();
    }
}
  1. 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

Rate limiting / cooldowns can be applied to components by using rateLimitReference(), then referencing an existing rate limiter.

Using an existing rate limiter

buttons.primary("Can't click me too fast").ephemeral {
    rateLimitReference("name of the rate limiter")
}
buttons.primary("Can't click me too fast")
    .ephemeral()
    .rateLimitReference("name of the rate limiter")
    .build()