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.3.0</version> <!-- State: 23rd of february 2025 -->
</dependency>
<dependency>
<groupId>pw.chew</groupId>
<artifactId>jda-chewtils</artifactId>
<version>2.1</version> <!-- State: 23rd of february 2025 -->
</dependency>
</dependencies>
repositories {
mavenCentral()
maven { url = "https://m2.chew.pro/snapshots/" }
}
dependencies {
implementation "net.dv8tion:JDA:5.3.0" // State: 23rd of february 2025
implementation "pw.chew:jda-chewtils:2.1" // State: 23rd of february 2025
}
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.
<T extends Event> void waitForEvent(
Class<T>, // (1)
Predicate<T>, // (2)
Consumer<T> // (3)
)
- This argument defines the Event that the EventWaiter should wait for. It needs to extend the
Eventclass of JDA to be valid. As an exampleGenericEvent.classcan be used here. - This Predicate is called when the event received matches the defined
Class<T>instance.Twould then be the event matchingClass<T>that the EventWaiter received.
A lambda (event -> ...) can be used for convenience. - This consumer is called when the
Predicate<T>returnstrue.
A lambda (event -> ...) can be used for convenience.
This waitForEvent method is similar in structure to the one without a Timeout.
The difference are additional arguments to define a Timer and a Runnable that should be executed when either no Event matching Class<T> was received, or Predicate<T> did not return true within time.
Click the for more information on a section.
<T extends Event> void waitForEvent(
Class<T>, // (1)
Predicate<T>, // (2)
Consumer<T> // (3)
int, // (4)
TimeUnit, // (5)
Runnable // (6)
)
- This argument defines the Event that the EventWaiter should wait for. It needs to extend the
Eventclass of JDA to be valid. As an exampleGenericEvent.classcan be used here. - This Predicate is called when the event received matches the defined
Class<T>instance.Twould then be the event matchingClass<T>that the EventWaiter received.
A lambda (event -> ...) can be used for convenience. - This consumer is called when the
Predicate<T>returnstrue.
A lambda (event -> ...) can be used for convenience. - This integer defines the number of
TimeUnits that the EventWaiter should wait for.
If no event matchingClass<T>is received that also passes thePredicate<T>will the event listening get cancelled and theRunnablebe executed instead. - This is used in combination with the integer argument, to define the number of
TimeUnits the EventWaiter should wait. This means setting this toTimeUnit.MINUTESand the integer to5would make the EventWaiter wait 5 minutes for an event matchingClass<T>that also passes thePredicate<T>. - This runnable is called when either no Event matching
Class<T>was received, or thePredicate<T>didn't returntruein time.
Given this being a Runnable, you won't have any access toTvalues. You can use a named or anonymous lambda (() -> ...) for convenience.
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.
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;
}
}
args[0]would be our Bot-token provided in thejavacommand as a Jar argument (java -jar Bot.jar <your-token-here>)- 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!
- 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!
- Priviledged intent required to see the content of a message. Make sure your bot has this intent enabled in its dashboard.
- See the Message Example below.
- See the Reactions Example below.
- See the Buttons Example below.
- Builds the JDA instance and waits for it to complete (Blocks thread). Not recommended for performance reasons.
Message Example¶
This example has the bot ask for a message it should repeat after the user typed !repeat inchat.
It will send a fail message should the user not reply within 1 Minute.
Click the for more information on a section.
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();
}
));
}
}
- We make sure the message is not sent by a bot (which also incudes our own) and that it was sent in a Guild (Not in Direct Messages).
- We setup our EventWaiter to listen for a message from the command executor, or time out after a minute of no valid response.
- Making sure the Channel is the same as the one the command was executed from. ID check is the most reliable way here.
- 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 example has the bot send a Message, add reactions to it and then wait for the user to react after they typed !apple in chat.
The bot will send a fail message should the user not react within 1 Minute.
Click the for more information on a section.
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?\nYou have 1 Minute to react!")
.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();
}
))
);
}
}
- We make sure the message is not sent by a bot (which also incudes our own) and that it was sent in a Guild (Not in Direct Messages).
RestAction.allOf()allows us to chain together multiple rest actions into a singlequeue()call, reducing callback hell.- We add the Unicdoe emoji
\u2705() as reaction to our sent message.
- We add the Unicode emoji
\u274c() as reaction to our sent message.
- 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.
- Ignoring Reactions sent by Bots. This includes our own bot.
- Making sure that the Emoji added is a unicode Emoji.
- Making sure the user reacting is the same as the one who executed the command. Using IDs is the most reliable way here.
- Return true if the reaction is either
or
- We check if the retrieved Reaction is
- Since our Predicate can only be true if the reaction is either
or
and since we already checked for the former, can the reaction only be
now.
Buttons Example¶
This example is similar to our Reactions Example above with the difference, that we attach Buttons to our Bot's message and listen for Button presses.
We use those to send a message with the Name of the animal they chose.
Click the for more information on a section.
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?\nYou have 1 Minute to choose!")
.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(
ButtonInteractionEvent.class,
e -> {
if (e.getMessageIdLong() != message.getIdLong()) // (7)
return false;
if (e.getUser().getIdLong() != 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") ||
id.equals("example-bot:button:pet:dog") ||
id.equals("example-bot:button:pet:bunny") ||
id.equals("example-bot:button:pet:fox");
}
}
- We make sure the message is not sent by a bot (which also incudes our own) and that it was sent in a Guild (Not in Direct Messages).
setActionRow(ItemComponent...)allows us to set up to 5ItemComponents (in our case Buttons).- Adding a Button with id
example-bot:button:pet:cat, display TextCatand Emojiy.
- Adding a Button with id
example-bot:button:pet:dog, display TextDogand Emojiy.
- Adding a Button with id
example-bot:button:pet:bunny, display TextBunnyand Emojiy.
- Adding a Button with id
example-bot:button:pet:fox, display TextFoxand Emojiy.
- Making sure the message whose button was pressed is the same as the one the bot sent. Comparing IDs is the most reliable way here.
- Making sure the user pressing the button is the same as the one who executed the command. Using IDs is the most reliable way here.
- We check if the event has already been acknowledged, and if not, make a
deferReply()call to do so. This avoids the "Interaction failed" reply Discord gives should our bot not aknowledge the button press within 3 seconds. - 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.
- 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
Comments
Comment system powered by Mastodon.
Leave a comment using Mastodon or another Fediverse-compatible account.