Skip to content

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 use deleteRows.
  • 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, using deleteRows.

  • 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
    }
}
  1. 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;
    }
}
  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).

@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.
@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

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