Skip to content

How to use JDA-Utilities' EventWaiter

If you're developing a bot with JDA1, changes are high that you want to implement a system where your bot is waiting for an input from the user after performing a command or action.
Doing this in JDA can be a bit tedious, especially if you want it to do things such as having the action expire after no input for a while. This is where JDA-Utilities2 comes into play. And this is where this blog post will explain how to use it and especially its EventWaiter functionality.

Note

Thos blog post will only cover and use JDA v5 and JDA-Chewtils3, an updated fork of the original JDA-Utilities.
While the shown examples may work with JDA-Utilities itself is there no guarantee for this.

Getting the dependency

You should first obtain the dependency to use.
To use Chewtils with JDA v5, you have to add this to your pom.xml or build.gradle, based on what build tool you use.

<repositories>
  <repository>
    <id>chewtils-snapshots</id>
    <url>https://m2.chew.pro/snapshots/</url>
  </repository>
</repositories>

<dependencies>
  <!-- JDA is available through MavenCentral -->
  <dependency>
    <groupId>net.dv8tion</groupId>
    <artifactId>JDA</artifactId>
    <version>5.0.0-beta.23</version> <!-- State: 22nd april 2024 -->
  </dependency>
  <dependency>
    <groupId>pw.chew</groupId>
    <artifactId>jda-chewtils</artifactId>
    <version>2.0-SNAPSHOT</version> <!-- State: 22nd april 2024 -->
  </dependency>
</dependencies>
repositories {
  mavenCentral()
  maven { url = "https://m2.chew.pro/snapshots/" }
}

dependencies {
  implementation "net.dv8tion:JDA:5.0.0-beta.23" // State: 22nd april 2024
  implementation "pw.chew:jda-chewtils:2.0-SNAPSHOT" // State: 22nd april 2024
}

Using the EventWaiter

What is the EventWaiter?

The EventWaiter is a utility class used within Chewtils. It allows you to setup a system, where Chewtils waits for a specific event to be triggered, to then check for certain conditions and execute an action for when conditions are met, or optionally execute an action should the timer run out (if one was set).
Note that this class is not limited to JDA events itself. Any class that extends the abstract Event class of JDA can be used to be listened for and handled by the EventWaiter.

waitForEvent method Structures

The EventWaiter offers two specific waitForEvent methods used for the event-waiting: One with and one without a timeout.
Which one you should use depends on if you need a timeout or not. Keep in mind that only the method with timeout has a Runnable argument to set.

This waitForEvent method can be used to wait for an event to happen indefinitely.
Using this method can cause an increase in RAM usage, since the event waiter doesn't time out, constantly waiting for the event to happen.

Click the for more information on a section.

Example without timeout
waitForEvent(
  Class<T>, // (1)
  Predicate<T>, // (2)
  Consumer<T> // (3)
)
  1. This argument defines the Event that the EventWaiter should wait for. It needs to extend the Event class of JDA to be valid. As an example GenericEvent.class can be used here.
  2. This Predicate is called when the event received matches the defined Class<T> instance. T would then be the event matching Class<T> that the EventWaiter received.
    You can use a Lambda here for convenience (i.e. event -> ...)
  3. This Consumer is called when the defined Predicate returns true. T would be the event matching Class<T> that the EventWaiter received.
    You can use a Lambda here for convenience (i.e. event -> ...).

This waitForEvent method can be used to wait for an event to happen or to time out after a certain duration, should the given event not be retrieved and the given Predicate not return true.

Click the for more information on a section.

