Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add JDA#listenOnce #2683

Merged
merged 15 commits into from
Aug 2, 2024
38 changes: 38 additions & 0 deletions src/main/java/net/dv8tion/jda/api/JDA.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -605,6 +607,42 @@ default boolean awaitShutdown() throws InterruptedException
@Nonnull
List<Object> getRegisteredListeners();

/**
* Returns a reusable builder for a one-time event listener.
*
* <p><b>Example:</b>
*
* <p>Listening to a message from a channel and a user, after using a slash command:
* <pre>{@code
* 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();
* });
* }</pre>
*
* @param eventType
* Type of the event to listen to
*
* @return The one-time event listener builder
*
* @throws IllegalArgumentException
freya022 marked this conversation as resolved.
Show resolved Hide resolved
* If the provided event type is {@code null}
*/
@Nonnull
@CheckReturnValue
<E extends GenericEvent> Once.Builder<E> listenOnce(@Nonnull Class<E> eventType);
freya022 marked this conversation as resolved.
Show resolved Hide resolved

/**
* Retrieves the list of global commands.
* <br>This list does not include guild commands! Use {@link Guild#retrieveCommands()} for guild commands.
Expand Down
214 changes: 214 additions & 0 deletions src/main/java/net/dv8tion/jda/api/utils/Once.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
package net.dv8tion.jda.api.utils;
freya022 marked this conversation as resolved.
Show resolved Hide resolved

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;

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 <E> Type of the event listened to
*
* @see JDA#listenOnce(Class)
*/
public class Once<E extends GenericEvent> implements EventListener
{
private final JDA jda;
private final Class<E> eventType;
private final List<Predicate<? super E>> filters;
private final CompletableFuture<E> future;
private final GatewayTask<E> task;
private final ScheduledFuture<?> timeoutFuture;
private final Runnable timeoutCallback;

private Once(@Nonnull Once.Builder<E> builder)
freya022 marked this conversation as resolved.
Show resolved Hide resolved
{
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<E> createTask(@Nonnull CompletableFuture<E> future)
{
final GatewayTask<E> 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<E> future)
{
if (timeout == null) return null;

return jda.getGatewayPool().schedule(() ->
freya022 marked this conversation as resolved.
Show resolved Hide resolved
{
// On timeout, throw timeout exception and run timeout callback
jda.removeEventListener(this);
future.completeExceptionally(new TimeoutException());
if (timeoutCallback != null)
MinnDevelopment marked this conversation as resolved.
Show resolved Hide resolved
{
try
{
timeoutCallback.run();
}
catch (Throwable e)
{
future.completeExceptionally(e);
freya022 marked this conversation as resolved.
Show resolved Hide resolved
}
}
}, timeout.toMillis(), TimeUnit.MILLISECONDS);
}

@Override
@SubscribeEvent
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)))
freya022 marked this conversation as resolved.
Show resolved Hide resolved
{
if (timeoutFuture != null)
timeoutFuture.cancel(false);
event.getJDA().removeEventListener(this);
future.complete(casted);
}
}

/**
* Builds a one-time event listener, can be reused.
*
* @param <E> Type of the event listened to
*/
public static class Builder<E extends GenericEvent>
{
private final JDA jda;
private final Class<E> eventType;
private final List<Predicate<? super E>> 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<E> 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<E> filter(@Nonnull Predicate<? super E> 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<E> 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<E> 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.
*
* <p>The task will be completed after all {@link #filter(Predicate) filters} return {@code true}.
*
* <p>Exceptions thrown in {@link Task#get() blocking} and {@link Task#onSuccess(Consumer) async} contexts includes:
* <ul>
* <li>{@link CancellationException} - When {@link Task#cancel()} is called</li>
* <li>{@link TimeoutException} - When the listener has expired</li>
* <li>Any exception thrown by the {@link #timeout(Duration, Runnable) timeout callback}</li>
* </ul>
*
* @return {@link Task} returning an event satisfying all preconditions
*
* @see Task#onSuccess(Consumer)
* @see Task#get()
*/
@Nonnull
@CheckReturnValue
public Task<E> submit()
{
final Once<E> once = new Once<>(this);
jda.addEventListener(once);
return once.task;
}
}
}
7 changes: 7 additions & 0 deletions src/main/java/net/dv8tion/jda/internal/JDAImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,13 @@ public List<Object> getRegisteredListeners()
return eventManager.getRegisteredListeners();
}

@Nonnull
@Override
public <E extends GenericEvent> Once.Builder<E> listenOnce(@Nonnull Class<E> eventType)
{
return new Once.Builder<>(this, eventType);
}

@Nonnull
@Override
public RestAction<List<Command>> retrieveCommands(boolean withLocalizations)
Expand Down
Loading