Skip to content

Use buttons and button callbacks

Robin Mercier edited this page Jun 29, 2021 · 5 revisions

When using buttons, we strongly recommend you to use our SlashButton implementation of a the JDA Button. Doing so allows you to use some few utilities we made to simplify the use of buttons.

Overall, the SlashButton is used to same way as the regular JDA Button but with a few differences and additions:

  • instead of defining an id for the button when creating it, you bind the button to a button callback
  • the method SlashButton#withData(String) allows you to set a payload to the button, which can be retrieved in the callback

Button Callbacks

As for the Slash Command callbacks, a button callback is simply done by adding a @Slash.Button annotation to a method.

@Slash.Tag("ping_command")
@Slash.Command(
    name = "ping",
    description = "Check if the application is online"
)
public class PingCommand {

    @Slash.Handler()
    public void callback(SlashCommandEvent event) {
        event.deferReply()
            .setContent("pong!")
            .addActionRow(
                SlashButton.primary("ping.refresh", "Refresh")
            )
            .queue();
    }

    @Slash.Button("ping.refresh")
    public void onRefresh(ButtonClickEvent event) {
        event.deferEdit()
            .setContent("pong! (refreshed)")
            .queue();
    }
}

The value of the @Slash.Button callback is the name use to bind buttons to the callback.

A few things to be aware of:

  • the method must only have one parameter, being of the ButtonClickEvent class type
  • the method must be public
  • the name of a button callback must be unique (for the whole application)

Button Payloads

When using payloads, we recommend using the Buffer.Writer class to write the payload and the Buffer.Reader to read the payload. These 2 tools make sure that you don't write (or read) a payload with more than 96 characters, which is the maximum size for the payload.

Note: The maximum size for the button payload is of 96 characters and not 100 characters as the 4 first characters are used to bind the button to its callback.

The Buffer.Writer works the following way:

  • new Buffer.Writer() or Buffer.Writer#create() to create a new instance
  • when writing data with the Buffer.Writer#write(int, ?) you need to specify the maximum size allocated to the data

And the Buffer.Reader:

  • new Buffer.Reader(String) or Buffer.Reader#of(String) to create a new instance, where the string is the button's id
  • then call Buffer.Reader#read(int) to read the next n characters and get the data in the wanted type

Here is an updated version of the previous example with the use of the Buffer.Writer and Buffer.Reader classes to create payloads.

Note: Payloads are persistent meaning that as long as the code related to the button hasn't been changed, the interaction will always work even after restarting the bot!

@Slash.Tag("ping_command")
@Slash.Command(
    name = "ping",
    description = "Check if the application is online"
)
public class PingCommand {

    @Slash.Handler()
    public void callback(SlashCommandEvent event) {
        final Buffer.Writer writer = Buffer.Writer.create();

        writer.write(18, event.getUser().getId()); // write the author id to restrict who can click on the button
        writer.write(8, 0); // write the refresh counter

        event.deferReply()
            .setContent("pong!")
            .addActionRow(
                SlashButton.primary("ping.refresh", "Refresh")
                    .withData(writer.toString())
            )
            .queue();
    }

    @Slash.Button("ping.refresh")
    public void onRefresh(ButtonClickEvent event) {
        final Buffer.Reader reader = Buffer.Reader.of(event.getComponentId());
        final Buffer.Writer writer = Buffer.Writer.create();

        final String authorId = reader.read(18).asString(); // get the author id
        int counter = reader.read(8).asUnsignedInt(); // get the refresh counter

        // cancel if the user who clicked on the button isn't the author
        if (!event.getUser().getId().equals(authorId)) {
            return;
        }

        counter++; // increment the refresh counter

        writer.write(18, authorId); // write the author id
        writer.write(8, counter); // write the refresh counter

        event.deferEdit()
            .setContent("pong! (refreshed " + counter + " time" + (counter == 1 ? "": "s") + ")")
            .setActionRow(
                SlashButton.primary("ping.refresh", "Refresh")
                    .withData(writer.toString())
            )
            .queue();
    }
}

Button Sessions

Compared to regular button payloads, button sessions can hold a lot more data in exchange of not being persistent anymore. A Session will automatically timeout after 1 minute (unless specified) and will be destroyed if the bot is restarted.

When creating the Session, you can customise the timeout and its action:

  • Session#create() - create a session which expires after 1 minute
  • Session#create(long, TimeUnit) - create a session which expires after the chosen time
  • Session#create(long, TimeUnit, InteractionHook, BiConsumer<InteractionHook, Session>) - create a session which expires after the chosen time and triggers an action on timeout

The Session class extends the DataObject one, so use it to store the variables you need. When creating the SlashButton set as its data either:

  • Session#getUuid() - to bind the session to the button
  • Session#store(Consumer<DataObject>) - to bind the session to the button in addition of setting more variables, or overwriting variables, for this button only

Note: Using the Session#store(Consumer<DataObject>) helps you to set variables for some buttons only. For example, you could have 2 buttons asking if the person is a male or female and have the variable gender=male or gender=female set depending on the button clicked.

When retrieving a Session, you can either:

  • Session#load(String) - load a session and destroy it afterwards
  • Session#renew(String) - load a session and reset its timeout

Taking the previous example, using a Session would look like:

@Slash.Tag("ping_command")
@Slash.Command(
    name = "ping",
    description = "Check if the application is online"
)
public class PingCommand {

    private final long timeout = 15L;
    private final TimeUnit unit = TimeUnit.SECONDS;
    private final BiConsumer<InteractionHook, Session> action = (hook, session) -> {
        hook.editOriginal("pong! (timed out)")
            .setActionRow(
                SlashButton.primary("ping.refresh", "Refresh")
                    .asDisabled()
            )
            .queue();
    };

    @Slash.Handler()
    public void callback(SlashCommandEvent event) {
        final Session session = Session.create(timeout, unit, event.getHook(), action);

        session.put("author_id", event.getUser().getId()) // store the author id
            .put("counter", 0); // store the refresh counter

        event.deferReply()
            .setContent("pong!")
            .addActionRow(
                SlashButton.primary("ping.refresh", "Refresh")
                    .withData(session.getUuid())
            )
            .queue();
    }

    @Slash.Button("ping.refresh")
    public void onRefresh(ButtonClickEvent event) {
        final Session session = Session.renew(event.getComponentId());

        // cancel if the user who clicked on the button isn't the author
        if (session == null || !event.getUser().getId().equals(session.getString("author_id"))) {
            return;
        }

        int counter = session.getUnsignedInt("counter");
        counter++; // increment the refresh counter

        session.put("counter", counter); // updated the refresh counter

        event.deferEdit()
            .setContent("pong! (refreshed " + counter + " time" + (counter == 1 ? "": "s") + ")")
            .setActionRow(
                SlashButton.primary("ping.refresh", "Refresh")
                    .withData(session.getUuid())
            )
            .queue();
    }
}