diff --git a/README.md b/README.md index 925a70ca05..bd4a909a15 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,27 @@ Through [RestAction](https://ci.dv8tion.net/job/JDA/javadoc/net/dv8tion/jda/api/ and it is up to the user to decide which pattern to utilize. It can be combined with reactive libraries such as [reactor-core](https://github.com/reactor/reactor-core) due to being lazy. +The RestAction interface also supports a number of operators to avoid callback hell: + +- [`map`](https://ci.dv8tion.net/job/JDA/javadoc/net/dv8tion/jda/api/requests/RestAction.html#map%28java.util.function.Function%29) + Convert the result of the `RestAction` to a different value +- [`flatMap`](https://ci.dv8tion.net/job/JDA/javadoc/net/dv8tion/jda/api/requests/RestAction.html#flatMap%28java.util.function.Function%29) + Chain another `RestAction` on the result +- [`delay`](https://ci.dv8tion.net/job/JDA/javadoc/net/dv8tion/jda/api/requests/RestAction.html#delay%28java.time.Duration%29) + Delay the element of the previous step + +**Example**: + +```java +public RestAction selfDestruct(MessageChannel channel, String content) { + return channel.sendMessage("The following message will destroy itself in 1 minute!") + .delay(10, SECONDS, scheduler) // edit 10 seconds later + .flatMap((it) -> it.editMessage(content)) + .delay(1, MINUTES, scheduler) // delete 1 minute later + .flatMap(Message::delete); +} +``` + ### More Examples We provide a small set of Examples in the [Example Directory](https://github.com/DV8FromTheWorld/JDA/tree/master/src/examples/java). diff --git a/build.gradle.kts b/build.gradle.kts index 6b66b93801..01fcf55d0f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,7 +33,7 @@ plugins { id("com.github.johnrengelman.shadow") version "5.1.0" } -val versionObj = Version(major = "4", minor = "1", revision = "0") +val versionObj = Version(major = "4", minor = "1", revision = "1") project.group = "net.dv8tion" project.version = "$versionObj" diff --git a/src/examples/java/MessageListenerExample.java b/src/examples/java/MessageListenerExample.java index 8796b8001f..990b5da971 100644 --- a/src/examples/java/MessageListenerExample.java +++ b/src/examples/java/MessageListenerExample.java @@ -26,6 +26,7 @@ import javax.security.auth.login.LoginException; import java.util.List; import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; public class MessageListenerExample extends ListenerAdapter { @@ -142,20 +143,19 @@ else if (event.isFromType(ChannelType.PRIVATE)) //If this message was sent to a } else if (msg.equals("!roll")) { - //In this case, we have an example showing how to use the Success consumer for a RestAction. The Success consumer + //In this case, we have an example showing how to use the flatMap operator for a RestAction. The operator // will provide you with the object that results after you execute your RestAction. As a note, not all RestActions - // have object returns and will instead have Void returns. You can still use the success consumer to determine when - // the action has been completed! + // have object returns and will instead have Void returns. You can still use the flatMap operator to run chain another RestAction! - Random rand = new Random(); + Random rand = ThreadLocalRandom.current(); int roll = rand.nextInt(6) + 1; //This results in 1 - 6 (instead of 0 - 5) - channel.sendMessage("Your roll: " + roll).queue(sentMessage -> //This is called a lambda statement. If you don't know - { // what they are or how they work, try google! - if (roll < 3) - { - channel.sendMessage("The roll for messageId: " + sentMessage.getId() + " wasn't very good... Must be bad luck!\n").queue(); - } - }); + channel.sendMessage("Your roll: " + roll) + .flatMap( + (v) -> roll < 3, // This is called a lambda expression. If you don't know what they are or how they work, try google! + // Send another message if the roll was bad (less than 3) + sentMessage -> channel.sendMessage("The roll for messageId: " + sentMessage.getId() + " wasn't very good... Must be bad luck!\n") + ) + .queue(); } else if (msg.startsWith("!kick")) //Note, I used "startsWith, not equals. { diff --git a/src/main/java/net/dv8tion/jda/api/JDA.java b/src/main/java/net/dv8tion/jda/api/JDA.java index 551119ebb5..6ae2b9879e 100644 --- a/src/main/java/net/dv8tion/jda/api/JDA.java +++ b/src/main/java/net/dv8tion/jda/api/JDA.java @@ -31,7 +31,7 @@ import net.dv8tion.jda.api.utils.MiscUtil; import net.dv8tion.jda.api.utils.cache.CacheView; import net.dv8tion.jda.api.utils.cache.SnowflakeCacheView; -import net.dv8tion.jda.internal.requests.EmptyRestAction; +import net.dv8tion.jda.internal.requests.CompletedRestAction; import net.dv8tion.jda.internal.requests.RestActionImpl; import net.dv8tion.jda.internal.requests.Route; import net.dv8tion.jda.internal.utils.Checks; @@ -813,6 +813,16 @@ default List getGuildsByName(@Nonnull String name, boolean ignoreCase) @Nonnull Set getUnavailableGuilds(); + /** + * Whether the guild is unavailable. If this returns true, the guild id should be in {@link #getUnavailableGuilds()}. + * + * @param guildId + * The guild id + * + * @return True, if this guild is unavailable + */ + boolean isUnavailable(long guildId); + /** * Unified {@link net.dv8tion.jda.api.utils.cache.SnowflakeCacheView SnowflakeCacheView} of * all cached {@link net.dv8tion.jda.api.entities.Role Roles} visible to this JDA session. @@ -1828,6 +1838,6 @@ default AuditableRestAction installAuxiliaryPort() } } else throw new IllegalStateException("No port available"); - return new EmptyRestAction<>(this, port); + return new CompletedRestAction<>(this, port); } } diff --git a/src/main/java/net/dv8tion/jda/api/JDABuilder.java b/src/main/java/net/dv8tion/jda/api/JDABuilder.java index 7b4ce0892a..735fbd6ffb 100644 --- a/src/main/java/net/dv8tion/jda/api/JDABuilder.java +++ b/src/main/java/net/dv8tion/jda/api/JDABuilder.java @@ -82,6 +82,7 @@ public class JDABuilder protected boolean idle = false; protected int maxReconnectDelay = 900; protected int largeThreshold = 250; + protected int maxBufferSize = 2048; protected EnumSet flags = ConfigFlag.getDefault(); protected ChunkingFilter chunkingFilter = ChunkingFilter.ALL; @@ -155,7 +156,7 @@ public JDABuilder setRawEventsEnabled(boolean enable) /** * Whether the rate-limit should be relative to the current time plus latency. - *
By default we use the {@code X-RateLimit-Rest-After} header to determine when + *
By default we use the {@code X-RateLimit-Reset-After} header to determine when * a rate-limit is no longer imminent. This has the disadvantage that it might wait longer than needed due * to the latency which is ignored by the reset-after relative delay. * @@ -920,6 +921,30 @@ public JDABuilder setLargeThreshold(int threshold) return this; } + /** + * The maximum size, in bytes, of the buffer used for decompressing discord payloads. + *
If the maximum buffer size is exceeded a new buffer will be allocated instead. + *
Setting this to {@link Integer#MAX_VALUE} would imply the buffer will never be resized unless memory starvation is imminent. + *
Setting this to {@code 0} would imply the buffer would need to be allocated again for every payload (not recommended). + * + *

Default: {@code 2048} + * + * @param bufferSize + * The maximum size the buffer should allow to retain + * + * @throws IllegalArgumentException + * If the provided buffer size is negative + * + * @return The JDABuilder instance. Useful for chaining. + */ + @Nonnull + public JDABuilder setMaxBufferSize(int bufferSize) + { + Checks.notNegative(bufferSize, "The buffer size"); + this.maxBufferSize = bufferSize; + return this; + } + /** * Builds a new {@link net.dv8tion.jda.api.JDA} instance and uses the provided token to start the login process. *
The login process runs in a different thread, so while this will return immediately, {@link net.dv8tion.jda.api.JDA} has not @@ -964,7 +989,7 @@ public JDA build() throws LoginException threadingConfig.setGatewayPool(mainWsPool, shutdownMainWsPool); threadingConfig.setRateLimitPool(rateLimitPool, shutdownRateLimitPool); SessionConfig sessionConfig = new SessionConfig(controller, httpClient, wsFactory, voiceDispatchInterceptor, flags, maxReconnectDelay, largeThreshold); - MetaConfig metaConfig = new MetaConfig(contextMap, cacheFlags, flags); + MetaConfig metaConfig = new MetaConfig(maxBufferSize, contextMap, cacheFlags, flags); JDAImpl jda = new JDAImpl(authConfig, sessionConfig, threadingConfig, metaConfig); jda.setChunkingFilter(chunkingFilter); diff --git a/src/main/java/net/dv8tion/jda/api/entities/Guild.java b/src/main/java/net/dv8tion/jda/api/entities/Guild.java index d1c7b4a633..e11e797d2b 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/Guild.java +++ b/src/main/java/net/dv8tion/jda/api/entities/Guild.java @@ -39,7 +39,7 @@ import net.dv8tion.jda.api.utils.cache.MemberCacheView; import net.dv8tion.jda.api.utils.cache.SnowflakeCacheView; import net.dv8tion.jda.api.utils.cache.SortedSnowflakeCacheView; -import net.dv8tion.jda.internal.requests.EmptyRestAction; +import net.dv8tion.jda.internal.requests.DeferredRestAction; import net.dv8tion.jda.internal.requests.Route; import net.dv8tion.jda.internal.requests.restaction.AuditableRestActionImpl; import net.dv8tion.jda.internal.utils.Checks; @@ -1628,13 +1628,18 @@ default RestAction retrieveEmote(@Nonnull Emote emote) Checks.notNull(emote, "Emote"); if (emote.getGuild() != null) Checks.check(emote.getGuild().equals(this), "Emote must be from the same Guild!"); - if (emote instanceof ListedEmote && !emote.isFake()) - { - ListedEmote listedEmote = (ListedEmote) emote; - if (listedEmote.hasUser() || !getSelfMember().hasPermission(Permission.MANAGE_EMOTES)) - return new EmptyRestAction<>(getJDA(), listedEmote); - } - return retrieveEmoteById(emote.getId()); + + JDA jda = getJDA(); + return new DeferredRestAction<>(jda, ListedEmote.class, + () -> { + if (emote instanceof ListedEmote && !emote.isFake()) + { + ListedEmote listedEmote = (ListedEmote) emote; + if (listedEmote.hasUser() || !getSelfMember().hasPermission(Permission.MANAGE_EMOTES)) + return listedEmote; + } + return null; + }, () -> retrieveEmoteById(emote.getId())); } /** @@ -2029,17 +2034,19 @@ default RestAction retrieveBan(@Nonnull User bannedUser) boolean checkVerification(); /** - * Returns whether or not this Guild is available. A Guild can be unavailable, if the Discord server has problems. - *
If a Guild is unavailable, no actions on it can be performed (Messages, Manager,...) + * Whether or not this Guild is available. A Guild can be unavailable, if the Discord server has problems. + *
If a Guild is unavailable, it will be removed from the guild cache. You cannot receive events for unavailable guilds. * * @return If the Guild is available * - * @deprecated - * This will be removed in a future version, unavailable guilds are now removed from cache + * @deprecated This will be removed in a future version, + * unavailable guilds are now removed from cache. + * Replace with {@link JDA#isUnavailable(long)} */ @ForRemoval @Deprecated @DeprecatedSince("4.1.0") + @ReplaceWith("getJDA().isUnavailable(guild.getIdLong())") boolean isAvailable(); /** diff --git a/src/main/java/net/dv8tion/jda/api/entities/GuildChannel.java b/src/main/java/net/dv8tion/jda/api/entities/GuildChannel.java index c20da3479e..8d4d44e7fb 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/GuildChannel.java +++ b/src/main/java/net/dv8tion/jda/api/entities/GuildChannel.java @@ -15,6 +15,7 @@ */ package net.dv8tion.jda.api.entities; +import gnu.trove.map.TLongObjectMap; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.exceptions.InsufficientPermissionException; @@ -24,6 +25,8 @@ import net.dv8tion.jda.api.requests.restaction.ChannelAction; import net.dv8tion.jda.api.requests.restaction.InviteAction; import net.dv8tion.jda.api.requests.restaction.PermissionOverrideAction; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.entities.GuildImpl; import javax.annotation.CheckReturnValue; import javax.annotation.Nonnull; @@ -363,7 +366,15 @@ default PermissionOverrideAction upsertPermissionOverride(@Nonnull IPermissionHo if (!getGuild().getSelfMember().hasPermission(this, Permission.MANAGE_PERMISSIONS)) throw new InsufficientPermissionException(this, Permission.MANAGE_PERMISSIONS); PermissionOverride override = getPermissionOverride(permissionHolder); - return override != null ? override.getManager() : putPermissionOverride(permissionHolder); + if (override != null) + return override.getManager(); + PermissionOverrideAction action = putPermissionOverride(permissionHolder); + // Check if we have some information cached already + TLongObjectMap cache = ((GuildImpl) getGuild()).getOverrideMap(permissionHolder.getIdLong()); + DataObject json = cache == null ? null : cache.get(getIdLong()); + if (json != null) + action = action.setPermissions(json.getLong("allow"), json.getLong("deny")); + return action; } /** diff --git a/src/main/java/net/dv8tion/jda/api/entities/Message.java b/src/main/java/net/dv8tion/jda/api/entities/Message.java index ffe5fad006..161234eb53 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/Message.java +++ b/src/main/java/net/dv8tion/jda/api/entities/Message.java @@ -38,6 +38,7 @@ import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.regex.Pattern; +import java.util.stream.Collectors; /** * Represents a Text message received from Discord. @@ -1589,6 +1590,65 @@ default boolean isFromGuild() @CheckReturnValue MessageReaction.ReactionEmote getReactionById(long id); + /** + * Enables/Disables suppression of Embeds on this Message. + *
Suppressing Embeds is equivalent to pressing the {@code X} in the top-right corner of an Embed inside the Discord client. + * + *

The following {@link net.dv8tion.jda.api.requests.ErrorResponse ErrorResponses} are possible: + *

    + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#MISSING_ACCESS MISSING_ACCESS} + *
    The clear-reactions request was attempted after the account lost access to the {@link net.dv8tion.jda.api.entities.TextChannel TextChannel} + * due to {@link net.dv8tion.jda.api.Permission#MESSAGE_READ Permission.MESSAGE_READ} being revoked, or the + * account lost access to the {@link net.dv8tion.jda.api.entities.Guild Guild} + * typically due to being kicked or removed.
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#MISSING_PERMISSIONS MISSING_PERMISSIONS} + *
    The suppress-embeds request was attempted after the account lost {@link net.dv8tion.jda.api.Permission#MESSAGE_MANAGE Permission.MESSAGE_MANAGE} + * in the {@link net.dv8tion.jda.api.entities.TextChannel TextChannel} when adding the reaction.
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_MESSAGE UNKNOWN_MESSAGE} + * The suppress-embeds request was attempted after the Message had been deleted.
  • + *
+ * + * @param suppressed + * Whether or not the embed should be suppressed + * @throws java.lang.UnsupportedOperationException + * If this is not a Received Message from {@link net.dv8tion.jda.api.entities.MessageType#DEFAULT MessageType.DEFAULT} + * @throws net.dv8tion.jda.api.exceptions.InsufficientPermissionException + * If the MessageChannel this message was sent in was a {@link net.dv8tion.jda.api.entities.TextChannel TextChannel} + * and the currently logged in account does not have + * {@link net.dv8tion.jda.api.Permission#MESSAGE_MANAGE Permission.MESSAGE_MANAGE} in the channel. + * @throws net.dv8tion.jda.api.exceptions.PermissionException + * If the MessageChannel this message was sent in was a {@link net.dv8tion.jda.api.entities.PrivateChannel PrivateChannel} + * and the message was not sent by the currently logged in account. + * @return {@link net.dv8tion.jda.api.requests.restaction.AuditableRestAction AuditableRestAction} - Type: {@link java.lang.Void} + * @see #isSuppressedEmbeds() + */ + @Nonnull + @CheckReturnValue + AuditableRestAction suppressEmbeds(boolean suppressed); + + /** + * Whether embeds are suppressed for this message. + * When Embeds are suppressed, they are not displayed on clients nor provided via API until un-suppressed. + *
This is a shortcut method for checking if {@link #getFlags() getFlags()} contains + * {@link net.dv8tion.jda.api.entities.Message.MessageFlag#EMBEDS_SUPPRESSED MessageFlag#EMBEDS_SUPPRESSED} + * + * @throws java.lang.UnsupportedOperationException + * If this is not a Received Message from {@link net.dv8tion.jda.api.entities.MessageType#DEFAULT MessageType.DEFAULT} + * @return Whether or not Embeds are suppressed for this Message. + * @see #suppressEmbeds(boolean) + */ + boolean isSuppressedEmbeds(); + + /** + * Returns an EnumSet of all {@link Message.MessageFlag MessageFlags} present for this Message. + * @return Never-Null EnumSet of present {@link Message.MessageFlag MessageFlags} + * @see Message.MessageFlag + */ + @Nonnull + EnumSet getFlags(); + /** * This specifies the {@link net.dv8tion.jda.api.entities.MessageType MessageType} of this Message. * @@ -1649,6 +1709,89 @@ public Pattern getPattern() } } + /** + * Enum representing the flags on a Message. + *

+ * Note: The Values defined in this Enum are not considered final and only represent the current State of known Flags. + */ + enum MessageFlag + { + /** + * The Message has been published to subscribed Channels (via Channel Following) + */ + CROSSPOSTED(0), + /** + * The Message originated from a Message in another Channel (via Channel Following) + */ + IS_CROSSPOST(1), + /** + * Embeds are suppressed on the Message. + * @see net.dv8tion.jda.api.entities.Message#isSuppressedEmbeds() Message#isSuppressedEmbeds() + */ + EMBEDS_SUPPRESSED(2), + /** + * Indicates, that the source message of this crosspost was deleted. + * This should only be possible in combination with {@link #IS_CROSSPOST} + */ + SOURCE_MESSAGE_DELETED(3), + /** + * Indicates, that this Message came from the urgent message system + */ + URGENT(4); + + private final int value; + + MessageFlag(int offset) + { + this.value = 1 << offset; + } + + /** + * Returns the value of the MessageFlag as represented in the bitfield. It is always a power of 2 (single bit) + * @return Non-Zero bit value of the field + */ + public int getValue() + { + return value; + } + + /** + * Given a bitfield, this function extracts all Enum values according to their bit values and returns + * an EnumSet containing all matching MessageFlags + * @param bitfield + * Non-Negative integer representing a bitfield of MessageFlags + * @return Never-Null EnumSet of MessageFlags being found in the bitfield + */ + @Nonnull + public static EnumSet fromBitField(int bitfield) + { + Set set = Arrays.stream(MessageFlag.values()) + .filter(e -> (e.value & bitfield) > 0) + .collect(Collectors.toSet()); + return set.isEmpty() ? EnumSet.noneOf(MessageFlag.class) : EnumSet.copyOf(set); + } + + /** + * Converts a Collection of MessageFlags back to the integer representing the bitfield. + * This is the reverse operation of {@link #fromBitField(int)}. + * @param coll + * A Non-Null Collection of MessageFlags + * @throws IllegalArgumentException + * If the provided Collection is {@code null} + * @return Integer value of the bitfield representing the given MessageFlags + */ + public static int toBitField(@Nonnull Collection coll) + { + Checks.notNull(coll, "Collection"); + int flags = 0; + for (MessageFlag messageFlag : coll) + { + flags |= messageFlag.value; + } + return flags; + } + } + /** * Represents a {@link net.dv8tion.jda.api.entities.Message Message} file attachment. */ diff --git a/src/main/java/net/dv8tion/jda/api/entities/MessageChannel.java b/src/main/java/net/dv8tion/jda/api/entities/MessageChannel.java index 179548fdea..94e9aa483e 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/MessageChannel.java +++ b/src/main/java/net/dv8tion/jda/api/entities/MessageChannel.java @@ -1018,26 +1018,17 @@ default MessageHistory getHistory() * overflows in channels with a long message history. * *

Examples

- *

-     * public boolean containsMessage(MessageChannel channel, String content, int checkAmount)
-     * {
-     *     for (Message message : channel.getIterableHistory())
-     *     {
-     *         if (message.getContentRaw().equals(content))
-     *             return true;
-     *         if (checkAmount--{@literal <=} 0) break;
-     *     }
-     *     return false;
+     * 
{@code
+     * public CompletableFuture> getMessagesByUser(MessageChannel channel, User user) {
+     *     return channel.getIterableHistory()
+     *         .takeAsync(1000) // Collect 1000 messages
+     *         .thenApply(list ->
+     *             list.stream()
+     *                 .filter(m -> m.getAuthor().equals(user)) // Filter messages by author
+     *                 .collect(Collectors.toList())
+     *         );
      * }
-     *
-     * public List{@literal } getMessagesByUser(MessageChannel channel, User user)
-     * {
-     *     return channel.getIterableHistory().stream()
-     *         .limit(1000)
-     *         .filter(m{@literal ->} m.getAuthor().equals(user))
-     *         .collect(Collectors.toList());
-     * }
-     * 
+ * }
* * @throws net.dv8tion.jda.api.exceptions.InsufficientPermissionException * If this is a {@link net.dv8tion.jda.api.entities.TextChannel TextChannel} diff --git a/src/main/java/net/dv8tion/jda/api/entities/MessageHistory.java b/src/main/java/net/dv8tion/jda/api/entities/MessageHistory.java index bfb32748ef..603845d206 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/MessageHistory.java +++ b/src/main/java/net/dv8tion/jda/api/entities/MessageHistory.java @@ -566,7 +566,7 @@ protected void handleSuccess(Response response, Request request) { final MessageHistory result = new MessageHistory(channel); final DataArray array = response.getArray(); - final EntityBuilder builder = api.get().getEntityBuilder(); + final EntityBuilder builder = api.getEntityBuilder(); for (int i = 0; i < array.length(); i++) { try diff --git a/src/main/java/net/dv8tion/jda/api/entities/PermissionOverride.java b/src/main/java/net/dv8tion/jda/api/entities/PermissionOverride.java index 8fa90ef5bb..a7c86a2296 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/PermissionOverride.java +++ b/src/main/java/net/dv8tion/jda/api/entities/PermissionOverride.java @@ -38,7 +38,7 @@ * @see GuildChannel#getMemberPermissionOverrides() * @see GuildChannel#getRolePermissionOverrides() */ -public interface PermissionOverride +public interface PermissionOverride extends ISnowflake { /** * This is the raw binary representation (as a base 10 long) of the permissions allowed by this override. diff --git a/src/main/java/net/dv8tion/jda/api/entities/TextChannel.java b/src/main/java/net/dv8tion/jda/api/entities/TextChannel.java index e7aaa23a60..1abfc5701e 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/TextChannel.java +++ b/src/main/java/net/dv8tion/jda/api/entities/TextChannel.java @@ -70,7 +70,6 @@ public interface TextChannel extends GuildChannel, MessageChannel, IMentionable /** * Whether or not this channel is considered as "NSFW" (Not-Safe-For-Work) - *
This will check whether the name of this TextChannel begins with {@code nsfw-} or is equal to {@code nsfw}! * * @return True, If this TextChannel is considered NSFW by the official Discord Client */ diff --git a/src/main/java/net/dv8tion/jda/api/entities/User.java b/src/main/java/net/dv8tion/jda/api/entities/User.java index 54b3d1ec0c..11abfb54aa 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/User.java +++ b/src/main/java/net/dv8tion/jda/api/entities/User.java @@ -176,17 +176,17 @@ default String getEffectiveAvatarUrl() *
{@code
      * // Send message without response handling
      * public void sendMessage(User user, String content) {
-     *     user.openPrivateChannel().queue(channel ->
-     *         channel.sendMessage(content).queue());
+     *     user.openPrivateChannel()
+     *         .flatMap(channel -> channel.sendMessage(content))
+     *         .queue();
      * }
      *
-     * // Send message and provide it to the future for further handling
-     * public CompletableFuture awaitMessage(User user, String content) {
-     *     return user.openPrivateChannel().submit()
-     *                .thenCompose(channel -> channel.sendMessage(content).submit())
-     *                .whenComplete((m, error) -> {
-     *                    if (error != null) error.printStackTrace());
-     *                });
+     * // Send message and delete 30 seconds later
+     * public RestAction sendSecretMessage(User user, String content) {
+     *     return user.openPrivateChannel() // RestAction
+     *                .flatMap(channel -> channel.sendMessage(content)) // RestAction
+     *                .delay(30, TimeUnit.SECONDS) // RestAction with delayed response
+     *                .flatMap(Message::delete); // RestAction (executed 30 seconds after sending)
      * }
      * }
* diff --git a/src/main/java/net/dv8tion/jda/api/events/guild/GuildJoinEvent.java b/src/main/java/net/dv8tion/jda/api/events/guild/GuildJoinEvent.java index d3e0ad1de4..fb465304e2 100644 --- a/src/main/java/net/dv8tion/jda/api/events/guild/GuildJoinEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/guild/GuildJoinEvent.java @@ -22,8 +22,11 @@ /** * Indicates that you joined a {@link net.dv8tion.jda.api.entities.Guild Guild}. + *
This requires that the guild is available when the guild leave happens. Otherwise a {@link UnavailableGuildJoinedEvent} is fired instead. * *

Warning: Discord already triggered a mass amount of these events due to a downtime. Be careful! + * + * @see UnavailableGuildJoinedEvent */ public class GuildJoinEvent extends GenericGuildEvent { diff --git a/src/main/java/net/dv8tion/jda/api/events/guild/GuildLeaveEvent.java b/src/main/java/net/dv8tion/jda/api/events/guild/GuildLeaveEvent.java index 5ac47e9d74..a67a7dc518 100644 --- a/src/main/java/net/dv8tion/jda/api/events/guild/GuildLeaveEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/guild/GuildLeaveEvent.java @@ -22,8 +22,11 @@ /** * Indicates that you left a {@link net.dv8tion.jda.api.entities.Guild Guild}. + *
This requires that the guild is available when the guild leave happens. Otherwise a {@link UnavailableGuildLeaveEvent} is fired instead. * *

Can be used to detect when you leave a Guild. + * + * @see UnavailableGuildLeaveEvent */ public class GuildLeaveEvent extends GenericGuildEvent { diff --git a/src/main/java/net/dv8tion/jda/api/events/guild/UnavailableGuildLeaveEvent.java b/src/main/java/net/dv8tion/jda/api/events/guild/UnavailableGuildLeaveEvent.java new file mode 100644 index 0000000000..d76bee3340 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/events/guild/UnavailableGuildLeaveEvent.java @@ -0,0 +1,60 @@ +/* + * Copyright 2015-2019 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.events.guild; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.events.Event; + +import javax.annotation.Nonnull; + +/** + * Indicates that you left a {@link net.dv8tion.jda.api.entities.Guild Guild} that is not yet available. + * This does not extend {@link net.dv8tion.jda.api.events.guild.GenericGuildEvent GenericGuildEvent} + * + *

Can be used to retrieve id of the unavailable Guild. + */ +public class UnavailableGuildLeaveEvent extends Event +{ + private final long guildId; + + public UnavailableGuildLeaveEvent(@Nonnull JDA api, long responseNumber, long guildId) + { + super(api, responseNumber); + this.guildId = guildId; + } + + /** + * The id for the guild we left. + * + * @return The id for the guild + */ + @Nonnull + public String getGuildId() + { + return Long.toUnsignedString(guildId); + } + + /** + * The id for the guild we left. + * + * @return The id for the guild + */ + public long getGuildIdLong() + { + return guildId; + } +} diff --git a/src/main/java/net/dv8tion/jda/api/exceptions/ContextException.java b/src/main/java/net/dv8tion/jda/api/exceptions/ContextException.java index 4d5f18de1b..b8b6f1c0ae 100644 --- a/src/main/java/net/dv8tion/jda/api/exceptions/ContextException.java +++ b/src/main/java/net/dv8tion/jda/api/exceptions/ContextException.java @@ -49,15 +49,29 @@ public static Consumer herePrintingTrace() @Nonnull public static Consumer here(@Nonnull Consumer acceptor) { - ContextException context = new ContextException(); - return (ex) -> + return new ContextConsumer(new ContextException(), acceptor); + } + + public static class ContextConsumer implements Consumer + { + private final ContextException context; + private final Consumer callback; + + private ContextConsumer(ContextException context, Consumer callback) + { + this.context = context; + this.callback = callback; + } + + @Override + public void accept(Throwable throwable) { - Throwable cause = ex; + Throwable cause = throwable; while (cause.getCause() != null) cause = cause.getCause(); cause.initCause(context); - if (acceptor != null) - acceptor.accept(ex); - }; + if (callback != null) + callback.accept(throwable); + } } } diff --git a/src/main/java/net/dv8tion/jda/api/exceptions/RateLimitedException.java b/src/main/java/net/dv8tion/jda/api/exceptions/RateLimitedException.java index b9937e7677..c4ebaefd07 100644 --- a/src/main/java/net/dv8tion/jda/api/exceptions/RateLimitedException.java +++ b/src/main/java/net/dv8tion/jda/api/exceptions/RateLimitedException.java @@ -28,7 +28,7 @@ public class RateLimitedException extends Exception public RateLimitedException(Route.CompiledRoute route, long retryAfter) { - this(route.getRatelimitRoute(), retryAfter); + this(route.getBaseRoute().getRoute() + ":" + route.getMajorParameters(), retryAfter); } public RateLimitedException(String route, long retryAfter) diff --git a/src/main/java/net/dv8tion/jda/api/hooks/ListenerAdapter.java b/src/main/java/net/dv8tion/jda/api/hooks/ListenerAdapter.java index 85add2f898..716f21d079 100644 --- a/src/main/java/net/dv8tion/jda/api/hooks/ListenerAdapter.java +++ b/src/main/java/net/dv8tion/jda/api/hooks/ListenerAdapter.java @@ -213,6 +213,7 @@ public void onGuildLeave(@Nonnull GuildLeaveEvent event) {} public void onGuildAvailable(@Nonnull GuildAvailableEvent event) {} public void onGuildUnavailable(@Nonnull GuildUnavailableEvent event) {} public void onUnavailableGuildJoined(@Nonnull UnavailableGuildJoinedEvent event) {} + public void onUnavailableGuildLeave(@Nonnull UnavailableGuildLeaveEvent event) {} public void onGuildBan(@Nonnull GuildBanEvent event) {} public void onGuildUnban(@Nonnull GuildUnbanEvent event) {} @@ -499,6 +500,8 @@ else if (event instanceof GuildUnavailableEvent) onGuildUnavailable((GuildUnavailableEvent) event); else if (event instanceof UnavailableGuildJoinedEvent) onUnavailableGuildJoined((UnavailableGuildJoinedEvent) event); + else if (event instanceof UnavailableGuildLeaveEvent) + onUnavailableGuildLeave((UnavailableGuildLeaveEvent) event); else if (event instanceof GuildBanEvent) onGuildBan((GuildBanEvent) event); else if (event instanceof GuildUnbanEvent) diff --git a/src/main/java/net/dv8tion/jda/api/requests/Request.java b/src/main/java/net/dv8tion/jda/api/requests/Request.java index 0d2f8938bd..3c4b09ec8e 100644 --- a/src/main/java/net/dv8tion/jda/api/requests/Request.java +++ b/src/main/java/net/dv8tion/jda/api/requests/Request.java @@ -58,7 +58,9 @@ public Request( { this.restAction = restAction; this.onSuccess = onSuccess; - if (RestActionImpl.isPassContext()) + if (onFailure instanceof ContextException.ContextConsumer) + this.onFailure = onFailure; + else if (RestActionImpl.isPassContext()) this.onFailure = ContextException.here(onFailure); else this.onFailure = onFailure; diff --git a/src/main/java/net/dv8tion/jda/api/requests/RestAction.java b/src/main/java/net/dv8tion/jda/api/requests/RestAction.java index 7c98f4a679..4f441837a5 100644 --- a/src/main/java/net/dv8tion/jda/api/requests/RestAction.java +++ b/src/main/java/net/dv8tion/jda/api/requests/RestAction.java @@ -17,20 +17,28 @@ package net.dv8tion.jda.api.requests; import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.exceptions.ContextException; import net.dv8tion.jda.api.exceptions.RateLimitedException; import net.dv8tion.jda.api.utils.concurrent.DelayedCompletableFuture; import net.dv8tion.jda.internal.requests.RestActionImpl; +import net.dv8tion.jda.internal.requests.restaction.operator.DelayRestAction; +import net.dv8tion.jda.internal.requests.restaction.operator.FlatMapRestAction; +import net.dv8tion.jda.internal.requests.restaction.operator.MapRestAction; import net.dv8tion.jda.internal.utils.Checks; import net.dv8tion.jda.internal.utils.ContextRunnable; +import javax.annotation.CheckReturnValue; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.time.Duration; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.function.BooleanSupplier; import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; /** * A class representing a terminal between the user and the discord API. @@ -426,6 +434,259 @@ default CompletableFuture submit() @Nonnull CompletableFuture submit(boolean shouldQueue); + /** + * Intermediate operator that returns a modified RestAction. + * + *

This does not modify this instance but returns a new RestAction which will apply + * the map function on successful execution. + * + *

Example

+ *
{@code
+     * public RestAction retrieveMemberNickname(Guild guild, String userId) {
+     *     return guild.retrieveMemberById(userId)
+     *                 .map(Member::getNickname);
+     * }
+     * }
+ * + * @param map + * The mapping function to apply to the action result + * + * @param + * The target output type + * + * @return RestAction for the mapped type + * + * @since 4.1.1 + */ + @Nonnull + @CheckReturnValue + default RestAction map(@Nonnull Function map) + { + Checks.notNull(map, "Function"); + return new MapRestAction<>(this, map); + } + + /** + * Intermediate operator that returns a modified RestAction. + * + *

This does not modify this instance but returns a new RestAction which will apply + * the map function on successful execution. This will compute the result of both RestActions. + *
The returned RestAction must not be null! + * To terminate the execution chain on a specific condition you can use {@link #flatMap(Predicate, Function)}. + * + *

Example

+ *
{@code
+     * public RestAction initializeGiveaway(Guild guild, String channelName) {
+     *     return guild.createTextChannel(channelName)
+     *          .addPermissionOverride(guild.getPublicRole(), null, EnumSet.of(Permission.MESSAGE_WRITE)) // deny write for everyone
+     *          .addPermissionOverride(guild.getSelfMember(), EnumSet.of(Permission.MESSAGE_WRITE), null) // allow for self user
+     *          .flatMap((channel) -> channel.sendMessage("React to enter giveaway!")) // send message
+     *          .flatMap((message) -> message.addReaction(REACTION)); // add reaction
+     * }
+     * }
+ * + * @param flatMap + * The mapping function to apply to the action result, must return a RestAction + * + * @param + * The target output type + * + * @return RestAction for the mapped type + * + * @since 4.1.1 + */ + @Nonnull + @CheckReturnValue + default RestAction flatMap(@Nonnull Function> flatMap) + { + return flatMap(null, flatMap); + } + + /** + * Intermediate operator that returns a modified RestAction. + * + *

This does not modify this instance but returns a new RestAction which will apply + * the map function on successful execution. This will compute the result of both RestActions. + *
The provided RestAction must not be null! + * + *

Example

+ *
{@code
+     * private static final int MAX_COUNT = 1000;
+     * public void updateCount(MessageChannel channel, String messageId, int count) {
+     *     channel.retrieveMessageById(messageId) // retrieve message for check
+     *         .map(Message::getContentRaw) // get content of the message
+     *         .map(Integer::parseInt) // convert it to an int
+     *         .flatMap(
+     *             (currentCount) -> currentCount + count <= MAX_COUNT, // Only edit if new count does not exceed maximum
+     *             (currentCount) -> channel.editMessageById(messageId, String.valueOf(currentCount + count)) // edit message
+     *         )
+     *         .map(Message::getContentRaw) // get content of the message
+     *         .map(Integer::parseInt) // convert it to an int
+     *         .queue((newCount) -> System.out.println("Updated count to " + newCount));
+     * }
+     * }
+ * + * @param condition + * A condition predicate that decides whether to apply the flat map operator or not + * @param flatMap + * The mapping function to apply to the action result, must return a RestAction + * + * @param + * The target output type + * + * @return RestAction for the mapped type + * + * @see #flatMap(Function) + * @see #map(Function) + * + * @since 4.1.1 + */ + @Nonnull + @CheckReturnValue + default RestAction flatMap(@Nullable Predicate condition, @Nonnull Function> flatMap) + { + Checks.notNull(flatMap, "Function"); + return new FlatMapRestAction<>(this, condition, flatMap); + } + + /** + * Intermediate operator that returns a modified RestAction. + * + *

This does not modify this instance but returns a new RestAction which will delay its result by the provided delay. + * + *

Example

+ *
{@code
+     * public RestAction selfDestruct(MessageChannel channel, String content) {
+     *     return channel.sendMessage("The following message will destroy itself in 1 minute!")
+     *         .delay(Duration.ofSeconds(10)) // edit 10 seconds later
+     *         .flatMap((it) -> it.editMessage(content))
+     *         .delay(Duration.ofMinutes(1)) // delete 1 minute later
+     *         .flatMap(Message::delete);
+     * }
+     * }
+ * + * @param duration + * The delay + * + * @return RestAction with delay + * + * @see #queueAfter(long, TimeUnit) + * + * @since 4.1.1 + */ + @Nonnull + @CheckReturnValue + default RestAction delay(@Nonnull Duration duration) + { + return delay(duration, null); + } + + /** + * Intermediate operator that returns a modified RestAction. + * + *

This does not modify this instance but returns a new RestAction which will delay its result by the provided delay. + * + *

Example

+ *
{@code
+     * public RestAction selfDestruct(MessageChannel channel, String content) {
+     *     return channel.sendMessage("The following message will destroy itself in 1 minute!")
+     *         .delay(Duration.ofSeconds(10), scheduler) // edit 10 seconds later
+     *         .flatMap((it) -> it.editMessage(content))
+     *         .delay(Duration.ofMinutes(1), scheduler) // delete 1 minute later
+     *         .flatMap(Message::delete);
+     * }
+     * }
+ * + * @param duration + * The delay + * @param scheduler + * The scheduler to use, null to use {@link JDA#getRateLimitPool()} + * + * @return RestAction with delay + * + * @see #queueAfter(long, TimeUnit, ScheduledExecutorService) + * + * @since 4.1.1 + */ + @Nonnull + @CheckReturnValue + default RestAction delay(@Nonnull Duration duration, @Nullable ScheduledExecutorService scheduler) + { + Checks.notNull(duration, "Duration"); + return new DelayRestAction<>(this, TimeUnit.MILLISECONDS, duration.toMillis(), scheduler); + } + + /** + * Intermediate operator that returns a modified RestAction. + * + *

This does not modify this instance but returns a new RestAction which will delay its result by the provided delay. + * + *

Example

+ *
{@code
+     * public RestAction selfDestruct(MessageChannel channel, String content) {
+     *     return channel.sendMessage("The following message will destroy itself in 1 minute!")
+     *         .delay(10, SECONDS) // edit 10 seconds later
+     *         .flatMap((it) -> it.editMessage(content))
+     *         .delay(1, MINUTES) // delete 1 minute later
+     *         .flatMap(Message::delete);
+     * }
+     * }
+ * + * @param delay + * The delay value + * @param unit + * The time unit for the delay value + * + * @return RestAction with delay + * + * @see #queueAfter(long, TimeUnit) + * + * @since 4.1.1 + */ + @Nonnull + @CheckReturnValue + default RestAction delay(long delay, @Nonnull TimeUnit unit) + { + return delay(delay, unit, null); + } + + /** + * Intermediate operator that returns a modified RestAction. + * + *

This does not modify this instance but returns a new RestAction which will delay its result by the provided delay. + * + *

Example

+ *
{@code
+     * public RestAction selfDestruct(MessageChannel channel, String content) {
+     *     return channel.sendMessage("The following message will destroy itself in 1 minute!")
+     *         .delay(10, SECONDS, scheduler) // edit 10 seconds later
+     *         .flatMap((it) -> it.editMessage(content))
+     *         .delay(1, MINUTES, scheduler) // delete 1 minute later
+     *         .flatMap(Message::delete);
+     * }
+     * }
+ * + * @param delay + * The delay value + * @param unit + * The time unit for the delay value + * @param scheduler + * The scheduler to use, null to use {@link JDA#getRateLimitPool()} + * + * @return RestAction with delay + * + * @see #queueAfter(long, TimeUnit, ScheduledExecutorService) + * + * @since 4.1.1 + */ + @Nonnull + @CheckReturnValue + default RestAction delay(long delay, @Nonnull TimeUnit unit, @Nullable ScheduledExecutorService scheduler) + { + Checks.notNull(unit, "TimeUnit"); + return new DelayRestAction<>(this, unit, delay, scheduler); + } + /** * Schedules a call to {@link #queue()} to be executed after the specified {@code delay}. *
This is an asynchronous operation that will return a @@ -486,7 +747,14 @@ default DelayedCompletableFuture submitAfter(long delay, @Nonnull TimeUnit un if (executor == null) executor = getJDA().getRateLimitPool(); return DelayedCompletableFuture.make(executor, delay, unit, - (task) -> new ContextRunnable<>(() -> queue(task::complete, task::completeExceptionally))); + (task) -> { + final Consumer onFailure; + if (isPassContext()) + onFailure = ContextException.here(task::completeExceptionally); + else + onFailure = task::completeExceptionally; + return new ContextRunnable(() -> queue(task::complete, onFailure)); + }); } /** @@ -713,6 +981,14 @@ default ScheduledFuture queueAfter(long delay, @Nonnull TimeUnit unit, @Nulla Checks.notNull(unit, "TimeUnit"); if (executor == null) executor = getJDA().getRateLimitPool(); - return executor.schedule((Runnable) new ContextRunnable<>(() -> queue(success, failure)), delay, unit); + + final Consumer onFailure; + if (isPassContext()) + onFailure = ContextException.here(failure == null ? getDefaultFailure() : failure); + else + onFailure = failure; + + Runnable task = new ContextRunnable(() -> queue(success, onFailure)); + return executor.schedule(task, delay, unit); } } diff --git a/src/main/java/net/dv8tion/jda/api/sharding/DefaultShardManager.java b/src/main/java/net/dv8tion/jda/api/sharding/DefaultShardManager.java index 26ca447e7f..447824ee28 100644 --- a/src/main/java/net/dv8tion/jda/api/sharding/DefaultShardManager.java +++ b/src/main/java/net/dv8tion/jda/api/sharding/DefaultShardManager.java @@ -486,7 +486,7 @@ protected JDAImpl buildInstance(final int shardId) throws LoginException, Interr threadingConfig.setRateLimitPool(rateLimitPool, shutdownRateLimitPool); threadingConfig.setGatewayPool(gatewayPool, shutdownGatewayPool); threadingConfig.setCallbackPool(callbackPool, shutdownCallbackPool); - MetaConfig metaConfig = new MetaConfig(this.metaConfig.getContextMap(shardId), this.metaConfig.getCacheFlags(), this.sessionConfig.getFlags()); + MetaConfig metaConfig = new MetaConfig(this.metaConfig.getMaxBufferSize(), this.metaConfig.getContextMap(shardId), this.metaConfig.getCacheFlags(), this.sessionConfig.getFlags()); final JDAImpl jda = new JDAImpl(authConfig, sessionConfig, threadingConfig, metaConfig); jda.setChunkingFilter(chunkingFilter); threadingConfig.init(jda::getIdentifierString); diff --git a/src/main/java/net/dv8tion/jda/api/sharding/DefaultShardManagerBuilder.java b/src/main/java/net/dv8tion/jda/api/sharding/DefaultShardManagerBuilder.java index ad04a17113..0963639417 100644 --- a/src/main/java/net/dv8tion/jda/api/sharding/DefaultShardManagerBuilder.java +++ b/src/main/java/net/dv8tion/jda/api/sharding/DefaultShardManagerBuilder.java @@ -65,6 +65,7 @@ public class DefaultShardManagerBuilder protected int shardsTotal = -1; protected int maxReconnectDelay = 900; protected int largeThreshold = 250; + protected int maxBufferSize = 2048; protected String token = null; protected IntFunction idleProvider = null; protected IntFunction statusProvider = null; @@ -1278,6 +1279,30 @@ public DefaultShardManagerBuilder setLargeThreshold(int threshold) return this; } + /** + * The maximum size, in bytes, of the buffer used for decompressing discord payloads. + *
If the maximum buffer size is exceeded a new buffer will be allocated instead. + *
Setting this to {@link Integer#MAX_VALUE} would imply the buffer will never be resized unless memory starvation is imminent. + *
Setting this to {@code 0} would imply the buffer would need to be allocated again for every payload (not recommended). + * + *

Default: {@code 2048} + * + * @param bufferSize + * The maximum size the buffer should allow to retain + * + * @throws IllegalArgumentException + * If the provided buffer size is negative + * + * @return The DefaultShardManagerBuilder instance. Useful for chaining. + */ + @Nonnull + public DefaultShardManagerBuilder setMaxBufferSize(int bufferSize) + { + Checks.notNegative(bufferSize, "The buffer size"); + this.maxBufferSize = bufferSize; + return this; + } + /** * Builds a new {@link net.dv8tion.jda.api.sharding.ShardManager ShardManager} instance and uses the provided token to start the login process. *
The login process runs in a different thread, so while this will return immediately, {@link net.dv8tion.jda.api.sharding.ShardManager ShardManager} has not @@ -1309,7 +1334,7 @@ public ShardManager build() throws LoginException, IllegalArgumentException presenceConfig.setIdleProvider(idleProvider); final ThreadingProviderConfig threadingConfig = new ThreadingProviderConfig(rateLimitPoolProvider, gatewayPoolProvider, callbackPoolProvider, threadFactory); final ShardingSessionConfig sessionConfig = new ShardingSessionConfig(sessionController, voiceDispatchInterceptor, httpClient, httpClientBuilder, wsFactory, audioSendFactory, flags, shardingFlags, maxReconnectDelay, largeThreshold); - final ShardingMetaConfig metaConfig = new ShardingMetaConfig(contextProvider, cacheFlags, flags, compression); + final ShardingMetaConfig metaConfig = new ShardingMetaConfig(maxBufferSize, contextProvider, cacheFlags, flags, compression); final DefaultShardManager manager = new DefaultShardManager(this.token, this.shards, shardingConfig, eventConfig, presenceConfig, threadingConfig, sessionConfig, metaConfig, chunkingFilter); manager.login(); diff --git a/src/main/java/net/dv8tion/jda/api/sharding/ShardManager.java b/src/main/java/net/dv8tion/jda/api/sharding/ShardManager.java index f9b68e4d47..2baef2a98d 100644 --- a/src/main/java/net/dv8tion/jda/api/sharding/ShardManager.java +++ b/src/main/java/net/dv8tion/jda/api/sharding/ShardManager.java @@ -27,7 +27,8 @@ import net.dv8tion.jda.api.utils.cache.ShardCacheView; import net.dv8tion.jda.api.utils.cache.SnowflakeCacheView; import net.dv8tion.jda.internal.JDAImpl; -import net.dv8tion.jda.internal.requests.EmptyRestAction; +import net.dv8tion.jda.internal.requests.CompletedRestAction; +import net.dv8tion.jda.internal.requests.DeferredRestAction; import net.dv8tion.jda.internal.requests.RestActionImpl; import net.dv8tion.jda.internal.requests.Route; import net.dv8tion.jda.internal.utils.Checks; @@ -556,7 +557,7 @@ default RestAction retrieveUserById(long id) api = shard; User user = shard.getUserById(id); if (user != null) - return new EmptyRestAction<>(shard, user); + return new CompletedRestAction<>(shard, user); } if (api == null) diff --git a/src/main/java/net/dv8tion/jda/api/utils/SessionControllerAdapter.java b/src/main/java/net/dv8tion/jda/api/utils/SessionControllerAdapter.java index 56db009371..ba055c1721 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/SessionControllerAdapter.java +++ b/src/main/java/net/dv8tion/jda/api/utils/SessionControllerAdapter.java @@ -109,7 +109,7 @@ public void handleResponse(Response response, Request request) } else if (response.code == 401) { - api.get().verifyToken(true); + api.verifyToken(true); } else { diff --git a/src/main/java/net/dv8tion/jda/internal/JDAImpl.java b/src/main/java/net/dv8tion/jda/internal/JDAImpl.java index 7c8b3c9104..32b9e8fe1d 100644 --- a/src/main/java/net/dv8tion/jda/internal/JDAImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/JDAImpl.java @@ -62,7 +62,6 @@ import net.dv8tion.jda.internal.utils.UnlockHook; import net.dv8tion.jda.internal.utils.cache.AbstractCacheView; import net.dv8tion.jda.internal.utils.cache.SnowflakeCacheViewImpl; -import net.dv8tion.jda.internal.utils.cache.UpstreamReference; import net.dv8tion.jda.internal.utils.config.AuthorizationConfig; import net.dv8tion.jda.internal.utils.config.MetaConfig; import net.dv8tion.jda.internal.utils.config.SessionConfig; @@ -111,7 +110,7 @@ public class JDAImpl implements JDA protected final SessionConfig sessionConfig; protected final MetaConfig metaConfig; - protected UpstreamReference client; + protected WebSocketClient client; protected Requester requester; protected IAudioSendFactory audioSendFactory = new DefaultSendFactory(); protected Status status = Status.INITIALIZING; @@ -177,6 +176,11 @@ public int getLargeThreshold() return sessionConfig.getLargeThreshold(); } + public int getMaxBufferSize() + { + return metaConfig.getMaxBufferSize(); + } + public boolean chunkGuild(long id) { try @@ -224,6 +228,7 @@ public int login(String gatewayUrl, ShardInfo shardInfo, Compression compression { this.shardInfo = shardInfo; threadConfig.init(this::getIdentifierString); + requester.getRateLimiter().init(); this.gatewayUrl = gatewayUrl == null ? getGateway() : gatewayUrl; Checks.notNull(this.gatewayUrl, "Gateway URL"); @@ -253,7 +258,7 @@ public int login(String gatewayUrl, ShardInfo shardInfo, Compression compression LOG.info("Login Successful!"); } - client = new UpstreamReference<>(new WebSocketClient(this, compression)); + client = new WebSocketClient(this, compression); // remove our MDC metadata when we exit our code if (previousContext != null) previousContext.forEach(MDC::put); @@ -503,6 +508,7 @@ public ExecutorService getCallbackPool() @Nonnull @Override + @SuppressWarnings("ConstantConditions") // this can't really happen unless you pass bad configs public OkHttpClient getHttpClient() { return sessionConfig.getHttpClient(); @@ -547,16 +553,11 @@ public RestAction retrieveUserById(@Nonnull String id) public RestAction retrieveUserById(long id) { AccountTypeException.check(getAccountType(), AccountType.BOT); - - // check cache - User user = this.getUserById(id); - // If guild subscriptions are disabled this user might not be up-to-date - if (user != null && isGuildSubscriptions()) - return new EmptyRestAction<>(this, user); - - Route.CompiledRoute route = Route.Users.GET_USER.compile(Long.toUnsignedString(id)); - return new RestActionImpl<>(this, route, - (response, request) -> getEntityBuilder().createFakeUser(response.getObject(), false)); + return new DeferredRestAction<>(this, User.class, () -> getUserById(id), () -> { + Route.CompiledRoute route = Route.Users.GET_USER.compile(Long.toUnsignedString(id)); + return new RestActionImpl<>(this, route, + (response, request) -> getEntityBuilder().createFakeUser(response.getObject(), false)); + }); } @Nonnull @@ -583,6 +584,12 @@ public Set getUnavailableGuilds() return copy; } + @Override + public boolean isUnavailable(long guildId) + { + return guildSetupController.isUnavailable(guildId); + } + @Nonnull @Override public SnowflakeCacheView getRoleCache() @@ -906,7 +913,7 @@ public WebSocketFactory getWebSocketFactory() public WebSocketClient getClient() { - return client == null ? null : client.get(); + return client; } public SnowflakeCacheViewImpl getUsersView() diff --git a/src/main/java/net/dv8tion/jda/internal/audio/AudioConnection.java b/src/main/java/net/dv8tion/jda/internal/audio/AudioConnection.java index fcdbaf0a05..8a20f7f946 100644 --- a/src/main/java/net/dv8tion/jda/internal/audio/AudioConnection.java +++ b/src/main/java/net/dv8tion/jda/internal/audio/AudioConnection.java @@ -37,7 +37,7 @@ import net.dv8tion.jda.internal.managers.AudioManagerImpl; import net.dv8tion.jda.internal.utils.IOUtil; import net.dv8tion.jda.internal.utils.JDALogger; -import net.dv8tion.jda.internal.utils.cache.UpstreamReference; +import net.dv8tion.jda.internal.utils.cache.SnowflakeReference; import org.slf4j.Logger; import tomp2p.opuswrapper.Opus; @@ -68,9 +68,9 @@ public class AudioConnection private final HashMap> combinedQueue = new HashMap<>(); private final String threadIdentifier; private final AudioWebSocket webSocket; - private final UpstreamReference api; + private final JDAImpl api; - private UpstreamReference channel; + private SnowflakeReference channel; private PointerByReference opusEncoder; private ScheduledExecutorService combinedAudioExecutor; private IAudioSendSystem sendSystem; @@ -89,10 +89,11 @@ public class AudioConnection public AudioConnection(AudioManagerImpl manager, String endpoint, String sessionId, String token) { - VoiceChannel channel = manager.getQueuedAudioConnection(); - this.channel = new UpstreamReference<>(channel); + VoiceChannel channel = Objects.requireNonNull(manager.getQueuedAudioConnection(), "Failed to create AudioConnection without queued channel!"); + + this.api = (JDAImpl) channel.getJDA(); + this.channel = new SnowflakeReference<>(channel, api::getVoiceChannelById); final JDAImpl api = (JDAImpl) channel.getJDA(); - this.api = new UpstreamReference<>(api); this.threadIdentifier = api.getIdentifierString() + " AudioConnection Guild: " + channel.getGuild().getId(); this.webSocket = new AudioWebSocket(this, manager.getListenerProxy(), endpoint, channel.getGuild(), sessionId, token, manager.isAutoReconnect()); } @@ -148,17 +149,17 @@ public void setQueueTimeout(long queueTimeout) public VoiceChannel getChannel() { - return channel.get(); + return channel.resolve(); } public void setChannel(VoiceChannel channel) { - this.channel = channel == null ? null : new UpstreamReference<>(channel); + this.channel = channel == null ? null : new SnowflakeReference<>(channel, api::getVoiceChannelById); } public JDAImpl getJDA() { - return api.get(); + return api; } public Guild getGuild() @@ -278,7 +279,7 @@ protected void updateUserSSRC(int ssrc, long userId) //Different User already existed with this ssrc. What should we do? Just replace? Probably should nuke the old opusDecoder. //Log for now and see if any user report the error. LOG.error("Yeah.. So.. JDA received a UserSSRC update for an ssrc that already had a User set. Inform DV8FromTheWorld.\nChannelId: {} SSRC: {} oldId: {} newId: {}", - channel.get().getId(), ssrc, previousId, userId); + channel.resolve().getId(), ssrc, previousId, userId); } } else diff --git a/src/main/java/net/dv8tion/jda/internal/entities/AbstractChannelImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/AbstractChannelImpl.java index 9b49ed253b..1c10bc3f29 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/AbstractChannelImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/AbstractChannelImpl.java @@ -37,7 +37,7 @@ import net.dv8tion.jda.internal.requests.restaction.InviteActionImpl; import net.dv8tion.jda.internal.requests.restaction.PermissionOverrideActionImpl; import net.dv8tion.jda.internal.utils.Checks; -import net.dv8tion.jda.internal.utils.cache.UpstreamReference; +import net.dv8tion.jda.internal.utils.cache.SnowflakeReference; import javax.annotation.Nonnull; import java.util.ArrayList; @@ -50,7 +50,8 @@ public abstract class AbstractChannelImpl> implements GuildChannel { protected final long id; - protected final UpstreamReference guild; + protected final SnowflakeReference guild; + protected final JDAImpl api; protected final TLongObjectMap overrides = MiscUtil.newLongMap(); @@ -64,7 +65,8 @@ public abstract class AbstractChannelImpl(guild); + this.api = guild.getJDA(); + this.guild = new SnowflakeReference<>(guild, api::getGuildById); } @Override @@ -100,7 +102,7 @@ public String getName() @Override public GuildImpl getGuild() { - return guild.get(); + return (GuildImpl) guild.resolve(); } @Override @@ -119,7 +121,7 @@ public int getPositionRaw() @Override public JDA getJDA() { - return getGuild().getJDA(); + return api; } @Override diff --git a/src/main/java/net/dv8tion/jda/internal/entities/AbstractMessage.java b/src/main/java/net/dv8tion/jda/internal/entities/AbstractMessage.java index e445669fa1..66c6ef9907 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/AbstractMessage.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/AbstractMessage.java @@ -29,6 +29,7 @@ import java.io.IOException; import java.io.UncheckedIOException; import java.time.OffsetDateTime; +import java.util.EnumSet; import java.util.FormattableFlags; import java.util.Formatter; import java.util.List; @@ -517,6 +518,29 @@ public MessageReaction.ReactionEmote getReactionById(long id) return null; } + @Nonnull + @Override + public AuditableRestAction suppressEmbeds(boolean suppressed) + { + unsupported(); + return null; + } + + @Override + public boolean isSuppressedEmbeds() + { + unsupported(); + return false; + } + + @Nonnull + @Override + public EnumSet getFlags() + { + unsupported(); + return null; + } + @Nonnull @Override public MessageType getType() diff --git a/src/main/java/net/dv8tion/jda/internal/entities/CategoryImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/CategoryImpl.java index cabe313b3b..b1db334dea 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/CategoryImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/CategoryImpl.java @@ -16,14 +16,12 @@ package net.dv8tion.jda.internal.entities; -import gnu.trove.map.TLongObjectMap; import net.dv8tion.jda.api.entities.*; import net.dv8tion.jda.api.requests.RestAction; import net.dv8tion.jda.api.requests.restaction.ChannelAction; import net.dv8tion.jda.api.requests.restaction.InviteAction; import net.dv8tion.jda.api.requests.restaction.order.CategoryOrderAction; -import net.dv8tion.jda.api.utils.MiscUtil; -import net.dv8tion.jda.internal.requests.EmptyRestAction; +import net.dv8tion.jda.internal.requests.CompletedRestAction; import net.dv8tion.jda.internal.utils.Checks; import javax.annotation.Nonnull; @@ -34,8 +32,6 @@ public class CategoryImpl extends AbstractChannelImpl implements Category { - protected final TLongObjectMap channels = MiscUtil.newLongMap(); - public CategoryImpl(long id, GuildImpl guild) { super(id, guild); @@ -80,7 +76,7 @@ public int getPosition() List channels = getGuild().getCategories(); for (int i = 0; i < channels.size(); i++) { - if (channels.get(i) == this) + if (equals(channels.get(i))) return i; } throw new AssertionError("Somehow when determining position we never found the Category in the Guild's channels? wtf?"); @@ -116,7 +112,7 @@ public InviteAction createInvite() @Override public RestAction> retrieveInvites() { - return new EmptyRestAction<>(getJDA(), Collections.emptyList()); + return new CompletedRestAction<>(getJDA(), Collections.emptyList()); } @Nonnull diff --git a/src/main/java/net/dv8tion/jda/internal/entities/EmoteImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/EmoteImpl.java index 58a6d20f00..5d216ed37d 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/EmoteImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/EmoteImpl.java @@ -17,6 +17,7 @@ package net.dv8tion.jda.internal.entities; import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.ListedEmote; import net.dv8tion.jda.api.entities.Role; import net.dv8tion.jda.api.entities.User; @@ -28,7 +29,7 @@ import net.dv8tion.jda.internal.managers.EmoteManagerImpl; import net.dv8tion.jda.internal.requests.Route; import net.dv8tion.jda.internal.requests.restaction.AuditableRestActionImpl; -import net.dv8tion.jda.internal.utils.cache.UpstreamReference; +import net.dv8tion.jda.internal.utils.cache.SnowflakeReference; import javax.annotation.Nonnull; import java.util.Collections; @@ -46,8 +47,8 @@ public class EmoteImpl implements ListedEmote { private final long id; - private final UpstreamReference guild; - private final UpstreamReference api; + private final SnowflakeReference guild; + private final JDAImpl api; private final Set roles; private final boolean fake; @@ -67,8 +68,8 @@ public EmoteImpl(long id, GuildImpl guild) public EmoteImpl(long id, GuildImpl guild, boolean fake) { this.id = id; - this.guild = new UpstreamReference<>(guild); - this.api = new UpstreamReference<>(guild.getJDA()); + this.api = guild.getJDA(); + this.guild = new SnowflakeReference<>(guild, api::getGuildById); this.roles = ConcurrentHashMap.newKeySet(); this.fake = fake; } @@ -76,7 +77,7 @@ public EmoteImpl(long id, GuildImpl guild, boolean fake) public EmoteImpl(long id, JDAImpl api) { this.id = id; - this.api = new UpstreamReference<>(api); + this.api = api; this.guild = null; this.roles = null; this.fake = true; @@ -85,7 +86,7 @@ public EmoteImpl(long id, JDAImpl api) @Override public GuildImpl getGuild() { - return guild == null ? null : guild.get(); + return guild == null ? null : (GuildImpl) guild.resolve(); } @Nonnull @@ -132,7 +133,7 @@ public long getIdLong() @Override public JDAImpl getJDA() { - return api.get(); + return api; } @Nonnull @@ -255,6 +256,5 @@ public EmoteImpl clone() EmoteImpl copy = new EmoteImpl(id, getGuild()).setUser(user).setManaged(managed).setAnimated(animated).setName(name); copy.roles.addAll(roles); return copy; - } } diff --git a/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java b/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java index fb6dbc0094..797d3869b0 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java @@ -44,7 +44,6 @@ import net.dv8tion.jda.internal.utils.UnlockHook; import net.dv8tion.jda.internal.utils.cache.MemberCacheViewImpl; import net.dv8tion.jda.internal.utils.cache.SnowflakeCacheViewImpl; -import net.dv8tion.jda.internal.utils.cache.UpstreamReference; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.map.CaseInsensitiveMap; import org.slf4j.Logger; @@ -80,16 +79,16 @@ public class EntityBuilder richGameFields = Collections.unmodifiableSet(tmp); } - protected final UpstreamReference api; + protected final JDAImpl api; public EntityBuilder(JDA api) { - this.api = new UpstreamReference<>((JDAImpl) api); + this.api = (JDAImpl) api; } public JDAImpl getJDA() { - return api.get(); + return api; } public SelfUser createSelfUser(DataObject self) @@ -204,6 +203,12 @@ public GuildImpl createGuild(long guildId, DataObject guildJson, TLongObjectMap< .setBoostTier(boostTier) .setMemberCount(memberCount); + SnowflakeCacheViewImpl guildView = getJDA().getGuildsView(); + try (UnlockHook hook = guildView.writeLock()) + { + guildView.getMap().put(guildId, guildObj); + } + guildObj.setFeatures(featuresArray.map(it -> StreamSupport.stream(it.spliterator(), false) .map(String::valueOf) @@ -272,11 +277,6 @@ public GuildImpl createGuild(long guildId, DataObject guildJson, TLongObjectMap< } }); - SnowflakeCacheViewImpl guildView = getJDA().getGuildsView(); - try (UnlockHook hook = guildView.writeLock()) - { - guildView.getMap().put(guildId, guildObj); - } return guildObj; } @@ -326,7 +326,7 @@ public void createGuildVoiceStatePass(GuildImpl guildObj, DataArray voiceStates) VoiceChannelImpl voiceChannel = (VoiceChannelImpl) guildObj.getVoiceChannelsView().get(channelId); if (voiceChannel != null) - voiceChannel.getConnectedMembersMap().put(member.getUser().getIdLong(), member); + voiceChannel.getConnectedMembersMap().put(member.getIdLong(), member); else LOG.error("Received a GuildVoiceState with a channel ID for a non-existent channel! ChannelId: {} GuildId: {} UserId: {}", channelId, guildObj.getId(), userId); @@ -974,11 +974,18 @@ public VoiceChannel createVoiceChannel(GuildImpl guild, DataObject json, long gu public PrivateChannel createPrivateChannel(DataObject json) { + final long channelId = json.getUnsignedLong("id"); + PrivateChannel channel = api.getPrivateChannelById(channelId); + if (channel == null) + channel = api.getFakePrivateChannelMap().get(channelId); + if (channel != null) + return channel; + DataObject recipient = json.hasKey("recipients") ? json.getArray("recipients").getObject(0) : json.getObject("recipient"); final long userId = recipient.getLong("id"); - UserImpl user = (UserImpl) getJDA().getUsersView().get(userId); + UserImpl user = (UserImpl) getJDA().getUserById(userId); if (user == null) { //The getJDA() can give us private channels connected to Users that we can no longer communicate with. // As such, make a fake user and fake private channel. @@ -1002,7 +1009,7 @@ public PrivateChannel createPrivateChannel(DataObject json, UserImpl user) getJDA().getFakePrivateChannelMap().put(channelId, priv); getJDA().getFakeUserMap().put(user.getIdLong(), user); } - else + else if (api.isGuildSubscriptions()) { SnowflakeCacheViewImpl privateView = getJDA().getPrivateChannelsView(); try (UnlockHook hook = privateView.writeLock()) @@ -1113,6 +1120,7 @@ public Message createMessage(DataObject jsonObject, MessageChannel chan, boolean final boolean mentionsEveryone = jsonObject.getBoolean("mention_everyone"); final OffsetDateTime editTime = jsonObject.isNull("edited_timestamp") ? null : OffsetDateTime.parse(jsonObject.getString("edited_timestamp")); final String nonce = jsonObject.isNull("nonce") ? null : jsonObject.get("nonce").toString(); + final int flags = jsonObject.getInt("flags", 0); final List attachments = map(jsonObject, "attachments", this::createMessageAttachment); final List embeds = map(jsonObject, "embeds", this::createMessageEmbed); @@ -1169,14 +1177,14 @@ public Message createMessage(DataObject jsonObject, MessageChannel chan, boolean case DEFAULT: message = new ReceivedMessage(id, chan, type, fromWebhook, mentionsEveryone, mentionedUsers, mentionedRoles, tts, pinned, - content, nonce, user, member, activity, editTime, reactions, attachments, embeds); + content, nonce, user, member, activity, editTime, reactions, attachments, embeds, flags); break; case UNKNOWN: throw new IllegalArgumentException(UNKNOWN_MESSAGE_TYPE); default: message = new SystemMessage(id, chan, type, fromWebhook, mentionsEveryone, mentionedUsers, mentionedRoles, tts, pinned, - content, nonce, user, member, activity, editTime, reactions, attachments, embeds); + content, nonce, user, member, activity, editTime, reactions, attachments, embeds, flags); break; } @@ -1430,7 +1438,7 @@ public PermissionOverride createPermissionOverride(DataObject override, Abstract PermissionOverrideImpl permOverride = (PermissionOverrideImpl) chan.getPermissionOverride(permHolder); if (permOverride == null) { - permOverride = new PermissionOverrideImpl(chan, permHolder.getIdLong(), permHolder); + permOverride = new PermissionOverrideImpl(chan, permHolder); chan.getOverrideMap().put(permHolder.getIdLong(), permOverride); } return permOverride.setAllow(allow).setDeny(deny); diff --git a/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java index 7c24b8823a..17e372a264 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java @@ -56,7 +56,10 @@ import net.dv8tion.jda.internal.requests.restaction.order.RoleOrderActionImpl; import net.dv8tion.jda.internal.requests.restaction.pagination.AuditLogPaginationActionImpl; import net.dv8tion.jda.internal.utils.*; -import net.dv8tion.jda.internal.utils.cache.*; +import net.dv8tion.jda.internal.utils.cache.AbstractCacheView; +import net.dv8tion.jda.internal.utils.cache.MemberCacheViewImpl; +import net.dv8tion.jda.internal.utils.cache.SnowflakeCacheViewImpl; +import net.dv8tion.jda.internal.utils.cache.SortedSnowflakeCacheViewImpl; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -73,7 +76,7 @@ public class GuildImpl implements Guild { private final long id; - private final UpstreamReference api; + private final JDAImpl api; private final SortedSnowflakeCacheViewImpl categoryCache = new SortedSnowflakeCacheViewImpl<>(Category.class, GuildChannel::getName, Comparator.naturalOrder()); private final SortedSnowflakeCacheViewImpl voiceChannelCache = new SortedSnowflakeCacheViewImpl<>(VoiceChannel.class, GuildChannel::getName, Comparator.naturalOrder()); @@ -116,7 +119,7 @@ public class GuildImpl implements Guild public GuildImpl(JDAImpl api, long id) { this.id = id; - this.api = new UpstreamReference<>(api); + this.api = api; } @Nonnull @@ -292,7 +295,7 @@ public RestAction> retrieveWebhooks() { DataArray array = response.getArray(); List webhooks = new ArrayList<>(array.length()); - EntityBuilder builder = api.get().getEntityBuilder(); + EntityBuilder builder = api.getEntityBuilder(); for (int i = 0; i < array.length(); i++) { @@ -497,18 +500,25 @@ public RestAction> retrieveEmotes() public RestAction retrieveEmoteById(@Nonnull String id) { Checks.isSnowflake(id, "Emote ID"); - Emote emote = getEmoteById(id); - if (emote != null) - { - ListedEmote listedEmote = (ListedEmote) emote; - if (listedEmote.hasUser() || !getSelfMember().hasPermission(Permission.MANAGE_EMOTES)) - return new EmptyRestAction<>(getJDA(), listedEmote); - } - Route.CompiledRoute route = Route.Emotes.GET_EMOTE.compile(getId(), id); - return new RestActionImpl<>(getJDA(), route, (response, request) -> - { - EntityBuilder builder = GuildImpl.this.getJDA().getEntityBuilder(); - return builder.createEmote(GuildImpl.this, response.getObject(), true); + + JDAImpl jda = getJDA(); + return new DeferredRestAction<>(jda, ListedEmote.class, + () -> { + Emote emote = getEmoteById(id); + if (emote != null) + { + ListedEmote listedEmote = (ListedEmote) emote; + if (listedEmote.hasUser() || !getSelfMember().hasPermission(Permission.MANAGE_EMOTES)) + return listedEmote; + } + return null; + }, () -> { + Route.CompiledRoute route = Route.Emotes.GET_EMOTE.compile(getId(), id); + return new AuditableRestActionImpl<>(jda, route, (response, request) -> + { + EntityBuilder builder = GuildImpl.this.getJDA().getEntityBuilder(); + return builder.createEmote(GuildImpl.this, response.getObject(), true); + }); }); } @@ -522,7 +532,7 @@ public RestActionImpl> retrieveBanList() Route.CompiledRoute route = Route.Guilds.GET_BANS.compile(getId()); return new RestActionImpl<>(getJDA(), route, (response, request) -> { - EntityBuilder builder = api.get().getEntityBuilder(); + EntityBuilder builder = api.getEntityBuilder(); List bans = new LinkedList<>(); DataArray bannedArr = response.getArray(); @@ -549,7 +559,7 @@ public RestAction retrieveBanById(@Nonnull String userId) return new RestActionImpl<>(getJDA(), route, (response, request) -> { - EntityBuilder builder = api.get().getEntityBuilder(); + EntityBuilder builder = api.getEntityBuilder(); DataObject bannedObj = response.getObject(); DataObject user = bannedObj.getObject("user"); return new Ban(builder.createFakeUser(user, false), bannedObj.getString("reason", null)); @@ -678,7 +688,7 @@ public AudioManager getAudioManager() @Override public JDAImpl getJDA() { - return api.get(); + return api; } @Nonnull @@ -772,13 +782,12 @@ public CompletableFuture retrieveMembers() @Override public RestAction retrieveMemberById(long id) { - Member member = getMemberById(id); - if (member != null) - return new EmptyRestAction<>(getJDA(), member); - - Route.CompiledRoute route = Route.Guilds.GET_MEMBER.compile(getId(), Long.toUnsignedString(id)); - return new RestActionImpl<>(getJDA(), route, (resp, req) -> - getJDA().getEntityBuilder().createMember(this, resp.getObject())); + JDAImpl jda = getJDA(); + return new DeferredRestAction<>(jda, Member.class, () -> getMemberById(id), () -> { + Route.CompiledRoute route = Route.Guilds.GET_MEMBER.compile(getId(), Long.toUnsignedString(id)); + return new RestActionImpl<>(jda, route, (resp, req) -> + jda.getEntityBuilder().createMember(this, resp.getObject())); + }); } @Override @@ -798,7 +807,7 @@ public RestAction> retrieveInvites() return new RestActionImpl<>(getJDA(), route, (response, request) -> { - EntityBuilder entityBuilder = api.get().getEntityBuilder(); + EntityBuilder entityBuilder = api.getEntityBuilder(); DataArray array = response.getArray(); List invites = new ArrayList<>(array.length()); for (int i = 0; i < array.length(); i++) @@ -856,21 +865,18 @@ public AuditableRestAction modifyNickname(@Nonnull Member member, String n checkPosition(member); } - if (Objects.equals(nickname, member.getNickname())) - return new EmptyRestAction<>(getJDA(), null); - - if (nickname == null) - nickname = ""; - - DataObject body = DataObject.empty().put("nick", nickname); + JDAImpl jda = getJDA(); + return new DeferredRestAction<>(jda, () -> { + DataObject body = DataObject.empty().put("nick", nickname == null ? "" : nickname); - Route.CompiledRoute route; - if (member.equals(getSelfMember())) - route = Route.Guilds.MODIFY_SELF_NICK.compile(getId()); - else - route = Route.Guilds.MODIFY_MEMBER.compile(getId(), member.getUser().getId()); + Route.CompiledRoute route; + if (member.equals(getSelfMember())) + route = Route.Guilds.MODIFY_SELF_NICK.compile(getId()); + else + route = Route.Guilds.MODIFY_MEMBER.compile(getId(), member.getUser().getId()); - return new AuditableRestActionImpl<>(getJDA(), route, body); + return new AuditableRestActionImpl(jda, route, body); + }).setCacheCheck(() -> !Objects.equals(nickname, member.getNickname())); } @Nonnull @@ -914,7 +920,7 @@ private AuditableRestAction kick0(@Nonnull String userId, @Nullable String { Route.CompiledRoute route = Route.Guilds.KICK_MEMBER.compile(getId(), userId); if (!Helpers.isBlank(reason)) - route.withQueryParams("reason", EncodingUtil.encodeUTF8(reason)); + route = route.withQueryParams("reason", EncodingUtil.encodeUTF8(reason)); return new AuditableRestActionImpl<>(getJDA(), route); } @@ -928,19 +934,7 @@ public AuditableRestAction ban(@Nonnull User user, int delDays, String rea if (isMember(user)) // If user is in guild. Check if we are able to ban. checkPosition(getMember(user)); - Checks.notNegative(delDays, "Deletion Days"); - - Checks.check(delDays <= 7, "Deletion Days must not be bigger than 7."); - - final String userId = user.getId(); - - Route.CompiledRoute route = Route.Guilds.BAN.compile(getId(), userId); - if (reason != null && !reason.isEmpty()) - route = route.withQueryParams("reason", EncodingUtil.encodeUTF8(reason)); - if (delDays > 0) - route = route.withQueryParams("delete-message-days", Integer.toString(delDays)); - - return new AuditableRestActionImpl<>(getJDA(), route); + return ban0(user.getId(), delDays, reason); } @Nonnull @@ -954,12 +948,17 @@ public AuditableRestAction ban(@Nonnull String userId, int delDays, String if (user != null) // If we have the user cached then we should use the additional information available to use during the ban process. return ban(user, delDays, reason); - Checks.notNegative(delDays, "Deletion Days"); + return ban0(userId, delDays, reason); + } + @Nonnull + private AuditableRestAction ban0(@Nonnull String userId, int delDays, String reason) + { + Checks.notNegative(delDays, "Deletion Days"); Checks.check(delDays <= 7, "Deletion Days must not be bigger than 7."); Route.CompiledRoute route = Route.Guilds.BAN.compile(getId(), userId); - if (reason != null && !reason.isEmpty()) + if (!Helpers.isBlank(reason)) route = route.withQueryParams("reason", EncodingUtil.encodeUTF8(reason)); if (delDays > 0) route = route.withQueryParams("delete-message-days", Integer.toString(delDays)); @@ -992,7 +991,7 @@ public AuditableRestAction deafen(@Nonnull Member member, boolean deafen) if (voiceState.getChannel() == null) throw new IllegalStateException("Can only deafen members who are currently in a voice channel"); if (voiceState.isGuildDeafened() == deafen) - return new EmptyRestAction<>(getJDA(), null); + return new CompletedRestAction<>(getJDA(), null); } DataObject body = DataObject.empty().put("deaf", deafen); @@ -1014,7 +1013,7 @@ public AuditableRestAction mute(@Nonnull Member member, boolean mute) if (voiceState.getChannel() == null) throw new IllegalStateException("Can only mute members who are currently in a voice channel"); if (voiceState.isGuildMuted() == mute) - return new EmptyRestAction<>(getJDA(), null); + return new CompletedRestAction<>(getJDA(), null); } DataObject body = DataObject.empty().put("mute", mute); @@ -1094,7 +1093,7 @@ public AuditableRestAction modifyMemberRoles(@Nonnull Member member, @Nonn // Return an empty rest action if there were no changes final List memberRoles = member.getRoles(); if (Helpers.deepEqualsUnordered(roles, memberRoles)) - return new EmptyRestAction<>(getJDA()); + return new CompletedRestAction<>(getJDA(), null); // Check removed roles for (Role r : memberRoles) @@ -1492,9 +1491,9 @@ public TLongObjectMap removeOverrideMap(long userId) public void pruneChannelOverrides(long channelId) { WebSocketClient.LOG.debug("Pruning cached overrides for channel with id {}", channelId); - overrideMap.transformValues((value) -> { + overrideMap.retainEntries((key, value) -> { DataObject removed = value.remove(channelId); - return value.isEmpty() ? null : value; + return !value.isEmpty(); }); } diff --git a/src/main/java/net/dv8tion/jda/internal/entities/GuildVoiceStateImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/GuildVoiceStateImpl.java index dfc541a842..bc25101bca 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/GuildVoiceStateImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/GuildVoiceStateImpl.java @@ -21,13 +21,15 @@ import net.dv8tion.jda.api.entities.GuildVoiceState; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.VoiceChannel; -import net.dv8tion.jda.internal.utils.cache.UpstreamReference; +import net.dv8tion.jda.internal.utils.cache.SnowflakeReference; import javax.annotation.Nonnull; public class GuildVoiceStateImpl implements GuildVoiceState { - private final UpstreamReference member; + private final SnowflakeReference guild; + private final SnowflakeReference member; + private final JDA api; private VoiceChannel connectedChannel; private String sessionId; @@ -39,7 +41,9 @@ public class GuildVoiceStateImpl implements GuildVoiceState public GuildVoiceStateImpl(Member member) { - this.member = new UpstreamReference<>(member); + this.api = member.getJDA(); + this.guild = new SnowflakeReference<>(member.getGuild(), api::getGuildById); + this.member = new SnowflakeReference<>(member, (id) -> guild.resolve().getMemberById(id)); } @Override @@ -58,7 +62,7 @@ public boolean isSelfDeafened() @Override public JDA getJDA() { - return getGuild().getJDA(); + return api; } @Override @@ -107,14 +111,14 @@ public VoiceChannel getChannel() @Override public Guild getGuild() { - return getMember().getGuild(); + return this.guild.resolve(); } @Nonnull @Override public Member getMember() { - return member.get(); + return this.member.resolve(); } @Override diff --git a/src/main/java/net/dv8tion/jda/internal/entities/InviteImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/InviteImpl.java index 24b16c2726..25cda7b91e 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/InviteImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/InviteImpl.java @@ -28,7 +28,8 @@ 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.requests.EmptyRestAction; +import net.dv8tion.jda.internal.requests.CompletedRestAction; +import net.dv8tion.jda.internal.requests.DeferredRestAction; import net.dv8tion.jda.internal.requests.RestActionImpl; import net.dv8tion.jda.internal.requests.Route; import net.dv8tion.jda.internal.requests.restaction.AuditableRestActionImpl; @@ -103,7 +104,7 @@ public AuditableRestAction delete() public RestAction expand() { if (this.expanded) - return new EmptyRestAction<>(getJDA(), this); + return new CompletedRestAction<>(getJDA(), this); if (this.type != Invite.InviteType.GUILD) throw new IllegalStateException("Only guild invites can be expanded"); diff --git a/src/main/java/net/dv8tion/jda/internal/entities/MemberImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/MemberImpl.java index 0abe172fe9..61f198dcc9 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/MemberImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/MemberImpl.java @@ -24,7 +24,7 @@ import net.dv8tion.jda.internal.JDAImpl; import net.dv8tion.jda.internal.utils.Checks; import net.dv8tion.jda.internal.utils.PermissionUtil; -import net.dv8tion.jda.internal.utils.cache.UpstreamReference; +import net.dv8tion.jda.internal.utils.cache.SnowflakeReference; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -39,8 +39,9 @@ public class MemberImpl implements Member { private static final ZoneOffset OFFSET = ZoneOffset.of("+00:00"); - private final UpstreamReference guild; + private final SnowflakeReference guild; private final User user; + private final JDAImpl api; private final Set roles = ConcurrentHashMap.newKeySet(); private final GuildVoiceState voiceState; private final Map clientStatus; @@ -52,11 +53,11 @@ public class MemberImpl implements Member public MemberImpl(GuildImpl guild, User user) { - this.guild = new UpstreamReference<>(guild); + this.api = (JDAImpl) user.getJDA(); + this.guild = new SnowflakeReference<>(guild, api::getGuildById); this.user = user; - JDAImpl jda = (JDAImpl) getJDA(); - boolean cacheState = jda.isCacheFlagSet(CacheFlag.VOICE_STATE) || user.equals(jda.getSelfUser()); - boolean cacheOnline = jda.isCacheFlagSet(CacheFlag.CLIENT_STATUS); + boolean cacheState = api.isCacheFlagSet(CacheFlag.VOICE_STATE) || user.equals(api.getSelfUser()); + boolean cacheOnline = api.isCacheFlagSet(CacheFlag.CLIENT_STATUS); this.voiceState = cacheState ? new GuildVoiceStateImpl(this) : null; this.clientStatus = cacheOnline ? Collections.synchronizedMap(new EnumMap<>(ClientType.class)) : null; } @@ -72,14 +73,14 @@ public User getUser() @Override public GuildImpl getGuild() { - return guild.get(); + return (GuildImpl) guild.resolve(); } @Nonnull @Override public JDA getJDA() { - return user.getJDA(); + return api; } @Nonnull @@ -146,7 +147,7 @@ public String getNickname() @Override public String getEffectiveName() { - return nickname != null ? nickname : user.getName(); + return nickname != null ? nickname : getUser().getName(); } @Nonnull @@ -336,30 +337,31 @@ public boolean equals(Object o) { if (o == this) return true; - if (!(o instanceof Member)) + if (!(o instanceof MemberImpl)) return false; - Member oMember = (Member) o; - return oMember.getUser().equals(user) && oMember.getGuild().equals(getGuild()); + MemberImpl oMember = (MemberImpl) o; + return oMember.user.getIdLong() == user.getIdLong() + && oMember.guild.getIdLong() == guild.getIdLong(); } @Override public int hashCode() { - return (getGuild().getId() + user.getId()).hashCode(); + return (guild.getIdLong() + user.getId()).hashCode(); } @Override public String toString() { - return "MB:" + getEffectiveName() + '(' + user.toString() + " / " + getGuild().toString() +')'; + return "MB:" + getEffectiveName() + '(' + getUser().toString() + " / " + getGuild().toString() +')'; } @Nonnull @Override public String getAsMention() { - return nickname == null ? user.getAsMention() : "<@!" + user.getIdLong() + '>'; + return (nickname == null ? "<@" : "<@!") + user.getId() + '>'; } @Nullable diff --git a/src/main/java/net/dv8tion/jda/internal/entities/PermissionOverrideImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/PermissionOverrideImpl.java index 341d0619f6..8f8e82275c 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/PermissionOverrideImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/PermissionOverrideImpl.java @@ -23,20 +23,24 @@ import net.dv8tion.jda.api.requests.restaction.AuditableRestAction; import net.dv8tion.jda.api.requests.restaction.PermissionOverrideAction; import net.dv8tion.jda.api.utils.MiscUtil; +import net.dv8tion.jda.internal.JDAImpl; import net.dv8tion.jda.internal.requests.Route; import net.dv8tion.jda.internal.requests.restaction.AuditableRestActionImpl; import net.dv8tion.jda.internal.requests.restaction.PermissionOverrideActionImpl; -import net.dv8tion.jda.internal.utils.cache.UpstreamReference; +import net.dv8tion.jda.internal.utils.cache.SnowflakeReference; import javax.annotation.Nonnull; import java.util.EnumSet; +import java.util.Objects; import java.util.concurrent.locks.ReentrantLock; public class PermissionOverrideImpl implements PermissionOverride { private final long id; - private final UpstreamReference channel; - private final IPermissionHolder permissionHolder; + private final SnowflakeReference channel; + private final ChannelType channelType; + private final boolean role; + private final JDAImpl api; protected final ReentrantLock mngLock = new ReentrantLock(); protected volatile PermissionOverrideAction manager; @@ -44,11 +48,13 @@ public class PermissionOverrideImpl implements PermissionOverride private long allow; private long deny; - public PermissionOverrideImpl(GuildChannel channel, long id, IPermissionHolder permissionHolder) + public PermissionOverrideImpl(GuildChannel channel, IPermissionHolder permissionHolder) { - this.channel = new UpstreamReference<>(channel); - this.id = id; - this.permissionHolder = permissionHolder; + this.role = permissionHolder instanceof Role; + this.channelType = channel.getType(); + this.api = (JDAImpl) channel.getJDA(); + this.channel = new SnowflakeReference<>(channel, (channelId) -> api.getGuildChannelById(channelType, channelId)); + this.id = permissionHolder.getIdLong(); } @Override @@ -94,26 +100,26 @@ public EnumSet getDenied() @Override public JDA getJDA() { - return getChannel().getJDA(); + return api; } @Override public Member getMember() { - return isMemberOverride() ? (Member) permissionHolder : null; + return getGuild().getMemberById(id); } @Override public Role getRole() { - return isRoleOverride() ? (Role) permissionHolder : null; + return getGuild().getRoleById(id); } @Nonnull @Override public GuildChannel getChannel() { - return channel.get(); + return channel.resolve(); } @Nonnull @@ -126,13 +132,13 @@ public Guild getGuild() @Override public boolean isMemberOverride() { - return permissionHolder instanceof Member; + return !role; } @Override public boolean isRoleOverride() { - return permissionHolder instanceof Role; + return role; } @Nonnull @@ -161,12 +167,16 @@ public AuditableRestAction delete() if (!getGuild().getSelfMember().hasPermission(getChannel(), Permission.MANAGE_PERMISSIONS)) throw new InsufficientPermissionException(getChannel(), Permission.MANAGE_PERMISSIONS); - @SuppressWarnings("ConstantConditions") - String targetId = isRoleOverride() ? getRole().getId() : getMember().getUser().getId(); - Route.CompiledRoute route = Route.Channels.DELETE_PERM_OVERRIDE.compile(getChannel().getId(), targetId); + Route.CompiledRoute route = Route.Channels.DELETE_PERM_OVERRIDE.compile(channel.getId(), getId()); return new AuditableRestActionImpl<>(getJDA(), route); } + @Override + public long getIdLong() + { + return id; + } + public PermissionOverrideImpl setAllow(long allow) { this.allow = allow; @@ -187,19 +197,18 @@ public boolean equals(Object o) if (!(o instanceof PermissionOverrideImpl)) return false; PermissionOverrideImpl oPerm = (PermissionOverrideImpl) o; - return this.permissionHolder.equals(oPerm.permissionHolder) && this.getChannel().equals(oPerm.getChannel()); + return id == oPerm.id && this.channel.getIdLong() == oPerm.channel.getIdLong(); } @Override public int hashCode() { - return toString().hashCode(); + return Objects.hash(id, channel.getIdLong()); } @Override public String toString() { - return "PermOver:(" + (isMemberOverride() ? "M" : "R") + ")(" + getChannel().getId() + " | " + id + ")"; + return "PermOver:(" + (isMemberOverride() ? "M" : "R") + ")(" + channel.getId() + " | " + getId() + ")"; } - } diff --git a/src/main/java/net/dv8tion/jda/internal/entities/PrivateChannelImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/PrivateChannelImpl.java index 9e54fda720..23371db930 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/PrivateChannelImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/PrivateChannelImpl.java @@ -24,7 +24,6 @@ import net.dv8tion.jda.api.utils.AttachmentOption; import net.dv8tion.jda.internal.requests.RestActionImpl; import net.dv8tion.jda.internal.requests.Route; -import net.dv8tion.jda.internal.utils.cache.UpstreamReference; import javax.annotation.Nonnull; import java.io.InputStream; @@ -35,22 +34,21 @@ public class PrivateChannelImpl implements PrivateChannel { private final long id; - private final UpstreamReference user; - + private final User user; private long lastMessageId; private boolean fake = false; public PrivateChannelImpl(long id, User user) { this.id = id; - this.user = new UpstreamReference<>(user); + this.user = user; } @Nonnull @Override public User getUser() { - return user.get(); + return user; } @Override @@ -86,7 +84,7 @@ public ChannelType getType() @Override public JDA getJDA() { - return getUser().getJDA(); + return user.getJDA(); } @Nonnull diff --git a/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java b/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java index 45e4f831c1..ca9f8ac2cc 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java @@ -28,7 +28,10 @@ import net.dv8tion.jda.api.requests.restaction.pagination.ReactionPaginationAction; import net.dv8tion.jda.api.utils.MarkdownSanitizer; import net.dv8tion.jda.api.utils.MiscUtil; +import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.internal.JDAImpl; +import net.dv8tion.jda.internal.requests.Route; +import net.dv8tion.jda.internal.requests.restaction.AuditableRestActionImpl; import net.dv8tion.jda.internal.requests.restaction.pagination.ReactionPaginationActionImpl; import net.dv8tion.jda.internal.utils.Checks; import net.dv8tion.jda.internal.utils.EncodingUtil; @@ -64,6 +67,7 @@ public class ReceivedMessage extends AbstractMessage protected final List embeds; protected final TLongSet mentionedUsers; protected final TLongSet mentionedRoles; + protected final int flags; // LAZY EVALUATED protected String altContent = null; @@ -80,7 +84,7 @@ public ReceivedMessage( long id, MessageChannel channel, MessageType type, boolean fromWebhook, boolean mentionsEveryone, TLongSet mentionedUsers, TLongSet mentionedRoles, boolean tts, boolean pinned, String content, String nonce, User author, Member member, MessageActivity activity, OffsetDateTime editTime, - List reactions, List attachments, List embeds) + List reactions, List attachments, List embeds, int flags) { super(content, nonce, tts); this.id = id; @@ -99,6 +103,7 @@ public ReceivedMessage( this.embeds = Collections.unmodifiableList(embeds); this.mentionedUsers = mentionedUsers; this.mentionedRoles = mentionedRoles; + this.flags = flags; } @Nonnull @@ -795,6 +800,34 @@ else if (!getGuild().getSelfMember() return channel.deleteMessageById(getIdLong()); } + @Nonnull + @Override + public AuditableRestAction suppressEmbeds(boolean suppressed) + { + JDAImpl jda = (JDAImpl) getJDA(); + Route.CompiledRoute route = Route.Messages.EDIT_MESSAGE.compile(getChannel().getId(), getId()); + int newFlags = flags; + int suppressionValue = MessageFlag.EMBEDS_SUPPRESSED.getValue(); + if (suppressed) + newFlags |= suppressionValue; + else + newFlags &= ~suppressionValue; + return new AuditableRestActionImpl<>(jda, route, DataObject.empty().put("flags", newFlags)); + } + + @Override + public boolean isSuppressedEmbeds() + { + return (this.flags & MessageFlag.EMBEDS_SUPPRESSED.getValue()) > 0; + } + + @Nonnull + @Override + public EnumSet getFlags() + { + return MessageFlag.fromBitField(flags); + } + @Override public boolean equals(Object o) { diff --git a/src/main/java/net/dv8tion/jda/internal/entities/RoleImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/RoleImpl.java index cc60ce5226..14ae34083c 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/RoleImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/RoleImpl.java @@ -27,13 +27,14 @@ import net.dv8tion.jda.api.requests.restaction.AuditableRestAction; import net.dv8tion.jda.api.requests.restaction.RoleAction; import net.dv8tion.jda.api.utils.MiscUtil; +import net.dv8tion.jda.internal.JDAImpl; import net.dv8tion.jda.internal.managers.RoleManagerImpl; import net.dv8tion.jda.internal.requests.Route; import net.dv8tion.jda.internal.requests.restaction.AuditableRestActionImpl; import net.dv8tion.jda.internal.utils.Checks; import net.dv8tion.jda.internal.utils.PermissionUtil; import net.dv8tion.jda.internal.utils.cache.SortedSnowflakeCacheViewImpl; -import net.dv8tion.jda.internal.utils.cache.UpstreamReference; +import net.dv8tion.jda.internal.utils.cache.SnowflakeReference; import javax.annotation.Nonnull; import java.awt.*; @@ -45,7 +46,8 @@ public class RoleImpl implements Role { private final long id; - private final UpstreamReference guild; + private final SnowflakeReference guild; + private final JDAImpl api; private final ReentrantLock mngLock = new ReentrantLock(); private volatile RoleManager manager; @@ -61,20 +63,22 @@ public class RoleImpl implements Role public RoleImpl(long id, Guild guild) { this.id = id; - this.guild = new UpstreamReference<>(guild); + this.api =(JDAImpl) guild.getJDA(); + this.guild = new SnowflakeReference<>(guild, api::getGuildById); } @Override public int getPosition() { - if (this == getGuild().getPublicRole()) + Guild guild = getGuild(); + if (equals(guild.getPublicRole())) return -1; //Subtract 1 to get into 0-index, and 1 to disregard the everyone role. - int i = getGuild().getRoles().size() - 2; - for (Role r : getGuild().getRoles()) + int i = guild.getRoles().size() - 2; + for (Role r : guild.getRoles()) { - if (r == this) + if (equals(r)) return i; i--; } @@ -216,7 +220,7 @@ public boolean canInteract(@Nonnull Role role) @Override public Guild getGuild() { - return guild.get(); + return guild.resolve(); } @Nonnull @@ -253,22 +257,23 @@ public RoleManager getManager() @Override public AuditableRestAction delete() { - if (!getGuild().getSelfMember().hasPermission(Permission.MANAGE_ROLES)) - throw new InsufficientPermissionException(getGuild(), Permission.MANAGE_ROLES); - if(!PermissionUtil.canInteract(getGuild().getSelfMember(), this)) + Guild guild = getGuild(); + if (!guild.getSelfMember().hasPermission(Permission.MANAGE_ROLES)) + throw new InsufficientPermissionException(guild, Permission.MANAGE_ROLES); + if(!PermissionUtil.canInteract(guild.getSelfMember(), this)) throw new HierarchyException("Can't delete role >= highest self-role"); if (managed) throw new UnsupportedOperationException("Cannot delete a Role that is managed. "); - Route.CompiledRoute route = Route.Roles.DELETE_ROLE.compile(getGuild().getId(), getId()); - return new AuditableRestActionImpl(getJDA(), route); + Route.CompiledRoute route = Route.Roles.DELETE_ROLE.compile(guild.getId(), getId()); + return new AuditableRestActionImpl<>(getJDA(), route); } @Nonnull @Override public JDA getJDA() { - return getGuild().getJDA(); + return api; } @Nonnull @@ -308,12 +313,15 @@ public String toString() } @Override - public int compareTo(Role r) + public int compareTo(@Nonnull Role r) { if (this == r) return 0; + if (!(r instanceof RoleImpl)) + throw new IllegalArgumentException("Cannot compare different role implementations"); + RoleImpl impl = (RoleImpl) r; - if (!this.getGuild().equals(r.getGuild())) + if (this.guild.getIdLong() != impl.guild.getIdLong()) throw new IllegalArgumentException("Cannot compare roles that aren't from the same guild!"); if (this.getPositionRaw() != r.getPositionRaw()) diff --git a/src/main/java/net/dv8tion/jda/internal/entities/SystemMessage.java b/src/main/java/net/dv8tion/jda/internal/entities/SystemMessage.java index 4e6b4715c1..c091710df2 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/SystemMessage.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/SystemMessage.java @@ -32,10 +32,10 @@ public SystemMessage( boolean fromWebhook, boolean mentionsEveryone, TLongSet mentionedUsers, TLongSet mentionedRoles, boolean tts, boolean pinned, String content, String nonce, User author, Member member, MessageActivity activity, OffsetDateTime editTime, - List reactions, List attachments, List embeds) + List reactions, List attachments, List embeds, int flags) { super(id, channel, type, fromWebhook, mentionsEveryone, mentionedUsers, mentionedRoles, - tts, pinned, content, nonce, author, member, activity, editTime, reactions, attachments, embeds); + tts, pinned, content, nonce, author, member, activity, editTime, reactions, attachments, embeds, flags); } @Nonnull diff --git a/src/main/java/net/dv8tion/jda/internal/entities/UserImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/UserImpl.java index 67e14ea140..f85612bbec 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/UserImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/UserImpl.java @@ -23,10 +23,9 @@ import net.dv8tion.jda.api.utils.MiscUtil; import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.internal.JDAImpl; -import net.dv8tion.jda.internal.requests.EmptyRestAction; +import net.dv8tion.jda.internal.requests.DeferredRestAction; import net.dv8tion.jda.internal.requests.RestActionImpl; import net.dv8tion.jda.internal.requests.Route; -import net.dv8tion.jda.internal.utils.cache.UpstreamReference; import javax.annotation.Nonnull; import java.util.FormattableFlags; @@ -36,7 +35,7 @@ public class UserImpl implements User { protected final long id; - protected final UpstreamReference api; + protected final JDAImpl api; protected short discriminator; protected String name; @@ -48,7 +47,7 @@ public class UserImpl implements User public UserImpl(long id, JDAImpl api) { this.id = id; - this.api = new UpstreamReference<>(api); + this.api = api; } @Nonnull @@ -95,16 +94,15 @@ public boolean hasPrivateChannel() @Override public RestAction openPrivateChannel() { - if (privateChannel != null) - return new EmptyRestAction<>(getJDA(), privateChannel); - - Route.CompiledRoute route = Route.Self.CREATE_PRIVATE_CHANNEL.compile(); - DataObject body = DataObject.empty().put("recipient_id", getId()); - return new RestActionImpl<>(getJDA(), route, body, (response, request) -> - { - PrivateChannel priv = api.get().getEntityBuilder().createPrivateChannel(response.getObject(), this); - UserImpl.this.privateChannel = priv; - return priv; + return new DeferredRestAction<>(getJDA(), PrivateChannel.class, () -> privateChannel, () -> { + Route.CompiledRoute route = Route.Self.CREATE_PRIVATE_CHANNEL.compile(); + DataObject body = DataObject.empty().put("recipient_id", getId()); + return new RestActionImpl<>(getJDA(), route, body, (response, request) -> + { + PrivateChannel priv = api.getEntityBuilder().createPrivateChannel(response.getObject(), this); + UserImpl.this.privateChannel = priv; + return priv; + }); }); } @@ -133,14 +131,14 @@ public boolean isBot() @Override public JDAImpl getJDA() { - return api.get(); + return api; } @Nonnull @Override public String getAsMention() { - return "<@" + id + '>'; + return "<@" + getId() + '>'; } @Override diff --git a/src/main/java/net/dv8tion/jda/internal/entities/VoiceChannelImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/VoiceChannelImpl.java index 68c07b459d..6710e9f0be 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/VoiceChannelImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/VoiceChannelImpl.java @@ -77,7 +77,7 @@ public int getPosition() List channels = getGuild().getVoiceChannels(); for (int i = 0; i < channels.size(); i++) { - if (channels.get(i) == this) + if (equals(channels.get(i))) return i; } throw new AssertionError("Somehow when determining position we never found the VoiceChannel in the Guild's channels? wtf?"); diff --git a/src/main/java/net/dv8tion/jda/internal/handle/GuildDeleteHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/GuildDeleteHandler.java index 40a38537f0..43d8923e7c 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/GuildDeleteHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/GuildDeleteHandler.java @@ -16,14 +16,12 @@ package net.dv8tion.jda.internal.handle; -import gnu.trove.map.TLongObjectMap; import gnu.trove.set.TLongSet; import net.dv8tion.jda.api.audio.hooks.ConnectionStatus; import net.dv8tion.jda.api.entities.*; import net.dv8tion.jda.api.events.guild.GuildLeaveEvent; import net.dv8tion.jda.api.events.guild.GuildUnavailableEvent; import net.dv8tion.jda.api.managers.AudioManager; -import net.dv8tion.jda.api.utils.MiscUtil; import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.internal.JDAImpl; import net.dv8tion.jda.internal.entities.GuildImpl; @@ -93,23 +91,14 @@ protected Long handleInternally(DataObject content) guild.getCategoryCache() .forEachUnordered(chan -> categoryView.getMap().remove(chan.getIdLong())); } + + // Clear audio connection getJDA().getClient().removeAudioConnection(id); final AbstractCacheView audioManagerView = getJDA().getAudioManagersView(); - try (UnlockHook hook = audioManagerView.writeLock()) - { - final TLongObjectMap audioManagerMap = audioManagerView.getMap(); - final AudioManagerImpl manager = (AudioManagerImpl) audioManagerMap.get(id); - if (manager != null) // close existing audio connection if needed - { - MiscUtil.locked(manager.CONNECTION_LOCK, () -> - { - if (manager.isConnected() || manager.isAttemptingToConnect()) - manager.closeAudioConnection(ConnectionStatus.DISCONNECTED_REMOVED_FROM_GUILD); - else - audioManagerMap.remove(id); - }); - } - } + final AudioManagerImpl manager = (AudioManagerImpl) audioManagerView.get(id); //read-lock access/release + if (manager != null) + manager.closeAudioConnection(ConnectionStatus.DISCONNECTED_REMOVED_FROM_GUILD); //connection-lock access/release + audioManagerView.remove(id); //write-lock access/release //cleaning up all users that we do not share a guild with anymore // Anything left in memberIds will be removed from the main userMap diff --git a/src/main/java/net/dv8tion/jda/internal/handle/GuildSetupController.java b/src/main/java/net/dv8tion/jda/internal/handle/GuildSetupController.java index 6fcca45580..7c25e1b3b7 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/GuildSetupController.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/GuildSetupController.java @@ -26,6 +26,7 @@ import gnu.trove.set.TLongSet; import gnu.trove.set.hash.TLongHashSet; import net.dv8tion.jda.api.AccountType; +import net.dv8tion.jda.api.events.guild.UnavailableGuildLeaveEvent; import net.dv8tion.jda.api.utils.MiscUtil; import net.dv8tion.jda.api.utils.data.DataArray; import net.dv8tion.jda.api.utils.data.DataObject; @@ -33,7 +34,6 @@ import net.dv8tion.jda.internal.requests.WebSocketClient; import net.dv8tion.jda.internal.requests.WebSocketCode; import net.dv8tion.jda.internal.utils.JDALogger; -import net.dv8tion.jda.internal.utils.cache.UpstreamReference; import org.slf4j.Logger; import javax.annotation.Nullable; @@ -48,7 +48,7 @@ public class GuildSetupController protected static final int CHUNK_TIMEOUT = 10000; protected static final Logger log = JDALogger.getLog(GuildSetupController.class); - private final UpstreamReference api; + private final JDAImpl api; private final TLongObjectMap setupNodes = new TLongObjectHashMap<>(); private final TLongSet chunkingGuilds = new TLongHashSet(); private final TLongLongMap pendingChunks = new TLongLongHashMap(); @@ -64,7 +64,7 @@ public class GuildSetupController public GuildSetupController(JDAImpl api) { - this.api = new UpstreamReference<>(api); + this.api = api; if (isClient()) syncingGuilds = new TLongHashSet(); else @@ -73,7 +73,7 @@ public GuildSetupController(JDAImpl api) JDAImpl getJDA() { - return api.get(); + return api; } boolean isClient() @@ -119,6 +119,7 @@ void addGuildForSyncing(long id, boolean join) void remove(long id) { + unavailableGuilds.remove(id); setupNodes.remove(id); chunkingGuilds.remove(id); synchronized (pendingChunks) { pendingChunks.remove(id); } @@ -201,6 +202,14 @@ else if (node.markedUnavailable && available && incompleteCount > 0) public boolean onDelete(long id, DataObject obj) { boolean available = obj.isNull("unavailable") || !obj.getBoolean("unavailable"); + if (isUnavailable(id) && available) + { + log.debug("Leaving unavailable guild with id {}", id); + remove(id); + api.getEventManager().handle(new UnavailableGuildLeaveEvent(api, api.getResponseTotal(), id)); + return true; + } + GuildSetupNode node = setupNodes.get(id); if (node == null) return false; @@ -237,6 +246,7 @@ public boolean onDelete(long id, DataObject obj) remove(id); else ready(id); + api.getEventManager().handle(new UnavailableGuildLeaveEvent(api, api.getResponseTotal(), id)); } log.debug("Updated incompleteCount to {} and syncCount to {}", incompleteCount, syncingCount); return true; diff --git a/src/main/java/net/dv8tion/jda/internal/handle/GuildSetupNode.java b/src/main/java/net/dv8tion/jda/internal/handle/GuildSetupNode.java index 00d15a77fd..cd40769bee 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/GuildSetupNode.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/GuildSetupNode.java @@ -37,7 +37,6 @@ import net.dv8tion.jda.internal.managers.AudioManagerImpl; import net.dv8tion.jda.internal.utils.UnlockHook; import net.dv8tion.jda.internal.utils.cache.AbstractCacheView; -import net.dv8tion.jda.internal.utils.cache.UpstreamReference; import javax.annotation.Nullable; import java.util.LinkedList; @@ -47,7 +46,7 @@ public class GuildSetupNode { private final long id; - private final UpstreamReference controller; + private final GuildSetupController controller; private final List cachedEvents = new LinkedList<>(); private TLongObjectMap members; private TLongSet removedMembers; @@ -65,7 +64,7 @@ public class GuildSetupNode GuildSetupNode(long id, GuildSetupController controller, Type type) { this.id = id; - this.controller = new UpstreamReference<>(controller); + this.controller = controller; this.type = type; this.sync = controller.isClient(); } @@ -166,7 +165,7 @@ public boolean equals(Object obj) private GuildSetupController getController() { - return controller.get(); + return controller; } void updateStatus(GuildSetupController.Status status) diff --git a/src/main/java/net/dv8tion/jda/internal/handle/SocketHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/SocketHandler.java index 42c177cde0..cdcad3546b 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/SocketHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/SocketHandler.java @@ -17,17 +17,16 @@ import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.internal.JDAImpl; -import net.dv8tion.jda.internal.utils.cache.UpstreamReference; public abstract class SocketHandler { - protected final UpstreamReference api; + protected final JDAImpl api; protected long responseNumber; protected DataObject allContent; public SocketHandler(JDAImpl api) { - this.api = new UpstreamReference<>(api); + this.api = api; } public final synchronized void handle(long responseTotal, DataObject o) @@ -42,7 +41,7 @@ public final synchronized void handle(long responseTotal, DataObject o) protected JDAImpl getJDA() { - return api.get(); + return api; } /** diff --git a/src/main/java/net/dv8tion/jda/internal/managers/AccountManagerImpl.java b/src/main/java/net/dv8tion/jda/internal/managers/AccountManagerImpl.java index 625b489b4f..57a8779c77 100644 --- a/src/main/java/net/dv8tion/jda/internal/managers/AccountManagerImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/managers/AccountManagerImpl.java @@ -25,7 +25,6 @@ import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.internal.requests.Route; import net.dv8tion.jda.internal.utils.Checks; -import net.dv8tion.jda.internal.utils.cache.UpstreamReference; import okhttp3.RequestBody; import javax.annotation.CheckReturnValue; @@ -33,7 +32,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager { - protected final UpstreamReference selfUser; + protected final SelfUser selfUser; protected String currentPassword; @@ -51,14 +50,14 @@ public class AccountManagerImpl extends ManagerBase implements A public AccountManagerImpl(SelfUser selfUser) { super(selfUser.getJDA(), Route.Self.MODIFY_SELF.compile()); - this.selfUser = new UpstreamReference<>(selfUser); + this.selfUser = selfUser; } @Nonnull @Override public SelfUser getSelfUser() { - return selfUser.get(); + return selfUser; } @Nonnull @@ -177,7 +176,7 @@ protected RequestBody finalizeData() protected void handleSuccess(Response response, Request request) { String newToken = response.getObject().getString("token").replace("Bot ", ""); - api.get().setToken(newToken); + api.setToken(newToken); request.onSuccess(null); } } diff --git a/src/main/java/net/dv8tion/jda/internal/managers/AudioManagerImpl.java b/src/main/java/net/dv8tion/jda/internal/managers/AudioManagerImpl.java index 6a4a846c9b..d1f1e8c4a1 100644 --- a/src/main/java/net/dv8tion/jda/internal/managers/AudioManagerImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/managers/AudioManagerImpl.java @@ -144,7 +144,7 @@ public void closeAudioConnection(ConnectionStatus reason) this.queuedAudioConnection = null; if (audioConnection != null) this.audioConnection.close(reason); - else + else if (reason != ConnectionStatus.DISCONNECTED_REMOVED_FROM_GUILD) getJDA().getDirectAudioController().disconnect(getGuild()); this.audioConnection = null; }); diff --git a/src/main/java/net/dv8tion/jda/internal/managers/ChannelManagerImpl.java b/src/main/java/net/dv8tion/jda/internal/managers/ChannelManagerImpl.java index 9087ca765d..c914d1a546 100644 --- a/src/main/java/net/dv8tion/jda/internal/managers/ChannelManagerImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/managers/ChannelManagerImpl.java @@ -19,6 +19,7 @@ import gnu.trove.map.hash.TLongObjectHashMap; import gnu.trove.set.TLongSet; import gnu.trove.set.hash.TLongHashSet; +import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.entities.*; import net.dv8tion.jda.api.exceptions.InsufficientPermissionException; @@ -28,7 +29,7 @@ import net.dv8tion.jda.internal.requests.Route; import net.dv8tion.jda.internal.requests.restaction.PermOverrideData; import net.dv8tion.jda.internal.utils.Checks; -import net.dv8tion.jda.internal.utils.cache.UpstreamReference; +import net.dv8tion.jda.internal.utils.cache.SnowflakeReference; import okhttp3.RequestBody; import javax.annotation.CheckReturnValue; @@ -37,7 +38,7 @@ public class ChannelManagerImpl extends ManagerBase implements ChannelManager { - protected final UpstreamReference channel; + protected final SnowflakeReference channel; protected String name; protected String parent; @@ -63,7 +64,9 @@ public ChannelManagerImpl(GuildChannel channel) { super(channel.getJDA(), Route.Channels.MODIFY_CHANNEL.compile(channel.getId())); - this.channel = new UpstreamReference<>(channel); + JDA jda = channel.getJDA(); + ChannelType type = channel.getType(); + this.channel = new SnowflakeReference<>(channel, (channelId) -> jda.getGuildChannelById(type, channelId)); if (isPermissionChecksEnabled()) checkPermissions(); this.overridesAdd = new TLongObjectHashMap<>(); @@ -74,7 +77,7 @@ public ChannelManagerImpl(GuildChannel channel) @Override public GuildChannel getChannel() { - return channel.get(); + return channel.resolve(); } @Nonnull diff --git a/src/main/java/net/dv8tion/jda/internal/managers/DirectAudioControllerImpl.java b/src/main/java/net/dv8tion/jda/internal/managers/DirectAudioControllerImpl.java index a298a2cc3a..f4a14188f4 100644 --- a/src/main/java/net/dv8tion/jda/internal/managers/DirectAudioControllerImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/managers/DirectAudioControllerImpl.java @@ -22,24 +22,23 @@ import net.dv8tion.jda.internal.JDAImpl; import net.dv8tion.jda.internal.requests.WebSocketClient; import net.dv8tion.jda.internal.utils.Checks; -import net.dv8tion.jda.internal.utils.cache.UpstreamReference; import javax.annotation.Nonnull; public class DirectAudioControllerImpl implements DirectAudioController { - private final UpstreamReference api; + private final JDAImpl api; public DirectAudioControllerImpl(JDAImpl api) { - this.api = new UpstreamReference<>(api); + this.api = api; } @Nonnull @Override public JDAImpl getJDA() { - return api.get(); + return api; } @Override diff --git a/src/main/java/net/dv8tion/jda/internal/managers/GuildManagerImpl.java b/src/main/java/net/dv8tion/jda/internal/managers/GuildManagerImpl.java index d9c6a15021..aa5eb04055 100644 --- a/src/main/java/net/dv8tion/jda/internal/managers/GuildManagerImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/managers/GuildManagerImpl.java @@ -16,6 +16,7 @@ package net.dv8tion.jda.internal.managers; +import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.Region; import net.dv8tion.jda.api.entities.Guild; @@ -27,7 +28,7 @@ import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.internal.requests.Route; import net.dv8tion.jda.internal.utils.Checks; -import net.dv8tion.jda.internal.utils.cache.UpstreamReference; +import net.dv8tion.jda.internal.utils.cache.SnowflakeReference; import okhttp3.RequestBody; import javax.annotation.CheckReturnValue; @@ -36,7 +37,7 @@ public class GuildManagerImpl extends ManagerBase implements GuildManager { - protected final UpstreamReference guild; + protected final SnowflakeReference guild; protected String name; protected String region; @@ -52,7 +53,8 @@ public class GuildManagerImpl extends ManagerBase implements Guild public GuildManagerImpl(Guild guild) { super(guild.getJDA(), Route.Guilds.MODIFY_GUILD.compile(guild.getId())); - this.guild = new UpstreamReference<>(guild); + JDA api = guild.getJDA(); + this.guild = new SnowflakeReference<>(guild, api::getGuildById); if (isPermissionChecksEnabled()) checkPermissions(); } @@ -61,7 +63,7 @@ public GuildManagerImpl(Guild guild) @Override public Guild getGuild() { - return guild.get(); + return guild.resolve(); } @Nonnull diff --git a/src/main/java/net/dv8tion/jda/internal/managers/PermOverrideManagerImpl.java b/src/main/java/net/dv8tion/jda/internal/managers/PermOverrideManagerImpl.java index 32062b0fdb..2dffc4f4fd 100644 --- a/src/main/java/net/dv8tion/jda/internal/managers/PermOverrideManagerImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/managers/PermOverrideManagerImpl.java @@ -16,13 +16,14 @@ package net.dv8tion.jda.internal.managers; +import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.Permission; -import net.dv8tion.jda.api.entities.PermissionOverride; +import net.dv8tion.jda.api.entities.*; import net.dv8tion.jda.api.exceptions.InsufficientPermissionException; import net.dv8tion.jda.api.managers.PermOverrideManager; import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.internal.requests.Route; -import net.dv8tion.jda.internal.utils.cache.UpstreamReference; +import net.dv8tion.jda.internal.utils.cache.SnowflakeReference; import okhttp3.RequestBody; import javax.annotation.CheckReturnValue; @@ -30,7 +31,8 @@ public class PermOverrideManagerImpl extends ManagerBase implements PermOverrideManager { - protected final UpstreamReference override; + protected final SnowflakeReference override; + protected final boolean role; protected long allowed; protected long denied; @@ -45,16 +47,38 @@ public PermOverrideManagerImpl(PermissionOverride override) { super(override.getJDA(), Route.Channels.MODIFY_PERM_OVERRIDE.compile( - override.getChannel().getId(), - override.isMemberOverride() ? override.getMember().getUser().getId() - : override.getRole().getId())); - this.override = new UpstreamReference<>(override); + override.getChannel().getId(), override.getId())); + this.override = setupReferent(override); + this.role = override.isRoleOverride(); this.allowed = override.getAllowedRaw(); this.denied = override.getDeniedRaw(); if (isPermissionChecksEnabled()) checkPermissions(); } + private SnowflakeReference setupReferent(PermissionOverride override) + { + JDA api = override.getJDA(); + GuildChannel channel = override.getChannel(); + long channelId = channel.getIdLong(); + ChannelType type = channel.getType(); + boolean role = override.isRoleOverride(); + return new SnowflakeReference<>(override, (holderId) -> { + GuildChannel targetChannel = api.getGuildChannelById(type, channelId); + if (targetChannel == null) + return null; + Guild guild = targetChannel.getGuild(); + IPermissionHolder holder; + if (role) + holder = guild.getRoleById(holderId); + else + holder = guild.getMemberById(holderId); + if (holder == null) + return null; + return targetChannel.getPermissionOverride(holder); + }); + } + private void setupValues() { if (!shouldUpdate(ALLOWED)) @@ -67,7 +91,7 @@ private void setupValues() @Override public PermissionOverride getPermissionOverride() { - return override.get(); + return override.resolve(); } @Nonnull @@ -149,13 +173,13 @@ public PermOverrideManagerImpl clear(long permissions) @Override protected RequestBody finalizeData() { - String targetId = getPermissionOverride().isMemberOverride() ? getPermissionOverride().getMember().getUser().getId() : getPermissionOverride().getRole().getId(); + String targetId = override.getId(); // setup missing values here setupValues(); RequestBody data = getRequestBody( DataObject.empty() .put("id", targetId) - .put("type", getPermissionOverride().isMemberOverride() ? "member" : "role") + .put("type", role ? "role" : "member") .put("allow", this.allowed) .put("deny", this.denied)); reset(); diff --git a/src/main/java/net/dv8tion/jda/internal/managers/PresenceImpl.java b/src/main/java/net/dv8tion/jda/internal/managers/PresenceImpl.java index d737dbb0ff..e27537ce92 100644 --- a/src/main/java/net/dv8tion/jda/internal/managers/PresenceImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/managers/PresenceImpl.java @@ -24,7 +24,6 @@ import net.dv8tion.jda.internal.JDAImpl; import net.dv8tion.jda.internal.requests.WebSocketCode; import net.dv8tion.jda.internal.utils.Checks; -import net.dv8tion.jda.internal.utils.cache.UpstreamReference; import javax.annotation.Nonnull; @@ -36,7 +35,7 @@ */ public class PresenceImpl implements Presence { - private final UpstreamReference api; + private final JDAImpl api; private boolean idle = false; private Activity activity = null; private OnlineStatus status = OnlineStatus.ONLINE; @@ -49,7 +48,7 @@ public class PresenceImpl implements Presence */ public PresenceImpl(JDAImpl jda) { - this.api = new UpstreamReference<>(jda); + this.api = jda; } @@ -60,7 +59,7 @@ public PresenceImpl(JDAImpl jda) @Override public JDA getJDA() { - return api.get(); + return api; } @Nonnull @@ -203,11 +202,10 @@ private DataObject getGameJson(Activity activity) protected void update(DataObject data) { - JDAImpl jda = api.get(); - JDA.Status status = jda.getStatus(); + JDA.Status status = api.getStatus(); if (status == JDA.Status.RECONNECT_QUEUED || status == JDA.Status.SHUTDOWN || status == JDA.Status.SHUTTING_DOWN) return; - jda.getClient().send(DataObject.empty() + api.getClient().send(DataObject.empty() .put("d", data) .put("op", WebSocketCode.PRESENCE).toString()); } diff --git a/src/main/java/net/dv8tion/jda/internal/managers/RoleManagerImpl.java b/src/main/java/net/dv8tion/jda/internal/managers/RoleManagerImpl.java index d137f1a7d0..bdb5014b9f 100644 --- a/src/main/java/net/dv8tion/jda/internal/managers/RoleManagerImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/managers/RoleManagerImpl.java @@ -16,6 +16,7 @@ package net.dv8tion.jda.internal.managers; +import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Role; @@ -26,7 +27,7 @@ import net.dv8tion.jda.internal.requests.Route; import net.dv8tion.jda.internal.utils.Checks; import net.dv8tion.jda.internal.utils.PermissionUtil; -import net.dv8tion.jda.internal.utils.cache.UpstreamReference; +import net.dv8tion.jda.internal.utils.cache.SnowflakeReference; import okhttp3.RequestBody; import javax.annotation.CheckReturnValue; @@ -36,7 +37,7 @@ public class RoleManagerImpl extends ManagerBase implements RoleManager { - protected final UpstreamReference role; + protected final SnowflakeReference role; protected String name; protected int color; @@ -53,7 +54,8 @@ public class RoleManagerImpl extends ManagerBase implements RoleMan public RoleManagerImpl(Role role) { super(role.getJDA(), Route.Roles.MODIFY_ROLE.compile(role.getGuild().getId(), role.getId())); - this.role = new UpstreamReference<>(role); + JDA api = role.getJDA(); + this.role = new SnowflakeReference<>(role, api::getRoleById); if (isPermissionChecksEnabled()) checkPermissions(); } @@ -62,7 +64,7 @@ public RoleManagerImpl(Role role) @Override public Role getRole() { - return role.get(); + return role.resolve(); } @Nonnull diff --git a/src/main/java/net/dv8tion/jda/internal/managers/WebhookManagerImpl.java b/src/main/java/net/dv8tion/jda/internal/managers/WebhookManagerImpl.java index a074da6013..31654b8e0f 100644 --- a/src/main/java/net/dv8tion/jda/internal/managers/WebhookManagerImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/managers/WebhookManagerImpl.java @@ -25,7 +25,6 @@ import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.internal.requests.Route; import net.dv8tion.jda.internal.utils.Checks; -import net.dv8tion.jda.internal.utils.cache.UpstreamReference; import okhttp3.RequestBody; import javax.annotation.CheckReturnValue; @@ -33,8 +32,7 @@ public class WebhookManagerImpl extends ManagerBase implements WebhookManager { - protected final UpstreamReference webhook; - + protected final Webhook webhook; protected String name; protected String channel; protected Icon avatar; @@ -48,7 +46,7 @@ public class WebhookManagerImpl extends ManagerBase implements W public WebhookManagerImpl(Webhook webhook) { super(webhook.getJDA(), Route.Webhooks.MODIFY_WEBHOOK.compile(webhook.getId())); - this.webhook = new UpstreamReference<>(webhook); + this.webhook = webhook; if (isPermissionChecksEnabled()) checkPermissions(); } @@ -57,7 +55,7 @@ public WebhookManagerImpl(Webhook webhook) @Override public Webhook getWebhook() { - return webhook.get(); + return webhook; } @Nonnull diff --git a/src/main/java/net/dv8tion/jda/internal/requests/CompletedRestAction.java b/src/main/java/net/dv8tion/jda/internal/requests/CompletedRestAction.java new file mode 100644 index 0000000000..f7e68b2085 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/internal/requests/CompletedRestAction.java @@ -0,0 +1,121 @@ +/* + * Copyright 2015-2019 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.internal.requests; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.exceptions.RateLimitedException; +import net.dv8tion.jda.api.requests.RestAction; +import net.dv8tion.jda.api.requests.restaction.AuditableRestAction; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.concurrent.CompletableFuture; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; + +public class CompletedRestAction implements AuditableRestAction +{ + private final JDA api; + private final T value; + private final Throwable error; + + public CompletedRestAction(JDA api, T value, Throwable error) + { + this.api = api; + this.value = value; + this.error = error; + } + + public CompletedRestAction(JDA api, T value) + { + this(api, value, null); + } + + public CompletedRestAction(JDA api, Throwable error) + { + this(api, null, error); + } + + + @Nonnull + @Override + public AuditableRestAction reason(@Nullable String reason) + { + return this; + } + + @Nonnull + @Override + public JDA getJDA() + { + return api; + } + + @Nonnull + @Override + public AuditableRestAction setCheck(@Nullable BooleanSupplier checks) + { + return this; + } + + @Override + public void queue(@Nullable Consumer success, @Nullable Consumer failure) + { + if (error == null) + { + if (success == null) + RestAction.getDefaultSuccess().accept(value); + else + success.accept(value); + } + else + { + if (failure == null) + RestAction.getDefaultFailure().accept(error); + else + failure.accept(error); + } + } + + @Override + public T complete(boolean shouldQueue) throws RateLimitedException + { + if (error != null) + { + if (error instanceof RateLimitedException) + throw (RateLimitedException) error; + if (error instanceof RuntimeException) + throw (RuntimeException) error; + if (error instanceof Error) + throw (Error) error; + throw new IllegalStateException(error); + } + return value; + } + + @Nonnull + @Override + public CompletableFuture submit(boolean shouldQueue) + { + CompletableFuture future = new CompletableFuture<>(); + if (error != null) + future.completeExceptionally(error); + else + future.complete(value); + return future; + } +} diff --git a/src/main/java/net/dv8tion/jda/internal/requests/DeferredRestAction.java b/src/main/java/net/dv8tion/jda/internal/requests/DeferredRestAction.java new file mode 100644 index 0000000000..ae3afc8f69 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/internal/requests/DeferredRestAction.java @@ -0,0 +1,151 @@ +/* + * Copyright 2015-2019 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.internal.requests; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.exceptions.RateLimitedException; +import net.dv8tion.jda.api.requests.RestAction; +import net.dv8tion.jda.api.requests.restaction.AuditableRestAction; + +import javax.annotation.Nonnull; +import java.util.concurrent.CompletableFuture; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class DeferredRestAction> implements AuditableRestAction +{ + private final JDA api; + private final Class type; + private final Supplier valueSupplier; + private final Supplier actionSupplier; + + private BooleanSupplier isAction; + private BooleanSupplier transitiveChecks; + + public DeferredRestAction(JDA api, Supplier actionSupplier) + { + this(api, null, null, actionSupplier); + } + + public DeferredRestAction(JDA api, Class type, + Supplier valueSupplier, + Supplier actionSupplier) + { + this.api = api; + this.type = type; + this.valueSupplier = valueSupplier; + this.actionSupplier = actionSupplier; + } + + @Nonnull + @Override + public JDA getJDA() + { + return api; + } + + @Nonnull + @Override + public AuditableRestAction reason(String reason) + { + return this; + } + + @Nonnull + @Override + public AuditableRestAction setCheck(BooleanSupplier checks) + { + this.transitiveChecks = checks; + return this; + } + + public AuditableRestAction setCacheCheck(BooleanSupplier checks) + { + this.isAction = checks; + return this; + } + + @Override + public void queue(Consumer success, Consumer failure) + { + Consumer finalSuccess; + if (success != null) + finalSuccess = success; + else + finalSuccess = RestAction.getDefaultSuccess(); + + if (type == null) + { + BooleanSupplier checks = this.isAction; + if (checks != null && checks.getAsBoolean()) + actionSupplier.get().queue(success, failure); + else + finalSuccess.accept(null); + return; + } + + T value = valueSupplier.get(); + if (value == null) + { + getAction().queue(success, failure); + } + else + { + finalSuccess.accept(value); + } + } + + @Nonnull + @Override + public CompletableFuture submit(boolean shouldQueue) + { + if (type == null) + { + BooleanSupplier checks = this.isAction; + if (checks != null && checks.getAsBoolean()) + return actionSupplier.get().submit(shouldQueue); + return CompletableFuture.completedFuture(null); + } + T value = valueSupplier.get(); + if (value != null) + return CompletableFuture.completedFuture(value); + return getAction().submit(shouldQueue); + } + + @Override + public T complete(boolean shouldQueue) throws RateLimitedException + { + if (type == null) + { + BooleanSupplier checks = this.isAction; + if (checks != null && checks.getAsBoolean()) + return actionSupplier.get().complete(shouldQueue); + return null; + } + T value = valueSupplier.get(); + if (value != null) + return value; + return getAction().complete(shouldQueue); + } + + @SuppressWarnings("unchecked") + private R getAction() + { + return (R) actionSupplier.get().setCheck(transitiveChecks); + } +} diff --git a/src/main/java/net/dv8tion/jda/internal/requests/EmptyRestAction.java b/src/main/java/net/dv8tion/jda/internal/requests/EmptyRestAction.java deleted file mode 100644 index 635754f0df..0000000000 --- a/src/main/java/net/dv8tion/jda/internal/requests/EmptyRestAction.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2015-2019 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.internal.requests; - -import net.dv8tion.jda.api.JDA; -import net.dv8tion.jda.api.requests.restaction.AuditableRestAction; - -import javax.annotation.Nonnull; -import java.util.concurrent.CompletableFuture; -import java.util.function.BooleanSupplier; -import java.util.function.Consumer; - -public class EmptyRestAction implements AuditableRestAction -{ - private final JDA api; - private final T returnObj; - - public EmptyRestAction(JDA api) - { - this(api, null); - } - - public EmptyRestAction(JDA api, T returnObj) - { - this.api = api; - this.returnObj = returnObj; - } - - @Nonnull - @Override - public JDA getJDA() - { - return api; - } - - @Nonnull - @Override - public AuditableRestAction reason(String reason) - { - return this; - } - - @Nonnull - @Override - public AuditableRestAction setCheck(BooleanSupplier checks) - { - return this; - } - - @Override - public void queue(Consumer success, Consumer failure) - { - if (success != null) - success.accept(returnObj); - } - - @Nonnull - @Override - public CompletableFuture submit(boolean shouldQueue) - { - return CompletableFuture.completedFuture(returnObj); - } - - @Override - public T complete(boolean shouldQueue) - { - return returnObj; - } -} diff --git a/src/main/java/net/dv8tion/jda/internal/requests/RateLimiter.java b/src/main/java/net/dv8tion/jda/internal/requests/RateLimiter.java index c9a2fc94c9..58f1a7e886 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/RateLimiter.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/RateLimiter.java @@ -96,6 +96,8 @@ public List getQueuedRouteBuckets() } } + public void init() {} + protected void shutdown() { isShutdown = true; diff --git a/src/main/java/net/dv8tion/jda/internal/requests/Requester.java b/src/main/java/net/dv8tion/jda/internal/requests/Requester.java index e0e4d1c617..63e93265b3 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/Requester.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/Requester.java @@ -37,10 +37,10 @@ import javax.net.ssl.SSLPeerUnverifiedException; import java.net.SocketException; import java.net.SocketTimeoutException; -import java.util.*; +import java.util.Collections; +import java.util.LinkedHashSet; import java.util.Map.Entry; -import java.util.concurrent.CancellationException; -import java.util.concurrent.CompletableFuture; +import java.util.Set; import java.util.concurrent.ConcurrentMap; public class Requester @@ -121,82 +121,7 @@ private static boolean isRetry(Throwable e) || e instanceof SSLPeerUnverifiedException; // SSL Certificate was wrong } - private void attemptRequest(CompletableFuture task, okhttp3.Request request, - List responses, Set rays, - Request apiRequest, String url, - boolean handleOnRatelimit, boolean timeout, int attempt) - { - Route.CompiledRoute route = apiRequest.getRoute(); - okhttp3.Response lastResponse = responses.isEmpty() ? null : responses.get(responses.size() - 1); - // If the request has been canceled via the Future, don't execute. - if (apiRequest.isCanceled()) - { - apiRequest.onFailure(new CancellationException("RestAction has been cancelled")); - task.complete(null); - return; - } - - if (attempt >= 4) - { - //Epic failure from other end. Attempted 4 times. - Response response = new Response(Objects.requireNonNull(lastResponse), -1, rays); - apiRequest.handleResponse(response); - task.complete(null); - return; - } - Call call = httpClient.newCall(request); - call.enqueue(FunctionalCallback.onFailure((c, e) -> { - if (isRetry(e)) - { - if (retryOnTimeout && !timeout) - { - // Retry once on timeout - attemptRequest(task, request, responses, rays, apiRequest, url, true, true, attempt + 1); - } - else - { - // Second attempt failed or we don't want to retry - LOG.error("Requester timed out while executing a request", e); - apiRequest.handleResponse(new Response(null, e, rays)); - task.complete(null); - } - return; - } - // Unexpected error, failed request - LOG.error("There was an exception while executing a REST request", e); //This originally only printed on DEBUG in 2.x - apiRequest.handleResponse(new Response(null, e, rays)); - task.complete(null); - }) - .onSuccess((c, response) -> { - responses.add(response); - String cfRay = response.header("CF-RAY"); - if (cfRay != null) - rays.add(cfRay); - - if (response.code() >= 500) - { - LOG.debug("Requesting {} -> {} returned status {}... retrying (attempt {})", - route.getMethod(), url, response.code(), attempt); - attemptRequest(task, request, responses, rays, apiRequest, url, true, timeout, attempt + 1); - return; - } - - Long retryAfter = rateLimiter.handleResponse(route, response); - if (!rays.isEmpty()) - LOG.debug("Received response with following cf-rays: {}", rays); - - LOG.trace("Finished Request {} {} with code {}", route.getMethod(), response.request().url(), response.code()); - - if (retryAfter == null) - apiRequest.handleResponse(new Response(response, -1, rays)); - else if (handleOnRatelimit) - apiRequest.handleResponse(new Response(response, retryAfter, rays)); - - task.complete(retryAfter); - }).build()); - } - - public CompletableFuture execute(Request apiRequest) + public Long execute(Request apiRequest) { return execute(apiRequest, false); } @@ -206,44 +131,142 @@ public CompletableFuture execute(Request apiRequest) * * @param apiRequest * The API request that needs to be sent - * @param handleOnRatelimit + * @param handleOnRateLimit * Whether to forward rate-limits, false if rate limit handling should take over * * @return Non-null if the request was ratelimited. Returns a Long containing retry_after milliseconds until * the request can be made again. This could either be for the Per-Route ratelimit or the Global ratelimit. *
Check if globalCooldown is {@code null} to determine if it was Per-Route or Global. */ - public CompletableFuture execute(Request apiRequest, boolean handleOnRatelimit) + public Long execute(Request apiRequest, boolean handleOnRateLimit) + { + return execute(apiRequest, false, handleOnRateLimit); + } + + public Long execute(Request apiRequest, boolean retried, boolean handleOnRatelimit) { Route.CompiledRoute route = apiRequest.getRoute(); Long retryAfter = rateLimiter.getRateLimit(route); - if (retryAfter != null) + if (retryAfter != null && retryAfter > 0) { if (handleOnRatelimit) apiRequest.handleResponse(new Response(retryAfter, Collections.emptySet())); - return CompletableFuture.completedFuture(retryAfter); + return retryAfter; } - // Build the request okhttp3.Request.Builder builder = new okhttp3.Request.Builder(); + String url = DISCORD_API_PREFIX + route.getCompiledRoute(); builder.url(url); - applyBody(apiRequest, builder); - applyHeaders(apiRequest, builder, url.startsWith(DISCORD_API_PREFIX)); + + String method = apiRequest.getRoute().getMethod().toString(); + RequestBody body = apiRequest.getBody(); + + if (body == null && HttpMethod.requiresRequestBody(method)) + body = EMPTY_BODY; + + builder.method(method, body) + .header("X-RateLimit-Precision", "millisecond") + .header("user-agent", USER_AGENT) + .header("accept-encoding", "gzip"); + + //adding token to all requests to the discord api or cdn pages + //we can check for startsWith(DISCORD_API_PREFIX) because the cdn endpoints don't need any kind of authorization + if (url.startsWith(DISCORD_API_PREFIX)) + builder.header("authorization", api.getToken()); + + // Apply custom headers like X-Audit-Log-Reason + // If customHeaders is null this does nothing + if (apiRequest.getHeaders() != null) + { + for (Entry header : apiRequest.getHeaders().entrySet()) + builder.addHeader(header.getKey(), header.getValue()); + } + okhttp3.Request request = builder.build(); - // Setup response handling Set rays = new LinkedHashSet<>(); - List responses = new ArrayList<>(4); - CompletableFuture task = new CompletableFuture<>(); - task.whenComplete((i1, i2) -> { + okhttp3.Response[] responses = new okhttp3.Response[4]; + // we have an array of all responses to later close them all at once + //the response below this comment is used as the first successful response from the server + okhttp3.Response lastResponse = null; + try + { + LOG.trace("Executing request {} {}", apiRequest.getRoute().getMethod(), url); + int attempt = 0; + do + { + //If the request has been canceled via the Future, don't execute. + //if (apiRequest.isCanceled()) + // return null; + Call call = httpClient.newCall(request); + lastResponse = call.execute(); + responses[attempt] = lastResponse; + String cfRay = lastResponse.header("CF-RAY"); + if (cfRay != null) + rays.add(cfRay); + + if (lastResponse.code() < 500) + break; // break loop, got a successful response! + + attempt++; + LOG.debug("Requesting {} -> {} returned status {}... retrying (attempt {})", + apiRequest.getRoute().getMethod(), + url, lastResponse.code(), attempt); + try + { + Thread.sleep(50 * attempt); + } + catch (InterruptedException ignored) {} + } + while (attempt < 3 && lastResponse.code() >= 500); + + LOG.trace("Finished Request {} {} with code {}", route.getMethod(), lastResponse.request().url(), lastResponse.code()); + + if (lastResponse.code() >= 500) + { + //Epic failure from other end. Attempted 4 times. + Response response = new Response(lastResponse, -1, rays); + apiRequest.handleResponse(response); + return null; + } + + retryAfter = rateLimiter.handleResponse(route, lastResponse); + if (!rays.isEmpty()) + LOG.debug("Received response with following cf-rays: {}", rays); + + if (retryAfter == null) + apiRequest.handleResponse(new Response(lastResponse, -1, rays)); + else if (handleOnRatelimit) + apiRequest.handleResponse(new Response(lastResponse, retryAfter, rays)); + + return retryAfter; + } + catch (SocketTimeoutException e) + { + if (retryOnTimeout && !retried) + return execute(apiRequest, true, handleOnRatelimit); + LOG.error("Requester timed out while executing a request", e); + apiRequest.handleResponse(new Response(lastResponse, e, rays)); + return null; + } + catch (Exception e) + { + if (retryOnTimeout && !retried && isRetry(e)) + return execute(apiRequest, true, handleOnRatelimit); + LOG.error("There was an exception while executing a REST request", e); //This originally only printed on DEBUG in 2.x + apiRequest.handleResponse(new Response(lastResponse, e, rays)); + return null; + } + finally + { for (okhttp3.Response r : responses) + { + if (r == null) + break; r.close(); - }); - LOG.trace("Executing request {} {}", apiRequest.getRoute().getMethod(), url); - // Initialize state-machine - attemptRequest(task, request, responses, rays, apiRequest, url, handleOnRatelimit, false, 0); - return task; + } + } } private void applyBody(Request apiRequest, okhttp3.Request.Builder builder) diff --git a/src/main/java/net/dv8tion/jda/internal/requests/RestActionImpl.java b/src/main/java/net/dv8tion/jda/internal/requests/RestActionImpl.java index 354b0a297c..5c4ac92de1 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/RestActionImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/RestActionImpl.java @@ -29,7 +29,6 @@ import net.dv8tion.jda.internal.JDAImpl; import net.dv8tion.jda.internal.utils.Checks; import net.dv8tion.jda.internal.utils.JDALogger; -import net.dv8tion.jda.internal.utils.cache.UpstreamReference; import okhttp3.RequestBody; import org.apache.commons.collections4.map.CaseInsensitiveMap; import org.slf4j.Logger; @@ -64,7 +63,7 @@ else if (t.getCause() != null) protected static boolean passContext = true; - protected final UpstreamReference api; + protected final JDAImpl api; private final Route.CompiledRoute route; private final RequestBody data; @@ -132,7 +131,7 @@ public RestActionImpl(JDA api, Route.CompiledRoute route, DataObject data, BiFun public RestActionImpl(JDA api, Route.CompiledRoute route, RequestBody data, BiFunction, T> handler) { Checks.notNull(api, "api"); - this.api = new UpstreamReference<>((JDAImpl) api); + this.api = (JDAImpl) api; this.route = route; this.data = data; this.handler = handler; @@ -142,7 +141,7 @@ public RestActionImpl(JDA api, Route.CompiledRoute route, RequestBody data, BiFu @Override public JDA getJDA() { - return api.get(); + return api; } @Nonnull @@ -165,7 +164,7 @@ public void queue(Consumer success, Consumer failu success = DEFAULT_SUCCESS; if (failure == null) failure = DEFAULT_FAILURE; - api.get().getRequester().request(new Request<>(this, success, failure, finisher, true, data, rawData, route, headers)); + api.getRequester().request(new Request<>(this, success, failure, finisher, true, data, rawData, route, headers)); } @Nonnull diff --git a/src/main/java/net/dv8tion/jda/internal/requests/Route.java b/src/main/java/net/dv8tion/jda/internal/requests/Route.java index c9303db837..6d7368fdf5 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/Route.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/Route.java @@ -19,10 +19,8 @@ import net.dv8tion.jda.internal.utils.Checks; import net.dv8tion.jda.internal.utils.Helpers; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import javax.annotation.CheckReturnValue; +import javax.annotation.Nonnull; import static net.dv8tion.jda.internal.requests.Method.*; @@ -31,10 +29,10 @@ public class Route { public static class Misc { - public static final Route TRACK = new Route(POST, true, "track"); - public static final Route GET_VOICE_REGIONS = new Route(GET, true, "voice/regions"); - public static final Route GATEWAY = new Route(GET, true, "gateway"); - public static final Route GATEWAY_BOT = new Route(GET, true, "gateway/bot"); + public static final Route TRACK = new Route(POST, "track"); + public static final Route GET_VOICE_REGIONS = new Route(GET, "voice/regions"); + public static final Route GATEWAY = new Route(GET, "gateway"); + public static final Route GATEWAY_BOT = new Route(GET, "gateway/bot"); } public static class Applications @@ -61,12 +59,12 @@ public static class Applications public static class Self { - public static final Route GET_SELF = new Route(GET, true, "users/@me"); - public static final Route MODIFY_SELF = new Route(PATCH, "users/@me"); - public static final Route GET_GUILDS = new Route(GET, "users/@me/guilds"); - public static final Route LEAVE_GUILD = new Route(DELETE, "users/@me/guilds/{guild_id}"); - public static final Route GET_PRIVATE_CHANNELS = new Route(GET, "users/@me/channels"); - public static final Route CREATE_PRIVATE_CHANNEL = new Route(POST, "users/@me/channels"); + public static final Route GET_SELF = new Route(GET, "users/@me"); + public static final Route MODIFY_SELF = new Route(PATCH, "users/@me"); + public static final Route GET_GUILDS = new Route(GET, "users/@me/guilds"); + public static final Route LEAVE_GUILD = new Route(DELETE, "users/@me/guilds/{guild_id}"); + public static final Route GET_PRIVATE_CHANNELS = new Route(GET, "users/@me/channels"); + public static final Route CREATE_PRIVATE_CHANNEL = new Route(POST, "users/@me/channels"); // Client only public static final Route USER_SETTINGS = new Route(GET, "users/@me/settings"); @@ -93,39 +91,39 @@ public static class Relationships public static class Guilds { - public static final Route GET_GUILD = new Route(GET, "guilds/{guild_id}", "guild_id"); - public static final Route MODIFY_GUILD = new Route(PATCH, "guilds/{guild_id}", "guild_id"); - public static final Route GET_VANITY_URL = new Route(GET, "guilds/{guild_id}/vanity-url", "guild_id"); - public static final Route CREATE_CHANNEL = new Route(POST, "guilds/{guild_id}/channels", "guild_id"); - public static final Route GET_CHANNELS = new Route(GET, "guilds/{guild_id}/channels", "guild_id"); - public static final Route MODIFY_CHANNELS = new Route(PATCH, "guilds/{guild_id}/channels", "guild_id"); - public static final Route MODIFY_ROLES = new Route(PATCH, "guilds/{guild_id}/roles", "guild_id"); - public static final Route GET_BANS = new Route(GET, "guilds/{guild_id}/bans", "guild_id"); - public static final Route GET_BAN = new Route(GET, "guilds/{guild_id}/bans/{user_id}", "guild_id"); - public static final Route UNBAN = new Route(DELETE, "guilds/{guild_id}/bans/{user_id}", "guild_id"); - public static final Route BAN = new Route(PUT, "guilds/{guild_id}/bans/{user_id}", "guild_id"); - public static final Route KICK_MEMBER = new Route(DELETE, "guilds/{guild_id}/members/{user_id}", "guild_id"); - public static final Route MODIFY_MEMBER = new Route(PATCH, "guilds/{guild_id}/members/{user_id}", "guild_id"); - public static final Route ADD_MEMBER = new Route(PUT, "guilds/{guild_id}/members/{user_id}", "guild_id"); - public static final Route GET_MEMBER = new Route(GET, "guilds/{guild_id}/members/{user_id}", "guild_id"); - public static final Route MODIFY_SELF_NICK = new Route(PATCH, "guilds/{guild_id}/members/@me/nick", "guild_id"); - public static final Route PRUNABLE_COUNT = new Route(GET, "guilds/{guild_id}/prune", "guild_id"); - public static final Route PRUNE_MEMBERS = new Route(POST, "guilds/{guild_id}/prune", "guild_id"); - public static final Route GET_WEBHOOKS = new Route(GET, "guilds/{guild_id}/webhooks", "guild_id"); - public static final Route GET_GUILD_EMBED = new Route(GET, "guilds/{guild_id}/embed", "guild_id"); - public static final Route MODIFY_GUILD_EMBED = new Route(PATCH, "guilds/{guild_id}/embed", "guild_id"); - public static final Route GET_GUILD_EMOTES = new Route(GET, "guilds/{guild_id}/emojis", "guild_id"); - public static final Route GET_AUDIT_LOGS = new Route(GET, true, "guilds/{guild_id}/audit-logs", "guild_id"); - public static final Route GET_VOICE_REGIONS = new Route(GET, true, "guilds/{guild_id}/regions", "guild_id"); - - public static final Route GET_INTEGRATIONS = new Route(GET, "guilds/{guild_id}/integrations", "guild_id"); - public static final Route CREATE_INTEGRATION = new Route(POST, "guilds/{guild_id}/integrations", "guild_id"); - public static final Route DELETE_INTEGRATION = new Route(DELETE, "guilds/{guild_id}/integrations/{integration_id}", "guild_id"); - public static final Route MODIFY_INTEGRATION = new Route(PATCH, "guilds/{guild_id}/integrations/{integration_id}", "guild_id"); - public static final Route SYNC_INTEGRATION = new Route(POST, "guilds/{guild_id}/integrations/{integration_id}/sync", "guild_id"); - - public static final Route ADD_MEMBER_ROLE = new Route(PUT, "guilds/{guild_id}/members/{user_id}/roles/{role_id}", "guild_id"); - public static final Route REMOVE_MEMBER_ROLE = new Route(DELETE, "guilds/{guild_id}/members/{user_id}/roles/{role_id}", "guild_id"); + public static final Route GET_GUILD = new Route(GET, "guilds/{guild_id}"); + public static final Route MODIFY_GUILD = new Route(PATCH, "guilds/{guild_id}"); + public static final Route GET_VANITY_URL = new Route(GET, "guilds/{guild_id}/vanity-url"); + public static final Route CREATE_CHANNEL = new Route(POST, "guilds/{guild_id}/channels"); + public static final Route GET_CHANNELS = new Route(GET, "guilds/{guild_id}/channels"); + public static final Route MODIFY_CHANNELS = new Route(PATCH, "guilds/{guild_id}/channels"); + public static final Route MODIFY_ROLES = new Route(PATCH, "guilds/{guild_id}/roles"); + public static final Route GET_BANS = new Route(GET, "guilds/{guild_id}/bans"); + public static final Route GET_BAN = new Route(GET, "guilds/{guild_id}/bans/{user_id}"); + public static final Route UNBAN = new Route(DELETE, "guilds/{guild_id}/bans/{user_id}"); + public static final Route BAN = new Route(PUT, "guilds/{guild_id}/bans/{user_id}"); + public static final Route KICK_MEMBER = new Route(DELETE, "guilds/{guild_id}/members/{user_id}"); + public static final Route MODIFY_MEMBER = new Route(PATCH, "guilds/{guild_id}/members/{user_id}"); + public static final Route ADD_MEMBER = new Route(PUT, "guilds/{guild_id}/members/{user_id}"); + public static final Route GET_MEMBER = new Route(GET, "guilds/{guild_id}/members/{user_id}"); + public static final Route MODIFY_SELF_NICK = new Route(PATCH, "guilds/{guild_id}/members/@me/nick"); + public static final Route PRUNABLE_COUNT = new Route(GET, "guilds/{guild_id}/prune"); + public static final Route PRUNE_MEMBERS = new Route(POST, "guilds/{guild_id}/prune"); + public static final Route GET_WEBHOOKS = new Route(GET, "guilds/{guild_id}/webhooks"); + public static final Route GET_GUILD_EMBED = new Route(GET, "guilds/{guild_id}/embed"); + public static final Route MODIFY_GUILD_EMBED = new Route(PATCH, "guilds/{guild_id}/embed"); + public static final Route GET_GUILD_EMOTES = new Route(GET, "guilds/{guild_id}/emojis"); + public static final Route GET_AUDIT_LOGS = new Route(GET, "guilds/{guild_id}/audit-logs"); + public static final Route GET_VOICE_REGIONS = new Route(GET, "guilds/{guild_id}/regions"); + + public static final Route GET_INTEGRATIONS = new Route(GET, "guilds/{guild_id}/integrations"); + public static final Route CREATE_INTEGRATION = new Route(POST, "guilds/{guild_id}/integrations"); + public static final Route DELETE_INTEGRATION = new Route(DELETE, "guilds/{guild_id}/integrations/{integration_id}"); + public static final Route MODIFY_INTEGRATION = new Route(PATCH, "guilds/{guild_id}/integrations/{integration_id}"); + public static final Route SYNC_INTEGRATION = new Route(POST, "guilds/{guild_id}/integrations/{integration_id}/sync"); + + public static final Route ADD_MEMBER_ROLE = new Route(PUT, "guilds/{guild_id}/members/{user_id}/roles/{role_id}"); + public static final Route REMOVE_MEMBER_ROLE = new Route(DELETE, "guilds/{guild_id}/members/{user_id}/roles/{role_id}"); //Client Only @@ -139,51 +137,51 @@ public static class Guilds public static class Emotes { // These are all client endpoints and thus don't need defined major parameters - public static final Route MODIFY_EMOTE = new Route(PATCH, true, "guilds/{guild_id}/emojis/{emote_id}", "guild_id"); - public static final Route DELETE_EMOTE = new Route(DELETE, true, "guilds/{guild_id}/emojis/{emote_id}", "guild_id"); - public static final Route CREATE_EMOTE = new Route(POST, true, "guilds/{guild_id}/emojis", "guild_id"); + public static final Route MODIFY_EMOTE = new Route(PATCH, "guilds/{guild_id}/emojis/{emote_id}"); + public static final Route DELETE_EMOTE = new Route(DELETE, "guilds/{guild_id}/emojis/{emote_id}"); + public static final Route CREATE_EMOTE = new Route(POST, "guilds/{guild_id}/emojis"); - public static final Route GET_EMOTES = new Route(GET, true, "guilds/{guild_id}/emojis", "guild_id"); - public static final Route GET_EMOTE = new Route(GET, true, "guilds/{guild_id}/emojis/{emoji_id}", "guild_id"); + public static final Route GET_EMOTES = new Route(GET, "guilds/{guild_id}/emojis"); + public static final Route GET_EMOTE = new Route(GET, "guilds/{guild_id}/emojis/{emoji_id}"); } public static class Webhooks { - public static final Route GET_WEBHOOK = new Route(GET, true, "webhooks/{webhook_id}"); - public static final Route GET_TOKEN_WEBHOOK = new Route(GET, true, "webhooks/{webhook_id}/{token}"); - public static final Route DELETE_WEBHOOK = new Route(DELETE, true, "webhooks/{webhook_id}"); - public static final Route DELETE_TOKEN_WEBHOOK = new Route(DELETE, true, "webhooks/{webhook_id}/{token}"); - public static final Route MODIFY_WEBHOOK = new Route(PATCH, true, "webhooks/{webhook_id}"); - public static final Route MODIFY_TOKEN_WEBHOOK = new Route(PATCH, true, "webhooks/{webhook_id}/{token}"); + public static final Route GET_WEBHOOK = new Route(GET, "webhooks/{webhook_id}"); + public static final Route GET_TOKEN_WEBHOOK = new Route(GET, "webhooks/{webhook_id}/{token}"); + public static final Route DELETE_WEBHOOK = new Route(DELETE, "webhooks/{webhook_id}"); + public static final Route DELETE_TOKEN_WEBHOOK = new Route(DELETE, "webhooks/{webhook_id}/{token}"); + public static final Route MODIFY_WEBHOOK = new Route(PATCH, "webhooks/{webhook_id}"); + public static final Route MODIFY_TOKEN_WEBHOOK = new Route(PATCH, "webhooks/{webhook_id}/{token}"); // Separate - public static final Route EXECUTE_WEBHOOK = new Route(POST, "webhooks/{webhook_id}/{token}", "webhook_id"); - public static final Route EXECUTE_WEBHOOK_SLACK = new Route(POST, "webhooks/{webhook_id}/{token}/slack", "webhook_id"); - public static final Route EXECUTE_WEBHOOK_GITHUB = new Route(POST, "webhooks/{webhook_id}/{token}/github", "webhook_id"); + public static final Route EXECUTE_WEBHOOK = new Route(POST, "webhooks/{webhook_id}/{token}"); + public static final Route EXECUTE_WEBHOOK_SLACK = new Route(POST, "webhooks/{webhook_id}/{token}/slack"); + public static final Route EXECUTE_WEBHOOK_GITHUB = new Route(POST, "webhooks/{webhook_id}/{token}/github"); } public static class Roles { - public static final Route GET_ROLES = new Route(GET, "guilds/{guild_id}/roles", "guild_id"); - public static final Route CREATE_ROLE = new Route(POST, "guilds/{guild_id}/roles", "guild_id"); - public static final Route GET_ROLE = new Route(GET, "guilds/{guild_id}/roles/{role_id}", "guild_id"); - public static final Route MODIFY_ROLE = new Route(PATCH, "guilds/{guild_id}/roles/{role_id}", "guild_id"); - public static final Route DELETE_ROLE = new Route(DELETE, "guilds/{guild_id}/roles/{role_id}", "guild_id"); + public static final Route GET_ROLES = new Route(GET, "guilds/{guild_id}/roles"); + public static final Route CREATE_ROLE = new Route(POST, "guilds/{guild_id}/roles"); + public static final Route GET_ROLE = new Route(GET, "guilds/{guild_id}/roles/{role_id}"); + public static final Route MODIFY_ROLE = new Route(PATCH, "guilds/{guild_id}/roles/{role_id}"); + public static final Route DELETE_ROLE = new Route(DELETE, "guilds/{guild_id}/roles/{role_id}"); } public static class Channels { - public static final Route DELETE_CHANNEL = new Route(DELETE, true, "channels/{channel_id}", "channel_id"); - public static final Route MODIFY_CHANNEL = new Route(PATCH, true, "channels/{channel_id}", "channel_id"); - public static final Route GET_WEBHOOKS = new Route(GET, true, "channels/{channel_id}/webhooks", "channel_id"); - public static final Route CREATE_WEBHOOK = new Route(POST, true, "channels/{channel_id}/webhooks", "channel_id"); - public static final Route CREATE_PERM_OVERRIDE = new Route(PUT, true, "channels/{channel_id}/permissions/{permoverride_id}", "channel_id"); - public static final Route MODIFY_PERM_OVERRIDE = new Route(PUT, true, "channels/{channel_id}/permissions/{permoverride_id}", "channel_id"); - public static final Route DELETE_PERM_OVERRIDE = new Route(DELETE, true, "channels/{channel_id}/permissions/{permoverride_id}", "channel_id"); - - public static final Route SEND_TYPING = new Route(POST, "channels/{channel_id}/typing", "channel_id"); - public static final Route GET_PERMISSIONS = new Route(GET, "channels/{channel_id}/permissions", "channel_id"); - public static final Route GET_PERM_OVERRIDE = new Route(GET, "channels/{channel_id}/permissions/{permoverride_id}", "channel_id"); + public static final Route DELETE_CHANNEL = new Route(DELETE, "channels/{channel_id}"); + public static final Route MODIFY_CHANNEL = new Route(PATCH, "channels/{channel_id}"); + public static final Route GET_WEBHOOKS = new Route(GET, "channels/{channel_id}/webhooks"); + public static final Route CREATE_WEBHOOK = new Route(POST, "channels/{channel_id}/webhooks"); + public static final Route CREATE_PERM_OVERRIDE = new Route(PUT, "channels/{channel_id}/permissions/{permoverride_id}"); + public static final Route MODIFY_PERM_OVERRIDE = new Route(PUT, "channels/{channel_id}/permissions/{permoverride_id}"); + public static final Route DELETE_PERM_OVERRIDE = new Route(DELETE, "channels/{channel_id}/permissions/{permoverride_id}"); + + public static final Route SEND_TYPING = new Route(POST, "channels/{channel_id}/typing"); + public static final Route GET_PERMISSIONS = new Route(GET, "channels/{channel_id}/permissions"); + public static final Route GET_PERM_OVERRIDE = new Route(GET, "channels/{channel_id}/permissions/{permoverride_id}"); // Client Only public static final Route GET_RECIPIENTS = new Route(GET, "channels/{channel_id}/recipients"); @@ -196,23 +194,23 @@ public static class Channels public static class Messages { - public static final Route SEND_MESSAGE = new Route(POST, "channels/{channel_id}/messages", "channel_id"); - public static final Route EDIT_MESSAGE = new Route(PATCH, "channels/{channel_id}/messages/{message_id}", "channel_id"); - public static final Route GET_PINNED_MESSAGES = new Route(GET, "channels/{channel_id}/pins", "channel_id"); - public static final Route ADD_PINNED_MESSAGE = new Route(PUT, "channels/{channel_id}/pins/{message_id}", "channel_id"); - public static final Route REMOVE_PINNED_MESSAGE = new Route(DELETE, "channels/{channel_id}/pins/{message_id}", "channel_id"); + public static final Route EDIT_MESSAGE = new Route(PATCH, "channels/{channel_id}/messages/{message_id}"); // requires special handling, same bucket but different endpoints + public static final Route SEND_MESSAGE = new Route(POST, "channels/{channel_id}/messages"); + public static final Route GET_PINNED_MESSAGES = new Route(GET, "channels/{channel_id}/pins"); + public static final Route ADD_PINNED_MESSAGE = new Route(PUT, "channels/{channel_id}/pins/{message_id}"); + public static final Route REMOVE_PINNED_MESSAGE = new Route(DELETE, "channels/{channel_id}/pins/{message_id}"); - public static final Route ADD_REACTION = new ReactionRoute(PUT); - public static final Route REMOVE_REACTION = new ReactionRoute(DELETE); - public static final Route REMOVE_ALL_REACTIONS = new Route(DELETE, "channels/{channel_id}/messages/{message_id}/reactions", "channel_id"); - public static final Route GET_REACTION_USERS = new Route(GET, "channels/{channel_id}/messages/{message_id}/reactions/{reaction_code}", "channel_id"); + public static final Route ADD_REACTION = new Route(PUT, "channels/{channel_id}/messages/{message_id}/reactions/{reaction_code}/{user_id}"); + public static final Route REMOVE_REACTION = new Route(DELETE, "channels/{channel_id}/messages/{message_id}/reactions/{reaction_code}/{user_id}"); + public static final Route REMOVE_ALL_REACTIONS = new Route(DELETE, "channels/{channel_id}/messages/{message_id}/reactions"); + public static final Route GET_REACTION_USERS = new Route(GET, "channels/{channel_id}/messages/{message_id}/reactions/{reaction_code}"); - public static final Route DELETE_MESSAGE = new DeleteMessageRoute(); - public static final Route GET_MESSAGE_HISTORY = new Route(GET, true, "channels/{channel_id}/messages", "channel_id"); + public static final Route DELETE_MESSAGE = new Route(DELETE, "channels/{channel_id}/messages/{message_id}"); + public static final Route GET_MESSAGE_HISTORY = new Route(GET, "channels/{channel_id}/messages"); //Bot only - public static final Route GET_MESSAGE = new Route(GET, true, "channels/{channel_id}/messages/{message_id}", "channel_id"); - public static final Route DELETE_MESSAGES = new Route(POST, "channels/{channel_id}/messages/bulk-delete", "channel_id"); + public static final Route GET_MESSAGE = new Route(GET, "channels/{channel_id}/messages/{message_id}"); + public static final Route DELETE_MESSAGES = new Route(POST, "channels/{channel_id}/messages/bulk-delete"); //Client only public static final Route ACK_MESSAGE = new Route(POST, "channels/{channel_id}/messages/{message_id}/ack"); @@ -220,79 +218,65 @@ public static class Messages public static class Invites { - public static final Route GET_INVITE = new Route(GET, true, "invites/{code}"); - public static final Route GET_GUILD_INVITES = new Route(GET, true, "guilds/{guild_id}/invites", "guild_id"); - public static final Route GET_CHANNEL_INVITES = new Route(GET, true, "channels/{channel_id}/invites", "channel_id"); - public static final Route CREATE_INVITE = new Route(POST, "channels/{channel_id}/invites", "channel_id"); - public static final Route DELETE_INVITE = new Route(DELETE, "invites/{code}"); + public static final Route GET_INVITE = new Route(GET, "invites/{code}"); + public static final Route GET_GUILD_INVITES = new Route(GET, "guilds/{guild_id}/invites"); + public static final Route GET_CHANNEL_INVITES = new Route(GET, "channels/{channel_id}/invites"); + public static final Route CREATE_INVITE = new Route(POST, "channels/{channel_id}/invites"); + public static final Route DELETE_INVITE = new Route(DELETE, "invites/{code}"); } - public static class Custom + @Nonnull + public static Route custom(@Nonnull Method method, @Nonnull String route) { - public static final Route DELETE_ROUTE = new Route(DELETE, "{}"); - public static final Route GET_ROUTE = new Route(GET, "{}"); - public static final Route POST_ROUTE = new Route(POST, "{}"); - public static final Route PUT_ROUTE = new Route(PUT, "{}"); - public static final Route PATCH_ROUTE = new Route(PATCH, "{}"); + Checks.notNull(method, "Method"); + Checks.notEmpty(route, "Route"); + Checks.noWhitespace(route, "Route"); + return new Route(method, route); } - private final String route; - private final String ratelimitRoute; - private final String compilableRoute; - private final int paramCount; - private final Method method; - private final List majorParamIndexes = new ArrayList<>(); - private final boolean missingHeaders; + @Nonnull + public static Route delete(@Nonnull String route) + { + return custom(DELETE, route); + } + + @Nonnull + public static Route post(@Nonnull String route) + { + return custom(POST, route); + } + + @Nonnull + public static Route put(@Nonnull String route) + { + return custom(PUT, route); + } + + @Nonnull + public static Route patch(@Nonnull String route) + { + return custom(PATCH, route); + } - private Route(Method method, String route, String... majorParameters) + @Nonnull + public static Route get(@Nonnull String route) { - this(method, false, route, majorParameters); + return custom(GET, route); } - private Route(Method method, boolean missingHeaders, String route, String... majorParameters) + private static final String majorParameters = "guild_id:channel_id:webhook_id"; + private final String route; + private final Method method; + private final int paramCount; + + private Route(Method method, String route) { this.method = method; - this.missingHeaders = missingHeaders; this.route = route; this.paramCount = Helpers.countMatches(route, '{'); //All parameters start with { if (paramCount != Helpers.countMatches(route, '}')) throw new IllegalArgumentException("An argument does not have both {}'s for route: " + method + " " + route); - - //Create a String.format compilable route for parameter compiling. - compilableRoute = route.replaceAll("\\{.*?\\}","%s"); - - //If this route has major parameters that are unique markers for the ratelimit route, then we need to - // create a ratelimit compilable route. This goes through and replaces the parameters specified by majorParameters - // and records the parameter index so that when we compile it later we can select the proper parameters - // from the ones provided to make sure we inject in the proper indexes. - if (majorParameters.length != 0) - { - int paramIndex = 0; - String replaceRoute = route; - Pattern keyP = Pattern.compile("\\{(.*?)\\}"); - Matcher keyM = keyP.matcher(route); - //Search the route for all parameters - while(keyM.find()) - { - String param = keyM.group(1); - //Attempt to match the found parameter with any of our majorParameters - for (String majorParam : majorParameters) - { - //If the parameter is a major parameter, replace it with a string token and record its - // parameter index for later ratelimitRoute compiling. - if (param.equals(majorParam)) - { - replaceRoute = replaceRoute.replace(keyM.group(0), "%s"); - majorParamIndexes.add(paramIndex); - } - } - paramIndex++; - } - ratelimitRoute = replaceRoute; - } - else - ratelimitRoute = route; } public Method getMethod() @@ -305,21 +289,6 @@ public String getRoute() return route; } - public boolean isMissingHeaders() - { - return missingHeaders; - } - - public String getRatelimitRoute() - { - return ratelimitRoute; - } - - public String getCompilableRoute() - { - return compilableRoute; - } - public int getParamCount() { return paramCount; @@ -334,23 +303,22 @@ public CompiledRoute compile(String... params) } //Compile the route for interfacing with discord. - String compiledRoute = String.format(compilableRoute, (Object[]) params); - String compiledRatelimitRoute = getRatelimitRoute(); - //If this route has major parameters which help to uniquely distinguish it from others of this route type then - // compile it using the major parameter indexes we discovered in the constructor. - if (!majorParamIndexes.isEmpty()) + StringBuilder majorParameter = new StringBuilder(majorParameters); + StringBuilder compiledRoute = new StringBuilder(route); + for (int i = 0; i < paramCount; i++) { - - String[] majorParams = new String[majorParamIndexes.size()]; - for (int i = 0; i < majorParams.length; i++) - { - majorParams[i] = params[majorParamIndexes.get(i)]; - } - compiledRatelimitRoute = String.format(compiledRatelimitRoute, (Object[]) majorParams); + int paramStart = compiledRoute.indexOf("{"); + int paramEnd = compiledRoute.indexOf("}"); + String paramName = compiledRoute.substring(paramStart+1, paramEnd); + int majorParamIndex = majorParameter.indexOf(paramName); + if (majorParamIndex > -1) + majorParameter.replace(majorParamIndex, majorParamIndex + paramName.length(), params[i]); + + compiledRoute.replace(paramStart, paramEnd + 1, params[i]); } - return new CompiledRoute(this, compiledRatelimitRoute, compiledRoute); + return new CompiledRoute(this, compiledRoute.toString(), majorParameter.toString()); } @Override @@ -372,29 +340,31 @@ public boolean equals(Object o) @Override public String toString() { - return "Route(" + method + ": " + route + ")"; + return method + "/" + route; } public class CompiledRoute { private final Route baseRoute; - private final String ratelimitRoute; + private final String major; private final String compiledRoute; private final boolean hasQueryParams; - private CompiledRoute(Route baseRoute, String ratelimitRoute, String compiledRoute, boolean hasQueryParams) + private CompiledRoute(Route baseRoute, String compiledRoute, String major, boolean hasQueryParams) { this.baseRoute = baseRoute; - this.ratelimitRoute = ratelimitRoute; this.compiledRoute = compiledRoute; + this.major = major; this.hasQueryParams = hasQueryParams; } - private CompiledRoute(Route baseRoute, String ratelimitRoute, String compiledRoute) + private CompiledRoute(Route baseRoute, String compiledRoute, String major) { - this(baseRoute, ratelimitRoute, compiledRoute, false); + this(baseRoute, compiledRoute, major, false); } + @Nonnull + @CheckReturnValue public CompiledRoute withQueryParams(String... params) { Checks.check(params.length >= 2, "params length must be at least 2"); @@ -405,12 +375,12 @@ public CompiledRoute withQueryParams(String... params) for (int i = 0; i < params.length; i++) newRoute.append(!hasQueryParams && i == 0 ? '?' : '&').append(params[i]).append('=').append(params[++i]); - return new CompiledRoute(baseRoute, ratelimitRoute, newRoute.toString(), true); + return new CompiledRoute(baseRoute, newRoute.toString(), major, true); } - public String getRatelimitRoute() + public String getMajorParameters() { - return ratelimitRoute; + return major; } public String getCompiledRoute() @@ -451,45 +421,4 @@ public String toString() return "CompiledRoute(" + method + ": " + compiledRoute + ")"; } } - - //edit message uses a different rate-limit bucket as delete message and thus we need a special handling - - /* - From the docs: - - There is currently a single exception to the above rule regarding different HTTP methods sharing the same rate limit, - and that is for the deletion of messages. - Deleting messages falls under a separate, higher rate limit so that bots are able - to more quickly delete content from channels (which is useful for moderation bots). - - As of 1st of September 2018 - */ - private static class DeleteMessageRoute extends Route - { - private DeleteMessageRoute() - { - super(DELETE, true, "channels/{channel_id}/messages/{message_id}", "channel_id"); - } - - @Override - public String getRatelimitRoute() - { - return "channels/%s/messages/{message_id}/delete"; //the additional "/delete" forces a new bucket - } - } - - // This endpoint shares the rate-limit bucket with REMOVE_ALL_REACTIONS - private static class ReactionRoute extends Route - { - private ReactionRoute(Method method) - { - super(method, "channels/{channel_id}/messages/{message_id}/reactions/{reaction_code}/{user_id}", "channel_id"); - } - - @Override - public String getRatelimitRoute() - { - return "channels/%s/messages/{message_id}/reactions"; - } - } } diff --git a/src/main/java/net/dv8tion/jda/internal/requests/WebSocketClient.java b/src/main/java/net/dv8tion/jda/internal/requests/WebSocketClient.java index f689fba7fe..009b264e18 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/WebSocketClient.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/WebSocketClient.java @@ -302,7 +302,7 @@ protected synchronized void connect() { case ZLIB: if (decompressor == null || decompressor.getType() != Compression.ZLIB) - decompressor = new ZlibDecompressor(); + decompressor = new ZlibDecompressor(api.getMaxBufferSize()); break; default: throw new IllegalStateException("Unknown compression"); @@ -350,7 +350,7 @@ public void onConnected(WebSocket websocket, Map> headers) else LOG.debug("Connected to WebSocket"); connected = true; - reconnectTimeoutS = 2; + //reconnectTimeoutS = 2; We will reset this when the session was started successfully (ready/resume) messagesSent.set(0); ratelimitResetTime = System.currentTimeMillis() + 60000; if (sessionId == null) @@ -538,6 +538,8 @@ public void reconnect(boolean callFromQueue) throws InterruptedException { api.setStatus(JDA.Status.WAITING_TO_RECONNECT); int delay = reconnectTimeoutS; + // Exponential backoff, reset on session creation (ready/resume) + reconnectTimeoutS = Math.min(reconnectTimeoutS << 1, api.getMaxReconnectDelay()); Thread.sleep(delay * 1000); handleIdentifyRateLimit = false; api.setStatus(JDA.Status.ATTEMPTING_TO_RECONNECT); @@ -556,7 +558,7 @@ public void reconnect(boolean callFromQueue) throws InterruptedException } catch (RuntimeException ex) { - reconnectTimeoutS = Math.min(reconnectTimeoutS << 1, api.getMaxReconnectDelay()); + // reconnectTimeoutS = Math.min(reconnectTimeoutS << 1, api.getMaxReconnectDelay()); LOG.warn("Reconnect failed! Next attempt in {}s", reconnectTimeoutS); } } @@ -815,6 +817,7 @@ protected void onDispatch(DataObject raw) { //INIT types case "READY": + reconnectTimeoutS = 2; api.setStatus(JDA.Status.LOADING_SUBSYSTEMS); processingReady = true; handleIdentifyRateLimit = false; @@ -825,6 +828,7 @@ protected void onDispatch(DataObject raw) sessionId = content.getString("session_id"); break; case "RESUMED": + reconnectTimeoutS = 2; sentAuthInfo = true; if (!processingReady) { @@ -838,6 +842,12 @@ protected void onDispatch(DataObject raw) } break; default: + long guildId = content.getLong("guild_id", 0L); + if (api.isUnavailable(guildId) && !type.equals("GUILD_CREATE") && !type.equals("GUILD_DELETE")) + { + LOG.warn("Ignoring {} for unavailable guild with id {}. JSON: {}", type, guildId, content); + break; + } SocketHandler handler = handlers.get(type); if (handler != null) handler.handle(responseTotal, raw); diff --git a/src/main/java/net/dv8tion/jda/internal/requests/ratelimit/BotRateLimiter.java b/src/main/java/net/dv8tion/jda/internal/requests/ratelimit/BotRateLimiter.java index c1d65793be..d2b2f5c9a6 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/ratelimit/BotRateLimiter.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/ratelimit/BotRateLimiter.java @@ -16,414 +16,413 @@ package net.dv8tion.jda.internal.requests.ratelimit; -import net.dv8tion.jda.api.events.ExceptionEvent; import net.dv8tion.jda.api.requests.Request; import net.dv8tion.jda.api.utils.MiscUtil; -import net.dv8tion.jda.api.utils.data.DataObject; -import net.dv8tion.jda.internal.JDAImpl; import net.dv8tion.jda.internal.requests.RateLimiter; import net.dv8tion.jda.internal.requests.Requester; import net.dv8tion.jda.internal.requests.Route; -import net.dv8tion.jda.internal.utils.IOUtil; -import net.dv8tion.jda.internal.utils.UnlockHook; import okhttp3.Headers; +import org.jetbrains.annotations.Contract; -import java.io.IOException; -import java.io.InputStream; -import java.io.UncheckedIOException; import java.util.Iterator; +import java.util.Map; import java.util.Queue; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.*; import java.util.concurrent.locks.ReentrantLock; +/* + +** How does it work? ** + +A bucket is determined via the Path+Method+Major in the following way: + + 1. Get Hash from Path+Method (we call this route) + 2. Get bucket from Hash+Major (we call this bucketid) + +If no hash is known we default to the constant "unlimited" hash. The hash is loaded from HTTP responses using the "X-RateLimit-Bucket" response header. +This hash is per Method+Path and can be stored indefinitely once received. +Some endpoints don't return a hash, this means that the endpoint is **unlimited** and will be in queue with only the major parameter. + +To explain this further, lets look at the example of message history. The endpoint to fetch message history is "GET/channels/{channel.id}/messages". +This endpoint does not have any rate limit (unlimited) and will thus use the hash "unlimited+GET/channels/{channel.id}/messages". +The bucket id for this will be "unlimited+GET/channels/{channel.id}/messages:guild_id:{channel.id}:webhook_id" where "{channel.id}" would be replaced with the respective id. +This means you can fetch history concurrently for multiple channels but it will be in sequence for the same channel. + +If the endpoint is not unlimited we will receive a hash on the first response. +Once this happens every unlimited bucket will start moving its queue to the correct bucket. +This is done during the queue work iteration so many requests to one endpoint would be moved correctly. + +For example, the first message sending: + + public void onReady(ReadyEvent event) { + TextChannel channel = event.getJDA().getTextChannelById("123"); + for (int i = 1; i <= 100; i++) { + channel.sendMessage("Message: " + i).queue(); + } + } + +This will send 100 messages on startup. At this point we don't yet know the hash for this route so we put them all in "unlimited+POST/channels/{channel.id}/messages:guild_id:123:webhook_id". +The bucket iterates the requests in sync and gets the first response. This response provides the hash for this route and we create a bucket for it. +Once the response is handled we continue with the next request in the unlimited bucket and notice the new bucket. We then move all related requests to this bucket. + + */ public class BotRateLimiter extends RateLimiter { private static final String RESET_AFTER_HEADER = "X-RateLimit-Reset-After"; private static final String RESET_HEADER = "X-RateLimit-Reset"; private static final String LIMIT_HEADER = "X-RateLimit-Limit"; private static final String REMAINING_HEADER = "X-RateLimit-Remaining"; + private static final String GLOBAL_HEADER = "X-RateLimit-Global"; + private static final String HASH_HEADER = "X-RateLimit-Bucket"; + private static final String RETRY_AFTER_HEADER = "Retry-After"; + private static final String UNLIMITED_BUCKET = "unlimited"; // we generate an unlimited bucket for every major parameter configuration + + private final ReentrantLock bucketLock = new ReentrantLock(); + // Route -> Hash + private final Map hash = new ConcurrentHashMap<>(); + // Hash + Major Parameter -> Bucket + private final Map bucket = new ConcurrentHashMap<>(); + // Bucket -> Rate-Limit Worker + private final Map> rateLimitQueue = new ConcurrentHashMap<>(); + private Future cleanupWorker; public BotRateLimiter(Requester requester) { super(requester); } + @Override + public void init() + { + cleanupWorker = getScheduler().scheduleAtFixedRate(this::cleanup, 30, 30, TimeUnit.SECONDS); + } + + private ScheduledExecutorService getScheduler() + { + return requester.getJDA().getRateLimitPool(); + } + + private void cleanup() + { + // This will remove buckets that are no longer needed every 30 seconds to avoid memory leakage + // We will keep the hashes in memory since they are very limited (by the amount of possible routes) + MiscUtil.locked(bucketLock, () -> { + int size = bucket.size(); + Iterator> entries = bucket.entrySet().iterator(); + + while (entries.hasNext()) + { + Map.Entry entry = entries.next(); + String key = entry.getKey(); + Bucket bucket = entry.getValue(); + if (bucket.isUnlimited() && bucket.requests.isEmpty()) + entries.remove(); // remove unlimited if requests are empty + // If the requests of the bucket are drained and the reset is expired the bucket has no valuable information + else if (bucket.requests.isEmpty() && bucket.reset <= getNow()) + entries.remove(); + } + // Log how many buckets were removed + size -= bucket.size(); + if (size > 0) + log.debug("Removed {} expired buckets", size); + }); + } + + private String getRouteHash(Route route) + { + return hash.getOrDefault(route, UNLIMITED_BUCKET + "+" + route); + } + + @Override + protected void shutdown() + { + super.shutdown(); + if (cleanupWorker != null) + cleanupWorker.cancel(false); + } + @Override public Long getRateLimit(Route.CompiledRoute route) { - Bucket bucket = getBucket(route); - synchronized (bucket) - { - return bucket.getRateLimit(); - } + Bucket bucket = getBucket(route, false); + return bucket == null ? 0L : bucket.getRateLimit(); } @Override + @SuppressWarnings("rawtypes") protected void queueRequest(Request request) { - Bucket bucket = getBucket(request.getRoute()); - synchronized (bucket) - { - bucket.addToQueue(request); - } + // Create bucket and enqueue request + MiscUtil.locked(bucketLock, () -> { + Bucket bucket = getBucket(request.getRoute(), true); + bucket.enqueue(request); + runBucket(bucket); + }); } @Override protected Long handleResponse(Route.CompiledRoute route, okhttp3.Response response) { - Bucket bucket = getBucket(route); - synchronized (bucket) + bucketLock.lock(); + try { - Headers headers = response.headers(); - int code = response.code(); + long rateLimit = updateBucket(route, response).getRateLimit(); + if (response.code() == 429) + return rateLimit; + else + return null; + } + finally + { + bucketLock.unlock(); + } + } - if (code == 429) + private Bucket updateBucket(Route.CompiledRoute route, okhttp3.Response response) + { + return MiscUtil.locked(bucketLock, () -> { + try { - String global = headers.get("X-RateLimit-Global"); - String retry = headers.get("Retry-After"); - if (retry == null || retry.isEmpty()) + Bucket bucket = getBucket(route, true); + Headers headers = response.headers(); + + boolean wasUnlimited = bucket.isUnlimited(); + boolean global = headers.get(GLOBAL_HEADER) != null; + String hash = headers.get(HASH_HEADER); + long now = getNow(); + + // Create a new bucket for the hash if needed + Route baseRoute = route.getBaseRoute(); + if (hash != null) { - try (InputStream in = IOUtil.getBody(response)) + if (!this.hash.containsKey(baseRoute)) { - DataObject limitObj = DataObject.fromJson(in); - retry = limitObj.get("retry_after").toString(); - } - catch (IOException e) - { - throw new UncheckedIOException(e); + this.hash.put(baseRoute, hash); + log.debug("Caching bucket hash {} -> {}", baseRoute, hash); } + + bucket = getBucket(route, true); } - long retryAfter = Long.parseLong(retry); - if (Boolean.parseBoolean(global)) //global ratelimit + + // Handle global rate limit if necessary + if (global) { - //If it is global, lock down the threads. - log.warn("Encountered global rate-limit! Retry-After: {}", retryAfter); - requester.getJDA().getSessionController().setGlobalRatelimit(getNow() + retryAfter); + String retryAfterHeader = headers.get(RETRY_AFTER_HEADER); + long retryAfter = parseLong(retryAfterHeader); + requester.getJDA().getSessionController().setGlobalRatelimit(now + retryAfter); + log.error("Encountered global rate limit! Retry-After: {} ms", retryAfter); } - else + // Handle hard rate limit, pretty much just log that it happened + else if (response.code() == 429) { - log.warn("Encountered 429 on route /{}", bucket.getRoute()); - updateBucket(bucket, headers, retryAfter); + // Update the bucket to the new information + String retryAfterHeader = headers.get(RETRY_AFTER_HEADER); + long retryAfter = parseLong(retryAfterHeader); + bucket.remaining = 0; + bucket.reset = getNow() + retryAfter; + // don't log warning if we are switching bucket, this means it was an issue with an un-hashed route that is now resolved + if (hash == null || !wasUnlimited) + log.warn("Encountered 429 on route {} with bucket {} Retry-After: {} ms", baseRoute, bucket.bucketId, retryAfter); + else + log.debug("Encountered 429 on route {} with bucket {} Retry-After: {} ms", baseRoute, bucket.bucketId, retryAfter); + return bucket; } - return retryAfter; + // If hash is null this means we didn't get enough information to update a bucket + if (hash == null) + return bucket; + + // Update the bucket parameters with new information + String limitHeader = headers.get(LIMIT_HEADER); + String remainingHeader = headers.get(REMAINING_HEADER); + String resetAfterHeader = headers.get(RESET_AFTER_HEADER); + String resetHeader = headers.get(RESET_HEADER); + + bucket.limit = (int) Math.max(1L, parseLong(limitHeader)); + bucket.remaining = (int) parseLong(remainingHeader); + if (requester.getJDA().isRelativeRateLimit()) + bucket.reset = now + parseDouble(resetAfterHeader); + else + bucket.reset = parseDouble(resetHeader); + log.trace("Updated bucket {} to ({}/{}, {})", bucket.bucketId, bucket.remaining, bucket.limit, bucket.reset - now); + return bucket; } - else + catch (Exception e) { - updateBucket(bucket, headers, -1); - return null; + Bucket bucket = getBucket(route, true); + log.error("Encountered Exception while updating a bucket. Route: {} Bucket: {} Code: {} Headers:\n{}", + route.getBaseRoute(), bucket, response.code(), response.headers(), e); + return bucket; } - } - + }); } - private Bucket getBucket(Route.CompiledRoute route) + @Contract("_,true->!null") + private Bucket getBucket(Route.CompiledRoute route, boolean create) { - String rateLimitRoute = route.getRatelimitRoute(); - Bucket bucket = (Bucket) buckets.get(rateLimitRoute); - if (bucket == null) + return MiscUtil.locked(bucketLock, () -> { - synchronized (buckets) - { - bucket = (Bucket) buckets.get(rateLimitRoute); - if (bucket == null) - { - Route baseRoute = route.getBaseRoute(); - bucket = new Bucket(rateLimitRoute, baseRoute.isMissingHeaders()); - buckets.put(rateLimitRoute, bucket); - } - } - } - return bucket; + // Retrieve the hash via the route + String hash = getRouteHash(route.getBaseRoute()); + // Get or create a bucket for the hash + major parameters + String bucketId = hash + ":" + route.getMajorParameters(); + Bucket bucket = this.bucket.get(bucketId); + if (bucket == null && create) + this.bucket.put(bucketId, bucket = new Bucket(bucketId)); + + return bucket; + }); } - public long getNow() + private void runBucket(Bucket bucket) { - return System.currentTimeMillis(); + if (isShutdown) + return; + // Schedule a new bucket worker if no worker is running + MiscUtil.locked(bucketLock, () -> + rateLimitQueue.computeIfAbsent(bucket, + (k) -> getScheduler().schedule(bucket, bucket.getRateLimit(), TimeUnit.MILLISECONDS))); } - private void updateBucket(Bucket bucket, Headers headers, long retryAfter) + private long parseLong(String input) { - int headerCount = 0; - if (retryAfter > 0) - { - bucket.resetTime = getNow() + retryAfter; - bucket.routeUsageRemaining = 0; - } - - // relative = reset-after - if (requester.getJDA().isRelativeRateLimit()) - headerCount += parseDouble(headers.get(RESET_AFTER_HEADER), bucket, (time, b) -> b.resetTime = getNow() + (long) (time * 1000)); //Seconds to milliseconds - else // absolute = reset - headerCount += parseDouble(headers.get(RESET_HEADER), bucket, (time, b) -> b.resetTime = (long) (time * 1000)); //Seconds to milliseconds - - headerCount += parseInt(headers.get(LIMIT_HEADER), bucket, (limit, b) -> b.routeUsageLimit = limit); - - //Currently, we check the remaining amount even for hardcoded ratelimits just to further respect Discord - // however, if there should ever be a case where Discord informs that the remaining is less than what - // it actually is and we add a custom ratelimit to handle that, we will need to instead move this to the - // above else statement and add a bucket.routeUsageRemaining-- decrement to the above if body. - //An example of this statement needing to be moved would be if the custom ratelimit reset time interval is - // equal to or greater than 1000ms, and the remaining count provided by discord is less than the ACTUAL - // amount that their systems allow in such a way that isn't a bug. - //The custom ratelimit system is primarily for ratelimits that can't be properly represented by Discord's - // header system due to their headers only supporting accuracy to the second. The custom ratelimit system - // allows for hardcoded ratelimits that allow accuracy to the millisecond which is important for some - // ratelimits like Reactions which is 1/0.25s, but discord reports the ratelimit as 1/1s with headers. - headerCount += parseInt(headers.get(REMAINING_HEADER), bucket, (remaining, b) -> b.routeUsageRemaining = remaining); - if (!bucket.missingHeaders && headerCount < 3) - { - log.debug("Encountered issue with headers when updating a bucket\n" + - "Route: {}\nHeaders: {}", bucket.getRoute(), headers); - } + return input == null ? 0L : Long.parseLong(input); } - private int parseInt(String input, Bucket bucket, IntObjectConsumer consumer) + private long parseDouble(String input) { - try - { - int parsed = Integer.parseInt(input); - consumer.accept(parsed, bucket); - return 1; - } - catch (NumberFormatException ignored) {} - catch (Exception e) - { - log.error("Uncaught exception parsing header value: {}", input, e); - } - return 0; + //The header value is using a double to represent milliseconds and seconds: + // 5.250 this is 5 seconds and 250 milliseconds (5250 milliseconds) + return input == null ? 0L : (long) (Double.parseDouble(input) * 1000); } - private int parseDouble(String input, Bucket bucket, DoubleObjectConsumer consumer) + + public long getNow() { - try - { - double parsed = Double.parseDouble(input); - consumer.accept(parsed, bucket); - return 1; - } - catch (NumberFormatException | NullPointerException ignored) {} - catch (Exception e) - { - log.error("Uncaught exception parsing header value: {}", input, e); - } - return 0; + return System.currentTimeMillis(); } + @SuppressWarnings("rawtypes") private class Bucket implements IBucket, Runnable { - final String route; - final boolean missingHeaders; - final ConcurrentLinkedQueue requests = new ConcurrentLinkedQueue<>(); - final ReentrantLock requestLock = new ReentrantLock(); - volatile boolean processing = false; - volatile long resetTime = 0; - volatile int routeUsageRemaining = 1; //These are default values to only allow 1 request until we have properly - volatile int routeUsageLimit = 1; // ratelimit information. - - public Bucket(String route, boolean missingHeaders) + private final String bucketId; + private final Queue requests = new ConcurrentLinkedQueue<>(); + + private long reset = 0; + private int remaining = 1; + private int limit = 1; + + public Bucket(String bucketId) { - this.route = route; - this.missingHeaders = missingHeaders; + this.bucketId = bucketId; } - void addToQueue(Request request) + public void enqueue(Request request) { requests.add(request); - submitForProcessing(); } - void submitForProcessing() + public long getRateLimit() { - synchronized (submittedBuckets) + long now = getNow(); + long global = requester.getJDA().getSessionController().getGlobalRatelimit(); + // Global rate limit is more important to handle + if (global > now) + return global - now; + // Check if the bucket reset time has expired + if (reset <= now) { - if (!submittedBuckets.contains(this)) - { - Long delay = getRateLimit(); - if (delay == null) - delay = 0L; - - if (delay > 0) - { - log.debug("Backing off {} milliseconds on route /{}", delay, getRoute()); - requester.getJDA().getRateLimitPool().schedule(this, delay, TimeUnit.MILLISECONDS); - } - else - { - requester.getJDA().getRateLimitPool().execute(this); - } - submittedBuckets.add(this); - } + // Update the remaining uses to the limit (we don't know better) + remaining = limit; + return 0L; } - } - Long getRateLimit() - { - long gCooldown = requester.getJDA().getSessionController().getGlobalRatelimit(); - if (gCooldown > 0) //Are we on global cooldown? - { - long now = getNow(); - if (now > gCooldown) //Verify that we should still be on cooldown. - { - //If we are done cooling down, reset the globalCooldown and continue. - requester.getJDA().getSessionController().setGlobalRatelimit(Long.MIN_VALUE); - } - else - { - //If we should still be on cooldown, return when we can go again. - return gCooldown - now; - } - } - if (this.routeUsageRemaining <= 0) - { - if (getNow() > this.resetTime) - { - this.routeUsageRemaining = this.routeUsageLimit; - this.resetTime = 0; - } - } - if (this.routeUsageRemaining > 0) - return null; - else - return this.resetTime - getNow(); + // If there are remaining requests we don't need to do anything, otherwise return backoff in milliseconds + return remaining < 1 ? reset - now : 0L; } - @Override - public boolean equals(Object o) + public long getReset() { - if (!(o instanceof Bucket)) - return false; - - Bucket oBucket = (Bucket) o; - return route.equals(oBucket.route); + return reset; } - @Override - public int hashCode() + public int getRemaining() { - return route.hashCode(); + return remaining; } - private void handleResponse(Iterator it, Long retryAfter) + public int getLimit() { - if (retryAfter == null) - { - // We were not rate-limited! Then just continue with the rest of the requests - it.remove(); - processIterator(it); - } - else - { - // Rate-limited D: Guess we have to backoff for now - finishProcess(); - } + return limit; } - private void processIterator(Iterator it) + private boolean isUnlimited() { - Request request = null; - try - { - do - { - if (!it.hasNext()) - { - // End loop, no more requests left - finishProcess(); - return; - } - request = it.next(); - } while (isSkipped(it, request)); - - CompletableFuture handle = requester.execute(request); - final Request request0 = request; - // Hook the callback for the request - handle.whenComplete((retryAfter, error) -> - { - requester.setContext(); - if (error != null) - { - // There was an error, handle it and continue with the next request - log.error("Requester system encountered internal error", error); - it.remove(); - request0.onFailure(error); - processIterator(it); - } - else - { - // Handle the response and backoff if necessary - handleResponse(it, retryAfter); - } - }); - } - catch (Throwable t) - { - log.error("Requester system encountered an internal error", t); - it.remove(); - if (request != null) - request.onFailure(t); - // don't forget to end the loop and start over - finishProcess(); - } + return bucketId.startsWith("unlimited"); } - private void finishProcess() + private void backoff() { - // We are done with processing - MiscUtil.locked(requestLock, () -> - { - processing = false; - }); - // Re-submit if new requests were added or rate-limit was hit - synchronized (submittedBuckets) - { - submittedBuckets.remove(this); + // Schedule backoff if requests are not done + MiscUtil.locked(bucketLock, () -> { + rateLimitQueue.remove(this); if (!requests.isEmpty()) - { - try - { - this.submitForProcessing(); - } - catch (RejectedExecutionException e) - { - log.debug("Caught RejectedExecutionException when re-queuing a ratelimited request. The requester is probably shutdown, thus, this can be ignored."); - } - } - } + runBucket(this); + }); } @Override public void run() { - requester.setContext(); - requestLock.lock(); - try (UnlockHook hook = new UnlockHook(requestLock)) - { - // Ensure the processing is synchronized - if (processing) - return; - processing = true; - // Start processing loop - Iterator it = requests.iterator(); - processIterator(it); - } - catch (Throwable err) + log.trace("Bucket {} is running {} requests", bucketId, requests.size()); + Iterator iterator = requests.iterator(); + while (iterator.hasNext()) { - log.error("Requester system encountered an internal error from beyond the synchronized execution blocks. NOT GOOD!", err); - if (err instanceof Error) + Long rateLimit = getRateLimit(); + if (rateLimit > 0L) { - JDAImpl api = requester.getJDA(); - api.handleEvent(new ExceptionEvent(api, err, true)); + // We need to backoff since we ran out of remaining uses or hit the global rate limit + log.debug("Backing off {} ms for bucket {}", rateLimit, bucketId); + break; + } + + Request request = iterator.next(); + if (isUnlimited()) + { + boolean shouldSkip = MiscUtil.locked(bucketLock, () -> { + // Attempt moving request to correct bucket if it has been created + Bucket bucket = getBucket(request.getRoute(), true); + if (bucket != this) + { + bucket.enqueue(request); + iterator.remove(); + runBucket(bucket); + return true; + } + return false; + }); + if (shouldSkip) continue; + } + + if (isSkipped(iterator, request)) + continue; + + try + { + rateLimit = requester.execute(request); + if (rateLimit != null) + break; // this means we hit a hard rate limit (429) so the request needs to be retried + + // The request went through so we can remove it + iterator.remove(); + } + catch (Exception ex) + { + log.error("Encountered exception trying to execute request", ex); + break; } } - } - @Override - public String getRoute() - { - return route; + backoff(); } @Override @@ -431,15 +430,11 @@ public Queue getRequests() { return requests; } - } - - private interface IntObjectConsumer - { - void accept(int n, T t); - } - private interface DoubleObjectConsumer - { - void accept(double n, T t); + @Override + public String toString() + { + return bucketId; + } } } diff --git a/src/main/java/net/dv8tion/jda/internal/requests/ratelimit/ClientRateLimiter.java b/src/main/java/net/dv8tion/jda/internal/requests/ratelimit/ClientRateLimiter.java index 29830d1c2d..ca87fc2ef9 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/ratelimit/ClientRateLimiter.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/ratelimit/ClientRateLimiter.java @@ -205,7 +205,7 @@ public void run() if (isSkipped(it, request)) continue; // Blocking code because I'm lazy and client accounts are not priority - Long retryAfter = requester.execute(request).get(); + Long retryAfter = requester.execute(request); if (retryAfter != null) break; else @@ -248,12 +248,6 @@ public void run() } } - @Override - public String getRoute() - { - return route; - } - @Override public Queue getRequests() { diff --git a/src/main/java/net/dv8tion/jda/internal/requests/ratelimit/IBucket.java b/src/main/java/net/dv8tion/jda/internal/requests/ratelimit/IBucket.java index fc5b919846..fb81554070 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/ratelimit/IBucket.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/ratelimit/IBucket.java @@ -22,6 +22,5 @@ public interface IBucket { - String getRoute(); Queue getRequests(); } diff --git a/src/main/java/net/dv8tion/jda/internal/requests/restaction/ChannelActionImpl.java b/src/main/java/net/dv8tion/jda/internal/requests/restaction/ChannelActionImpl.java index 7fb09bac2f..eccf985821 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/restaction/ChannelActionImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/restaction/ChannelActionImpl.java @@ -248,7 +248,7 @@ protected RequestBody finalizeData() @Override protected void handleSuccess(Response response, Request request) { - EntityBuilder builder = api.get().getEntityBuilder(); + EntityBuilder builder = api.getEntityBuilder(); GuildChannel channel; switch (type) { diff --git a/src/main/java/net/dv8tion/jda/internal/requests/restaction/InviteActionImpl.java b/src/main/java/net/dv8tion/jda/internal/requests/restaction/InviteActionImpl.java index 7ca5a6e31c..4fbd2308ab 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/restaction/InviteActionImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/restaction/InviteActionImpl.java @@ -126,6 +126,6 @@ protected RequestBody finalizeData() @Override protected void handleSuccess(final Response response, final Request request) { - request.onSuccess(this.api.get().getEntityBuilder().createInvite(response.getObject())); + request.onSuccess(this.api.getEntityBuilder().createInvite(response.getObject())); } } diff --git a/src/main/java/net/dv8tion/jda/internal/requests/restaction/MessageActionImpl.java b/src/main/java/net/dv8tion/jda/internal/requests/restaction/MessageActionImpl.java index ca244aba2b..c035a0c690 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/restaction/MessageActionImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/restaction/MessageActionImpl.java @@ -407,7 +407,7 @@ else if (!isEmpty()) @Override protected void handleSuccess(Response response, Request request) { - request.onSuccess(api.get().getEntityBuilder().createMessage(response.getObject(), channel, false)); + request.onSuccess(api.getEntityBuilder().createMessage(response.getObject(), channel, false)); } @Override diff --git a/src/main/java/net/dv8tion/jda/internal/requests/restaction/PermissionOverrideActionImpl.java b/src/main/java/net/dv8tion/jda/internal/requests/restaction/PermissionOverrideActionImpl.java index b6a5c981b3..279828e9a5 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/restaction/PermissionOverrideActionImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/restaction/PermissionOverrideActionImpl.java @@ -24,7 +24,6 @@ import net.dv8tion.jda.api.requests.Response; import net.dv8tion.jda.api.requests.restaction.PermissionOverrideAction; import net.dv8tion.jda.api.utils.data.DataObject; -import net.dv8tion.jda.internal.entities.AbstractChannelImpl; import net.dv8tion.jda.internal.entities.PermissionOverrideImpl; import net.dv8tion.jda.internal.requests.Route; import net.dv8tion.jda.internal.utils.Checks; @@ -216,12 +215,10 @@ protected void handleSuccess(Response response, Request requ { long id = permissionHolder.getIdLong(); DataObject object = (DataObject) request.getRawBody(); - PermissionOverrideImpl override = new PermissionOverrideImpl(channel, id, permissionHolder); + PermissionOverrideImpl override = new PermissionOverrideImpl(channel, permissionHolder); override.setAllow(object.getLong("allow")); override.setDeny(object.getLong("deny")); - - ((AbstractChannelImpl) channel).getOverrideMap().put(id, override); - + //((AbstractChannelImpl) channel).getOverrideMap().put(id, override); This is added by the event later request.onSuccess(override); } } diff --git a/src/main/java/net/dv8tion/jda/internal/requests/restaction/RoleActionImpl.java b/src/main/java/net/dv8tion/jda/internal/requests/restaction/RoleActionImpl.java index 1dd2e593d0..5d62826b51 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/restaction/RoleActionImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/restaction/RoleActionImpl.java @@ -142,7 +142,7 @@ protected RequestBody finalizeData() @Override protected void handleSuccess(Response response, Request request) { - request.onSuccess(api.get().getEntityBuilder().createRole((GuildImpl) guild, response.getObject(), guild.getIdLong())); + request.onSuccess(api.getEntityBuilder().createRole((GuildImpl) guild, response.getObject(), guild.getIdLong())); } private void checkPermission(Permission permission) diff --git a/src/main/java/net/dv8tion/jda/internal/requests/restaction/WebhookActionImpl.java b/src/main/java/net/dv8tion/jda/internal/requests/restaction/WebhookActionImpl.java index af6ee4b34d..81eca4a61b 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/restaction/WebhookActionImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/restaction/WebhookActionImpl.java @@ -98,7 +98,7 @@ public RequestBody finalizeData() protected void handleSuccess(Response response, Request request) { DataObject json = response.getObject(); - Webhook webhook = api.get().getEntityBuilder().createWebhook(json); + Webhook webhook = api.getEntityBuilder().createWebhook(json); request.onSuccess(webhook); } diff --git a/src/main/java/net/dv8tion/jda/internal/requests/restaction/operator/DelayRestAction.java b/src/main/java/net/dv8tion/jda/internal/requests/restaction/operator/DelayRestAction.java new file mode 100644 index 0000000000..a9715f61a0 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/internal/requests/restaction/operator/DelayRestAction.java @@ -0,0 +1,76 @@ +/* + * Copyright 2015-2019 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.internal.requests.restaction.operator; + +import net.dv8tion.jda.api.exceptions.RateLimitedException; +import net.dv8tion.jda.api.requests.RestAction; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +public class DelayRestAction extends RestActionOperator +{ + private final TimeUnit unit; + private final long delay; + private final ScheduledExecutorService scheduler; + + public DelayRestAction(RestAction action, TimeUnit unit, long delay, ScheduledExecutorService scheduler) + { + super(action); + this.unit = unit; + this.delay = delay; + this.scheduler = scheduler == null ? action.getJDA().getRateLimitPool() : scheduler; + } + + @Override + public void queue(@Nullable Consumer success, @Nullable Consumer failure) + { + action.queue((result) -> + scheduler.schedule(() -> + doSuccess(success, result), + delay, unit), + contextWrap(failure)); + } + + @Override + public T complete(boolean shouldQueue) throws RateLimitedException + { + T result = action.complete(shouldQueue); + try + { + unit.sleep(delay); + return result; + } + catch (InterruptedException e) + { + throw new RuntimeException(e); + } + } + + @Nonnull + @Override + public CompletableFuture submit(boolean shouldQueue) + { + CompletableFuture future = new CompletableFuture<>(); + queue(future::complete, future::completeExceptionally); + return future; + } +} diff --git a/src/main/java/net/dv8tion/jda/internal/requests/restaction/operator/FlatMapRestAction.java b/src/main/java/net/dv8tion/jda/internal/requests/restaction/operator/FlatMapRestAction.java new file mode 100644 index 0000000000..f97196bad8 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/internal/requests/restaction/operator/FlatMapRestAction.java @@ -0,0 +1,70 @@ +/* + * Copyright 2015-2019 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.internal.requests.restaction.operator; + +import net.dv8tion.jda.api.exceptions.RateLimitedException; +import net.dv8tion.jda.api.requests.RestAction; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +public class FlatMapRestAction extends RestActionOperator +{ + private final Function> function; + private final Predicate condition; + + public FlatMapRestAction(RestAction action, Predicate condition, + Function> function) + { + super(action); + this.function = function; + this.condition = condition; + } + + @Override + public void queue(@Nullable Consumer success, @Nullable Consumer failure) + { + Consumer onFailure = contextWrap(failure); + action.queue((result) -> { + if (condition != null && !condition.test(result)) + return; + RestAction then = function.apply(result); + if (then == null) + doFailure(onFailure, new IllegalStateException("FlatMap operand is null")); + else + then.queue(success, onFailure); + }, onFailure); + } + + @Override + public O complete(boolean shouldQueue) throws RateLimitedException + { + return function.apply(action.complete(shouldQueue)).complete(shouldQueue); + } + + @Nonnull + @Override + public CompletableFuture submit(boolean shouldQueue) + { + return action.submit(shouldQueue) + .thenCompose((result) -> function.apply(result).submit(shouldQueue)); + } +} diff --git a/src/main/java/net/dv8tion/jda/internal/requests/restaction/operator/MapRestAction.java b/src/main/java/net/dv8tion/jda/internal/requests/restaction/operator/MapRestAction.java new file mode 100644 index 0000000000..103f7ebc9b --- /dev/null +++ b/src/main/java/net/dv8tion/jda/internal/requests/restaction/operator/MapRestAction.java @@ -0,0 +1,56 @@ +/* + * Copyright 2015-2019 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.internal.requests.restaction.operator; + +import net.dv8tion.jda.api.exceptions.RateLimitedException; +import net.dv8tion.jda.api.requests.RestAction; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Function; + +public class MapRestAction extends RestActionOperator +{ + private final Function function; + + public MapRestAction(RestAction action, Function function) + { + super(action); + this.function = function; + } + + @Override + public void queue(@Nullable Consumer success, @Nullable Consumer failure) + { + action.queue((result) -> doSuccess(success, function.apply(result)), contextWrap(failure)); + } + + @Override + public O complete(boolean shouldQueue) throws RateLimitedException + { + return function.apply(action.complete(shouldQueue)); + } + + @Nonnull + @Override + public CompletableFuture submit(boolean shouldQueue) + { + return action.submit(shouldQueue).thenApply(function); + } +} diff --git a/src/main/java/net/dv8tion/jda/internal/requests/restaction/operator/RestActionOperator.java b/src/main/java/net/dv8tion/jda/internal/requests/restaction/operator/RestActionOperator.java new file mode 100644 index 0000000000..08355e5d67 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/internal/requests/restaction/operator/RestActionOperator.java @@ -0,0 +1,76 @@ +/* + * Copyright 2015-2019 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.internal.requests.restaction.operator; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.exceptions.ContextException; +import net.dv8tion.jda.api.requests.RestAction; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; + +public abstract class RestActionOperator implements RestAction +{ + protected final RestAction action; + + public RestActionOperator(RestAction action) + { + this.action = action; + } + + protected void doSuccess(Consumer callback, E value) + { + if (callback == null) + RestAction.getDefaultSuccess().accept(value); + else + callback.accept(value); + } + + protected void doFailure(Consumer callback, Throwable throwable) + { + if (callback == null) + RestAction.getDefaultFailure().accept(throwable); + else + callback.accept(throwable); + } + + @Nonnull + @Override + public JDA getJDA() + { + return action.getJDA(); + } + + @Nonnull + @Override + public RestAction setCheck(@Nullable BooleanSupplier checks) + { + action.setCheck(checks); + return this; + } + + protected Consumer contextWrap(@Nullable Consumer callback) + { + if (callback instanceof ContextException.ContextConsumer) + return callback; + else if (RestAction.isPassContext()) + return ContextException.here(callback == null ? RestAction.getDefaultFailure() : callback); + return callback; + } +} diff --git a/src/main/java/net/dv8tion/jda/internal/requests/restaction/pagination/AuditLogPaginationActionImpl.java b/src/main/java/net/dv8tion/jda/internal/requests/restaction/pagination/AuditLogPaginationActionImpl.java index cfd68833fd..a167548411 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/restaction/pagination/AuditLogPaginationActionImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/restaction/pagination/AuditLogPaginationActionImpl.java @@ -126,7 +126,7 @@ protected void handleSuccess(Response response, Request> req DataArray entries = obj.getArray("audit_log_entries"); List list = new ArrayList<>(entries.length()); - EntityBuilder builder = api.get().getEntityBuilder(); + EntityBuilder builder = api.getEntityBuilder(); TLongObjectMap userMap = new TLongObjectHashMap<>(); for (int i = 0; i < users.length(); i++) diff --git a/src/main/java/net/dv8tion/jda/internal/requests/restaction/pagination/MessagePaginationActionImpl.java b/src/main/java/net/dv8tion/jda/internal/requests/restaction/pagination/MessagePaginationActionImpl.java index ef04ea0b88..459ba27aae 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/restaction/pagination/MessagePaginationActionImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/restaction/pagination/MessagePaginationActionImpl.java @@ -82,7 +82,7 @@ protected void handleSuccess(Response response, Request> request) { DataArray array = response.getArray(); List messages = new ArrayList<>(array.length()); - EntityBuilder builder = api.get().getEntityBuilder(); + EntityBuilder builder = api.getEntityBuilder(); for (int i = 0; i < array.length(); i++) { try diff --git a/src/main/java/net/dv8tion/jda/internal/requests/restaction/pagination/ReactionPaginationActionImpl.java b/src/main/java/net/dv8tion/jda/internal/requests/restaction/pagination/ReactionPaginationActionImpl.java index 7b0b1713e7..1262e3c885 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/restaction/pagination/ReactionPaginationActionImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/restaction/pagination/ReactionPaginationActionImpl.java @@ -96,7 +96,7 @@ protected Route.CompiledRoute finalizeRoute() @Override protected void handleSuccess(Response response, Request> request) { - final EntityBuilder builder = api.get().getEntityBuilder(); + final EntityBuilder builder = api.getEntityBuilder(); final DataArray array = response.getArray(); final List users = new LinkedList<>(); for (int i = 0; i < array.length(); i++) diff --git a/src/main/java/net/dv8tion/jda/internal/utils/cache/SnowflakeReference.java b/src/main/java/net/dv8tion/jda/internal/utils/cache/SnowflakeReference.java new file mode 100644 index 0000000000..cda3b2331c --- /dev/null +++ b/src/main/java/net/dv8tion/jda/internal/utils/cache/SnowflakeReference.java @@ -0,0 +1,79 @@ +/* + * Copyright 2015-2019 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.internal.utils.cache; + +import net.dv8tion.jda.api.entities.ISnowflake; + +import javax.annotation.Nonnull; +import java.lang.ref.WeakReference; +import java.util.function.LongFunction; + +public class SnowflakeReference implements ISnowflake +{ + private final LongFunction fallbackProvider; + private final long id; + + //We intentionally use a WeakReference rather than a SoftReference: + // The reasoning is that we want to replace an old reference as soon as possible with a more up-to-date instance. + // A soft reference would not be released until the user stops using it (ideally) so that is the wrong reference to use. + private WeakReference reference; + + public SnowflakeReference(T referent, LongFunction fallback) + { + this.fallbackProvider = fallback; + this.reference = new WeakReference<>(referent); + this.id = referent.getIdLong(); + } + + @Nonnull + public T resolve() + { + T referent = reference.get(); + if (referent == null) + { + referent = fallbackProvider.apply(id); + if (referent == null) + throw new IllegalStateException("Cannot get reference as it has already been Garbage Collected"); + reference = new WeakReference<>(referent); + } + return referent; + } + + @Override + public int hashCode() + { + return resolve().hashCode(); + } + + @Override + public boolean equals(Object obj) + { + return resolve().equals(obj); + } + + @Override + public String toString() + { + return resolve().toString(); + } + + @Override + public long getIdLong() + { + return id; + } +} diff --git a/src/main/java/net/dv8tion/jda/internal/utils/cache/UpstreamReference.java b/src/main/java/net/dv8tion/jda/internal/utils/cache/UpstreamReference.java deleted file mode 100644 index 81536676e0..0000000000 --- a/src/main/java/net/dv8tion/jda/internal/utils/cache/UpstreamReference.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2015-2019 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.internal.utils.cache; - -import javax.annotation.Nonnull; -import java.lang.ref.ReferenceQueue; -import java.lang.ref.WeakReference; - -public class UpstreamReference extends WeakReference -{ - public UpstreamReference(T referent) - { - super(referent); - } - - public UpstreamReference(T referent, ReferenceQueue q) - { - super(referent, q); - } - - @Nonnull - @Override - public T get() - { - T tmp = super.get(); - if (tmp == null) - throw new IllegalStateException("Cannot get reference as it has already been Garbage Collected"); - return tmp; - } - - @Override - public int hashCode() - { - return get().hashCode(); - } - - @Override - public boolean equals(Object obj) - { - return get().equals(obj); - } - - @Override - public String toString() - { - return get().toString(); - } -} diff --git a/src/main/java/net/dv8tion/jda/internal/utils/compress/ZlibDecompressor.java b/src/main/java/net/dv8tion/jda/internal/utils/compress/ZlibDecompressor.java index 216ba848a1..4c15489900 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/compress/ZlibDecompressor.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/compress/ZlibDecompressor.java @@ -33,13 +33,19 @@ public class ZlibDecompressor implements Decompressor { private static final int Z_SYNC_FLUSH = 0x0000FFFF; + private final int maxBufferSize; private final Inflater inflater = new Inflater(); private ByteBuffer flushBuffer = null; private SoftReference decompressBuffer = null; + public ZlibDecompressor(int maxBufferSize) + { + this.maxBufferSize = maxBufferSize; + } + private SoftReference newDecompressBuffer() { - return new SoftReference<>(new ByteArrayOutputStream(1024)); + return new SoftReference<>(new ByteArrayOutputStream(Math.min(1024, maxBufferSize))); } private ByteArrayOutputStream getDecompressBuffer() @@ -50,7 +56,7 @@ private ByteArrayOutputStream getDecompressBuffer() // Check if the buffer has been collected by the GC or not ByteArrayOutputStream buffer = decompressBuffer.get(); if (buffer == null) // create a ne buffer because the GC got it - decompressBuffer = new SoftReference<>(buffer = new ByteArrayOutputStream(1024)); + decompressBuffer = new SoftReference<>(buffer = new ByteArrayOutputStream(Math.min(1024, maxBufferSize))); return buffer; } @@ -103,7 +109,6 @@ public void shutdown() } @Override - @SuppressWarnings("CharsetObjectCanBeUsed") public String decompress(byte[] data) throws DataFormatException { //Handle split messages @@ -144,7 +149,10 @@ else if (flushBuffer != null) finally { // When done with decompression we want to reset the buffer so it can be used again later - buffer.reset(); + if (buffer.size() > maxBufferSize) + decompressBuffer = newDecompressBuffer(); + else + buffer.reset(); } } } diff --git a/src/main/java/net/dv8tion/jda/internal/utils/config/MetaConfig.java b/src/main/java/net/dv8tion/jda/internal/utils/config/MetaConfig.java index ae27e3c63e..5fc8ceb2d6 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/config/MetaConfig.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/config/MetaConfig.java @@ -27,17 +27,20 @@ public class MetaConfig { - private static final MetaConfig defaultConfig = new MetaConfig(null, EnumSet.allOf(CacheFlag.class), ConfigFlag.getDefault()); + private static final MetaConfig defaultConfig = new MetaConfig(2048, null, EnumSet.allOf(CacheFlag.class), ConfigFlag.getDefault()); private final ConcurrentMap mdcContextMap; private final EnumSet cacheFlags; private final boolean enableMDC; private final boolean useShutdownHook; private final boolean guildSubscriptions; + private final int maxBufferSize; public MetaConfig( + int maxBufferSize, @Nullable ConcurrentMap mdcContextMap, @Nullable EnumSet cacheFlags, EnumSet flags) { + this.maxBufferSize = maxBufferSize; this.cacheFlags = cacheFlags == null ? EnumSet.allOf(CacheFlag.class) : cacheFlags; this.enableMDC = flags.contains(ConfigFlag.MDC_CONTEXT); if (enableMDC) @@ -75,6 +78,11 @@ public boolean isGuildSubscriptions() return guildSubscriptions; } + public int getMaxBufferSize() + { + return maxBufferSize; + } + @Nonnull public static MetaConfig getDefault() { diff --git a/src/main/java/net/dv8tion/jda/internal/utils/config/SessionConfig.java b/src/main/java/net/dv8tion/jda/internal/utils/config/SessionConfig.java index f3c4175255..f5a18631d2 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/config/SessionConfig.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/config/SessionConfig.java @@ -126,6 +126,6 @@ public EnumSet getFlags() @Nonnull public static SessionConfig getDefault() { - return new SessionConfig(null, null, null, null, ConfigFlag.getDefault(), 900, 250); + return new SessionConfig(null, new OkHttpClient(), null, null, ConfigFlag.getDefault(), 900, 250); } } diff --git a/src/main/java/net/dv8tion/jda/internal/utils/config/sharding/ShardingMetaConfig.java b/src/main/java/net/dv8tion/jda/internal/utils/config/sharding/ShardingMetaConfig.java index f1ab79daa1..9fd0e0d752 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/config/sharding/ShardingMetaConfig.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/config/sharding/ShardingMetaConfig.java @@ -29,15 +29,16 @@ public class ShardingMetaConfig extends MetaConfig { - private static final ShardingMetaConfig defaultConfig = new ShardingMetaConfig(null, null, ConfigFlag.getDefault(), Compression.ZLIB); + private static final ShardingMetaConfig defaultConfig = new ShardingMetaConfig(2048, null, null, ConfigFlag.getDefault(), Compression.ZLIB); private final Compression compression; private final IntFunction> contextProvider; public ShardingMetaConfig( + int maxBufferSize, @Nullable IntFunction> contextProvider, @Nullable EnumSet cacheFlags, EnumSet flags, Compression compression) { - super(null, cacheFlags, flags); + super(maxBufferSize, null, cacheFlags, flags); this.compression = compression; this.contextProvider = contextProvider; diff --git a/src/main/java/net/dv8tion/jda/internal/utils/config/sharding/ShardingSessionConfig.java b/src/main/java/net/dv8tion/jda/internal/utils/config/sharding/ShardingSessionConfig.java index 984051fae6..6f723b5d8e 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/config/sharding/ShardingSessionConfig.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/config/sharding/ShardingSessionConfig.java @@ -76,6 +76,6 @@ public IAudioSendFactory getAudioSendFactory() @Nonnull public static ShardingSessionConfig getDefault() { - return new ShardingSessionConfig(null, null, null, null, null, null, ConfigFlag.getDefault(), ShardingConfigFlag.getDefault(), 900, 250); + return new ShardingSessionConfig(null, null, new OkHttpClient(), null, null, null, ConfigFlag.getDefault(), ShardingConfigFlag.getDefault(), 900, 250); } }