diff --git a/src/main/java/net/dv8tion/jda/api/exceptions/FirstAcknowledgementException.java b/src/main/java/net/dv8tion/jda/api/exceptions/FirstAcknowledgementException.java new file mode 100644 index 0000000000..553f18011e --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/exceptions/FirstAcknowledgementException.java @@ -0,0 +1,28 @@ +/* + * 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.exceptions; + +/** + * Used to indicate where an interaction was first acknowledged at, for debugging purposes. + */ +public class FirstAcknowledgementException extends RuntimeException +{ + public FirstAcknowledgementException() + { + super("This is where the interaction was first acknowledged at"); + } +} diff --git a/src/main/java/net/dv8tion/jda/internal/interactions/InteractionHookImpl.java b/src/main/java/net/dv8tion/jda/internal/interactions/InteractionHookImpl.java index 07dba40bbf..54800038a6 100644 --- a/src/main/java/net/dv8tion/jda/internal/interactions/InteractionHookImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/interactions/InteractionHookImpl.java @@ -74,11 +74,6 @@ public InteractionHookImpl(@Nonnull JDA api, @Nonnull String token) this.isReady = true; } - public boolean ack() - { - return interaction == null || interaction.ack(); - } - public boolean isAck() { return interaction == null || interaction.isAcknowledged(); diff --git a/src/main/java/net/dv8tion/jda/internal/interactions/InteractionImpl.java b/src/main/java/net/dv8tion/jda/internal/interactions/InteractionImpl.java index cce804a8bd..d35555ea8b 100644 --- a/src/main/java/net/dv8tion/jda/internal/interactions/InteractionImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/interactions/InteractionImpl.java @@ -25,22 +25,32 @@ import net.dv8tion.jda.api.entities.channel.ChannelType; import net.dv8tion.jda.api.entities.channel.concrete.PrivateChannel; import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; +import net.dv8tion.jda.api.exceptions.FirstAcknowledgementException; import net.dv8tion.jda.api.interactions.DiscordLocale; import net.dv8tion.jda.api.interactions.Interaction; import net.dv8tion.jda.api.utils.data.DataArray; import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.internal.JDAImpl; -import net.dv8tion.jda.internal.entities.*; +import net.dv8tion.jda.internal.entities.GuildImpl; +import net.dv8tion.jda.internal.entities.MemberImpl; +import net.dv8tion.jda.internal.entities.UserImpl; import net.dv8tion.jda.internal.entities.channel.concrete.PrivateChannelImpl; import net.dv8tion.jda.internal.utils.Helpers; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.List; -import java.util.stream.Collectors; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; public class InteractionImpl implements Interaction { + // Enables recording where the first ack is done, + // to help with debugging when an interaction is acknowledged twice + private static boolean recordAckTraces = false; + private static ScheduledFuture scheduledRecordDeactivation; + private Throwable firstAckTrace = null; + protected final long id; protected final long channelId; protected final int type; @@ -120,11 +130,41 @@ public InteractionImpl(JDAImpl jda, DataObject data) public synchronized void releaseHook(boolean success) {} // Ensures that one cannot acknowledge an interaction twice - public synchronized boolean ack() + @Nullable + public synchronized IllegalStateException tryAck() { - boolean wasAck = isAck; - this.isAck = true; - return wasAck; + // If not already acknowledged => no exception + if (!isAck) + { + // Store where the first ack was made, so we can use show it on the 2nd ack + if (recordAckTraces) + firstAckTrace = new FirstAcknowledgementException(); + isAck = true; + return null; + } + + // Enable saving stack traces of acknowledgements. + // On future acknowledgements, the stack trace of the first ack will be kept, and added to the second ack, + // so the user doesn't have to figure out where the first ack was at. + if (firstAckTrace == null) + { + recordAckTraces = true; + + // Stop recording after 15 minutes if the issue was not reproduced + if (scheduledRecordDeactivation != null) + scheduledRecordDeactivation.cancel(false); + scheduledRecordDeactivation = api.getGatewayPool().schedule(() -> + { + recordAckTraces = false; + }, 15, TimeUnit.MINUTES); + return new IllegalStateException("This interaction has already been acknowledged or replied to. You can only reply or acknowledge an interaction once! Retry using this interaction for more details."); + } + else + { + recordAckTraces = false; + scheduledRecordDeactivation.cancel(false); + return new IllegalStateException("This interaction has already been acknowledged or replied to. You can only reply or acknowledge an interaction once!", firstAckTrace); + } } @Override diff --git a/src/main/java/net/dv8tion/jda/internal/requests/restaction/interactions/InteractionCallbackImpl.java b/src/main/java/net/dv8tion/jda/internal/requests/restaction/interactions/InteractionCallbackImpl.java index f89f4ed9b4..2ee048e115 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/restaction/interactions/InteractionCallbackImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/restaction/interactions/InteractionCallbackImpl.java @@ -49,19 +49,10 @@ public InteractionCallbackAction closeResources() // Here we intercept calls to queue/submit/complete to prevent double ack/reply scenarios with a better error message than discord provides // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // This is an exception factory method that only returns an exception if we would have to throw it or fail in another way. - protected final IllegalStateException tryAck() // note that interaction.ack() is already synchronized so this is actually thread-safe! - { - // true => we already called this before => this will never succeed! - return interaction.ack() - ? new IllegalStateException("This interaction has already been acknowledged or replied to. You can only reply or acknowledge an interaction once!") - : null; // null indicates we were successful, no exception means we can't fail :) - } - @Override public final void queue(Consumer success, Consumer failure) { - IllegalStateException exception = tryAck(); + IllegalStateException exception = interaction.tryAck(); if (exception != null) { if (failure != null) @@ -78,7 +69,7 @@ public final void queue(Consumer success, Consumer @Override public final CompletableFuture submit(boolean shouldQueue) { - IllegalStateException exception = tryAck(); + IllegalStateException exception = interaction.tryAck(); if (exception != null) { CompletableFuture future = new CompletableFuture<>();