From 77b4a887b1dba9d990022b1848bb5d5d01b3baa8 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Mon, 27 May 2024 19:13:17 +0200 Subject: [PATCH 01/15] Add JDA#listenOnce --- src/main/java/net/dv8tion/jda/api/JDA.java | 33 +++ .../java/net/dv8tion/jda/api/utils/Once.java | 212 ++++++++++++++++++ .../net/dv8tion/jda/internal/JDAImpl.java | 7 + 3 files changed, 252 insertions(+) create mode 100644 src/main/java/net/dv8tion/jda/api/utils/Once.java diff --git a/src/main/java/net/dv8tion/jda/api/JDA.java b/src/main/java/net/dv8tion/jda/api/JDA.java index e5fb70beee..402d5bf339 100644 --- a/src/main/java/net/dv8tion/jda/api/JDA.java +++ b/src/main/java/net/dv8tion/jda/api/JDA.java @@ -25,6 +25,7 @@ import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; import net.dv8tion.jda.api.entities.emoji.RichCustomEmoji; import net.dv8tion.jda.api.entities.sticker.*; +import net.dv8tion.jda.api.events.GenericEvent; import net.dv8tion.jda.api.hooks.IEventManager; import net.dv8tion.jda.api.interactions.commands.Command; import net.dv8tion.jda.api.interactions.commands.build.CommandData; @@ -39,6 +40,7 @@ import net.dv8tion.jda.api.requests.restaction.pagination.EntitlementPaginationAction; import net.dv8tion.jda.api.sharding.ShardManager; import net.dv8tion.jda.api.utils.MiscUtil; +import net.dv8tion.jda.api.utils.Once; import net.dv8tion.jda.api.utils.cache.CacheFlag; import net.dv8tion.jda.api.utils.cache.CacheView; import net.dv8tion.jda.api.utils.cache.SnowflakeCacheView; @@ -605,6 +607,37 @@ default boolean awaitShutdown() throws InterruptedException @Nonnull List getRegisteredListeners(); + /** + * Returns a reusable builder for a one-time event listener. + * + *

Example: + * + *

Listening to a message from a channel and a user, after using a slash command: + *

