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.
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
Event
class of JDA to be valid. As an exampleGenericEvent.class
can be used here. - This Predicate is called when the event received matches the defined
Class<T>
instance.T
would then be the event matchingClass<T>
that the EventWaiter received.
You can use a Lambda here for convenience (i.e.event -> ...
) - This Consumer is called when the defined Predicate returns
true
.T
would be the event matchingClass<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.
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
Event
class of JDA to be valid. As an exampleGenericEvent.class
can be used here. - This Predicate is called when the event received matches the defined
Class<T>
instance.T
would then be the event matchingClass<T>
that the EventWaiter received.
You can use a Lambda here for convenience (i.e.event -> ...
) - This Consumer is called when the defined Predicate returns
true
.T
would be the event matchingClass<T>
that the EventWaiter received.
You can use a Lambda here for convenience (i.e.event -> ...
). - This integer defines the number of
TimeUnit
s 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 theRunnable
be executed instead. - This is used in combination with the integer argument, to define the number of
TimeUnit
s the EventWaiter should wait. This means setting this toTimeUnit.MINUTES
and the integer to 5 would make the EventWaiter wait 5 minutes for an event matchingClass<T>
that also passes thePredicate<T>
- This runnable is executed when either no event matching
Class<T>
is received or no received event passed through thePredicate<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.
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 out Bot-token provided in thejava
command 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).
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.
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 (including ours) and that it was sent in a Guild (Not DMs).
- We setup our EventWaiter to listen for a message from the command executor, or time out after a minute of no 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 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.
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();
}
))
);
}
}
- We make sure the message is not sent by a bot (including ours) and that it was sent in a Guild (Not DMs).
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.
- Returning whether the reaction's unicode is either or
- We check if the retrieved Reaction is
- 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.
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") ||
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 (including ours) and that it was sent in a Guild (Not DMs).
- We can add up to 5 Components in one action row. Using this method only sets a single row.
- Adding a Button with id
example-bot:button:pet:cat
, display TextCat
and Emojiy . - Adding a Button with id
example-bot:button:pet:dog
, display TextDog
and Emojiy . - Adding a Button with id
example-bot:button:pet:bunny
, display TextBunny
and Emojiy . - Adding a Button with id
example-bot:button:pet:fox
, display TextFox
and Emojiy . - 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.
- Making sure the user reacting 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. - 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.