Example with timeout
waitForEvent(
  Class<T>, // (1)
  Predicate<T>, // (2)
  Consumer<T> // (3)
  int, // (4)
  TimeUnit, // (5)
  Runnable // (6)
)
  1. This argument defines the Event that the EventWaiter should wait for. It needs to extend the Event class of JDA to be valid. As an example GenericEvent.class can be used here.
  2. This Predicate is called when the event received matches the defined Class<T> instance. T would then be the event matching Class<T> that the EventWaiter received.
    You can use a Lambda here for convenience (i.e. event -> ...)
  3. This Consumer is called when the defined Predicate returns true. T would be the event matching Class<T> that the EventWaiter received.
    You can use a Lambda here for convenience (i.e. event -> ...).
  4. This integer defines the number of TimeUnits that the EventWaiter should wait for.
    If no event matching Class<T> is received that also passes the Predicate<T> will the event listening get cancelled and the Runnable be executed instead.
  5. This is used in combination with the integer argument, to define the number of TimeUnits the EventWaiter should wait. This means setting this to TimeUnit.MINUTES and the integer to 5 would make the EventWaiter wait 5 minutes for an event matching Class<T> that also passes the Predicate<T>
  6. This runnable is executed when either no event matching Class<T> is received or no received event passed through the Predicate<T>.
    Given this is a runnable is only anonymous lambdas possible (() -> ...).

Examples

Here are some examples of common situations where you want your bot to wait for an input of some sort.

Prerequisite: Setting up EventWaiter instance

It is recommended to have one single EventWaiter instance used to avoid problems.
Make sure to add the EventWaiter instance as a new Event Listener to JDA, or else it cannot work properly.

Click the for more information on a section.

Bot.java
public class Bot {
  private final EventWaiter waiter = new EventWaiter();

  public static void main(String[] args) {
    try {
      new Bot().start(args[0]) // (1)
    } catch (LoginException | InterruptedException ex) {
      ex.printStackTrace();
    }
  }

  public void start(String token) throws LoginException, InterruptedException {
    JDA jda = JDABuilder.createDefault(token)
      .enableIntents(
        GatewayIntents.GUILD_MEMBERS, // (2)
        GatewayIntents.GUILD_MESSAGES, // (3)
        GatewayIntents.MESSAGE_CONTENT // (4)
      )
      .addEventListeners(
        new MessageExample(this), // (5)
        new ReactionExample(this), // (6)
        new ButtonExample(this), // (7)
        waiter
      )
      .build().awaitReady(); // (8)
  }

  public EventWaiter getWaiter() {
    return waiter;
  }
}
  1. args[0] would be out Bot-token provided in the java command as a Jar argument (java -jar Bot.jar <your-token-here>)
  2. Priviledged intent required to handle the text-based commands used in the examples. Make sure the intent is enabled in your bot's developer portal!
  3. Priviledged intent required to handle the text-based commands used in the examples. Make sure the intent is enabled in your bot's developer portal!
  4. Priviledged intent required to see the content of a message. Make sure your bot has this intent enabled in its dashboard.
  5. See the Message Example below.
  6. See the Reactions Example below.
  7. See the Buttons Example below.
  8. Builds the JDA instance and waits for it to complete (Blocks thread).

Message Example

Below can you see an example of waiting for a message to repeat, with a timeout where the original message is deleted after a timeout or when the Predicate returns true in time.

Click the for more information on a section.

MessageExample.java
public class MessageExample extends ListenerAdabter {
  private final Bot bot;

  public MessageExample(Bot bot) {
    this.bot = bot;
  }

