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
43 changes: 43 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,47 @@ default boolean awaitShutdown() throws InterruptedException
@Nonnull
List<Object> getRegisteredListeners();

/**
* Returns a reusable builder for a one-time event listener.
*
* <p>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}.
* <br>Other implementations can support it as long as they call
* {@link net.dv8tion.jda.api.hooks.EventListener#onEvent(GenericEvent) EventListener.onEvent(GenericEvent)}.
*
* <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();
* })
* .subscribe(messageEvent -> {
* event.getHook().editOriginal("You sent: " + messageEvent.getMessage().getContentRaw()).queue();
* });
* }</pre>
*
* @param eventType
* Type of the event to listen to
*
* @throws IllegalArgumentException
freya022 marked this conversation as resolved.
Show resolved Hide resolved
* If the provided event type is {@code null}
*
* @return The one-time event listener builder
*/
@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
287 changes: 287 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,287 @@
/*
* 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;
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.JDALogger;
import net.dv8tion.jda.internal.utils.concurrent.task.GatewayTask;
import org.slf4j.Logger;

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 static final Logger LOG = JDALogger.getLog(Once.class);

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;

protected Once(JDA jda, Class<E> eventType, List<Predicate<? super E>> filters, Runnable timeoutCallback, Duration timeout, ScheduledExecutorService timeoutPool)
{
this.jda = jda;
this.eventType = eventType;
this.filters = new ArrayList<>(filters);
this.timeoutCallback = timeoutCallback;

this.future = new CompletableFuture<>();
this.task = createTask();
this.timeoutFuture = scheduleTimeout(timeout, timeoutPool);
}

@Nonnull
private GatewayTask<E> createTask()
{
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, @Nullable ScheduledExecutorService timeoutPool)
{
if (timeout == null) return null;
if (timeoutPool == null) timeoutPool = jda.getGatewayPool();

return timeoutPool.schedule(() ->
{
// On timeout, throw timeout exception and run timeout callback
jda.removeEventListener(this);
if (!future.completeExceptionally(new TimeoutException()))
return;
if (timeoutCallback != null)
MinnDevelopment marked this conversation as resolved.
Show resolved Hide resolved
{
try
{
timeoutCallback.run();
}
catch (Throwable e)
{
LOG.error("An error occurred while running the timeout callback", e);
freya022 marked this conversation as resolved.
Show resolved Hide resolved
if (e instanceof Error)
throw (Error) e;
}
}
}, timeout.toMillis(), TimeUnit.MILLISECONDS);
}

@Override
@SubscribeEvent
public void onEvent(@Nonnull GenericEvent event)
{
if (!eventType.isInstance(event))
return;
final E casted = eventType.cast(event);
try
{
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))
freya022 marked this conversation as resolved.
Show resolved Hide resolved
event.getJDA().removeEventListener(this);
if (e instanceof Error)
throw (Error) e;
}
}

/**
* 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 ScheduledExecutorService timeoutPool;
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
*
* @throws IllegalArgumentException
* If any of the parameters is null
*/
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.
*
* <p>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
*
* @throws IllegalArgumentException
* If the filter is null
*
* @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
*
* @throws IllegalArgumentException
* If the timeout is null
*
* @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
*
* @throws IllegalArgumentException
* If the timeout is null
*
* @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;
}

/**
* Sets the thread pool used to schedule timeouts and run its callback.
*
* <p>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<E> setTimeoutPool(@Nonnull ScheduledExecutorService timeoutPool)
{
Checks.notNull(timeoutPool, "Timeout pool");
this.timeoutPool = timeoutPool;
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>
*
* @throws IllegalArgumentException
* If the callback is null
*
* @return {@link Task} returning an event satisfying all preconditions
*
* @see Task#onSuccess(Consumer)
* @see Task#get()
*/
@Nonnull
@CheckReturnValue
public Task<E> subscribe(@Nonnull Consumer<E> callback)
{
final Once<E> once = new Once<>(jda, eventType, filters, timeoutCallback, timeout, timeoutPool);
jda.addEventListener(once);
return once.task.onSuccess(callback);
}
}
}
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