{@code
+     * jda.listenOnce(MessageReceivedEvent.class)
+     *     .filter(messageEvent -> messageEvent.getChannel().getIdLong() == event.getChannel().getIdLong())
+     *     .filter(messageEvent -> messageEvent.getAuthor().getIdLong() == event.getUser().getIdLong())
+     *     .timeout(Duration.ofSeconds(5), () -> {
+     *         event.getHook().sendMessage("Timeout!").queue();
+     *     })
+     *     .submit()
+     *     .onSuccess(messageEvent -> {
+     *         event.getHook().sendMessage("You sent: " + messageEvent.getMessage().getContentRaw()).queue();
+     *     });
+     * }
+ * + * @param eventType + * Type of the event to listen to + * + * @return The one-time event listener builder + * + * @throws IllegalArgumentException + * If the provided event type is {@code null} + */ + @Nonnull + @CheckReturnValue + Once.Builder listenOnce(@Nonnull Class eventType); + /** * Retrieves the list of global commands. *
This list does not include guild commands! Use {@link Guild#retrieveCommands()} for guild commands. diff --git a/src/main/java/net/dv8tion/jda/api/utils/Once.java b/src/main/java/net/dv8tion/jda/api/utils/Once.java new file mode 100644 index 0000000000..60ca3a5556 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/utils/Once.java @@ -0,0 +1,212 @@ +package net.dv8tion.jda.api.utils; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.events.GenericEvent; +import net.dv8tion.jda.api.hooks.EventListener; +import net.dv8tion.jda.api.utils.concurrent.Task; +import net.dv8tion.jda.internal.utils.Checks; +import net.dv8tion.jda.internal.utils.concurrent.task.GatewayTask; + +import javax.annotation.CheckReturnValue; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.*; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * Helper class to listen to an event, once. + * + * @param Type of the event listened to + * + * @see JDA#listenOnce(Class) + */ +public class Once implements EventListener +{ + private final JDA jda; + private final Class eventType; + private final List> filters; + private final CompletableFuture future; + private final GatewayTask task; + private final ScheduledFuture timeoutFuture; + private final Runnable timeoutCallback; + + private Once(@Nonnull Once.Builder builder) + { + this.jda = builder.jda; + this.eventType = builder.eventType; + this.filters = new ArrayList<>(builder.filters); + this.timeoutCallback = builder.timeoutCallback; + + this.future = new CompletableFuture<>(); + this.task = createTask(future); + this.timeoutFuture = scheduleTimeout(builder.timeout, future); + } + + @Nonnull + private GatewayTask createTask(@Nonnull CompletableFuture future) + { + final GatewayTask task = new GatewayTask<>(future, () -> + { + // On cancellation, throw cancellation exception and cancel timeout + jda.removeEventListener(this); + future.completeExceptionally(new CancellationException()); + if (timeoutFuture != null) + timeoutFuture.cancel(false); + }); + task.onSetTimeout(e -> + { + throw new UnsupportedOperationException("You must set the timeout on Once.Builder#timeout"); + }); + return task; + } + + @Nullable + private ScheduledFuture scheduleTimeout(@Nullable Duration timeout, @Nonnull CompletableFuture future) + { + if (timeout == null) return null; + + return jda.getGatewayPool().schedule(() -> + { + // On timeout, throw timeout exception and run timeout callback + jda.removeEventListener(this); + future.completeExceptionally(new TimeoutException()); + if (timeoutCallback != null) + { + try + { + timeoutCallback.run(); + } + catch (Throwable e) + { + future.completeExceptionally(e); + } + } + }, timeout.toMillis(), TimeUnit.MILLISECONDS); + } + + @Override + public void onEvent(@Nonnull GenericEvent event) + { + if (!eventType.isInstance(event)) + return; + final E casted = eventType.cast(event); + if (filters.stream().allMatch(p -> p.test(casted))) + { + if (timeoutFuture != null) + timeoutFuture.cancel(false); + event.getJDA().removeEventListener(this); + future.complete(casted); + } + } + + /** + * Builds a one-time event listener, can be reused. + * + * @param Type of the event listened to + */ + public static class Builder + { + private final JDA jda; + private final Class eventType; + private final List> filters = new ArrayList<>(); + + private Duration timeout; + private Runnable timeoutCallback; + + /** + * Creates a builder for a one-time event listener + * + * @param jda + * The JDA instance + * @param eventType + * The event type to listen for + */ + public Builder(@Nonnull JDA jda, @Nonnull Class eventType) + { + Checks.notNull(jda, "JDA"); + Checks.notNull(eventType, "Event type"); + this.jda = jda; + this.eventType = eventType; + } + + /** + * Adds an event filter, all filters need to return {@code true} for the event to be consumed. + * + * @param filter + * The filter to add, returns {@code true} if the event can be consumed + * + * @return This instance for chaining convenience + */ + @Nonnull + public Builder filter(@Nonnull Predicate filter) + { + Checks.notNull(filter, "Filter"); + filters.add(filter); + return this; + } + + /** + * Sets the timeout duration, after which the event is no longer listener for. + * + * @param timeout + * The duration after which the event is no longer listener for + * + * @return This instance for chaining convenience + */ + @Nonnull + public Builder timeout(@Nonnull Duration timeout) + { + return timeout(timeout, null); + } + + /** + * Sets the timeout duration, after which the event is no longer listener for, + * and the callback is run. + * + * @param timeout + * The duration after which the event is no longer listener for + * @param timeoutCallback + * The callback run after the duration + * + * @return This instance for chaining convenience + */ + @Nonnull + public Builder timeout(@Nonnull Duration timeout, @Nullable Runnable timeoutCallback) + { + Checks.notNull(timeout, "Timeout"); + this.timeout = timeout; + this.timeoutCallback = timeoutCallback; + return this; + } + + /** + * Starts listening for the event, once. + * + *

The task will be completed after all {@link #filter(Predicate) filters} return {@code true}. + * + *

Exceptions thrown in {@link Task#get() blocking} and {@link Task#onSuccess(Consumer) async} contexts includes: + *

    + *
  • {@link CancellationException} - When {@link Task#cancel()} is called
  • + *
  • {@link TimeoutException} - When the listener has expired
  • + *
  • Any exception thrown by the {@link #timeout(Duration, Runnable) timeout callback}
  • + *
+ * + * @return {@link Task} returning an event satisfying all preconditions + * + * @see Task#onSuccess(Consumer) + * @see Task#get() + */ + @Nonnull + @CheckReturnValue + public Task submit() + { + final Once once = new Once<>(this); + jda.addEventListener(once); + return once.task; + } + } +} diff --git a/src/main/java/net/dv8tion/jda/internal/JDAImpl.java b/src/main/java/net/dv8tion/jda/internal/JDAImpl.java index f269cfc0ce..8ef202f4d3 100644 --- a/src/main/java/net/dv8tion/jda/internal/JDAImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/JDAImpl.java @@ -1009,6 +1009,13 @@ public List getRegisteredListeners() return eventManager.getRegisteredListeners(); } + @Nonnull + @Override + public Once.Builder listenOnce(@Nonnull Class eventType) + { + return new Once.Builder<>(this, eventType); + } + @Nonnull @Override public RestAction> retrieveCommands(boolean withLocalizations) From 0199b0958cf1b47c66fe19ea7241390bd4d75201 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Mon, 27 May 2024 19:15:34 +0200 Subject: [PATCH 02/15] Use more complete example --- src/main/java/net/dv8tion/jda/api/JDA.java | 25 +++++++++++++--------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/main/java/net/dv8tion/jda/api/JDA.java b/src/main/java/net/dv8tion/jda/api/JDA.java index 402d5bf339..e801717f48 100644 --- a/src/main/java/net/dv8tion/jda/api/JDA.java +++ b/src/main/java/net/dv8tion/jda/api/JDA.java @@ -614,16 +614,21 @@ default boolean awaitShutdown() throws InterruptedException * *

Listening to a message from a channel and a user, after using a slash command: *

{@code
-     * jda.listenOnce(MessageReceivedEvent.class)
-     *     .filter(messageEvent -> messageEvent.getChannel().getIdLong() == event.getChannel().getIdLong())
-     *     .filter(messageEvent -> messageEvent.getAuthor().getIdLong() == event.getUser().getIdLong())
-     *     .timeout(Duration.ofSeconds(5), () -> {
-     *         event.getHook().sendMessage("Timeout!").queue();
-     *     })
-     *     .submit()
-     *     .onSuccess(messageEvent -> {
-     *         event.getHook().sendMessage("You sent: " + messageEvent.getMessage().getContentRaw()).queue();
-     *     });
+     * final Duration timeout = Duration.ofSeconds(5);
+     * event.reply("Reply in " + TimeFormat.RELATIVE.after(timeout) + " if you can!")
+     *         .setEphemeral(true)
+     *         .queue();
+     *
+     * event.getJDA().listenOnce(MessageReceivedEvent.class)
+     *         .filter(messageEvent -> messageEvent.getChannel().getIdLong() == event.getChannel().getIdLong())
+     *         .filter(messageEvent -> messageEvent.getAuthor().getIdLong() == event.getUser().getIdLong())
+     *         .timeout(timeout, () -> {
+     *             event.getHook().editOriginal("Timeout!").queue();
+     *         })
+     *         .submit()
+     *         .onSuccess(messageEvent -> {
+     *             event.getHook().editOriginal("You sent: " + messageEvent.getMessage().getContentRaw()).queue();
+     *         });
      * }
* * @param eventType From db351faafda6a79d782755b6284ff7fa688c10e9 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Tue, 4 Jun 2024 23:19:55 +0200 Subject: [PATCH 03/15] Add AnnotatedEventManager support --- src/main/java/net/dv8tion/jda/api/utils/Once.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/net/dv8tion/jda/api/utils/Once.java b/src/main/java/net/dv8tion/jda/api/utils/Once.java index 60ca3a5556..15cc3c58d3 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/Once.java +++ b/src/main/java/net/dv8tion/jda/api/utils/Once.java @@ -3,6 +3,7 @@ import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.events.GenericEvent; import net.dv8tion.jda.api.hooks.EventListener; +import net.dv8tion.jda.api.hooks.SubscribeEvent; import net.dv8tion.jda.api.utils.concurrent.Task; import net.dv8tion.jda.internal.utils.Checks; import net.dv8tion.jda.internal.utils.concurrent.task.GatewayTask; @@ -89,6 +90,7 @@ private ScheduledFuture scheduleTimeout(@Nullable Duration timeout, @Nonnull } @Override + @SubscribeEvent public void onEvent(@Nonnull GenericEvent event) { if (!eventType.isInstance(event)) From 2bfc0f1eede075fdf3a6d07f255f0f26056bed56 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 21 Jul 2024 13:49:37 +0200 Subject: [PATCH 04/15] Improve docs --- src/main/java/net/dv8tion/jda/api/JDA.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/net/dv8tion/jda/api/JDA.java b/src/main/java/net/dv8tion/jda/api/JDA.java index e801717f48..aff1bd31b2 100644 --- a/src/main/java/net/dv8tion/jda/api/JDA.java +++ b/src/main/java/net/dv8tion/jda/api/JDA.java @@ -610,6 +610,12 @@ default boolean awaitShutdown() throws InterruptedException /** * Returns a reusable builder for a one-time event listener. * + *

Note that this method only works if the {@link JDABuilder#setEventManager(IEventManager) event manager} + * is either the {@link net.dv8tion.jda.api.hooks.InterfacedEventManager InterfacedEventManager} + * or {@link net.dv8tion.jda.api.hooks.AnnotatedEventManager AnnotatedEventManager}. + *
Other implementations can support it as long as they call + * {@link net.dv8tion.jda.api.hooks.EventListener#onEvent(GenericEvent) EventListener.onEvent(GenericEvent)}. + * *

Example: * *

Listening to a message from a channel and a user, after using a slash command: @@ -634,10 +640,10 @@ default boolean awaitShutdown() throws InterruptedException * @param eventType * Type of the event to listen to * - * @return The one-time event listener builder - * * @throws IllegalArgumentException * If the provided event type is {@code null} + * + * @return The one-time event listener builder */ @Nonnull @CheckReturnValue From 559aa202b2001f25cc3f0a99217f2a9d9426fa5a Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 21 Jul 2024 14:45:56 +0200 Subject: [PATCH 05/15] Document not null checks --- src/main/java/net/dv8tion/jda/api/utils/Once.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/net/dv8tion/jda/api/utils/Once.java b/src/main/java/net/dv8tion/jda/api/utils/Once.java index 15cc3c58d3..1f1e35cb19 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/Once.java +++ b/src/main/java/net/dv8tion/jda/api/utils/Once.java @@ -126,6 +126,9 @@ public static class Builder * The JDA instance * @param eventType * The event type to listen for + * + * @throws IllegalArgumentException + * If any of the parameters is null */ public Builder(@Nonnull JDA jda, @Nonnull Class eventType) { @@ -141,6 +144,9 @@ public Builder(@Nonnull JDA jda, @Nonnull Class eventType) * @param filter * The filter to add, returns {@code true} if the event can be consumed * + * @throws IllegalArgumentException + * If the filter is null + * * @return This instance for chaining convenience */ @Nonnull @@ -157,6 +163,9 @@ public Builder filter(@Nonnull Predicate filter) * @param timeout * The duration after which the event is no longer listener for * + * @throws IllegalArgumentException + * If the timeout is null + * * @return This instance for chaining convenience */ @Nonnull @@ -174,6 +183,9 @@ public Builder timeout(@Nonnull Duration timeout) * @param timeoutCallback * The callback run after the duration * + * @throws IllegalArgumentException + * If the timeout is null + * * @return This instance for chaining convenience */ @Nonnull From 2a1242f036ba5a0174cbcd104f48d4b6bf9dc17e Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 21 Jul 2024 14:46:26 +0200 Subject: [PATCH 06/15] Add Once.Builder#setTimeoutPool --- .../java/net/dv8tion/jda/api/utils/Once.java | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/main/java/net/dv8tion/jda/api/utils/Once.java b/src/main/java/net/dv8tion/jda/api/utils/Once.java index 1f1e35cb19..b048175f3d 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/Once.java +++ b/src/main/java/net/dv8tion/jda/api/utils/Once.java @@ -44,7 +44,7 @@ private Once(@Nonnull Once.Builder builder) this.future = new CompletableFuture<>(); this.task = createTask(future); - this.timeoutFuture = scheduleTimeout(builder.timeout, future); + this.timeoutFuture = scheduleTimeout(builder.timeout, builder.timeoutPool, future); } @Nonnull @@ -66,11 +66,12 @@ private GatewayTask createTask(@Nonnull CompletableFuture future) } @Nullable - private ScheduledFuture scheduleTimeout(@Nullable Duration timeout, @Nonnull CompletableFuture future) + private ScheduledFuture scheduleTimeout(@Nullable Duration timeout, @Nullable ScheduledExecutorService timeoutPool, @Nonnull CompletableFuture future) { if (timeout == null) return null; + if (timeoutPool == null) timeoutPool = jda.getGatewayPool(); - return jda.getGatewayPool().schedule(() -> + return timeoutPool.schedule(() -> { // On timeout, throw timeout exception and run timeout callback jda.removeEventListener(this); @@ -116,6 +117,7 @@ public static class Builder private final Class eventType; private final List> filters = new ArrayList<>(); + private ScheduledExecutorService timeoutPool; private Duration timeout; private Runnable timeoutCallback; @@ -197,6 +199,27 @@ public Builder timeout(@Nonnull Duration timeout, @Nullable Runnable timeoutC return this; } + /** + * Sets the thread pool used to schedule timeouts and run its callback. + * + *

By default {@link JDA#getGatewayPool()} is used. + * + * @param timeoutPool + * The thread pool to use for timeouts + * + * @throws IllegalArgumentException + * If the timeout pool is null + * + * @return This instance for chaining convenience + */ + @Nonnull + public Builder setTimeoutPool(@Nonnull ScheduledExecutorService timeoutPool) + { + Checks.notNull(timeoutPool, "Timeout pool"); + this.timeoutPool = timeoutPool; + return this; + } + /** * Starts listening for the event, once. * From 760556c804b4fead90c5d4e60e8859ece05757cd Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 21 Jul 2024 14:47:35 +0200 Subject: [PATCH 07/15] Replace Once.Builder#submit with subscribe(Consumer) --- src/main/java/net/dv8tion/jda/api/JDA.java | 3 +-- src/main/java/net/dv8tion/jda/api/utils/Once.java | 7 +++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/net/dv8tion/jda/api/JDA.java b/src/main/java/net/dv8tion/jda/api/JDA.java index aff1bd31b2..463212491e 100644 --- a/src/main/java/net/dv8tion/jda/api/JDA.java +++ b/src/main/java/net/dv8tion/jda/api/JDA.java @@ -631,8 +631,7 @@ default boolean awaitShutdown() throws InterruptedException * .timeout(timeout, () -> { * event.getHook().editOriginal("Timeout!").queue(); * }) - * .submit() - * .onSuccess(messageEvent -> { + * .subscribe(messageEvent -> { * event.getHook().editOriginal("You sent: " + messageEvent.getMessage().getContentRaw()).queue(); * }); * } diff --git a/src/main/java/net/dv8tion/jda/api/utils/Once.java b/src/main/java/net/dv8tion/jda/api/utils/Once.java index b048175f3d..cd22543794 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/Once.java +++ b/src/main/java/net/dv8tion/jda/api/utils/Once.java @@ -232,6 +232,9 @@ public Builder setTimeoutPool(@Nonnull ScheduledExecutorService timeoutPool) *

  • Any exception thrown by the {@link #timeout(Duration, Runnable) timeout callback}
  • * * + * @throws IllegalArgumentException + * If the callback is null + * * @return {@link Task} returning an event satisfying all preconditions * * @see Task#onSuccess(Consumer) @@ -239,11 +242,11 @@ public Builder setTimeoutPool(@Nonnull ScheduledExecutorService timeoutPool) */ @Nonnull @CheckReturnValue - public Task submit() + public Task subscribe(@Nonnull Consumer callback) { final Once once = new Once<>(this); jda.addEventListener(once); - return once.task; + return once.task.onSuccess(callback); } } } From fd0c30a2c2c0dd5516ef6ef488c6c6c9b0c33a17 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 21 Jul 2024 15:01:35 +0200 Subject: [PATCH 08/15] Small refactor --- src/main/java/net/dv8tion/jda/api/utils/Once.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/net/dv8tion/jda/api/utils/Once.java b/src/main/java/net/dv8tion/jda/api/utils/Once.java index cd22543794..4cbe25b622 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/Once.java +++ b/src/main/java/net/dv8tion/jda/api/utils/Once.java @@ -43,12 +43,12 @@ private Once(@Nonnull Once.Builder builder) this.timeoutCallback = builder.timeoutCallback; this.future = new CompletableFuture<>(); - this.task = createTask(future); - this.timeoutFuture = scheduleTimeout(builder.timeout, builder.timeoutPool, future); + this.task = createTask(); + this.timeoutFuture = scheduleTimeout(builder.timeout, builder.timeoutPool); } @Nonnull - private GatewayTask createTask(@Nonnull CompletableFuture future) + private GatewayTask createTask() { final GatewayTask task = new GatewayTask<>(future, () -> { @@ -66,7 +66,7 @@ private GatewayTask createTask(@Nonnull CompletableFuture future) } @Nullable - private ScheduledFuture scheduleTimeout(@Nullable Duration timeout, @Nullable ScheduledExecutorService timeoutPool, @Nonnull CompletableFuture future) + private ScheduledFuture scheduleTimeout(@Nullable Duration timeout, @Nullable ScheduledExecutorService timeoutPool) { if (timeout == null) return null; if (timeoutPool == null) timeoutPool = jda.getGatewayPool(); From 6223801919ff8ff40f80c1be2d0237525755d611 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 21 Jul 2024 15:02:02 +0200 Subject: [PATCH 09/15] Log timeout callback exceptions --- src/main/java/net/dv8tion/jda/api/utils/Once.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/dv8tion/jda/api/utils/Once.java b/src/main/java/net/dv8tion/jda/api/utils/Once.java index 4cbe25b622..3daa4e7f26 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/Once.java +++ b/src/main/java/net/dv8tion/jda/api/utils/Once.java @@ -6,7 +6,9 @@ import net.dv8tion.jda.api.hooks.SubscribeEvent; import net.dv8tion.jda.api.utils.concurrent.Task; import net.dv8tion.jda.internal.utils.Checks; +import net.dv8tion.jda.internal.utils.JDALogger; import net.dv8tion.jda.internal.utils.concurrent.task.GatewayTask; +import org.slf4j.Logger; import javax.annotation.CheckReturnValue; import javax.annotation.Nonnull; @@ -27,6 +29,8 @@ */ public class Once implements EventListener { + private static final Logger LOG = JDALogger.getLog(Once.class); + private final JDA jda; private final Class eventType; private final List> filters; @@ -84,7 +88,7 @@ private ScheduledFuture scheduleTimeout(@Nullable Duration timeout, @Nullable } catch (Throwable e) { - future.completeExceptionally(e); + LOG.error("An error occurred while running the timeout callback", e); } } }, timeout.toMillis(), TimeUnit.MILLISECONDS); From 220f88b0f53241a7e1106ba6c5fa344d38d15e1f Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 21 Jul 2024 15:02:42 +0200 Subject: [PATCH 10/15] Don't run timeout callback if the future was already completed --- src/main/java/net/dv8tion/jda/api/utils/Once.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/dv8tion/jda/api/utils/Once.java b/src/main/java/net/dv8tion/jda/api/utils/Once.java index 3daa4e7f26..4377d65d02 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/Once.java +++ b/src/main/java/net/dv8tion/jda/api/utils/Once.java @@ -79,7 +79,9 @@ private ScheduledFuture scheduleTimeout(@Nullable Duration timeout, @Nullable { // On timeout, throw timeout exception and run timeout callback jda.removeEventListener(this); - future.completeExceptionally(new TimeoutException()); + if (!future.completeExceptionally(new TimeoutException())) + // Return if the future was already completed + return; if (timeoutCallback != null) { try From da699fedf6f9dd04658b965d5a04a7e34257d538 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 21 Jul 2024 15:03:09 +0200 Subject: [PATCH 11/15] Handle filter exceptions --- .../java/net/dv8tion/jda/api/utils/Once.java | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/main/java/net/dv8tion/jda/api/utils/Once.java b/src/main/java/net/dv8tion/jda/api/utils/Once.java index 4377d65d02..c5f6641eee 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/Once.java +++ b/src/main/java/net/dv8tion/jda/api/utils/Once.java @@ -103,12 +103,20 @@ public void onEvent(@Nonnull GenericEvent event) if (!eventType.isInstance(event)) return; final E casted = eventType.cast(event); - if (filters.stream().allMatch(p -> p.test(casted))) + try { - if (timeoutFuture != null) - timeoutFuture.cancel(false); - event.getJDA().removeEventListener(this); - future.complete(casted); + if (filters.stream().allMatch(p -> p.test(casted))) + { + if (timeoutFuture != null) + timeoutFuture.cancel(false); + event.getJDA().removeEventListener(this); + future.complete(casted); + } + } + catch (Throwable e) + { + if (future.completeExceptionally(e)) + event.getJDA().removeEventListener(this); } } @@ -149,6 +157,8 @@ public Builder(@Nonnull JDA jda, @Nonnull Class eventType) /** * Adds an event filter, all filters need to return {@code true} for the event to be consumed. * + *

    If the filter throws an exception, this listener will unregister itself. + * * @param filter * The filter to add, returns {@code true} if the event can be consumed * From ac2ca3dc4c325f8dd7ada8dc6206b389a363605f Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 21 Jul 2024 19:19:33 +0200 Subject: [PATCH 12/15] Rethrow errors --- src/main/java/net/dv8tion/jda/api/utils/Once.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/net/dv8tion/jda/api/utils/Once.java b/src/main/java/net/dv8tion/jda/api/utils/Once.java index c5f6641eee..9165e59ab7 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/Once.java +++ b/src/main/java/net/dv8tion/jda/api/utils/Once.java @@ -91,6 +91,8 @@ private ScheduledFuture scheduleTimeout(@Nullable Duration timeout, @Nullable catch (Throwable e) { LOG.error("An error occurred while running the timeout callback", e); + if (e instanceof Error) + throw (Error) e; } } }, timeout.toMillis(), TimeUnit.MILLISECONDS); @@ -117,6 +119,8 @@ public void onEvent(@Nonnull GenericEvent event) { if (future.completeExceptionally(e)) event.getJDA().removeEventListener(this); + if (e instanceof Error) + throw (Error) e; } } From c5969688de53372b6641e533c0fd9fd2728c4127 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 21 Jul 2024 19:20:04 +0200 Subject: [PATCH 13/15] Update src/main/java/net/dv8tion/jda/api/utils/Once.java MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Florian Spieß --- src/main/java/net/dv8tion/jda/api/utils/Once.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/net/dv8tion/jda/api/utils/Once.java b/src/main/java/net/dv8tion/jda/api/utils/Once.java index 9165e59ab7..fbcbf8d156 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/Once.java +++ b/src/main/java/net/dv8tion/jda/api/utils/Once.java @@ -80,7 +80,6 @@ private ScheduledFuture scheduleTimeout(@Nullable Duration timeout, @Nullable // On timeout, throw timeout exception and run timeout callback jda.removeEventListener(this); if (!future.completeExceptionally(new TimeoutException())) - // Return if the future was already completed return; if (timeoutCallback != null) { From 350375bf49d8ab05bec1091388cb30281d5c4b61 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 21 Jul 2024 19:55:57 +0200 Subject: [PATCH 14/15] Add copyright --- .../java/net/dv8tion/jda/api/utils/Once.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/main/java/net/dv8tion/jda/api/utils/Once.java b/src/main/java/net/dv8tion/jda/api/utils/Once.java index fbcbf8d156..02ac5c5df9 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/Once.java +++ b/src/main/java/net/dv8tion/jda/api/utils/Once.java @@ -1,3 +1,19 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package net.dv8tion.jda.api.utils; import net.dv8tion.jda.api.JDA; From ca66633d7f26cc0e0d7f9f3e60d4010db08b65ca Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Thu, 25 Jul 2024 18:27:30 +0200 Subject: [PATCH 15/15] Don't pass builder to constructor --- src/main/java/net/dv8tion/jda/api/utils/Once.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/net/dv8tion/jda/api/utils/Once.java b/src/main/java/net/dv8tion/jda/api/utils/Once.java index 02ac5c5df9..3081a213b6 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/Once.java +++ b/src/main/java/net/dv8tion/jda/api/utils/Once.java @@ -55,16 +55,16 @@ public class Once implements EventListener private final ScheduledFuture timeoutFuture; private final Runnable timeoutCallback; - private Once(@Nonnull Once.Builder builder) + protected Once(JDA jda, Class eventType, List> filters, Runnable timeoutCallback, Duration timeout, ScheduledExecutorService timeoutPool) { - this.jda = builder.jda; - this.eventType = builder.eventType; - this.filters = new ArrayList<>(builder.filters); - this.timeoutCallback = builder.timeoutCallback; + this.jda = jda; + this.eventType = eventType; + this.filters = new ArrayList<>(filters); + this.timeoutCallback = timeoutCallback; this.future = new CompletableFuture<>(); this.task = createTask(); - this.timeoutFuture = scheduleTimeout(builder.timeout, builder.timeoutPool); + this.timeoutFuture = scheduleTimeout(timeout, timeoutPool); } @Nonnull @@ -279,7 +279,7 @@ public Builder setTimeoutPool(@Nonnull ScheduledExecutorService timeoutPool) @CheckReturnValue public Task subscribe(@Nonnull Consumer callback) { - final Once once = new Once<>(this); + final Once once = new Once<>(jda, eventType, filters, timeoutCallback, timeout, timeoutPool); jda.addEventListener(once); return once.task.onSuccess(callback); }