  @Override
  public void onMessageReceived(MessageReceivedEvent event) {
    if (event.getAuthor().isBot() || !event.isFromGuild()) // (1)
      return;

    String msg = event.getMessage().getContentRaw();
    if (!msg.equalsIgnoreCase("!repeat"))
      return;

    TextChannel tc = event.getGuildChannel().asTextChannel();
    User author = event.getAuthor();

    tc.sendMessage("Hello " + author.getAsMention() + ". What should I say?\nYou have 1 Minute to type something!")
      .queue(message -> bot.getWaiter().waitForEvent( // (2)
        MessageReceivedEvent.class,
        e -> {
          if (e.getChannel().getIdLong() != tc.getIdLong()) // (3)
            return false;

          return e.getAuthor().getIdLong() == author.getIdLong(); // (4)
        },
        e -> {
          tc.sendMessage(e.getMessage().getContentRaw()).queue();
          message.delete().queue();
        },
        1, TimeUnit.MINUTES,
        () -> {
          tc.sendMessage("You didn't respond in time!").queue();
          message.delete().queue();
        }
      ));
  }
}
  1. We make sure the message is not sent by a bot (including ours) and that it was sent in a Guild (Not DMs).
  2. We setup our EventWaiter to listen for a message from the command executor, or time out after a minute of no response.
  3. Making sure the Channel is the same as the one the command was executed from. ID check is the most reliable way here.
  4. Making sure the Author of the message is the same as the one who executed the command. ID check is the most reliable way here.

Reactions Example

This is an example where the user executes a command and gets prompted to select an option through a reaction.

Click the for more information on a section.

ReactionExample.java
public class ReactionExample extends ListenerAdabter {
  private final Bot bot;

  public ReactionExample(Bot bot) {
    this.bot = bot;
  }

  @Override
  public void onMessageReceived(MessageReceivedEvent event) {
    if (event.getAuthor().isBot() || !event.isFromGuild()) // (1)
      return;

    String msg = event.getMessage().getContentRaw();
    if (!msg.equalsIgnoreCase("!apple"))
      return;

    TextChannel tc = event.getGuildChannel().asTextChannel();
    User author = event.getAuthor();

    tc.sendMessage("Hello " + author.getAsMention() + ". Do you like apples?").queue(message -> RestAction.allOf( // (2)
        message.addReaction(Emoji.fromUnicode("\u2705")), // (3)
        message.addReaction(Emoji.fromUnicode("\u274c")) // (4)
      ).queue(v -> bot.getWaiter().waitForEvent(
        MessageReactionAddEvent.class,
        e -> {
          if (e.getMessageIdLong() != message.getIdLong()) // (5)
            return false;

          if (e.getUser().isBot()) // (6)
            return false;

          EmojiUnion emoji = event.getEmoji();
          if (emoji.getType() != Emoji.Type.UNICODE) // (7)
            return false;

          if (e.getAuthorIdLong() != event.getAuthorIdLong()) // (8)
            return false;

          return emoji.getAsReactionCode().equals("\u2705") || emoji.getAsReactionCode().equals("\u274c"); // (9)
        },
        e -> {
          String unicode = e.getEmoji().getAsReactionCode();

          if (unicode.equals("\u2705")) { // (10)
            tc.sendMessage("You like apples!").queue();
          } else { // (11)
            tc.sendMessage("You **don't** like apples!").queue();
          }

          message.delete().queue();
        },
        1, TimeUnit.MINUTES,
        () -> {
          tc.sendMessage("You didn't respond in time!").queue();
          message.delete().queue();
        }
      ))
    );
  }
}
  1. We make sure the message is not sent by a bot (including ours) and that it was sent in a Guild (Not DMs).
  2. RestAction.allOf() allows us to chain together multiple rest actions into a single queue() call, reducing callback hell.
  3. We add the Unicdoe emoji \u2705 (✅) as reaction to our sent message.
  4. We add the Unicode emoji \u274c (❌) as reaction to our sent message.
  5. Making sure the Message that was reacted to is the same as the one the bot sent. Using IDs is the most reliable way here.
  6. Ignoring Reactions sent by Bots. This includes our own bot.
  7. Making sure that the Emoji added is a unicode Emoji.
  8. Making sure the user reacting is the same as the one who executed the command. Using IDs is the most reliable way here.
  9. Returning whether the reaction's unicode is either ✅ or ❌
  10. We check if the retrieved Reaction is ✅
  11. Since our Predicate only returns true for ✅ and ❌, and since our reaction is not ✅ can this only be ❌.

Buttons Example

This is similar to the Reaction Example above, with the difference that we use Button Message Components to choose between different Pets.

Click the for more information on a section.

ButtonExample.java
public class ButtonExample extends ListenerAdabter {
  private final Bot bot;

  public ButtonExample(Bot bot) {
    this.bot = bot;
  }

  @Override
  public void onMessageReceived(MessageReceivedEvent event) {
    if (event.getAuthor().isBot() || !event.isFromGuild()) // (1)
      return;

    String msg = event.getMessage().getContentRaw();
    if (!msg.equalsIgnoreCase("!pet"))
      return;

    TextChannel tc = event.getGuildChannel().asTextChannel();
    User author = event.getAuthor();

    tc.sendMessage("Hello " + author.getAsMention() + ". What is your favourite Pet?")
      .setActionRow( // (2)
        Button.of(ButtonStyle.PRIMARY, "example-bot:button:pet:cat", "Cat", Emoji.fromUnicode("\uD83D\uDC31")), // (3)
        Button.of(ButtonStyle.PRIMARY, "example-bot:button:pet:dog", "Dog", Emoji.fromUnicode("\uD83D\uDC36")), // (4)
        Button.of(ButtonStyle.PRIMARY, "example-bot:button:pet:bunny", "Bunny", Emoji.fromUnicode("\uD83D\uDC30")), // (5)
        Button.of(ButtonStyle.PRIMARY, "example-bot:button:pet:fox", "Fox", Emoji.fromUnicode("\uD83D\uDD8A")), // (6)
      ).queue(message -> bot.getWaiter().waitForEvent(
        MessageReactionAddEvent.class,
        e -> {
          if (e.getMessageIdLong() != message.getIdLong()) // (7)
            return false;

          if (e.getAuthorIdLong() != event.getAuthorIdLong()) // (8)
            return false;

          if (!e.isAcknowledged()) // (9)
            e.deferReply().queue();

          return equalsAny(e.getComponentId()); // (10)
        },
        e -> {
          String selection = e.getComponentId().split(":")[3]; // (11)

          tc.sendMessage("You chose **" + selection + "** as your favourite Pet!").queue();
          message.delete().queue();
        },
        1, TimeUnit.MINUTES,
        () -> {
          tc.sendMessage("You didn't respond in time!").queue();
          message.delete().queue();
        }
      )
    );
  }

  private boolean equalsAny(String id) {
    return id.equals("example-bot:button:pet:cat") ||
    return id.equals("example-bot:button:pet:dog") ||
    return id.equals("example-bot:button:pet:bunny") ||
    return id.equals("example-bot:button:pet:fox");
  }
}
  1. We make sure the message is not sent by a bot (including ours) and that it was sent in a Guild (Not DMs).
  2. We can add up to 5 Components in one action row. Using this method only sets a single row.
  3. Adding a Button with id example-bot:button:pet:cat, display Text Cat and Emojiy 🐱.
  4. Adding a Button with id example-bot:button:pet:dog, display Text Dog and Emojiy 🐶.
  5. Adding a Button with id example-bot:button:pet:bunny, display Text Bunny and Emojiy :bunny:.
  6. Adding a Button with id example-bot:button:pet:fox, display Text Fox and Emojiy 🦊.
  7. Making sure the Message that was reacted to is the same as the one the bot sent. Using IDs is the most reliable way here.
  8. Making sure the user reacting is the same as the one who executed the command. Using IDs is the most reliable way here.
  9. We check if the event has already been acknowledged, and if not, make a deferReply() call to do so.
  10. We have a convenience method used here to check if the Component's ID is any of the ones we specified. This is solely for cleaner looking code.
  11. We split the Button ID at colons and get the 4th part. Since we made the necessary check before can we be sure that this is the animal name.

Footnotes


Last update: 24. April 2024 ()

Comments

Comment system powered by Mastodon.
Leave a comment using Mastodon or another Fediverse-compatible account.