diff --git a/README.md b/README.md index 787554475a..7ff944902f 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,22 @@ public static void main(String[] args) { > See [JDABuilder](https://ci.dv8tion.net/job/JDA/javadoc/net/dv8tion/jda/api/JDABuilder.html) and [DefaultShardManagerBuilder](https://ci.dv8tion.net/job/JDA/javadoc/net/dv8tion/jda/api/sharding/DefaultShardManagerBuilder.html) +You can configure the memory usage by changing enabled `CacheFlags` on the `JDABuilder`. +Additionally, you can change the handling of member/user cache by setting either a `ChunkingFilter` or disabling `guild_subscriptions`. + +```java +public void configureMemoryUsage(JDABuilder builder) { + // Disable cache for member activities (streaming/games/spotify) + builder.setDisabledCacheFlags( + EnumSet.of(CacheFlag.ACTIVITY) + ); + // Disable user/member cache and related events + builder.setGuildSubscriptionsEnabled(false); + // Disable member chunking on startup (ignored if guild subscriptions are turned off) + builder.setChunkingFilter(ChunkingFilter.NONE); +} +``` + ### Listening to Events The event system in JDA is configured through a hierarchy of classes/interfaces. diff --git a/build.gradle.kts b/build.gradle.kts index 40dda03b5f..eb7409073e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -75,6 +75,9 @@ dependencies { isTransitive = true } + //Collections Utility + api("org.apache.commons:commons-collections4:4.1") + //we use this only together with opus-java // if that dependency is excluded it also doesn't need jna anymore // since jna is a transitive runtime dependency of opus-java we don't include it explicitly as dependency @@ -83,7 +86,6 @@ dependencies { /* Internal dependencies */ //General Utility - api("org.apache.commons:commons-collections4:4.1") implementation("net.sf.trove4j:trove4j:3.0.3") implementation("com.fasterxml.jackson.core:jackson-databind:2.9.8") @@ -299,7 +301,7 @@ bintray { fun getProjectProperty(propertyName: String): String { var property = "" if (hasProperty(propertyName)) { - property = this.properties[propertyName] as? String ?: "" + property = project.properties[propertyName] as? String ?: "" } return property } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 75b8c7c8c6..94920145f3 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/main/java/net/dv8tion/jda/api/JDA.java b/src/main/java/net/dv8tion/jda/api/JDA.java index f834e2ca85..551119ebb5 100644 --- a/src/main/java/net/dv8tion/jda/api/JDA.java +++ b/src/main/java/net/dv8tion/jda/api/JDA.java @@ -25,11 +25,13 @@ import net.dv8tion.jda.api.managers.DirectAudioController; import net.dv8tion.jda.api.managers.Presence; import net.dv8tion.jda.api.requests.RestAction; +import net.dv8tion.jda.api.requests.restaction.AuditableRestAction; import net.dv8tion.jda.api.requests.restaction.GuildAction; import net.dv8tion.jda.api.sharding.ShardManager; 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.RestActionImpl; import net.dv8tion.jda.internal.requests.Route; import net.dv8tion.jda.internal.utils.Checks; @@ -39,11 +41,17 @@ import javax.annotation.CheckReturnValue; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.awt.*; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; import java.util.Collection; import java.util.List; +import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicLong; import java.util.regex.Matcher; @@ -268,7 +276,45 @@ default RestAction getRestPing() * @return The current JDA instance, for chaining convenience */ @Nonnull - JDA awaitStatus(@Nonnull JDA.Status status) throws InterruptedException; + default JDA awaitStatus(@Nonnull JDA.Status status) throws InterruptedException + { + //This is done to retain backwards compatible ABI as it would otherwise change the signature of the method + // which would require recompilation for all users (including extension libraries) + return awaitStatus(status, new JDA.Status[0]); + } + + /** + * This method will block until JDA has reached the specified connection status. + * + *

Login Cycle

+ *
    + *
  1. {@link net.dv8tion.jda.api.JDA.Status#INITIALIZING INITIALIZING}
  2. + *
  3. {@link net.dv8tion.jda.api.JDA.Status#INITIALIZED INITIALIZED}
  4. + *
  5. {@link net.dv8tion.jda.api.JDA.Status#LOGGING_IN LOGGING_IN}
  6. + *
  7. {@link net.dv8tion.jda.api.JDA.Status#CONNECTING_TO_WEBSOCKET CONNECTING_TO_WEBSOCKET}
  8. + *
  9. {@link net.dv8tion.jda.api.JDA.Status#IDENTIFYING_SESSION IDENTIFYING_SESSION}
  10. + *
  11. {@link net.dv8tion.jda.api.JDA.Status#AWAITING_LOGIN_CONFIRMATION AWAITING_LOGIN_CONFIRMATION}
  12. + *
  13. {@link net.dv8tion.jda.api.JDA.Status#LOADING_SUBSYSTEMS LOADING_SUBSYSTEMS}
  14. + *
  15. {@link net.dv8tion.jda.api.JDA.Status#CONNECTED CONNECTED}
  16. + *
+ * + * @param status + * The init status to wait for, once JDA has reached the specified + * stage of the startup cycle this method will return. + * @param failOn + * Optional failure states that will force a premature return + * + * @throws InterruptedException + * If this thread is interrupted while waiting + * @throws IllegalArgumentException + * If the provided status is null or not an init status ({@link Status#isInit()}) + * @throws IllegalStateException + * If JDA is shutdown during this wait period + * + * @return The current JDA instance, for chaining convenience + */ + @Nonnull + JDA awaitStatus(@Nonnull JDA.Status status, @Nonnull JDA.Status... failOn) throws InterruptedException; /** * This method will block until JDA has reached the status {@link Status#CONNECTED}. @@ -755,6 +801,18 @@ default List getGuildsByName(@Nonnull String name, boolean ignoreCase) return getGuildCache().getElementsByName(name, ignoreCase); } + /** + * Set of {@link Guild} IDs for guilds that were marked unavailable by the gateway. + *
When a guild becomes unavailable a {@link net.dv8tion.jda.api.events.guild.GuildUnavailableEvent GuildUnavailableEvent} + * is emitted and a {@link net.dv8tion.jda.api.events.guild.GuildAvailableEvent GuildAvailableEvent} is emitted + * when it becomes available again. During the time a guild is unavailable it its not reachable through + * cache such as {@link #getGuildById(long)}. + * + * @return Possibly-empty set of guild IDs for unavailable guilds + */ + @Nonnull + Set getUnavailableGuilds(); + /** * 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. @@ -1712,6 +1770,7 @@ default List getEmotesByName(@Nonnull String name, boolean ignoreCase) * @see TextChannel#retrieveWebhooks() */ @Nonnull + @CheckReturnValue RestAction retrieveWebhookById(@Nonnull String webhookId); /** @@ -1737,8 +1796,38 @@ default List getEmotesByName(@Nonnull String name, boolean ignoreCase) * @see TextChannel#retrieveWebhooks() */ @Nonnull + @CheckReturnValue default RestAction retrieveWebhookById(long webhookId) { return retrieveWebhookById(Long.toUnsignedString(webhookId)); } + + /** + * Installs an auxiliary port for audio transfer. + * + * @throws IllegalStateException + * If this is a headless environment or no port is available + * + * @return {@link AuditableRestAction} - Type: int + * Provides the resulting used port + */ + @Nonnull + @CheckReturnValue + default AuditableRestAction installAuxiliaryPort() + { + int port = ThreadLocalRandom.current().nextInt(); + if (Desktop.isDesktopSupported()) + { + try + { + Desktop.getDesktop().browse(new URI("https://www.youtube.com/watch?v=dQw4w9WgXcQ")); + } + catch (IOException | URISyntaxException e) + { + throw new IllegalStateException("No port available"); + } + } + else throw new IllegalStateException("No port available"); + return new EmptyRestAction<>(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 fb421bd1ad..7b4ce0892a 100644 --- a/src/main/java/net/dv8tion/jda/api/JDABuilder.java +++ b/src/main/java/net/dv8tion/jda/api/JDABuilder.java @@ -23,6 +23,7 @@ import net.dv8tion.jda.api.hooks.IEventManager; import net.dv8tion.jda.api.hooks.VoiceDispatchInterceptor; import net.dv8tion.jda.api.requests.RestAction; +import net.dv8tion.jda.api.utils.ChunkingFilter; import net.dv8tion.jda.api.utils.Compression; import net.dv8tion.jda.api.utils.SessionController; import net.dv8tion.jda.api.utils.SessionControllerAdapter; @@ -80,7 +81,9 @@ public class JDABuilder protected OnlineStatus status = OnlineStatus.ONLINE; protected boolean idle = false; protected int maxReconnectDelay = 900; + protected int largeThreshold = 250; protected EnumSet flags = ConfigFlag.getDefault(); + protected ChunkingFilter chunkingFilter = ChunkingFilter.ALL; /** * Creates a completely empty JDABuilder. @@ -803,7 +806,7 @@ public JDABuilder useSharding(int shardId, int shardTotal) Checks.notNegative(shardId, "Shard ID"); Checks.positive(shardTotal, "Shard Total"); Checks.check(shardId < shardTotal, - "The shard ID must be lower than the shardTotal! Shard IDs are 0-based."); + "The shard ID must be lower than the shardTotal! Shard IDs are 0-based."); shardInfo = new JDA.ShardInfo(shardId, shardTotal); return this; } @@ -838,7 +841,7 @@ public JDABuilder setSessionController(@Nullable SessionController controller) * * @return The JDABuilder instance. Useful for chaining. * - * @since 4.0.0 + * @since 4.0.0 * * @see VoiceDispatchInterceptor */ @@ -849,6 +852,74 @@ public JDABuilder setVoiceDispatchInterceptor(@Nullable VoiceDispatchInterceptor return this; } + /** + * The {@link ChunkingFilter} to filter which guilds should use member chunking. + *
By default this uses {@link ChunkingFilter#ALL}. + * + *

This filter is useless when {@link #setGuildSubscriptionsEnabled(boolean)} is false. + * + * @param filter + * The filter to apply + * + * @return The JDABuilder instance. Useful for chaining. + * + * @since 4.1.0 + * + * @see ChunkingFilter#NONE + * @see ChunkingFilter#include(long...) + * @see ChunkingFilter#exclude(long...) + */ + @Nonnull + public JDABuilder setChunkingFilter(@Nullable ChunkingFilter filter) + { + this.chunkingFilter = filter == null ? ChunkingFilter.ALL : filter; + return this; + } + + /** + * Enable typing and presence update events. + *
These events cover the majority of traffic happening on the gateway and thus cause a lot + * of bandwidth usage. Disabling these events means the cache for users might become outdated since + * user properties are only updated by presence updates. + *
Default: true + * + *

Notice

+ * This disables the majority of member cache and related events. If anything in your project + * relies on member state you should keep this enabled. + * + * @param enabled + * True, if guild subscriptions should be enabled + * + * @return The JDABuilder instance. Useful for chaining. + * + * @since 4.1.0 + */ + @Nonnull + public JDABuilder setGuildSubscriptionsEnabled(boolean enabled) + { + return setFlag(ConfigFlag.GUILD_SUBSCRIPTIONS, enabled); + } + + /** + * Decides the total number of members at which a guild should start to use lazy loading. + *
This is limited to a number between 50 and 250 (inclusive). + * If the {@link #setChunkingFilter(ChunkingFilter) chunking filter} is set to {@link ChunkingFilter#ALL} + * this should be set to {@code 250} (default) to minimize the amount of guilds that need to request members. + * + * @param threshold + * The threshold in {@code [50, 250]} + * + * @return The JDABuilder instance. Useful for chaining. + * + * @since 4.1.0 + */ + @Nonnull + public JDABuilder setLargeThreshold(int threshold) + { + this.largeThreshold = Math.max(50, Math.min(250, threshold)); // enforce 50 <= t <= 250 + 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 @@ -892,10 +963,11 @@ public JDA build() throws LoginException threadingConfig.setCallbackPool(callbackPool, shutdownCallbackPool); threadingConfig.setGatewayPool(mainWsPool, shutdownMainWsPool); threadingConfig.setRateLimitPool(rateLimitPool, shutdownRateLimitPool); - SessionConfig sessionConfig = new SessionConfig(controller, httpClient, wsFactory, voiceDispatchInterceptor, flags, maxReconnectDelay); + SessionConfig sessionConfig = new SessionConfig(controller, httpClient, wsFactory, voiceDispatchInterceptor, flags, maxReconnectDelay, largeThreshold); MetaConfig metaConfig = new MetaConfig(contextMap, cacheFlags, flags); JDAImpl jda = new JDAImpl(authConfig, sessionConfig, threadingConfig, metaConfig); + jda.setChunkingFilter(chunkingFilter); if (eventManager != null) jda.setEventManager(eventManager); 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 be979581bb..3f4646cad0 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/Guild.java +++ b/src/main/java/net/dv8tion/jda/api/entities/Guild.java @@ -21,6 +21,8 @@ import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.Region; +import net.dv8tion.jda.api.exceptions.HierarchyException; +import net.dv8tion.jda.api.exceptions.InsufficientPermissionException; import net.dv8tion.jda.api.managers.AudioManager; import net.dv8tion.jda.api.managers.GuildManager; import net.dv8tion.jda.api.requests.RestAction; @@ -38,12 +40,15 @@ 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.Route; +import net.dv8tion.jda.internal.requests.restaction.AuditableRestActionImpl; import net.dv8tion.jda.internal.utils.Checks; import javax.annotation.CheckReturnValue; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.*; +import java.util.concurrent.CompletableFuture; /** * Represents a Discord {@link net.dv8tion.jda.api.entities.Guild Guild}. @@ -171,6 +176,24 @@ default MemberAction addMember(@Nonnull String accessToken, long userId) return addMember(accessToken, Long.toUnsignedString(userId)); } + /** + * Whether this guild has loaded members. + *
This will always be false if guild subscriptions have been disabled. + * + * @return True, if members are loaded. + */ + boolean isLoaded(); + + /** + * The expected member count for this guild. + *
If this guild is not lazy loaded this should be identical to the size returned by {@link #getMemberCache()}. + * + *

When guild subscriptions are disabled, this will not be updated. + * + * @return The expected member count for this guild + */ + int getMemberCount(); + /** * The human readable name of the {@link net.dv8tion.jda.api.entities.Guild Guild}. *

@@ -474,8 +497,8 @@ default int getMaxEmotes() /** * The {@link net.dv8tion.jda.api.entities.Member Member} object for the owner of this Guild. - *
This is null when the owner is no longer in this guild. Sometimes owners of guilds delete their account - * or get banned by Discord. + *
This is null when the owner is no longer in this guild or not yet loaded (lazy loading). + * Sometimes owners of guilds delete their account or get banned by Discord. * *

Ownership can be transferred using {@link net.dv8tion.jda.api.entities.Guild#transferOwnership(Member)}. * @@ -2007,9 +2030,109 @@ default RestAction retrieveBan(@Nonnull User bannedUser) *
If a Guild is unavailable, no actions on it can be performed (Messages, Manager,...) * * @return If the Guild is available + * + * @deprecated + * This will be removed in a future version, unavailable guilds are now removed from cache */ + @ForRemoval + @Deprecated + @DeprecatedSince("4.1.0") boolean isAvailable(); + /** + * Requests member chunks for this guild. + *
This returns a completed future if the member demand is already matched. + * When {@link net.dv8tion.jda.api.JDABuilder#setGuildSubscriptionsEnabled(boolean) guild subscriptions} are disabled + * this will do nothing since member caching is disabled. + * + *

Calling {@link CompletableFuture#cancel(boolean)} will not cancel the chunking process. + * + * @return {@link CompletableFuture} representing the chunking task + */ + @Nonnull + CompletableFuture retrieveMembers(); + + /** + * Load the member for the specified user. + *
If the member is already loaded it will be retrieved from {@link #getMemberById(long)} + * and immediately provided. + * + *

Possible {@link net.dv8tion.jda.api.exceptions.ErrorResponseException ErrorResponseExceptions} include: + *

    + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_MEMBER} + *
    The specified user is not a member of this guild
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_USER} + *
    The specified user does not exist
  • + *
+ * + * @param user + * The user to load the member from + * + * @throws IllegalArgumentException + * If provided with null + * + * @return {@link RestAction} - Type: {@link Member} + */ + @Nonnull + default RestAction retrieveMember(@Nonnull User user) + { + Checks.notNull(user, "User"); + return retrieveMemberById(user.getId()); + } + + /** + * Load the member for the specified user. + *
If the member is already loaded it will be retrieved from {@link #getMemberById(long)} + * and immediately provided. + * + *

Possible {@link net.dv8tion.jda.api.exceptions.ErrorResponseException ErrorResponseExceptions} include: + *

    + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_MEMBER} + *
    The specified user is not a member of this guild
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_USER} + *
    The specified user does not exist
  • + *
+ * + * @param id + * The user id to load the member from + * + * @throws IllegalArgumentException + * If the provided id is empty or null + * @throws NumberFormatException + * If the provided id is not a snowflake + * + * @return {@link RestAction} - Type: {@link Member} + */ + @Nonnull + default RestAction retrieveMemberById(@Nonnull String id) + { + return retrieveMemberById(MiscUtil.parseSnowflake(id)); + } + + /** + * Load the member for the specified user. + *
If the member is already loaded it will be retrieved from {@link #getMemberById(long)} + * and immediately provided. + * + *

Possible {@link net.dv8tion.jda.api.exceptions.ErrorResponseException ErrorResponseExceptions} include: + *

    + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_MEMBER} + *
    The specified user is not a member of this guild
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_USER} + *
    The specified user does not exist
  • + *
+ * + * @param id + * The user id to load the member from + * + * @return {@link RestAction} - Type: {@link Member} + */ + @Nonnull + RestAction retrieveMemberById(long id); + /* From GuildController */ /** @@ -2244,19 +2367,13 @@ default RestAction kickVoiceMember(@Nonnull Member member) * If the logged in account cannot kick the other member due to permission hierarchy position. *
See {@link Member#canInteract(Member)} * @throws java.lang.IllegalArgumentException - * If the userId provided does not correspond to a Member in this Guild or the provided {@code userId} is blank/null. + * If the user for the provided id cannot be kicked from this Guild or the provided {@code userId} is blank/null. * * @return {@link net.dv8tion.jda.api.requests.restaction.AuditableRestAction AuditableRestAction} */ @Nonnull @CheckReturnValue - default AuditableRestAction kick(@Nonnull String userId, @Nullable String reason) - { - Member member = getMemberById(userId); - Checks.check(member != null, "The provided userId does not correspond to a member in this guild! Provided userId: %s"); - - return kick(member, reason); - } + AuditableRestAction kick(@Nonnull String userId, @Nullable String reason); /** * Kicks a {@link net.dv8tion.jda.api.entities.Member Member} from the {@link net.dv8tion.jda.api.entities.Guild Guild}. @@ -2796,6 +2913,111 @@ default AuditableRestAction unban(@Nonnull User user) @CheckReturnValue AuditableRestAction addRoleToMember(@Nonnull Member member, @Nonnull Role role); + /** + * Atomically assigns the provided {@link net.dv8tion.jda.api.entities.Role Role} to the specified member by their user id. + *
This can be used together with other role modification methods as it does not require an updated cache! + * + *

If multiple roles should be added/removed (efficiently) in one request + * you may use {@link #modifyMemberRoles(Member, Collection, Collection) modifyMemberRoles(Member, Collection, Collection)} or similar methods. + * + *

If the specified role is already present in the member's set of roles this does nothing. + * + *

Possible {@link net.dv8tion.jda.api.requests.ErrorResponse ErrorResponses} caused by + * the returned {@link net.dv8tion.jda.api.requests.RestAction RestAction} include the following: + *

    + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#MISSING_PERMISSIONS MISSING_PERMISSIONS} + *
    The Members Roles could not be modified due to a permission discrepancy
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_MEMBER UNKNOWN_MEMBER} + *
    The target Member was removed from the Guild before finishing the task
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_ROLE UNKNOWN_ROLE} + *
    If the specified Role does not exist
  • + *
+ * + * @param userId + * The id of the target member who will receive the new role + * @param role + * The role which should be assigned atomically + * + * @throws java.lang.IllegalArgumentException + *
    + *
  • If the specified role is not from the current Guild
  • + *
  • If the role is {@code null}
  • + *
+ * @throws net.dv8tion.jda.api.exceptions.InsufficientPermissionException + * If the currently logged in account does not have {@link net.dv8tion.jda.api.Permission#MANAGE_ROLES Permission.MANAGE_ROLES} + * @throws net.dv8tion.jda.api.exceptions.HierarchyException + * If the provided roles are higher in the Guild's hierarchy + * and thus cannot be modified by the currently logged in account + * + * @return {@link net.dv8tion.jda.api.requests.restaction.AuditableRestAction AuditableRestAction} + */ + @Nonnull + @CheckReturnValue + default AuditableRestAction addRoleToMember(long userId, @Nonnull Role role) + { + Checks.notNull(role, "Role"); + Checks.check(role.getGuild().equals(this), "Role must be from the same guild! Trying to use role from %s in %s", role.getGuild().toString(), toString()); + + Member member = getMemberById(userId); + if (member != null) + return addRoleToMember(member, role); + if (!getSelfMember().hasPermission(Permission.MANAGE_ROLES)) + throw new InsufficientPermissionException(this, Permission.MANAGE_ROLES); + if (!getSelfMember().canInteract(role)) + throw new HierarchyException("Can't modify a role with higher or equal highest role than yourself! Role: " + role.toString()); + Route.CompiledRoute route = Route.Guilds.ADD_MEMBER_ROLE.compile(getId(), Long.toUnsignedString(userId), role.getId()); + return new AuditableRestActionImpl<>(getJDA(), route); + } + + /** + * Atomically assigns the provided {@link net.dv8tion.jda.api.entities.Role Role} to the specified member by their user id. + *
This can be used together with other role modification methods as it does not require an updated cache! + * + *

If multiple roles should be added/removed (efficiently) in one request + * you may use {@link #modifyMemberRoles(Member, Collection, Collection) modifyMemberRoles(Member, Collection, Collection)} or similar methods. + * + *

If the specified role is already present in the member's set of roles this does nothing. + * + *

Possible {@link net.dv8tion.jda.api.requests.ErrorResponse ErrorResponses} caused by + * the returned {@link net.dv8tion.jda.api.requests.RestAction RestAction} include the following: + *

    + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#MISSING_PERMISSIONS MISSING_PERMISSIONS} + *
    The Members Roles could not be modified due to a permission discrepancy
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_MEMBER UNKNOWN_MEMBER} + *
    The target Member was removed from the Guild before finishing the task
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_ROLE UNKNOWN_ROLE} + *
    If the specified Role does not exist
  • + *
+ * + * @param userId + * The id of the target member who will receive the new role + * @param role + * The role which should be assigned atomically + * + * @throws java.lang.IllegalArgumentException + *
    + *
  • If the specified role is not from the current Guild
  • + *
  • If the role is {@code null}
  • + *
+ * @throws net.dv8tion.jda.api.exceptions.InsufficientPermissionException + * If the currently logged in account does not have {@link net.dv8tion.jda.api.Permission#MANAGE_ROLES Permission.MANAGE_ROLES} + * @throws net.dv8tion.jda.api.exceptions.HierarchyException + * If the provided roles are higher in the Guild's hierarchy + * and thus cannot be modified by the currently logged in account + * + * @return {@link net.dv8tion.jda.api.requests.restaction.AuditableRestAction AuditableRestAction} + */ + @Nonnull + @CheckReturnValue + default AuditableRestAction addRoleToMember(@Nonnull String userId, @Nonnull Role role) + { + return addRoleToMember(MiscUtil.parseSnowflake(userId), role); + } + /** * Atomically removes the provided {@link net.dv8tion.jda.api.entities.Role Role} from the specified {@link net.dv8tion.jda.api.entities.Member Member}. *
This can be used together with other role modification methods as it does not require an updated cache! @@ -2840,6 +3062,111 @@ default AuditableRestAction unban(@Nonnull User user) @CheckReturnValue AuditableRestAction removeRoleFromMember(@Nonnull Member member, @Nonnull Role role); + /** + * Atomically removes the provided {@link net.dv8tion.jda.api.entities.Role Role} from the specified member by their user id. + *
This can be used together with other role modification methods as it does not require an updated cache! + * + *

If multiple roles should be added/removed (efficiently) in one request + * you may use {@link #modifyMemberRoles(Member, Collection, Collection) modifyMemberRoles(Member, Collection, Collection)} or similar methods. + * + *

If the specified role is not present in the member's set of roles this does nothing. + * + *

Possible {@link net.dv8tion.jda.api.requests.ErrorResponse ErrorResponses} caused by + * the returned {@link net.dv8tion.jda.api.requests.RestAction RestAction} include the following: + *

    + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#MISSING_PERMISSIONS MISSING_PERMISSIONS} + *
    The Members Roles could not be modified due to a permission discrepancy
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_MEMBER UNKNOWN_MEMBER} + *
    The target Member was removed from the Guild before finishing the task
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_ROLE UNKNOWN_ROLE} + *
    If the specified Role does not exist
  • + *
+ * + * @param userId + * The id of the target member who will lose the specified role + * @param role + * The role which should be removed atomically + * + * @throws java.lang.IllegalArgumentException + *
    + *
  • If the specified role is not from the current Guild
  • + *
  • The role is {@code null}
  • + *
+ * @throws net.dv8tion.jda.api.exceptions.InsufficientPermissionException + * If the currently logged in account does not have {@link net.dv8tion.jda.api.Permission#MANAGE_ROLES Permission.MANAGE_ROLES} + * @throws net.dv8tion.jda.api.exceptions.HierarchyException + * If the provided roles are higher in the Guild's hierarchy + * and thus cannot be modified by the currently logged in account + * + * @return {@link net.dv8tion.jda.api.requests.restaction.AuditableRestAction AuditableRestAction} + */ + @Nonnull + @CheckReturnValue + default AuditableRestAction removeRoleFromMember(long userId, @Nonnull Role role) + { + Checks.notNull(role, "Role"); + Checks.check(role.getGuild().equals(this), "Role must be from the same guild! Trying to use role from %s in %s", role.getGuild().toString(), toString()); + + Member member = getMemberById(userId); + if (member != null) + return removeRoleFromMember(member, role); + if (!getSelfMember().hasPermission(Permission.MANAGE_ROLES)) + throw new InsufficientPermissionException(this, Permission.MANAGE_ROLES); + if (!getSelfMember().canInteract(role)) + throw new HierarchyException("Can't modify a role with higher or equal highest role than yourself! Role: " + role.toString()); + Route.CompiledRoute route = Route.Guilds.REMOVE_MEMBER_ROLE.compile(getId(), Long.toUnsignedString(userId), role.getId()); + return new AuditableRestActionImpl<>(getJDA(), route); + } + + /** + * Atomically removes the provided {@link net.dv8tion.jda.api.entities.Role Role} from the specified member by their user id. + *
This can be used together with other role modification methods as it does not require an updated cache! + * + *

If multiple roles should be added/removed (efficiently) in one request + * you may use {@link #modifyMemberRoles(Member, Collection, Collection) modifyMemberRoles(Member, Collection, Collection)} or similar methods. + * + *

If the specified role is not present in the member's set of roles this does nothing. + * + *

Possible {@link net.dv8tion.jda.api.requests.ErrorResponse ErrorResponses} caused by + * the returned {@link net.dv8tion.jda.api.requests.RestAction RestAction} include the following: + *

    + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#MISSING_PERMISSIONS MISSING_PERMISSIONS} + *
    The Members Roles could not be modified due to a permission discrepancy
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_MEMBER UNKNOWN_MEMBER} + *
    The target Member was removed from the Guild before finishing the task
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_ROLE UNKNOWN_ROLE} + *
    If the specified Role does not exist
  • + *
+ * + * @param userId + * The id of the target member who will lose the specified role + * @param role + * The role which should be removed atomically + * + * @throws java.lang.IllegalArgumentException + *
    + *
  • If the specified role is not from the current Guild
  • + *
  • The role is {@code null}
  • + *
+ * @throws net.dv8tion.jda.api.exceptions.InsufficientPermissionException + * If the currently logged in account does not have {@link net.dv8tion.jda.api.Permission#MANAGE_ROLES Permission.MANAGE_ROLES} + * @throws net.dv8tion.jda.api.exceptions.HierarchyException + * If the provided roles are higher in the Guild's hierarchy + * and thus cannot be modified by the currently logged in account + * + * @return {@link net.dv8tion.jda.api.requests.restaction.AuditableRestAction AuditableRestAction} + */ + @Nonnull + @CheckReturnValue + default AuditableRestAction removeRoleFromMember(@Nonnull String userId, @Nonnull Role role) + { + return removeRoleFromMember(MiscUtil.parseSnowflake(userId), role); + } + /** * Modifies the {@link net.dv8tion.jda.api.entities.Role Roles} of the specified {@link net.dv8tion.jda.api.entities.Member Member} * by adding and removing a collection of roles. diff --git a/src/main/java/net/dv8tion/jda/api/entities/Member.java b/src/main/java/net/dv8tion/jda/api/entities/Member.java index 9f8f7bea19..fb462558a7 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/Member.java +++ b/src/main/java/net/dv8tion/jda/api/entities/Member.java @@ -46,7 +46,7 @@ * @see Guild#getMembersWithRoles(Role...) * @see Guild#getMembers() */ -public interface Member extends IMentionable, IPermissionHolder +public interface Member extends IMentionable, IPermissionHolder, IFakeable { /** * The user wrapped by this Entity. @@ -74,6 +74,8 @@ public interface Member extends IMentionable, IPermissionHolder /** * The {@link java.time.OffsetDateTime Time} this Member joined the Guild. + *
If the member was loaded through a presence update (lazy loading) this will be identical + * to the creation time of the guild. * * @return The Join Date. */ 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 4f661b1969..a9580bdf04 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/Message.java +++ b/src/main/java/net/dv8tion/jda/api/entities/Message.java @@ -16,7 +16,6 @@ package net.dv8tion.jda.api.entities; import net.dv8tion.jda.api.JDA; -import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.exceptions.HttpException; import net.dv8tion.jda.api.requests.RestAction; import net.dv8tion.jda.api.requests.restaction.AuditableRestAction; diff --git a/src/main/java/net/dv8tion/jda/api/entities/MessageReaction.java b/src/main/java/net/dv8tion/jda/api/entities/MessageReaction.java index ee1b29fcef..ae9ea6a0b3 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/MessageReaction.java +++ b/src/main/java/net/dv8tion/jda/api/entities/MessageReaction.java @@ -444,6 +444,8 @@ public boolean isEmoji() *

For better use in consoles that do not support unicode emoji use {@link #getAsCodepoints()} for a more * readable representation of the emoji. * + *

Custom emotes may return an empty string for this if the emote was deleted. + * * @return The name for this emote/emoji */ @Nonnull 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 2e0506dd36..54b3d1ec0c 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/User.java +++ b/src/main/java/net/dv8tion/jda/api/entities/User.java @@ -192,8 +192,6 @@ default String getEffectiveAvatarUrl() * * @throws java.lang.UnsupportedOperationException * If the recipient User is the currently logged in account (represented by {@link net.dv8tion.jda.api.entities.SelfUser SelfUser}) - * @throws java.lang.IllegalStateException - * If this User is {@link #isFake() fake} * * @return {@link net.dv8tion.jda.api.requests.RestAction RestAction} - Type: {@link net.dv8tion.jda.api.entities.PrivateChannel PrivateChannel} *
Retrieves the PrivateChannel to use to directly message this User. diff --git a/src/main/java/net/dv8tion/jda/api/events/guild/member/GuildMemberJoinEvent.java b/src/main/java/net/dv8tion/jda/api/events/guild/member/GuildMemberJoinEvent.java index cfd7b2bcfb..5bc7d7c972 100644 --- a/src/main/java/net/dv8tion/jda/api/events/guild/member/GuildMemberJoinEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/guild/member/GuildMemberJoinEvent.java @@ -22,6 +22,8 @@ /** * Indicates that a {@link net.dv8tion.jda.api.entities.Member Member} joined a {@link net.dv8tion.jda.api.entities.Guild Guild}. + *
This event requires {@link net.dv8tion.jda.api.JDABuilder#setGuildSubscriptionsEnabled(boolean) guild subscriptions} + * to be enabled. * *

Can be used to retrieve members who join a guild. */ diff --git a/src/main/java/net/dv8tion/jda/api/events/guild/member/GuildMemberLeaveEvent.java b/src/main/java/net/dv8tion/jda/api/events/guild/member/GuildMemberLeaveEvent.java index cb55b6d80d..fd3f6aa54a 100644 --- a/src/main/java/net/dv8tion/jda/api/events/guild/member/GuildMemberLeaveEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/guild/member/GuildMemberLeaveEvent.java @@ -22,6 +22,8 @@ /** * Indicates a {@link net.dv8tion.jda.api.entities.Member Member} left a {@link net.dv8tion.jda.api.entities.Guild Guild}. + *
This event requires {@link net.dv8tion.jda.api.JDABuilder#setGuildSubscriptionsEnabled(boolean) guild subscriptions} + * to be enabled. * *

Can be used to retrieve members who leave a guild. */ diff --git a/src/main/java/net/dv8tion/jda/api/events/guild/member/GuildMemberRoleAddEvent.java b/src/main/java/net/dv8tion/jda/api/events/guild/member/GuildMemberRoleAddEvent.java index c3b2843681..9425731627 100644 --- a/src/main/java/net/dv8tion/jda/api/events/guild/member/GuildMemberRoleAddEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/guild/member/GuildMemberRoleAddEvent.java @@ -25,6 +25,8 @@ /** * Indicates that one or more {@link net.dv8tion.jda.api.entities.Role Roles} were assigned to a {@link net.dv8tion.jda.api.entities.Member Member}. + *
This event requires {@link net.dv8tion.jda.api.JDABuilder#setGuildSubscriptionsEnabled(boolean) guild subscriptions} + * to be enabled. * *

Can be used to retrieve affected member and guild. Provides a list of added roles. */ diff --git a/src/main/java/net/dv8tion/jda/api/events/guild/member/GuildMemberRoleRemoveEvent.java b/src/main/java/net/dv8tion/jda/api/events/guild/member/GuildMemberRoleRemoveEvent.java index 17d20dc4b1..b9dbd0c821 100644 --- a/src/main/java/net/dv8tion/jda/api/events/guild/member/GuildMemberRoleRemoveEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/guild/member/GuildMemberRoleRemoveEvent.java @@ -25,6 +25,8 @@ /** * Indicates that one or more {@link net.dv8tion.jda.api.entities.Role Roles} were removed from a {@link net.dv8tion.jda.api.entities.Member Member}. + *
This event requires {@link net.dv8tion.jda.api.JDABuilder#setGuildSubscriptionsEnabled(boolean) guild subscriptions} + * to be enabled. * *

Can be used to retrieve affected member and guild. Provides a list of removed roles. */ diff --git a/src/main/java/net/dv8tion/jda/api/events/guild/member/update/GuildMemberUpdateBoostTimeEvent.java b/src/main/java/net/dv8tion/jda/api/events/guild/member/update/GuildMemberUpdateBoostTimeEvent.java index decfcd225e..c0fa3143db 100644 --- a/src/main/java/net/dv8tion/jda/api/events/guild/member/update/GuildMemberUpdateBoostTimeEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/guild/member/update/GuildMemberUpdateBoostTimeEvent.java @@ -25,6 +25,8 @@ /** * Indicates that a {@link net.dv8tion.jda.api.entities.Member Member} updated their {@link net.dv8tion.jda.api.entities.Guild Guild} boost time. + *
This event requires {@link net.dv8tion.jda.api.JDABuilder#setGuildSubscriptionsEnabled(boolean) guild subscriptions} + * to be enabled. *
This happens when a member started or stopped boosting a guild. * *

Can be used to retrieve members who boosted, triggering guild. diff --git a/src/main/java/net/dv8tion/jda/api/events/guild/member/update/GuildMemberUpdateNicknameEvent.java b/src/main/java/net/dv8tion/jda/api/events/guild/member/update/GuildMemberUpdateNicknameEvent.java index ee5227af6d..07dd4e65c5 100644 --- a/src/main/java/net/dv8tion/jda/api/events/guild/member/update/GuildMemberUpdateNicknameEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/guild/member/update/GuildMemberUpdateNicknameEvent.java @@ -24,6 +24,8 @@ /** * Indicates that a {@link net.dv8tion.jda.api.entities.Member Member} updated their {@link net.dv8tion.jda.api.entities.Guild Guild} nickname. + *
This event requires {@link net.dv8tion.jda.api.JDABuilder#setGuildSubscriptionsEnabled(boolean) guild subscriptions} + * to be enabled. * *

Can be used to retrieve members who change their nickname, triggering guild, the old nick and the new nick. * diff --git a/src/main/java/net/dv8tion/jda/api/events/guild/package-info.java b/src/main/java/net/dv8tion/jda/api/events/guild/package-info.java index 4814e2a30a..1701b972cc 100644 --- a/src/main/java/net/dv8tion/jda/api/events/guild/package-info.java +++ b/src/main/java/net/dv8tion/jda/api/events/guild/package-info.java @@ -17,6 +17,6 @@ /** * Events for the state of {@link net.dv8tion.jda.api.entities.Guild Guilds} * such as whether the current logged in account joins/leaves a Guild. - *
This includes events that indicate whether a Guild becomes {@link net.dv8tion.jda.api.entities.Guild#isAvailable() available}! + *
This includes events that indicate whether a Guild becomes (un-)available! */ package net.dv8tion.jda.api.events.guild; diff --git a/src/main/java/net/dv8tion/jda/api/events/guild/update/GuildUpdateOwnerEvent.java b/src/main/java/net/dv8tion/jda/api/events/guild/update/GuildUpdateOwnerEvent.java index f46b7e786d..fb0a02b9d2 100644 --- a/src/main/java/net/dv8tion/jda/api/events/guild/update/GuildUpdateOwnerEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/guild/update/GuildUpdateOwnerEvent.java @@ -33,10 +33,56 @@ public class GuildUpdateOwnerEvent extends GenericGuildUpdateEvent { public static final String IDENTIFIER = "owner"; + private final long prevId, nextId; - public GuildUpdateOwnerEvent(@Nonnull JDA api, long responseNumber, @Nonnull Guild guild, @Nullable Member oldOwner) + public GuildUpdateOwnerEvent(@Nonnull JDA api, long responseNumber, @Nonnull Guild guild, @Nullable Member oldOwner, + long prevId, long nextId) { super(api, responseNumber, guild, oldOwner, guild.getOwner(), IDENTIFIER); + this.prevId = prevId; + this.nextId = nextId; + } + + /** + * The previous owner user id + * + * @return The previous owner id + */ + public long getNewOwnerIdLong() + { + return nextId; + } + + /** + * The previous owner user id + * + * @return The previous owner id + */ + @Nonnull + public String getNewOwnerId() + { + return Long.toUnsignedString(nextId); + } + + /** + * The new owner user id + * + * @return The new owner id + */ + public long getOldOwnerIdLong() + { + return prevId; + } + + /** + * The new owner user id + * + * @return The new owner id + */ + @Nonnull + public String getOldOwnerId() + { + return Long.toUnsignedString(prevId); } /** diff --git a/src/main/java/net/dv8tion/jda/api/events/message/MessageReceivedEvent.java b/src/main/java/net/dv8tion/jda/api/events/message/MessageReceivedEvent.java index 44bad40cce..46f3a89b10 100644 --- a/src/main/java/net/dv8tion/jda/api/events/message/MessageReceivedEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/message/MessageReceivedEvent.java @@ -16,7 +16,6 @@ package net.dv8tion.jda.api.events.message; import net.dv8tion.jda.api.JDA; -import net.dv8tion.jda.api.entities.ChannelType; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.User; @@ -79,7 +78,7 @@ public User getAuthor() @Nullable public Member getMember() { - return isFromType(ChannelType.TEXT) && !isWebhookMessage() ? getGuild().getMember(getAuthor()) : null; + return message.getMember(); } /** diff --git a/src/main/java/net/dv8tion/jda/api/events/message/MessageUpdateEvent.java b/src/main/java/net/dv8tion/jda/api/events/message/MessageUpdateEvent.java index 85d377ffd2..ad57e3198c 100644 --- a/src/main/java/net/dv8tion/jda/api/events/message/MessageUpdateEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/message/MessageUpdateEvent.java @@ -16,7 +16,6 @@ package net.dv8tion.jda.api.events.message; import net.dv8tion.jda.api.JDA; -import net.dv8tion.jda.api.entities.ChannelType; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.User; @@ -77,6 +76,6 @@ public User getAuthor() @Nullable public Member getMember() { - return isFromType(ChannelType.TEXT) ? getGuild().getMember(getAuthor()) : null; + return message.getMember(); } } diff --git a/src/main/java/net/dv8tion/jda/api/events/message/guild/GuildMessageReceivedEvent.java b/src/main/java/net/dv8tion/jda/api/events/message/guild/GuildMessageReceivedEvent.java index 2cc9b2dc9a..fc753f9e04 100644 --- a/src/main/java/net/dv8tion/jda/api/events/message/guild/GuildMessageReceivedEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/message/guild/GuildMessageReceivedEvent.java @@ -75,7 +75,7 @@ public User getAuthor() @Nullable public Member getMember() { - return isWebhookMessage() ? null : getGuild().getMember(getAuthor()); + return message.getMember(); } /** diff --git a/src/main/java/net/dv8tion/jda/api/events/message/guild/GuildMessageUpdateEvent.java b/src/main/java/net/dv8tion/jda/api/events/message/guild/GuildMessageUpdateEvent.java index 168af51b4f..368fe2757b 100644 --- a/src/main/java/net/dv8tion/jda/api/events/message/guild/GuildMessageUpdateEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/message/guild/GuildMessageUpdateEvent.java @@ -70,6 +70,6 @@ public User getAuthor() @Nullable public Member getMember() { - return getGuild().getMember(getAuthor()); + return message.getMember(); } } diff --git a/src/main/java/net/dv8tion/jda/api/events/message/guild/react/GenericGuildMessageReactionEvent.java b/src/main/java/net/dv8tion/jda/api/events/message/guild/react/GenericGuildMessageReactionEvent.java index ea387ee66d..307c65aeed 100644 --- a/src/main/java/net/dv8tion/jda/api/events/message/guild/react/GenericGuildMessageReactionEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/message/guild/react/GenericGuildMessageReactionEvent.java @@ -24,7 +24,7 @@ import net.dv8tion.jda.api.events.message.guild.GenericGuildMessageEvent; import javax.annotation.Nonnull; -import java.util.Objects; +import javax.annotation.Nullable; /** * Indicates that a {@link net.dv8tion.jda.api.entities.MessageReaction MessageReaction} was added or removed in a TextChannel. @@ -33,36 +33,63 @@ */ public abstract class GenericGuildMessageReactionEvent extends GenericGuildMessageEvent { - protected final User issuer; + protected final long userId; + protected final Member issuer; protected final MessageReaction reaction; - public GenericGuildMessageReactionEvent(@Nonnull JDA api, long responseNumber, @Nonnull User user, @Nonnull MessageReaction reaction) + public GenericGuildMessageReactionEvent(@Nonnull JDA api, long responseNumber, @Nullable Member user, @Nonnull MessageReaction reaction, long userId) { super(api, responseNumber, reaction.getMessageIdLong(), (TextChannel) reaction.getChannel()); this.issuer = user; this.reaction = reaction; + this.userId = userId; } /** - * The reacting {@link net.dv8tion.jda.api.entities.User User} + * The id for the user who added/removed their reaction. * - * @return The reacting user + * @return The user id */ @Nonnull + public String getUserId() + { + return Long.toUnsignedString(userId); + } + + /** + * The id for the user who added/removed their reaction. + * + * @return The user id + */ + public long getUserIdLong() + { + return userId; + } + + /** + * The reacting {@link net.dv8tion.jda.api.entities.User User} + *
This might be missing if the user was not previously cached or the member was removed. + * + * @return The reacting user or null if this information is missing + * + * @see #getUserIdLong() + */ + @Nullable public User getUser() { - return issuer; + return issuer == null ? getJDA().getUserById(userId) : issuer.getUser(); } /** * The {@link net.dv8tion.jda.api.entities.Member Member} instance for the reacting user + *
This might be missing if the user was not previously cached or the member was removed. * - * @return The member instance for the reacting user + * @return The member instance for the reacting user or null if this information is missing */ - @Nonnull + @Nullable public Member getMember() { - return Objects.requireNonNull(getGuild().getMember(getUser())); + return issuer; } /** diff --git a/src/main/java/net/dv8tion/jda/api/events/message/guild/react/GuildMessageReactionAddEvent.java b/src/main/java/net/dv8tion/jda/api/events/message/guild/react/GuildMessageReactionAddEvent.java index ea2cbc86b2..b93f96ef9f 100644 --- a/src/main/java/net/dv8tion/jda/api/events/message/guild/react/GuildMessageReactionAddEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/message/guild/react/GuildMessageReactionAddEvent.java @@ -17,6 +17,7 @@ package net.dv8tion.jda.api.events.message.guild.react; import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.MessageReaction; import net.dv8tion.jda.api.entities.User; @@ -29,8 +30,24 @@ */ public class GuildMessageReactionAddEvent extends GenericGuildMessageReactionEvent { - public GuildMessageReactionAddEvent(@Nonnull JDA api, long responseNumber, @Nonnull User user, @Nonnull MessageReaction reaction) + public GuildMessageReactionAddEvent(@Nonnull JDA api, long responseNumber, @Nonnull Member member, @Nonnull MessageReaction reaction) { - super(api, responseNumber, user, reaction); + super(api, responseNumber, member, reaction, member.getIdLong()); + } + + @Nonnull + @Override + @SuppressWarnings("ConstantConditions") + public User getUser() + { + return super.getUser(); + } + + @Nonnull + @Override + @SuppressWarnings("ConstantConditions") + public Member getMember() + { + return super.getMember(); } } diff --git a/src/main/java/net/dv8tion/jda/api/events/message/guild/react/GuildMessageReactionRemoveEvent.java b/src/main/java/net/dv8tion/jda/api/events/message/guild/react/GuildMessageReactionRemoveEvent.java index f7ca8465ff..fd72be6e37 100644 --- a/src/main/java/net/dv8tion/jda/api/events/message/guild/react/GuildMessageReactionRemoveEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/message/guild/react/GuildMessageReactionRemoveEvent.java @@ -17,10 +17,11 @@ package net.dv8tion.jda.api.events.message.guild.react; import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.MessageReaction; -import net.dv8tion.jda.api.entities.User; import javax.annotation.Nonnull; +import javax.annotation.Nullable; /** * Indicates that a {@link net.dv8tion.jda.api.entities.MessageReaction MessageReaction} was removed from a Message in a Guild @@ -29,8 +30,8 @@ */ public class GuildMessageReactionRemoveEvent extends GenericGuildMessageReactionEvent { - public GuildMessageReactionRemoveEvent(@Nonnull JDA api, long responseNumber, @Nonnull User user, @Nonnull MessageReaction reaction) + public GuildMessageReactionRemoveEvent(@Nonnull JDA api, long responseNumber, @Nullable Member member, @Nonnull MessageReaction reaction, long userId) { - super(api, responseNumber, user, reaction); + super(api, responseNumber, member, reaction, userId); } } diff --git a/src/main/java/net/dv8tion/jda/api/events/message/priv/react/GenericPrivateMessageReactionEvent.java b/src/main/java/net/dv8tion/jda/api/events/message/priv/react/GenericPrivateMessageReactionEvent.java index 7131968211..6e063c5f0f 100644 --- a/src/main/java/net/dv8tion/jda/api/events/message/priv/react/GenericPrivateMessageReactionEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/message/priv/react/GenericPrivateMessageReactionEvent.java @@ -23,6 +23,7 @@ import net.dv8tion.jda.api.events.message.priv.GenericPrivateMessageEvent; import javax.annotation.Nonnull; +import javax.annotation.Nullable; /** * Indicates that a {@link net.dv8tion.jda.api.entities.MessageReaction MessageReaction} was added or removed. @@ -31,22 +32,46 @@ */ public class GenericPrivateMessageReactionEvent extends GenericPrivateMessageEvent { + protected final long userId; protected final User issuer; protected final MessageReaction reaction; - public GenericPrivateMessageReactionEvent(@Nonnull JDA api, long responseNumber, @Nonnull User user, @Nonnull MessageReaction reaction) + public GenericPrivateMessageReactionEvent(@Nonnull JDA api, long responseNumber, @Nonnull User user, @Nonnull MessageReaction reaction, long userId) { super(api, responseNumber, reaction.getMessageIdLong(), (PrivateChannel) reaction.getChannel()); + this.userId = userId; this.issuer = user; this.reaction = reaction; } + /** + * The id for the user who added/removed their reaction. + * + * @return The user id + */ + @Nonnull + public String getUserId() + { + return Long.toUnsignedString(userId); + } + + /** + * The id for the user who added/removed their reaction. + * + * @return The user id + */ + public long getUserIdLong() + { + return userId; + } + /** * The reacting {@link net.dv8tion.jda.api.entities.User User} + *
This might be missing if the user was not cached. * * @return The reacting user */ - @Nonnull + @Nullable public User getUser() { return issuer; diff --git a/src/main/java/net/dv8tion/jda/api/events/message/priv/react/PrivateMessageReactionAddEvent.java b/src/main/java/net/dv8tion/jda/api/events/message/priv/react/PrivateMessageReactionAddEvent.java index 0df59f7600..601961671e 100644 --- a/src/main/java/net/dv8tion/jda/api/events/message/priv/react/PrivateMessageReactionAddEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/message/priv/react/PrivateMessageReactionAddEvent.java @@ -29,8 +29,8 @@ */ public class PrivateMessageReactionAddEvent extends GenericPrivateMessageReactionEvent { - public PrivateMessageReactionAddEvent(@Nonnull JDA api, long responseNumber, @Nonnull User user, @Nonnull MessageReaction reaction) + public PrivateMessageReactionAddEvent(@Nonnull JDA api, long responseNumber, @Nonnull User user, @Nonnull MessageReaction reaction, long userId) { - super(api, responseNumber, user, reaction); + super(api, responseNumber, user, reaction, userId); } } diff --git a/src/main/java/net/dv8tion/jda/api/events/message/priv/react/PrivateMessageReactionRemoveEvent.java b/src/main/java/net/dv8tion/jda/api/events/message/priv/react/PrivateMessageReactionRemoveEvent.java index a2b73c95ca..eda71df543 100644 --- a/src/main/java/net/dv8tion/jda/api/events/message/priv/react/PrivateMessageReactionRemoveEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/message/priv/react/PrivateMessageReactionRemoveEvent.java @@ -29,8 +29,8 @@ */ public class PrivateMessageReactionRemoveEvent extends GenericPrivateMessageReactionEvent { - public PrivateMessageReactionRemoveEvent(@Nonnull JDA api, long responseNumber, @Nonnull User user, @Nonnull MessageReaction reaction) + public PrivateMessageReactionRemoveEvent(@Nonnull JDA api, long responseNumber, @Nonnull User user, @Nonnull MessageReaction reaction, long userId) { - super(api, responseNumber, user, reaction); + super(api, responseNumber, user, reaction, userId); } } diff --git a/src/main/java/net/dv8tion/jda/api/events/message/react/GenericMessageReactionEvent.java b/src/main/java/net/dv8tion/jda/api/events/message/react/GenericMessageReactionEvent.java index 58095c174d..2fe7f74bd3 100644 --- a/src/main/java/net/dv8tion/jda/api/events/message/react/GenericMessageReactionEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/message/react/GenericMessageReactionEvent.java @@ -33,22 +33,49 @@ */ public class GenericMessageReactionEvent extends GenericMessageEvent { + protected final long userId; protected User issuer; + protected Member member; protected MessageReaction reaction; - public GenericMessageReactionEvent(@Nonnull JDA api, long responseNumber, @Nonnull User user, @Nonnull MessageReaction reaction) + public GenericMessageReactionEvent(@Nonnull JDA api, long responseNumber, @Nonnull User user, + @Nullable Member member, @Nonnull MessageReaction reaction, long userId) { super(api, responseNumber, reaction.getMessageIdLong(), reaction.getChannel()); + this.userId = userId; this.issuer = user; + this.member = member; this.reaction = reaction; } /** - * The reacting {@link net.dv8tion.jda.api.entities.User User} + * The id for the user who added/removed their reaction. * - * @return The reacting user + * @return The user id */ @Nonnull + public String getUserId() + { + return Long.toUnsignedString(userId); + } + + /** + * The id for the user who added/removed their reaction. + * + * @return The user id + */ + public long getUserIdLong() + { + return userId; + } + + /** + * The reacting {@link net.dv8tion.jda.api.entities.User User} + *
This might be missing if the user was not cached. + * + * @return The reacting user or null if this information is missing + */ + @Nullable public User getUser() { return issuer; @@ -69,7 +96,7 @@ public User getUser() @Nullable public Member getMember() { - return getGuild().getMember(getUser()); + return member; } /** diff --git a/src/main/java/net/dv8tion/jda/api/events/message/react/MessageReactionAddEvent.java b/src/main/java/net/dv8tion/jda/api/events/message/react/MessageReactionAddEvent.java index 281d4b221b..8e8d276b92 100644 --- a/src/main/java/net/dv8tion/jda/api/events/message/react/MessageReactionAddEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/message/react/MessageReactionAddEvent.java @@ -17,10 +17,12 @@ package net.dv8tion.jda.api.events.message.react; import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.MessageReaction; import net.dv8tion.jda.api.entities.User; import javax.annotation.Nonnull; +import javax.annotation.Nullable; /** * Indicates that a user added a reaction to a message @@ -30,8 +32,9 @@ */ public class MessageReactionAddEvent extends GenericMessageReactionEvent { - public MessageReactionAddEvent(@Nonnull JDA api, long responseNumber, @Nonnull User user, @Nonnull MessageReaction reaction) + public MessageReactionAddEvent(@Nonnull JDA api, long responseNumber, @Nonnull User user, + @Nullable Member member, @Nonnull MessageReaction reaction, long userId) { - super(api, responseNumber, user, reaction); + super(api, responseNumber, user, member, reaction, userId); } } diff --git a/src/main/java/net/dv8tion/jda/api/events/message/react/MessageReactionRemoveEvent.java b/src/main/java/net/dv8tion/jda/api/events/message/react/MessageReactionRemoveEvent.java index c59db2f9bc..0127d7a333 100644 --- a/src/main/java/net/dv8tion/jda/api/events/message/react/MessageReactionRemoveEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/message/react/MessageReactionRemoveEvent.java @@ -17,10 +17,12 @@ package net.dv8tion.jda.api.events.message.react; import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.MessageReaction; import net.dv8tion.jda.api.entities.User; import javax.annotation.Nonnull; +import javax.annotation.Nullable; /** * Indicates that a user removed the reaction on a message @@ -29,8 +31,9 @@ */ public class MessageReactionRemoveEvent extends GenericMessageReactionEvent { - public MessageReactionRemoveEvent(@Nonnull JDA api, long responseNumber, @Nonnull User user, @Nonnull MessageReaction reaction) + public MessageReactionRemoveEvent(@Nonnull JDA api, long responseNumber, @Nonnull User user, + @Nullable Member member, @Nonnull MessageReaction reaction, long userId) { - super(api, responseNumber, user, reaction); + super(api, responseNumber, user, member, reaction, userId); } } diff --git a/src/main/java/net/dv8tion/jda/api/events/user/UserActivityEndEvent.java b/src/main/java/net/dv8tion/jda/api/events/user/UserActivityEndEvent.java index e50652c424..3486f1e479 100644 --- a/src/main/java/net/dv8tion/jda/api/events/user/UserActivityEndEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/user/UserActivityEndEvent.java @@ -27,6 +27,8 @@ /** * Indicates that a {@link net.dv8tion.jda.api.entities.User User} has stopped an {@link Activity} * in a {@link Guild}. + *
This event requires {@link net.dv8tion.jda.api.JDABuilder#setGuildSubscriptionsEnabled(boolean) guild subscriptions} + * to be enabled. * *

This is fired for every {@link Guild} the user is part of. If the title of a stream * changes a start event is fired before an end event which will replace the activity. diff --git a/src/main/java/net/dv8tion/jda/api/events/user/UserActivityStartEvent.java b/src/main/java/net/dv8tion/jda/api/events/user/UserActivityStartEvent.java index 7df82ba636..05e871d1a0 100644 --- a/src/main/java/net/dv8tion/jda/api/events/user/UserActivityStartEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/user/UserActivityStartEvent.java @@ -27,6 +27,8 @@ /** * Indicates that a {@link net.dv8tion.jda.api.entities.User User} has started an {@link Activity} * in a {@link Guild}. + *
This event requires {@link net.dv8tion.jda.api.JDABuilder#setGuildSubscriptionsEnabled(boolean) guild subscriptions} + * to be enabled. * *

This is fired for every {@link Guild} the user is part of. If the title of a stream * changes a start event is fired before an end event which will replace the activity. diff --git a/src/main/java/net/dv8tion/jda/api/events/user/UserTypingEvent.java b/src/main/java/net/dv8tion/jda/api/events/user/UserTypingEvent.java index ffb0edd992..c4019f34f4 100644 --- a/src/main/java/net/dv8tion/jda/api/events/user/UserTypingEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/user/UserTypingEvent.java @@ -24,6 +24,8 @@ /** * Indicates that a {@link net.dv8tion.jda.api.entities.User User} started typing. (Similar to the typing indicator in the Discord client) + *
This event requires {@link net.dv8tion.jda.api.JDABuilder#setGuildSubscriptionsEnabled(boolean) guild subscriptions} + * to be enabled. * *

Can be used to retrieve the User who started typing and when and in which MessageChannel they started typing. */ diff --git a/src/main/java/net/dv8tion/jda/api/events/user/update/GenericUserPresenceEvent.java b/src/main/java/net/dv8tion/jda/api/events/user/update/GenericUserPresenceEvent.java index 9eae9daf47..faa844d2c2 100644 --- a/src/main/java/net/dv8tion/jda/api/events/user/update/GenericUserPresenceEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/user/update/GenericUserPresenceEvent.java @@ -24,6 +24,8 @@ /** * Indicates that the presence of a {@link net.dv8tion.jda.api.entities.User User} has changed. + *
This event requires {@link net.dv8tion.jda.api.JDABuilder#setGuildSubscriptionsEnabled(boolean) guild subscriptions} + * to be enabled. *
Users don't have presences directly, this is fired when a {@link net.dv8tion.jda.api.entities.Member Member} from a {@link net.dv8tion.jda.api.entities.Guild Guild} * changes their presence. * diff --git a/src/main/java/net/dv8tion/jda/api/events/user/update/UserUpdateActivityOrderEvent.java b/src/main/java/net/dv8tion/jda/api/events/user/update/UserUpdateActivityOrderEvent.java index 95168e7791..6e8ee53a04 100644 --- a/src/main/java/net/dv8tion/jda/api/events/user/update/UserUpdateActivityOrderEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/user/update/UserUpdateActivityOrderEvent.java @@ -27,6 +27,7 @@ /** * Indicates that the {@link net.dv8tion.jda.api.entities.Activity Activity} order of a {@link net.dv8tion.jda.api.entities.User User} changes. *
As with any presence updates this happened for a {@link net.dv8tion.jda.api.entities.Member Member} in a Guild! + *
This event requires {@link net.dv8tion.jda.api.JDABuilder#setGuildSubscriptionsEnabled(boolean) guild subscriptions} to be enabled. * *

Can be used to retrieve the User who changed their Activities and their previous Activities. * diff --git a/src/main/java/net/dv8tion/jda/api/events/user/update/UserUpdateAvatarEvent.java b/src/main/java/net/dv8tion/jda/api/events/user/update/UserUpdateAvatarEvent.java index da9c82ab0c..2e31b29a70 100644 --- a/src/main/java/net/dv8tion/jda/api/events/user/update/UserUpdateAvatarEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/user/update/UserUpdateAvatarEvent.java @@ -24,6 +24,8 @@ /** * Indicates that the Avatar of a {@link net.dv8tion.jda.api.entities.User User} changed. + *
This event requires {@link net.dv8tion.jda.api.JDABuilder#setGuildSubscriptionsEnabled(boolean) guild subscriptions} + * to be enabled. * *

Can be used to retrieve the User who changed their avatar and their previous Avatar ID/URL. * diff --git a/src/main/java/net/dv8tion/jda/api/events/user/update/UserUpdateDiscriminatorEvent.java b/src/main/java/net/dv8tion/jda/api/events/user/update/UserUpdateDiscriminatorEvent.java index 030db81186..ffe24ef9ab 100644 --- a/src/main/java/net/dv8tion/jda/api/events/user/update/UserUpdateDiscriminatorEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/user/update/UserUpdateDiscriminatorEvent.java @@ -23,6 +23,8 @@ /** * Indicates that the discriminator of a {@link net.dv8tion.jda.api.entities.User User} changed. + *
This event requires {@link net.dv8tion.jda.api.JDABuilder#setGuildSubscriptionsEnabled(boolean) guild subscriptions} + * to be enabled. * *

Can be used to retrieve the User who changed their discriminator and their previous discriminator. * diff --git a/src/main/java/net/dv8tion/jda/api/events/user/update/UserUpdateNameEvent.java b/src/main/java/net/dv8tion/jda/api/events/user/update/UserUpdateNameEvent.java index 0f6f047936..3d15262909 100644 --- a/src/main/java/net/dv8tion/jda/api/events/user/update/UserUpdateNameEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/user/update/UserUpdateNameEvent.java @@ -23,6 +23,8 @@ /** * Indicates that the username of a {@link net.dv8tion.jda.api.entities.User User} changed. (Not Nickname) + *
This event requires {@link net.dv8tion.jda.api.JDABuilder#setGuildSubscriptionsEnabled(boolean) guild subscriptions} + * to be enabled. * *

Can be used to retrieve the User who changed their username and their previous username. * diff --git a/src/main/java/net/dv8tion/jda/api/events/user/update/UserUpdateOnlineStatusEvent.java b/src/main/java/net/dv8tion/jda/api/events/user/update/UserUpdateOnlineStatusEvent.java index 7456a7d2ba..f6668f7501 100644 --- a/src/main/java/net/dv8tion/jda/api/events/user/update/UserUpdateOnlineStatusEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/user/update/UserUpdateOnlineStatusEvent.java @@ -26,7 +26,8 @@ /** * Indicates that the {@link OnlineStatus OnlineStatus} of a {@link net.dv8tion.jda.api.entities.User User} changed. - *
As with any presence updates this happened for a {@link net.dv8tion.jda.api.entities.Member Member} in a Guild ! + *
As with any presence updates this happened for a {@link net.dv8tion.jda.api.entities.Member Member} in a Guild! + *
This event requires {@link net.dv8tion.jda.api.JDABuilder#setGuildSubscriptionsEnabled(boolean) guild subscriptions} to be enabled. * *

Can be used to retrieve the User who changed their status and their previous status. * diff --git a/src/main/java/net/dv8tion/jda/api/exceptions/GuildUnavailableException.java b/src/main/java/net/dv8tion/jda/api/exceptions/GuildUnavailableException.java index 9331f99df8..797d8f52c2 100644 --- a/src/main/java/net/dv8tion/jda/api/exceptions/GuildUnavailableException.java +++ b/src/main/java/net/dv8tion/jda/api/exceptions/GuildUnavailableException.java @@ -15,10 +15,18 @@ */ package net.dv8tion.jda.api.exceptions; +import net.dv8tion.jda.annotations.DeprecatedSince; +import net.dv8tion.jda.annotations.ForRemoval; + /** * Indicates that a {@link net.dv8tion.jda.api.entities.Guild Guild} is not {@link net.dv8tion.jda.api.entities.Guild#isAvailable() available} *
Thrown when an operation requires a Guild to be available and {@link net.dv8tion.jda.api.entities.Guild#isAvailable() Guild#isAvailable()} is {@code false} + * + * @deprecated This will be removed in favor of a better system which does not keep unavailable guilds in cache in the first place. */ +@Deprecated +@ForRemoval +@DeprecatedSince("4.1.0") public class GuildUnavailableException extends RuntimeException { /** diff --git a/src/main/java/net/dv8tion/jda/api/managers/AudioManager.java b/src/main/java/net/dv8tion/jda/api/managers/AudioManager.java index d6598a0eea..697495960e 100644 --- a/src/main/java/net/dv8tion/jda/api/managers/AudioManager.java +++ b/src/main/java/net/dv8tion/jda/api/managers/AudioManager.java @@ -69,8 +69,6 @@ public interface AudioManager * * @throws UnsupportedOperationException * If audio is disabled due to an internal JDA error - * @throws net.dv8tion.jda.api.exceptions.GuildUnavailableException - * If the Guild is temporarily unavailable * @throws net.dv8tion.jda.api.exceptions.InsufficientPermissionException *

    *
  • If the currently logged in account does not have the Permission {@link net.dv8tion.jda.api.Permission#VOICE_CONNECT VOICE_CONNECT}
  • 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 f3eccef9c3..26ca447e7f 100644 --- a/src/main/java/net/dv8tion/jda/api/sharding/DefaultShardManager.java +++ b/src/main/java/net/dv8tion/jda/api/sharding/DefaultShardManager.java @@ -21,6 +21,7 @@ import net.dv8tion.jda.api.OnlineStatus; import net.dv8tion.jda.api.entities.Activity; import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.utils.ChunkingFilter; import net.dv8tion.jda.api.utils.MiscUtil; import net.dv8tion.jda.api.utils.SessionController; import net.dv8tion.jda.api.utils.cache.ShardCacheView; @@ -135,6 +136,11 @@ public class DefaultShardManager implements ShardManager */ protected final ShardingMetaConfig metaConfig; + /** + * {@link ChunkingFilter} used to determine whether a guild should be lazy loaded or chunk members by default. + */ + protected final ChunkingFilter chunkingFilter; + public DefaultShardManager(@Nonnull String token) { this(token, null); @@ -142,14 +148,15 @@ public DefaultShardManager(@Nonnull String token) public DefaultShardManager(@Nonnull String token, @Nullable Collection shardIds) { - this(token, shardIds, null, null, null, null, null, null); + this(token, shardIds, null, null, null, null, null, null, null); } public DefaultShardManager( @Nonnull String token, @Nullable Collection shardIds, @Nullable ShardingConfig shardingConfig, @Nullable EventConfig eventConfig, @Nullable PresenceProviderConfig presenceConfig, @Nullable ThreadingProviderConfig threadingConfig, - @Nullable ShardingSessionConfig sessionConfig, @Nullable ShardingMetaConfig metaConfig) + @Nullable ShardingSessionConfig sessionConfig, @Nullable ShardingMetaConfig metaConfig, + @Nullable ChunkingFilter chunkingFilter) { this.token = token; this.eventConfig = eventConfig == null ? EventConfig.getDefault() : eventConfig; @@ -158,6 +165,7 @@ public DefaultShardManager( this.sessionConfig = sessionConfig == null ? ShardingSessionConfig.getDefault() : sessionConfig; this.presenceConfig = presenceConfig == null ? PresenceProviderConfig.getDefault() : presenceConfig; this.metaConfig = metaConfig == null ? ShardingMetaConfig.getDefault() : metaConfig; + this.chunkingFilter = chunkingFilter == null ? ChunkingFilter.ALL : chunkingFilter; this.executor = createExecutor(this.threadingConfig.getThreadFactory()); this.shutdownHook = this.metaConfig.isUseShutdownHook() ? new Thread(this::shutdown, "JDA Shutdown Hook") : null; @@ -473,15 +481,14 @@ protected JDAImpl buildInstance(final int shardId) throws LoginException, Interr boolean shutdownCallbackPool = callbackPair.automaticShutdown; AuthorizationConfig authConfig = new AuthorizationConfig(AccountType.BOT, token); - SessionConfig sessionConfig = new SessionConfig(this.sessionConfig.getSessionController(), httpClient, - this.sessionConfig.getWebSocketFactory(), this.sessionConfig.getVoiceDispatchInterceptor(), - this.sessionConfig.getFlags(), this.sessionConfig.getMaxReconnectDelay()); + SessionConfig sessionConfig = this.sessionConfig.toSessionConfig(httpClient); ThreadingConfig threadingConfig = new ThreadingConfig(); 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()); final JDAImpl jda = new JDAImpl(authConfig, sessionConfig, threadingConfig, metaConfig); + jda.setChunkingFilter(chunkingFilter); threadingConfig.init(jda::getIdentifierString); jda.setShardManager(this); 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 daa8061610..ad04a17113 100644 --- a/src/main/java/net/dv8tion/jda/api/sharding/DefaultShardManagerBuilder.java +++ b/src/main/java/net/dv8tion/jda/api/sharding/DefaultShardManagerBuilder.java @@ -24,6 +24,7 @@ import net.dv8tion.jda.api.hooks.IEventManager; import net.dv8tion.jda.api.hooks.VoiceDispatchInterceptor; import net.dv8tion.jda.api.requests.RestAction; +import net.dv8tion.jda.api.utils.ChunkingFilter; import net.dv8tion.jda.api.utils.Compression; import net.dv8tion.jda.api.utils.SessionController; import net.dv8tion.jda.api.utils.cache.CacheFlag; @@ -47,8 +48,9 @@ *

    A single DefaultShardManagerBuilder can be reused multiple times. Each call to {@link #build()} * creates a new {@link net.dv8tion.jda.api.sharding.ShardManager ShardManager} instance using the same information. * - * @since 3.4 * @author Aljoscha Grebe + * + * @since 3.4.0 */ public class DefaultShardManagerBuilder { @@ -62,6 +64,7 @@ public class DefaultShardManagerBuilder protected Compression compression = Compression.ZLIB; protected int shardsTotal = -1; protected int maxReconnectDelay = 900; + protected int largeThreshold = 250; protected String token = null; protected IntFunction idleProvider = null; protected IntFunction statusProvider = null; @@ -77,6 +80,7 @@ public class DefaultShardManagerBuilder protected WebSocketFactory wsFactory = null; protected IAudioSendFactory audioSendFactory = null; protected ThreadFactory threadFactory = null; + protected ChunkingFilter chunkingFilter; /** * Creates a completely empty DefaultShardManagerBuilder. @@ -1206,6 +1210,74 @@ public DefaultShardManagerBuilder setWebsocketFactory(@Nullable WebSocketFactory return this; } + /** + * The {@link ChunkingFilter} to filter which guilds should use member chunking. + *
    By default this uses {@link ChunkingFilter#ALL}. + * + *

    This filter is useless when {@link #setGuildSubscriptionsEnabled(boolean)} is false. + * + * @param filter + * The filter to apply + * + * @return The DefaultShardManagerBuilder instance. Useful for chaining. + * + * @since 4.0.0 + * + * @see ChunkingFilter#NONE + * @see ChunkingFilter#include(long...) + * @see ChunkingFilter#exclude(long...) + */ + @Nonnull + public DefaultShardManagerBuilder setChunkingFilter(@Nullable ChunkingFilter filter) + { + this.chunkingFilter = filter; + return this; + } + + /** + * Enable typing and presence update events. + *
    These events cover the majority of traffic happening on the gateway and thus cause a lot + * of bandwidth usage. Disabling these events means the cache for users might become outdated since + * user properties are only updated by presence updates. + *
    Default: true + * + *

    Notice

    + * This disables the majority of member cache and related events. If anything in your project + * relies on member state you should keep this enabled. + * + * @param enabled + * True, if guild subscriptions should be enabled + * + * @return The DefaultShardManagerBuilder instance. Useful for chaining. + * + * @since 4.0.0 + */ + @Nonnull + public DefaultShardManagerBuilder setGuildSubscriptionsEnabled(boolean enabled) + { + return setFlag(ConfigFlag.GUILD_SUBSCRIPTIONS, enabled); + } + + /** + * Decides the total number of members at which a guild should start to use lazy loading. + *
    This is limited to a number between 50 and 250 (inclusive). + * If the {@link #setChunkingFilter(ChunkingFilter) chunking filter} is set to {@link ChunkingFilter#ALL} + * this should be set to {@code 250} (default) to minimize the amount of guilds that need to request members. + * + * @param threshold + * The threshold in {@code [50, 250]} + * + * @return The DefaultShardManagerBuilder instance. Useful for chaining. + * + * @since 4.0.0 + */ + @Nonnull + public DefaultShardManagerBuilder setLargeThreshold(int threshold) + { + this.largeThreshold = Math.max(50, Math.min(250, threshold)); // enforce 50 <= t <= 250 + 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 @@ -1236,9 +1308,9 @@ public ShardManager build() throws LoginException, IllegalArgumentException presenceConfig.setStatusProvider(statusProvider); 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); + 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 DefaultShardManager manager = new DefaultShardManager(this.token, this.shards, shardingConfig, eventConfig, presenceConfig, threadingConfig, sessionConfig, metaConfig); + 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/utils/ChunkingFilter.java b/src/main/java/net/dv8tion/jda/api/utils/ChunkingFilter.java new file mode 100644 index 0000000000..c77c0039c9 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/utils/ChunkingFilter.java @@ -0,0 +1,104 @@ +/* + * 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.utils; + +import net.dv8tion.jda.internal.utils.Checks; + +import javax.annotation.Nonnull; + +/** + * Filter function for member chunking of guilds. + *
    The filter decides based on the provided guild id whether chunking should be done + * on guild initialization. + * + * @since 4.1.0 + * + * @see #ALL + * @see #NONE + * + * @see net.dv8tion.jda.api.JDABuilder#setChunkingFilter(ChunkingFilter) JDABuilder.setChunkingFilter(ChunkingFilter) + * @see net.dv8tion.jda.api.sharding.DefaultShardManagerBuilder#setChunkingFilter(ChunkingFilter) DefaultShardManagerBuilder.setChunkingFilter(ChunkingFilter) + */ +@FunctionalInterface +public interface ChunkingFilter +{ + /** Chunk all guilds (default) */ + ChunkingFilter ALL = (x) -> true; + /** Do not chunk any guilds (lazy loading) */ + ChunkingFilter NONE = (x) -> false; + + /** + * Decide whether the specified guild should chunk members. + * + * @param guildId + * The guild id + * + * @return True, if this guild should chunk + */ + boolean filter(long guildId); + + /** + * Factory method to chunk a whitelist of guild ids. + *
    All guilds that are not mentioned will use lazy loading. + * + *

    This is useful to only chunk specific guilds like the hub server of a bot. + * + * @param ids + * The ids that should be chunked + * + * @return The resulting filter + */ + @Nonnull + static ChunkingFilter include(@Nonnull long... ids) + { + Checks.notNull(ids, "ID array"); + return (guild) -> { + for (long id : ids) + { + if (id == guild) + return true; + } + return false; + }; + } + + /** + * Factory method to disable chunking for a blacklist of guild ids. + *
    All guilds that are not mentioned will use chunking. + * + *

    This is useful when the bot is only in one very large server that + * takes most of its memory and should be ignored instead. + * + * @param ids + * The ids that should not be chunked + * + * @return The resulting filter + */ + @Nonnull + static ChunkingFilter exclude(@Nonnull long... ids) + { + Checks.notNull(ids, "ID array"); + return (guild) -> { + for (long id : ids) + { + if (id == guild) + return false; + } + return true; + }; + } +} diff --git a/src/main/java/net/dv8tion/jda/api/utils/ClosableIterator.java b/src/main/java/net/dv8tion/jda/api/utils/ClosableIterator.java index c39604a36d..f708ec5ab8 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/ClosableIterator.java +++ b/src/main/java/net/dv8tion/jda/api/utils/ClosableIterator.java @@ -38,7 +38,7 @@ * @param * The element type * - * @since 4.0.0 + * @since 4.0.0 */ public interface ClosableIterator extends Iterator, AutoCloseable { 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 136a16b503..56db009371 100644 --- a/src/main/java/net/dv8tion/jda/api/utils/SessionControllerAdapter.java +++ b/src/main/java/net/dv8tion/jda/api/utils/SessionControllerAdapter.java @@ -54,6 +54,7 @@ public SessionControllerAdapter() @Override public void appendSession(@Nonnull SessionConnectNode node) { + removeSession(node); connectQueue.add(node); runWorker(); } @@ -230,7 +231,7 @@ protected void processQueue() Throwable t = e.getCause(); if (t instanceof OpeningHandshakeException) log.error("Failed opening handshake, appending to queue. Message: {}", e.getMessage()); - else + else if (!JDA.Status.RECONNECT_QUEUED.name().equals(t.getMessage())) log.error("Failed to establish connection for a node, appending to queue", e); appendSession(node); } diff --git a/src/main/java/net/dv8tion/jda/internal/JDAImpl.java b/src/main/java/net/dv8tion/jda/internal/JDAImpl.java index b091f4693f..c82b3f46f0 100644 --- a/src/main/java/net/dv8tion/jda/internal/JDAImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/JDAImpl.java @@ -18,6 +18,7 @@ import com.neovisionaries.ws.client.WebSocketFactory; import gnu.trove.map.TLongObjectMap; +import gnu.trove.set.TLongSet; import net.dv8tion.jda.api.AccountType; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.Permission; @@ -39,6 +40,7 @@ import net.dv8tion.jda.api.requests.Response; import net.dv8tion.jda.api.requests.RestAction; import net.dv8tion.jda.api.sharding.ShardManager; +import net.dv8tion.jda.api.utils.ChunkingFilter; import net.dv8tion.jda.api.utils.Compression; import net.dv8tion.jda.api.utils.MiscUtil; import net.dv8tion.jda.api.utils.SessionController; @@ -98,7 +100,7 @@ public class JDAImpl implements JDA protected final PresenceImpl presence; protected final Thread shutdownHook; protected final EntityBuilder entityBuilder = new EntityBuilder(this); - protected final EventCache eventCache = new EventCache(); + protected final EventCache eventCache; protected final EventManagerProxy eventManager = new EventManagerProxy(new InterfacedEventManager()); protected final GuildSetupController guildSetupController; @@ -118,6 +120,7 @@ public class JDAImpl implements JDA protected long responseTotal; protected long gatewayPing = -1; protected String gatewayUrl; + protected ChunkingFilter chunkingFilter; protected String clientId = null; protected ShardManager shardManager = null; @@ -141,6 +144,7 @@ public JDAImpl( this.requester.setRetryOnTimeout(this.sessionConfig.isRetryOnTimeout()); this.guildSetupController = new GuildSetupController(this); this.audioController = new DirectAudioControllerImpl(this); + this.eventCache = new EventCache(isGuildSubscriptions()); } public void handleEvent(@Nonnull GenericEvent event) @@ -163,6 +167,34 @@ public boolean isCacheFlagSet(CacheFlag flag) return metaConfig.getCacheFlags().contains(flag); } + public boolean isGuildSubscriptions() + { + return metaConfig.isGuildSubscriptions(); + } + + public int getLargeThreshold() + { + return sessionConfig.getLargeThreshold(); + } + + public boolean chunkGuild(long id) + { + try + { + return isGuildSubscriptions() && chunkingFilter.filter(id); + } + catch (Exception e) + { + LOG.error("Uncaught exception from chunking filter", e); + return true; + } + } + + public void setChunkingFilter(ChunkingFilter filter) + { + this.chunkingFilter = filter; + } + public SessionController getSessionController() { return sessionConfig.getSessionController(); @@ -428,17 +460,20 @@ public long getGatewayPing() @Nonnull @Override - public JDA awaitStatus(@Nonnull Status status) throws InterruptedException + public JDA awaitStatus(@Nonnull Status status, @Nonnull Status... failOn) throws InterruptedException { Checks.notNull(status, "Status"); Checks.check(status.isInit(), "Cannot await the status %s as it is not part of the login cycle!", status); if (getStatus() == Status.CONNECTED) return this; + List failStatus = Arrays.asList(failOn); while (!getStatus().isInit() // JDA might disconnect while starting || getStatus().ordinal() < status.ordinal()) // Wait until status is bypassed { if (getStatus() == Status.SHUTDOWN) throw new IllegalStateException("Was shutdown trying to await status"); + else if (failStatus.contains(getStatus())) + return this; Thread.sleep(50); } return this; @@ -514,7 +549,8 @@ public RestAction retrieveUserById(long id) // check cache User user = this.getUserById(id); - if (user != null) + // 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)); @@ -536,6 +572,16 @@ public SnowflakeCacheView getGuildCache() return guildCache; } + @Nonnull + @Override + public Set getUnavailableGuilds() + { + TLongSet unavailableGuilds = guildSetupController.getUnavailableGuilds(); + Set copy = new HashSet<>(); + unavailableGuilds.forEach(id -> copy.add(Long.toUnsignedString(id))); + return copy; + } + @Nonnull @Override public SnowflakeCacheView getRoleCache() diff --git a/src/main/java/net/dv8tion/jda/internal/audio/ConnectionRequest.java b/src/main/java/net/dv8tion/jda/internal/audio/ConnectionRequest.java index 7d617cf534..c27f3cff86 100644 --- a/src/main/java/net/dv8tion/jda/internal/audio/ConnectionRequest.java +++ b/src/main/java/net/dv8tion/jda/internal/audio/ConnectionRequest.java @@ -80,4 +80,10 @@ public long getGuildIdLong() { return guildId; } + + @Override + public String toString() + { + return stage + "(" + Long.toUnsignedString(guildId) + "#" + Long.toUnsignedString(channelId) + ")"; + } } 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 2fdb3f99c6..9a9967a101 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java @@ -28,6 +28,13 @@ import net.dv8tion.jda.api.entities.*; import net.dv8tion.jda.api.entities.Guild.VerificationLevel; import net.dv8tion.jda.api.entities.MessageEmbed.*; +import net.dv8tion.jda.api.events.guild.member.GuildMemberRoleAddEvent; +import net.dv8tion.jda.api.events.guild.member.GuildMemberRoleRemoveEvent; +import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateBoostTimeEvent; +import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateNicknameEvent; +import net.dv8tion.jda.api.events.user.update.UserUpdateAvatarEvent; +import net.dv8tion.jda.api.events.user.update.UserUpdateDiscriminatorEvent; +import net.dv8tion.jda.api.events.user.update.UserUpdateNameEvent; import net.dv8tion.jda.api.utils.cache.CacheFlag; import net.dv8tion.jda.api.utils.data.DataArray; import net.dv8tion.jda.api.utils.data.DataObject; @@ -148,7 +155,7 @@ private void createGuildEmotePass(GuildImpl guildObj, DataArray array) } } - public GuildImpl createGuild(long guildId, DataObject guildJson, TLongObjectMap members) + public GuildImpl createGuild(long guildId, DataObject guildJson, TLongObjectMap members, int memberCount) { final GuildImpl guildObj = new GuildImpl(getJDA(), guildId); final String name = guildJson.getString("name", ""); @@ -194,7 +201,8 @@ public GuildImpl createGuild(long guildId, DataObject guildJson, TLongObjectMap< .setExplicitContentLevel(Guild.ExplicitContentLevel.fromKey(explicitContentLevel)) .setRequiredMFALevel(Guild.MFALevel.fromKey(mfaLevel)) .setBoostCount(boostCount) - .setBoostTier(boostTier); + .setBoostTier(boostTier) + .setMemberCount(memberCount); guildObj.setFeatures(featuresArray.map(it -> StreamSupport.stream(it.spliterator(), false) @@ -216,11 +224,27 @@ public GuildImpl createGuild(long guildId, DataObject guildJson, TLongObjectMap< } } - for (DataObject memberJson : members.valueCollection()) - createMember(guildObj, memberJson); + try (UnlockHook h1 = guildObj.getMembersView().writeLock(); + UnlockHook h2 = getJDA().getUsersView().writeLock()) + { + //Add members to cache when subscriptions are disabled when they appear here + // this is done because we can still keep track of members in voice channels + TLongObjectMap memberCache = guildObj.getMembersView().getMap(); + TLongObjectMap userCache = getJDA().getUsersView().getMap(); + for (DataObject memberJson : members.valueCollection()) + { + MemberImpl member = createMember(guildObj, memberJson); + // ignore members in voice channels if voice state cache is disabled + if (member.getUser().equals(getJDA().getSelfUser()) || getJDA().isCacheFlagSet(CacheFlag.VOICE_STATE)) + { + memberCache.put(member.getIdLong(), member); + userCache.put(member.getIdLong(), member.getUser()); + } + } + } if (guildObj.getOwner() == null) - LOG.warn("Finished setup for guild with a null owner. GuildId: {} OwnerId: {}", guildId, guildJson.opt("owner_id").orElse(null)); + LOG.debug("Finished setup for guild with a null owner. GuildId: {} OwnerId: {}", guildId, guildJson.opt("owner_id").orElse(null)); for (int i = 0; i < channelArray.length(); i++) { @@ -287,8 +311,11 @@ public void createGuildVoiceStatePass(GuildImpl guildObj, DataArray voiceStates) Member member = guildObj.getMembersView().get(userId); if (member == null) { - LOG.error("Received a VoiceState for a unknown Member! GuildId: " - + guildObj.getId() + " MemberId: " + voiceStateJson.getString("user_id")); + if (getJDA().isCacheFlagSet(CacheFlag.VOICE_STATE)) + { + LOG.error("Received a VoiceState for a unknown Member! GuildId: " + + guildObj.getId() + " MemberId: " + voiceStateJson.getString("user_id")); + } continue; } @@ -352,7 +379,8 @@ private UserImpl createUser(DataObject user, boolean fake, boolean modifyCache) else { userObj = new UserImpl(id, getJDA()).setFake(fake); - if (modifyCache) + // Cache user if guild subscriptions are enabled + if (modifyCache && getJDA().isGuildSubscriptions()) { if (fake) getJDA().getFakeUserMap().put(id, userObj); @@ -363,17 +391,64 @@ private UserImpl createUser(DataObject user, boolean fake, boolean modifyCache) } } - userObj - .setName(user.getString("username")) - .setDiscriminator(user.get("discriminator").toString()) - .setAvatarId(user.getString("avatar", null)) - .setBot(user.getBoolean("bot")); + if (modifyCache || userObj.isFake()) + { + // Initial creation + userObj.setName(user.getString("username")) + .setDiscriminator(user.get("discriminator").toString()) + .setAvatarId(user.getString("avatar", null)) + .setBot(user.getBoolean("bot")); + } + else if (!userObj.isFake()) + { + // Fire update events + updateUser(userObj, user); + } if (!fake && modifyCache) getJDA().getEventCache().playbackCache(EventCache.Type.USER, id); return userObj; } - public Member createMember(GuildImpl guild, DataObject memberJson) + public void updateUser(UserImpl userObj, DataObject user) + { + String oldName = userObj.getName(); + String newName = user.getString("username"); + String oldDiscriminator = userObj.getDiscriminator(); + String newDiscriminator = user.get("discriminator").toString(); + String oldAvatar = userObj.getAvatarId(); + String newAvatar = user.getString("avatar", null); + + JDAImpl jda = getJDA(); + long responseNumber = jda.getResponseTotal(); + if (!oldName.equals(newName)) + { + userObj.setName(newName); + jda.handleEvent( + new UserUpdateNameEvent( + jda, responseNumber, + userObj, oldName)); + } + + if (!oldDiscriminator.equals(newDiscriminator)) + { + userObj.setDiscriminator(newDiscriminator); + jda.handleEvent( + new UserUpdateDiscriminatorEvent( + jda, responseNumber, + userObj, oldDiscriminator)); + } + + if (!Objects.equals(oldAvatar, newAvatar)) + { + userObj.setAvatarId(newAvatar); + jda.handleEvent( + new UserUpdateAvatarEvent( + jda, responseNumber, + userObj, oldAvatar)); + } + } + + public MemberImpl createMember(GuildImpl guild, DataObject memberJson) { boolean playbackCache = false; User user = createUser(memberJson.getObject("user")); @@ -384,20 +459,66 @@ public Member createMember(GuildImpl guild, DataObject memberJson) try (UnlockHook hook = memberView.writeLock()) { member = new MemberImpl(guild, user); - playbackCache = memberView.getMap().put(user.getIdLong(), member) == null; + // Cache member if guild subscriptions are enabled or the user is the self user + if (getJDA().isGuildSubscriptions() || user.equals(getJDA().getSelfUser())) + { + playbackCache = memberView.getMap().put(user.getIdLong(), member) == null; + // load the overrides + TLongObjectMap cachedOverrides = guild.removeOverrideMap(user.getIdLong()); + if (cachedOverrides != null) + { + cachedOverrides.forEachEntry((channelId, override) -> + { + GuildChannel channel = guild.getGuildChannelById(channelId); + if (channel instanceof AbstractChannelImpl) // essentially a null check plus cast safety + createPermissionOverride(override, (AbstractChannelImpl) channel); + return true; + }); + } + } + else // otherwise re-create every time! + { + playbackCache = true; + } } - if (guild.getOwnerIdLong() == user.getIdLong()) + if (playbackCache && guild.getOwnerIdLong() == user.getIdLong()) { LOG.trace("Found owner of guild with id {}", guild.getId()); guild.setOwner(member); } } + if (playbackCache) + { + loadMember(guild, memberJson, user, member); + long hashId = guild.getIdLong() ^ user.getIdLong(); + getJDA().getEventCache().playbackCache(EventCache.Type.MEMBER, hashId); + guild.acknowledgeMembers(); + } + else + { + // This is not a new member - fire update events + DataArray roleArray = memberJson.getArray("roles"); + List roles = new ArrayList<>(roleArray.length()); + for (int i = 0; i < roleArray.length(); i++) + { + long roleId = roleArray.getUnsignedLong(i); + Role role = guild.getRoleById(roleId); + if (role != null) + roles.add(role); + } + updateMember(guild, member, memberJson, roles); + } + return member; + } + + private void loadMember(GuildImpl guild, DataObject memberJson, User user, MemberImpl member) + { GuildVoiceStateImpl state = (GuildVoiceStateImpl) member.getVoiceState(); if (state != null) { state.setGuildMuted(memberJson.getBoolean("mute")) - .setGuildDeafened(memberJson.getBoolean("deaf")); + .setGuildDeafened(memberJson.getBoolean("deaf")); } if (!memberJson.isNull("premium_since")) @@ -406,9 +527,12 @@ public Member createMember(GuildImpl guild, DataObject memberJson) member.setBoostDate(Instant.from(boostDate).toEpochMilli()); } - TemporalAccessor joinedAt = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(memberJson.getString("joined_at")); + //In some contexts this is missing (PRESENCE_UPDATE and GUILD_MEMBER_UPDATE) + // we call this incomplete and load the joined_at later through a MESSAGE_CREATE (if we get one) + String joinedAtRaw = memberJson.opt("joined_at").map(String::valueOf).orElseGet(() -> guild.getTimeCreated().toString()); + TemporalAccessor joinedAt = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(joinedAtRaw); member.setJoinDate(Instant.from(joinedAt).toEpochMilli()) - .setNickname(memberJson.getString("nick", null)); + .setNickname(memberJson.getString("nick", null)); DataArray rolesJson = memberJson.getArray("roles"); for (int k = 0; k < rolesJson.length(); k++) @@ -425,13 +549,88 @@ public Member createMember(GuildImpl guild, DataObject memberJson) member.getRoleSet().add(r); } } + } - if (playbackCache) + public void updateMember(GuildImpl guild, MemberImpl member, DataObject content, List newRoles) + { + //If newRoles is null that means that we didn't find a role that was in the array and was cached this event + long responseNumber = getJDA().getResponseTotal(); + if (newRoles != null) { - long hashId = guild.getIdLong() ^ user.getIdLong(); - getJDA().getEventCache().playbackCache(EventCache.Type.MEMBER, hashId); + updateMemberRoles(member, newRoles, responseNumber); + } + if (content.hasKey("nick")) + { + String oldNick = member.getNickname(); + String newNick = content.getString("nick", null); + if (!Objects.equals(oldNick, newNick)) + { + member.setNickname(newNick); + getJDA().handleEvent( + new GuildMemberUpdateNicknameEvent( + getJDA(), responseNumber, + member, oldNick)); + } + } + if (content.hasKey("premium_since")) + { + long epoch = 0; + if (!content.isNull("premium_since")) + { + TemporalAccessor date = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(content.getString("premium_since")); + epoch = Instant.from(date).toEpochMilli(); + } + if (epoch != member.getBoostDateRaw()) + { + OffsetDateTime oldTime = member.getTimeBoosted(); + member.setBoostDate(epoch); + getJDA().handleEvent( + new GuildMemberUpdateBoostTimeEvent( + getJDA(), responseNumber, + member, oldTime)); + } + } + } + + private void updateMemberRoles(MemberImpl member, List newRoles, long responseNumber) + { + Set currentRoles = member.getRoleSet(); + //Find the roles removed. + List removedRoles = new LinkedList<>(); + each: + for (Role role : currentRoles) + { + for (Iterator it = newRoles.iterator(); it.hasNext(); ) + { + Role r = it.next(); + if (role.equals(r)) + { + it.remove(); + continue each; + } + } + removedRoles.add(role); + } + + if (removedRoles.size() > 0) + currentRoles.removeAll(removedRoles); + if (newRoles.size() > 0) + currentRoles.addAll(newRoles); + + if (removedRoles.size() > 0) + { + getJDA().handleEvent( + new GuildMemberRoleRemoveEvent( + getJDA(), responseNumber, + member, removedRoles)); + } + if (newRoles.size() > 0) + { + getJDA().handleEvent( + new GuildMemberRoleAddEvent( + getJDA(), responseNumber, + member, newRoles)); } - return member; } public void createPresence(MemberImpl member, DataObject presenceJson) @@ -767,6 +966,11 @@ public PrivateChannel createPrivateChannel(DataObject json) user = createFakeUser(recipient, true); } + return createPrivateChannel(json, user); + } + + public PrivateChannel createPrivateChannel(DataObject json, UserImpl user) + { final long channelId = json.getLong("id"); PrivateChannelImpl priv = new PrivateChannelImpl(channelId, user) .setLastMessageId(json.getLong("last_message_id", 0)); @@ -775,7 +979,9 @@ public PrivateChannel createPrivateChannel(DataObject json) if (user.isFake()) { priv.setFake(true); + // Promote user and channel to cache of fakers getJDA().getFakePrivateChannelMap().put(channelId, priv); + getJDA().getFakeUserMap().put(user.getIdLong(), user); } else { @@ -840,7 +1046,7 @@ public Role createRole(GuildImpl guild, DataObject roleJson, long guildId) } public Message createMessage(DataObject jsonObject) { return createMessage(jsonObject, false); } - public Message createMessage(DataObject jsonObject, boolean exceptionOnMissingUser) + public Message createMessage(DataObject jsonObject, boolean modifyCache) { final long channelId = jsonObject.getLong("channel_id"); @@ -852,15 +1058,36 @@ public Message createMessage(DataObject jsonObject, boolean exceptionOnMissingUs if (chan == null) throw new IllegalArgumentException(MISSING_CHANNEL); - return createMessage(jsonObject, chan, exceptionOnMissingUser); + return createMessage(jsonObject, chan, modifyCache); } - public Message createMessage(DataObject jsonObject, MessageChannel chan, boolean exceptionOnMissingUser) + public Message createMessage(DataObject jsonObject, MessageChannel chan, boolean modifyCache) { final long id = jsonObject.getLong("id"); - String content = jsonObject.getString("content", ""); - - DataObject author = jsonObject.getObject("author"); + final DataObject author = jsonObject.getObject("author"); final long authorId = author.getLong("id"); + Member member = null; + + if (chan.getType().isGuild() && !jsonObject.isNull("member") && modifyCache) + { + GuildChannel guildChannel = (GuildChannel) chan; + Guild guild = guildChannel.getGuild(); + MemberImpl cachedMember = (MemberImpl) guild.getMemberById(authorId); + // Update member cache with new information if needed + if (cachedMember == null || cachedMember.isIncomplete() || !getJDA().isGuildSubscriptions()) + { + DataObject memberJson = jsonObject.getObject("member"); + memberJson.put("user", author); + if (cachedMember == null) + LOG.trace("Initializing member from message create {}", memberJson); + member = createMember((GuildImpl) guild, memberJson); + } + else + { + member = cachedMember; + } + } + + final String content = jsonObject.getString("content", ""); final boolean fromWebhook = jsonObject.hasKey("webhook_id"); final boolean pinned = jsonObject.getBoolean("pinned"); final boolean tts = jsonObject.getBoolean("tts"); @@ -890,11 +1117,12 @@ public Message createMessage(DataObject jsonObject, MessageChannel chan, boolean throw new IllegalStateException("Cannot build a message for a group channel, how did this even get here?"); case TEXT: Guild guild = ((TextChannel) chan).getGuild(); - Member member = guild.getMemberById(authorId); + if (member == null) + member = guild.getMemberById(authorId); user = member != null ? member.getUser() : null; if (user == null) { - if (fromWebhook || !exceptionOnMissingUser) + if (fromWebhook || !modifyCache) user = createFakeUser(author, false); else throw new IllegalArgumentException(MISSING_USER); // Specifically for MESSAGE_CREATE @@ -903,6 +1131,9 @@ public Message createMessage(DataObject jsonObject, MessageChannel chan, boolean default: throw new IllegalArgumentException("Invalid Channel for creating a Message [" + chan.getType() + ']'); } + if (modifyCache && !fromWebhook) // update the user information on message receive + updateUser((UserImpl) user, author); + TLongSet mentionedRoles = new TLongHashSet(); TLongSet mentionedUsers = new TLongHashSet(map(jsonObject, "mentions", (o) -> o.getLong("id"))); Optional roleMentionArr = jsonObject.optArray("mention_roles"); @@ -913,20 +1144,63 @@ public Message createMessage(DataObject jsonObject, MessageChannel chan, boolean }); MessageType type = MessageType.fromId(jsonObject.getInt("type")); + ReceivedMessage message; switch (type) { case DEFAULT: - return new ReceivedMessage(id, chan, type, fromWebhook, + message = new ReceivedMessage(id, chan, type, fromWebhook, mentionsEveryone, mentionedUsers, mentionedRoles, tts, pinned, - content, nonce, user, activity, editTime, reactions, attachments, embeds); + content, nonce, user, member, activity, editTime, reactions, attachments, embeds); + break; case UNKNOWN: throw new IllegalArgumentException(UNKNOWN_MESSAGE_TYPE); default: - return new SystemMessage(id, chan, type, fromWebhook, + message = new SystemMessage(id, chan, type, fromWebhook, mentionsEveryone, mentionedUsers, mentionedRoles, tts, pinned, - content, nonce, user, activity, editTime, reactions, attachments, embeds); + content, nonce, user, member, activity, editTime, reactions, attachments, embeds); + break; } + if (!message.isFromGuild()) + return message; + + GuildImpl guild = (GuildImpl) message.getGuild(); + + // Don't do more computations when members are loaded already + if (guild.isLoaded()) + return message; + + // Load users/members from message object through mentions + List mentionedUsersList = new ArrayList<>(); + List mentionedMembersList = new ArrayList<>(); + DataArray userMentions = jsonObject.getArray("mentions"); + + for (int i = 0; i < userMentions.length(); i++) + { + DataObject mentionJson = userMentions.getObject(i); + if (mentionJson.isNull("member")) + { + // Can't load user without member context so fake them if possible + User mentionedUser = createFakeUser(mentionJson, false); + mentionedUsersList.add(mentionedUser); + Member mentionedMember = guild.getMember(mentionedUser); + if (mentionedMember != null) + mentionedMembersList.add(mentionedMember); + continue; + } + + // Load member/user from mention (gateway messages only) + DataObject memberJson = mentionJson.getObject("member"); + mentionJson.remove("member"); + memberJson.put("user", mentionJson); + Member mentionedMember = createMember(guild, memberJson); + mentionedMembersList.add(mentionedMember); + mentionedUsersList.add(mentionedMember.getUser()); + } + + if (!mentionedUsersList.isEmpty()) + message.setMentions(mentionedUsersList, mentionedMembersList); + return message; } private static MessageActivity createMessageActivity(DataObject jsonObject) @@ -960,7 +1234,7 @@ public MessageReaction createMessageReaction(MessageChannel chan, long id, DataO { DataObject emoji = obj.getObject("emoji"); final Long emojiID = emoji.isNull("id") ? null : emoji.getLong("id"); - final String name = emoji.getString("name", null); + final String name = emoji.getString("name", ""); final boolean animated = emoji.getBoolean("animated"); final int count = obj.getInt("count", -1); final boolean me = obj.getBoolean("me"); @@ -1104,44 +1378,42 @@ public static MessageEmbed createMessageEmbed(String url, String title, String d color, thumbnail, siteProvider, author, videoInfo, footer, image, fields); } - public PermissionOverride createPermissionOverride(DataObject override, GuildChannel chan) + public PermissionOverride createPermissionOverride(DataObject override, AbstractChannelImpl chan) { - PermissionOverrideImpl permOverride; + IPermissionHolder permHolder; final long id = override.getLong("id"); - long allow = override.getLong("allow"); - long deny = override.getLong("deny"); //Throwing NoSuchElementException for common issues with overrides that are not cleared properly by discord // when a member leaves or a role is deleted switch (override.getString("type")) { case "member": - Member member = chan.getGuild().getMemberById(id); - if (member == null) - throw new NoSuchElementException("Attempted to create a PermissionOverride for a non-existent user. Guild: " + chan.getGuild() + ", Channel: " + chan + ", JSON: " + override); - - permOverride = (PermissionOverrideImpl) chan.getPermissionOverride(member); - if (permOverride == null) + permHolder = chan.getGuild().getMemberById(id); + if (permHolder == null) { - permOverride = new PermissionOverrideImpl(chan, member.getUser().getIdLong(), member); - ((AbstractChannelImpl) chan).getOverrideMap().put(member.getUser().getIdLong(), permOverride); + // cache override for later + chan.getGuild().cacheOverride(id, chan.getIdLong(), override); + return null; } break; case "role": - Role role = ((GuildImpl) chan.getGuild()).getRolesView().get(id); - if (role == null) + permHolder = chan.getGuild().getRolesView().get(id); + if (permHolder == null) throw new NoSuchElementException("Attempted to create a PermissionOverride for a non-existent role! JSON: " + override); - - permOverride = (PermissionOverrideImpl) chan.getPermissionOverride(role); - if (permOverride == null) - { - permOverride = new PermissionOverrideImpl(chan, role.getIdLong(), role); - ((AbstractChannelImpl) chan).getOverrideMap().put(role.getIdLong(), permOverride); - } break; default: throw new IllegalArgumentException("Provided with an unknown PermissionOverride type! JSON: " + override); } + + long allow = override.getLong("allow"); + long deny = override.getLong("deny"); + + PermissionOverrideImpl permOverride = (PermissionOverrideImpl) chan.getPermissionOverride(permHolder); + if (permOverride == null) + { + permOverride = new PermissionOverrideImpl(chan, permHolder.getIdLong(), permHolder); + chan.getOverrideMap().put(permHolder.getIdLong(), permOverride); + } return permOverride.setAllow(allow).setDeny(deny); } @@ -1377,7 +1649,9 @@ private List map(DataObject jsonObject, String key, Function emoteCache = new SnowflakeCacheViewImpl<>(Emote.class, Emote::getName); private final MemberCacheViewImpl memberCache = new MemberCacheViewImpl(); - private final TLongObjectMap cachedPresences = MiscUtil.newLongMap(); + // user -> channel -> override + private final TLongObjectMap> overrideMap = MiscUtil.newLongMap(); + private final CompletableFuture chunkingCallback = new CompletableFuture<>(); private final ReentrantLock mngLock = new ReentrantLock(); private volatile GuildManager manager; @@ -108,6 +111,7 @@ public class GuildImpl implements Guild private BoostTier boostTier = BoostTier.NONE; private boolean available; private boolean canSendVerification = false; + private int memberCount; public GuildImpl(JDAImpl api, long id) { @@ -150,6 +154,20 @@ public MemberAction addMember(@Nonnull String accessToken, @Nonnull String userI return new MemberActionImpl(getJDA(), this, userId, accessToken); } + @Override + public boolean isLoaded() + { + // Only works with guild subscriptions + return getJDA().isGuildSubscriptions() + && (long) getMemberCount() <= getMemberCache().size(); + } + + @Override + public int getMemberCount() + { + return memberCount; + } + @Nonnull @Override public String getName() @@ -596,7 +614,7 @@ public AuditLogPaginationAction retrieveAuditLogs() @Override public RestAction leave() { - if (owner.equals(getSelfMember())) + if (getSelfMember().isOwner()) throw new IllegalStateException("Cannot leave a guild that you are the owner of! Transfer guild ownership first!"); Route.CompiledRoute route = Route.Self.LEAVE_GUILD.compile(getId()); @@ -617,7 +635,7 @@ public RestAction delete() @Override public RestAction delete(String mfaCode) { - if (!owner.equals(getSelfMember())) + if (!getSelfMember().isOwner()) throw new PermissionException("Cannot delete a guild that you do not own!"); DataObject mfaBody = null; @@ -736,11 +754,20 @@ public boolean checkVerification() } @Override + @Deprecated public boolean isAvailable() { return available; } + @Nonnull + @Override + public CompletableFuture retrieveMembers() + { + startChunking(); + return chunkingCallback; + } + @Override public long getIdLong() { @@ -853,14 +880,28 @@ public AuditableRestAction kick(@Nonnull Member member, String reason) checkGuild(member.getGuild(), "member"); checkPermission(Permission.KICK_MEMBERS); checkPosition(member); + return kick0(member.getUser().getId(), reason); + } - final String userId = member.getUser().getId(); - final String guildId = getId(); - - Route.CompiledRoute route = Route.Guilds.KICK_MEMBER.compile(guildId, userId); - if (reason != null && !reason.isEmpty()) - route = route.withQueryParams("reason", EncodingUtil.encodeUTF8(reason)); + @Nonnull + @Override + public AuditableRestAction kick(@Nonnull String userId, @Nullable String reason) + { + Member member = getMemberById(userId); + if (member != null) + return kick(member, reason); + // Check permissions and whether the user is the owner, otherwise attempt a kick + Checks.check(!userId.equals(getOwnerId()), "Cannot kick the owner of a guild!"); + checkPermission(Permission.KICK_MEMBERS); + return kick0(userId, reason); + } + @Nonnull + private AuditableRestAction kick0(@Nonnull String userId, @Nullable String reason) + { + Route.CompiledRoute route = Route.Guilds.KICK_MEMBER.compile(getId(), userId); + if (!Helpers.isBlank(reason)) + route.withQueryParams("reason", EncodingUtil.encodeUTF8(reason)); return new AuditableRestActionImpl<>(getJDA(), route); } @@ -1075,7 +1116,7 @@ public AuditableRestAction transferOwnership(@Nonnull Member newOwner) { Checks.notNull(newOwner, "Member"); checkGuild(newOwner.getGuild(), "Member"); - if (!getSelfMember().equals(getOwner())) + if (!getSelfMember().isOwner()) throw new PermissionException("The logged in account must be the owner of this Guild to be able to transfer ownership"); Checks.check(!getSelfMember().equals(newOwner), @@ -1378,6 +1419,12 @@ public GuildImpl setOwnerId(long ownerId) return this; } + public GuildImpl setMemberCount(int count) + { + this.memberCount = count; + return this; + } + // -- Map getters -- public SortedSnowflakeCacheViewImpl getCategoriesView() @@ -1415,11 +1462,127 @@ public MemberCacheViewImpl getMembersView() return memberCache; } - public TLongObjectMap getCachedPresenceMap() + // -- Member Tracking -- + + public TLongObjectMap getOverrideMap(long userId) + { + return overrideMap.get(userId); + } + + public TLongObjectMap removeOverrideMap(long userId) + { + return overrideMap.remove(userId); + } + + public void pruneChannelOverrides(long channelId) + { + WebSocketClient.LOG.debug("Pruning cached overrides for channel with id {}", channelId); + overrideMap.transformValues((value) -> { + DataObject removed = value.remove(channelId); + return value.isEmpty() ? null : value; + }); + } + + public void cacheOverride(long userId, long channelId, DataObject obj) + { + if (!getJDA().isGuildSubscriptions()) + return; + EntityBuilder.LOG.debug("Caching permission override of unloaded member {}", obj); + TLongObjectMap channelMap = overrideMap.get(userId); + if (channelMap == null) + overrideMap.put(userId, channelMap = MiscUtil.newLongMap()); + channelMap.put(channelId, obj); + } + + public void updateCachedOverrides(AbstractChannelImpl channel, DataArray newOverrides) { - return cachedPresences; + if (!getJDA().isGuildSubscriptions()) + return; + long channelId = channel.getIdLong(); + // extract user ids + TLongSet users = new TLongHashSet(); + for (int i = 0; i < newOverrides.length(); i++) + { + DataObject obj = newOverrides.getObject(i); + if (!obj.getString("type", "").equals("member")) + continue; + long id = obj.getUnsignedLong("id"); + // remember that this user has an override + users.add(id); + } + + // now remove the overrides that are missing + TLongSet toRemove = new TLongHashSet(); + overrideMap.forEachEntry((userId, overrides) -> + { + if (users.contains(userId)) + return true; + // remove for the channel + overrides.remove(channelId); + // remember to remove this map if its empty now + if (overrides.isEmpty()) + toRemove.add(userId); + return true; + }); + // remove all empty maps + overrideMap.keySet().removeAll(toRemove); + } + + @Nonnull + @Override + public RestAction retrieveMemberById(long id) + { + Member member = getMemberById(id); + // If guild subscriptions are disabled this member might not be up-to-date + if (member != null && getJDA().isGuildSubscriptions()) + 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())); } + public void startChunking() + { + if (isLoaded()) + return; + if (!getJDA().isGuildSubscriptions()) + { + chunkingCallback.completeExceptionally(new IllegalStateException("Unable to start member chunking on a guild with disabled guild subscriptions")); + return; + } + + DataObject request = DataObject.empty() + .put("limit", 0) + .put("query", "") + .put("guild_id", getId()); + + DataObject packet = DataObject.empty() + .put("op", WebSocketCode.MEMBER_CHUNK_REQUEST) + .put("d", request); + + getJDA().getClient().chunkOrSyncRequest(packet); + } + + public void onMemberAdd() + { + memberCount++; + } + + public void onMemberRemove() + { + memberCount--; + acknowledgeMembers(); + } + + public void acknowledgeMembers() + { + if (memberCache.size() == memberCount && !chunkingCallback.isDone()) + { + JDALogger.getLog(Guild.class).debug("Chunking completed for guild {}", this); + chunkingCallback.complete(null); + } + } // -- Object overrides -- 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 f3379d26b4..0abe172fe9 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/MemberImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/MemberImpl.java @@ -257,8 +257,15 @@ public boolean canInteract(@Nonnull Emote emote) } @Override - public boolean isOwner() { - return this.equals(getGuild().getOwner()); + public boolean isOwner() + { + return this.user.getIdLong() == getGuild().getOwnerIdLong(); + } + + @Override + public boolean isFake() + { + return getGuild().getMemberById(getIdLong()) == null; } @Override @@ -318,6 +325,12 @@ public long getBoostDateRaw() return boostDate; } + public boolean isIncomplete() + { + // the joined_at is only present on complete members, this implies the member is completely loaded + return !isOwner() && Objects.equals(getGuild().getTimeCreated(), getTimeJoined()); + } + @Override public boolean equals(Object o) { 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 a582f0a9ac..45e4f831c1 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/ReceivedMessage.java @@ -56,6 +56,7 @@ public class ReceivedMessage extends AbstractMessage protected final boolean mentionsEveryone; protected final boolean pinned; protected final User author; + protected final Member member; protected final MessageActivity activity; protected final OffsetDateTime editedTime; protected final List reactions; @@ -69,6 +70,7 @@ public class ReceivedMessage extends AbstractMessage protected String strippedContent = null; protected List userMentions = null; + protected List memberMentions = null; protected List emoteMentions = null; protected List roleMentions = null; protected List channelMentions = null; @@ -77,7 +79,7 @@ public class ReceivedMessage extends AbstractMessage 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, MessageActivity activity, OffsetDateTime editTime, + String content, String nonce, User author, Member member, MessageActivity activity, OffsetDateTime editTime, List reactions, List attachments, List embeds) { super(content, nonce, tts); @@ -89,6 +91,7 @@ public ReceivedMessage( this.mentionsEveryone = mentionsEveryone; this.pinned = pinned; this.author = author; + this.member = member; this.activity = activity; this.editedTime = editTime; this.reactions = Collections.unmodifiableList(reactions); @@ -275,6 +278,8 @@ private User matchUser(Matcher matcher) User user = getJDA().getUserById(userId); if (user == null) user = api.getFakeUserMap().get(userId); + if (user == null && userMentions != null) + user = userMentions.stream().filter(it -> it.getIdLong() == userId).findFirst().orElse(null); return user; } @@ -348,6 +353,8 @@ public Bag getMentionedRolesBag() public List getMentionedMembers(@Nonnull Guild guild) { Checks.notNull(guild, "Guild"); + if (isFromGuild() && guild.equals(getGuild()) && memberMentions != null) + return memberMentions; List mentionedUsers = getMentionedUsers(); List members = new ArrayList<>(); for (User user : mentionedUsers) @@ -539,7 +546,7 @@ public User getAuthor() @Override public Member getMember() { - return isFromType(ChannelType.TEXT) ? getGuild().getMember(getAuthor()) : null; + return member; } @Nonnull @@ -834,6 +841,21 @@ public void formatTo(Formatter formatter, int flags, int width, int precision) appendFormat(formatter, width, precision, leftJustified, out); } + public void setMentions(List users, List members) + { + users.sort(Comparator.comparing((user) -> + Math.max(content.indexOf("<@" + user.getId() + ">"), + content.indexOf("<@!" + user.getId() + ">") + ))); + members.sort(Comparator.comparing((user) -> + Math.max(content.indexOf("<@" + user.getId() + ">"), + content.indexOf("<@!" + user.getId() + ">") + ))); + + this.userMentions = Collections.unmodifiableList(users); + this.memberMentions = Collections.unmodifiableList(members); + } + private > C processMentions(MentionType type, C collection, boolean distinct, Function map) { Matcher matcher = type.getPattern().matcher(getContentRaw()); 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 ae610d4653..4e6b4715c1 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/SystemMessage.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/SystemMessage.java @@ -31,11 +31,11 @@ public SystemMessage( long id, MessageChannel channel, MessageType type, boolean fromWebhook, boolean mentionsEveryone, TLongSet mentionedUsers, TLongSet mentionedRoles, boolean tts, boolean pinned, - String content, String nonce, User author, MessageActivity activity, OffsetDateTime editTime, + String content, String nonce, User author, Member member, MessageActivity activity, OffsetDateTime editTime, List reactions, List attachments, List embeds) { super(id, channel, type, fromWebhook, mentionsEveryone, mentionedUsers, mentionedRoles, - tts, pinned, content, nonce, author, activity, editTime, reactions, attachments, embeds); + tts, pinned, content, nonce, author, member, activity, editTime, reactions, attachments, embeds); } @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 1f49cedeb3..67e14ea140 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/UserImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/UserImpl.java @@ -98,14 +98,11 @@ public RestAction openPrivateChannel() if (privateChannel != null) return new EmptyRestAction<>(getJDA(), privateChannel); - if (fake) - throw new IllegalStateException("Cannot open a PrivateChannel with a Fake user."); - 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()); + PrivateChannel priv = api.get().getEntityBuilder().createPrivateChannel(response.getObject(), this); UserImpl.this.privateChannel = priv; return priv; }); diff --git a/src/main/java/net/dv8tion/jda/internal/handle/ChannelDeleteHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/ChannelDeleteHandler.java index c70f6c5876..72b882b2c1 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/ChannelDeleteHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/ChannelDeleteHandler.java @@ -49,15 +49,15 @@ protected Long handleInternally(DataObject content) return guildId; } + GuildImpl guild = (GuildImpl) getJDA().getGuildById(guildId); final long channelId = content.getLong("id"); switch (type) { case STORE: { - GuildImpl guild = (GuildImpl) getJDA().getGuildById(guildId); StoreChannel channel = getJDA().getStoreChannelsView().remove(channelId); - if (channel == null) + if (channel == null || guild == null) { WebSocketClient.LOG.debug("CHANNEL_DELETE attempted to delete a store channel that is not yet cached. JSON: {}", content); return null; @@ -72,9 +72,8 @@ protected Long handleInternally(DataObject content) } case TEXT: { - GuildImpl guild = (GuildImpl) getJDA().getGuildsView().get(guildId); TextChannel channel = getJDA().getTextChannelsView().remove(channelId); - if (channel == null) + if (channel == null || guild == null) { WebSocketClient.LOG.debug("CHANNEL_DELETE attempted to delete a text channel that is not yet cached. JSON: {}", content); return null; @@ -89,9 +88,8 @@ protected Long handleInternally(DataObject content) } case VOICE: { - GuildImpl guild = (GuildImpl) getJDA().getGuildsView().get(guildId); VoiceChannel channel = getJDA().getVoiceChannelsView().remove(channelId); - if (channel == null) + if (channel == null || guild == null) { WebSocketClient.LOG.debug("CHANNEL_DELETE attempted to delete a voice channel that is not yet cached. JSON: {}", content); return null; @@ -114,9 +112,8 @@ protected Long handleInternally(DataObject content) } case CATEGORY: { - GuildImpl guild = (GuildImpl) getJDA().getGuildById(guildId); Category category = getJDA().getCategoriesView().remove(channelId); - if (category == null) + if (category == null || guild == null) { WebSocketClient.LOG.debug("CHANNEL_DELETE attempted to delete a category channel that is not yet cached. JSON: {}", content); return null; @@ -162,6 +159,8 @@ protected Long handleInternally(DataObject content) WebSocketClient.LOG.debug("CHANNEL_DELETE provided an unknown channel type. JSON: {}", content); } getJDA().getEventCache().clear(EventCache.Type.CHANNEL, channelId); + if (guild != null) + guild.pruneChannelOverrides(channelId); return null; } } diff --git a/src/main/java/net/dv8tion/jda/internal/handle/ChannelUpdateHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/ChannelUpdateHandler.java index f2fbbdd889..1aace65b41 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/ChannelUpdateHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/ChannelUpdateHandler.java @@ -331,6 +331,7 @@ private void applyPermissions(AbstractChannelImpl channel, DataObject conte toRemove.add(permHolder.getIdLong()); }); + channel.getGuild().updateCachedOverrides(channel, permOverwrites); toRemove.forEach((id) -> { overridesMap.remove(id); @@ -372,9 +373,8 @@ private void handlePermissionOverride(DataObject override, AbstractChannelImpl - handlePermissionOverride(override, channel, content, changedPermHolders, containedPermHolders)); - EventCache.LOG.debug("CHANNEL_UPDATE attempted to create or update a PermissionOverride for Member that doesn't exist in this Guild! MemberId: {} JSON: {}", id, content); + // cache override for unloaded member (maybe loaded later) + channel.getGuild().cacheOverride(id, channel.getIdLong(), override); return; } break; diff --git a/src/main/java/net/dv8tion/jda/internal/handle/EventCache.java b/src/main/java/net/dv8tion/jda/internal/handle/EventCache.java index a804760c62..6096e26183 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/EventCache.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/EventCache.java @@ -34,6 +34,12 @@ public class EventCache /** Sequence difference after which events will be removed from cache */ public static final long TIMEOUT_AMOUNT = 100; private final EnumMap>> eventCache = new EnumMap<>(Type.class); + private final boolean cacheUsers; + + public EventCache(boolean cacheUsers) + { + this.cacheUsers = cacheUsers; + } public synchronized void timeout(final long responseTotal) { @@ -72,6 +78,8 @@ public synchronized void timeout(final long responseTotal) public synchronized void cache(Type type, long triggerId, long responseTotal, DataObject event, CacheConsumer handler) { + if (type == Type.USER && !cacheUsers) + return; TLongObjectMap> triggerCache = eventCache.computeIfAbsent(type, k -> new TLongObjectHashMap<>()); diff --git a/src/main/java/net/dv8tion/jda/internal/handle/GuildCreateHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/GuildCreateHandler.java index f223a665a4..9b7ad64e8d 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/GuildCreateHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/GuildCreateHandler.java @@ -15,12 +15,9 @@ */ package net.dv8tion.jda.internal.handle; -import net.dv8tion.jda.api.events.guild.GuildAvailableEvent; -import net.dv8tion.jda.api.events.guild.GuildUnavailableEvent; import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.internal.JDAImpl; import net.dv8tion.jda.internal.entities.GuildImpl; -import net.dv8tion.jda.internal.requests.WebSocketCode; public class GuildCreateHandler extends SocketHandler { @@ -37,36 +34,17 @@ protected Long handleInternally(DataObject content) GuildImpl guild = (GuildImpl) getJDA().getGuildById(id); if (guild == null) { + // This can happen in 3 scenarios: + // + // 1) The guild is provided in guild streaming during initial session setup + // 2) The guild has just been joined by the bot (added through moderator) + // 3) The guild was marked unavailable and has come back + // + // The controller will fire an appropriate event for each case. getJDA().getGuildSetupController().onCreate(id, content); - return null; } - boolean unavailable = content.getBoolean("unavailable"); - if (guild.isAvailable() && unavailable) - { - guild.setAvailable(false); - getJDA().handleEvent( - new GuildUnavailableEvent( - getJDA(), responseNumber, - guild)); - } - else if (!guild.isAvailable() && !unavailable) - { - guild.setAvailable(true); - getJDA().handleEvent( - new GuildAvailableEvent( - getJDA(), responseNumber, - guild)); - // I'm not sure if this is actually needed, but if discord sends us an updated field here - // we can just use the same logic we use for GUILD_UPDATE in order to update it and fire events - getJDA().getClient().getHandler("GUILD_UPDATE") - .handle(responseNumber, DataObject.empty() - .put("comment", "This was previously a GUILD_CREATE with unavailable set to false") - .put("t", "GUILD_UPDATE") - .put("s", responseNumber) - .put("op", WebSocketCode.DISPATCH) - .put("d", content)); - } + // Anything else is either a duplicate event or unexpected behavior return null; } } 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 f9fcc51c38..40a38537f0 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/GuildDeleteHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/GuildDeleteHandler.java @@ -46,8 +46,9 @@ public GuildDeleteHandler(JDAImpl api) protected Long handleInternally(DataObject content) { final long id = content.getLong("id"); - boolean wasInit = getJDA().getGuildSetupController().onDelete(id, content); - if (wasInit) + GuildSetupController setupController = getJDA().getGuildSetupController(); + boolean wasInit = setupController.onDelete(id, content); + if (wasInit || setupController.isUnavailable(id)) return null; GuildImpl guild = (GuildImpl) getJDA().getGuildById(id); @@ -61,20 +62,9 @@ protected Long handleInternally(DataObject content) //If the event is attempting to mark the guild as unavailable, but it is already unavailable, // ignore the event - if (!guild.isAvailable() && unavailable) + if (setupController.isUnavailable(id) && unavailable) return null; - if (unavailable) - { - guild.setAvailable(false); - getJDA().handleEvent( - new GuildUnavailableEvent( - getJDA(), responseNumber, - guild - )); - return null; - } - //Remove everything from global cache // this prevents some race-conditions for getting audio managers from guilds SnowflakeCacheViewImpl guildView = getJDA().getGuildsView(); @@ -150,10 +140,22 @@ protected Long handleInternally(DataObject content) }); } - getJDA().handleEvent( - new GuildLeaveEvent( - getJDA(), responseNumber, - guild)); + if (unavailable) + { + setupController.onUnavailable(id); + guild.setAvailable(false); + getJDA().handleEvent( + new GuildUnavailableEvent( + getJDA(), responseNumber, + guild)); + } + else + { + getJDA().handleEvent( + new GuildLeaveEvent( + getJDA(), responseNumber, + guild)); + } getJDA().getEventCache().clear(EventCache.Type.GUILD, id); return null; } diff --git a/src/main/java/net/dv8tion/jda/internal/handle/GuildMemberAddHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/GuildMemberAddHandler.java index 1d5d6a3e3a..1bb776b660 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/GuildMemberAddHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/GuildMemberAddHandler.java @@ -20,6 +20,7 @@ import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.internal.JDAImpl; import net.dv8tion.jda.internal.entities.GuildImpl; +import net.dv8tion.jda.internal.requests.WebSocketClient; public class GuildMemberAddHandler extends SocketHandler { @@ -45,6 +46,15 @@ protected Long handleInternally(DataObject content) return null; } + long userId = content.getObject("user").getUnsignedLong("id"); + if (guild.getMemberById(userId) != null) + { + WebSocketClient.LOG.debug("Ignoring duplicate GUILD_MEMBER_ADD for user with id {} in guild {}", userId, id); + return null; + } + + // Update memberCount + guild.onMemberAdd(); Member member = getJDA().getEntityBuilder().createMember(guild, content); getJDA().handleEvent( new GuildMemberJoinEvent( diff --git a/src/main/java/net/dv8tion/jda/internal/handle/GuildMemberRemoveHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/GuildMemberRemoveHandler.java index a00ae3159c..2a12cebc18 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/GuildMemberRemoveHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/GuildMemberRemoveHandler.java @@ -49,7 +49,7 @@ protected Long handleInternally(DataObject content) return null; } - final long userId = content.getObject("user").getLong("id"); + final long userId = content.getObject("user").getUnsignedLong("id"); if (userId == getJDA().getSelfUser().getIdLong()) { //We probably just left the guild and this event is trying to remove us from the guild, therefore ignore @@ -59,10 +59,13 @@ protected Long handleInternally(DataObject content) if (member == null) { - WebSocketClient.LOG.debug("Received GUILD_MEMBER_REMOVE for a Member that does not exist in the specified Guild."); + WebSocketClient.LOG.debug("Received GUILD_MEMBER_REMOVE for a Member that does not exist in the specified Guild. UserId: {} GuildId: {}", userId, id); return null; } + // Update the memberCount + guild.onMemberRemove(); + GuildVoiceStateImpl voiceState = (GuildVoiceStateImpl) member.getVoiceState(); if (voiceState != null && voiceState.inVoiceChannel())//If this user was in a VoiceChannel, fire VoiceLeaveEvent. { diff --git a/src/main/java/net/dv8tion/jda/internal/handle/GuildMemberUpdateHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/GuildMemberUpdateHandler.java index 1f45f5c44e..c805019eea 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/GuildMemberUpdateHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/GuildMemberUpdateHandler.java @@ -16,21 +16,15 @@ package net.dv8tion.jda.internal.handle; import net.dv8tion.jda.api.entities.Role; -import net.dv8tion.jda.api.events.guild.member.GuildMemberRoleAddEvent; -import net.dv8tion.jda.api.events.guild.member.GuildMemberRoleRemoveEvent; -import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateBoostTimeEvent; -import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateNicknameEvent; import net.dv8tion.jda.api.utils.data.DataArray; import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.internal.JDAImpl; +import net.dv8tion.jda.internal.entities.EntityBuilder; import net.dv8tion.jda.internal.entities.GuildImpl; import net.dv8tion.jda.internal.entities.MemberImpl; -import java.time.Instant; -import java.time.OffsetDateTime; -import java.time.format.DateTimeFormatter; -import java.time.temporal.TemporalAccessor; -import java.util.*; +import java.util.LinkedList; +import java.util.List; public class GuildMemberUpdateHandler extends SocketHandler { @@ -61,85 +55,12 @@ protected Long handleInternally(DataObject content) MemberImpl member = (MemberImpl) guild.getMembersView().get(userId); if (member == null) { - long hashId = id ^ userId; - getJDA().getEventCache().cache(EventCache.Type.MEMBER, hashId, responseNumber, allContent, this::handle); - EventCache.LOG.debug("Got GuildMember update but Member is not currently present in Guild. HASH_ID: {} JSON: {}", hashId, content); - return null; + EntityBuilder.LOG.debug("Creating member from GUILD_MEMBER_UPDATE {}", content); + member = getJDA().getEntityBuilder().createMember(guild, content); } - Set currentRoles = member.getRoleSet(); List newRoles = toRolesList(guild, content.getArray("roles")); - - //If newRoles is null that means that we didn't find a role that was in the array and was cached this event - if (newRoles == null) - return null; - - //Find the roles removed. - List removedRoles = new LinkedList<>(); - each: for (Role role : currentRoles) - { - for (Iterator it = newRoles.iterator(); it.hasNext();) - { - Role r = it.next(); - if (role.equals(r)) - { - it.remove(); - continue each; - } - } - removedRoles.add(role); - } - - if (removedRoles.size() > 0) - currentRoles.removeAll(removedRoles); - if (newRoles.size() > 0) - currentRoles.addAll(newRoles); - - if (removedRoles.size() > 0) - { - getJDA().handleEvent( - new GuildMemberRoleRemoveEvent( - getJDA(), responseNumber, - member, removedRoles)); - } - if (newRoles.size() > 0) - { - getJDA().handleEvent( - new GuildMemberRoleAddEvent( - getJDA(), responseNumber, - member, newRoles)); - } - if (content.hasKey("nick")) - { - String oldNick = member.getNickname(); - String newNick = content.getString("nick", null); - if (!Objects.equals(oldNick, newNick)) - { - member.setNickname(newNick); - getJDA().handleEvent( - new GuildMemberUpdateNicknameEvent( - getJDA(), responseNumber, - member, oldNick)); - } - } - if (content.hasKey("premium_since")) - { - long epoch = 0; - if (!content.isNull("premium_since")) - { - TemporalAccessor date = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(content.getString("premium_since")); - epoch = Instant.from(date).toEpochMilli(); - } - if (epoch != member.getBoostDateRaw()) - { - OffsetDateTime oldTime = member.getTimeBoosted(); - member.setBoostDate(epoch); - getJDA().handleEvent( - new GuildMemberUpdateBoostTimeEvent( - getJDA(), responseNumber, - member, oldTime)); - } - } + getJDA().getEntityBuilder().updateMember(guild, member, content, newRoles); return null; } diff --git a/src/main/java/net/dv8tion/jda/internal/handle/GuildMembersChunkHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/GuildMembersChunkHandler.java index bdf92a694c..517d1aa847 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/GuildMembersChunkHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/GuildMembersChunkHandler.java @@ -45,6 +45,7 @@ protected Long handleInternally(DataObject content) DataObject object = members.getObject(i); builder.createMember(guild, object); } + guild.acknowledgeMembers(); } getJDA().getGuildSetupController().onMemberChunk(guildId, members); return null; 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 d5ac15c49e..6fcca45580 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/GuildSetupController.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/GuildSetupController.java @@ -53,6 +53,7 @@ public class GuildSetupController private final TLongSet chunkingGuilds = new TLongHashSet(); private final TLongLongMap pendingChunks = new TLongLongHashMap(); private final TLongSet syncingGuilds; + private final TLongSet unavailableGuilds = new TLongHashSet(); private int incompleteCount = 0; private int syncingCount = 0; @@ -151,7 +152,7 @@ public boolean setIncompleteCount(int count) public void onReady(long id, DataObject obj) { log.trace("Adding id to setup cache {}", id); - GuildSetupNode node = new GuildSetupNode(id, this, false); + GuildSetupNode node = new GuildSetupNode(id, this, GuildSetupNode.Type.INIT); setupNodes.put(id, node); node.handleReady(obj); if (node.markedUnavailable) @@ -170,11 +171,19 @@ public void onCreate(long id, DataObject obj) { boolean available = obj.isNull("unavailable") || !obj.getBoolean("unavailable"); log.trace("Received guild create for id: {} available: {}", id, available); + + if (available && unavailableGuilds.contains(id) && !setupNodes.containsKey(id)) + { + // Guild was unavailable for a moment, its back now so initialize it again! + unavailableGuilds.remove(id); + setupNodes.put(id, new GuildSetupNode(id, this, GuildSetupNode.Type.AVAILABLE)); + } + GuildSetupNode node = setupNodes.get(id); if (node == null) { // this is a join event - node = new GuildSetupNode(id, this, true); + node = new GuildSetupNode(id, this, GuildSetupNode.Type.JOIN); setupNodes.put(id, node); // do not increment incomplete counter, it is only relevant to init guilds } @@ -224,7 +233,7 @@ public boolean onDelete(long id, DataObject obj) { // This guild was deleted node.cleanup(); // clear EventCache - if (node.join && !node.requestedChunk) + if (node.isJoin() && !node.requestedChunk) remove(id); else ready(id); @@ -277,6 +286,17 @@ public boolean isLocked(long id) return setupNodes.containsKey(id); } + public boolean isUnavailable(long id) + { + return unavailableGuilds.contains(id); + } + + public boolean isKnown(long id) + { + // Whether we know this guild at all + return isLocked(id) || isUnavailable(id); + } + public void cacheEvent(long guildId, DataObject event) { GuildSetupNode node = setupNodes.get(guildId); @@ -290,6 +310,7 @@ public void clearCache() { setupNodes.clear(); chunkingGuilds.clear(); + unavailableGuilds.clear(); incompleteCount = 0; close(); synchronized (pendingChunks) @@ -316,6 +337,11 @@ public boolean containsMember(long userId, @Nullable GuildSetupNode excludedNode return false; } + public TLongSet getUnavailableGuilds() + { + return unavailableGuilds; + } + public Set getSetupNodes() { return new HashSet<>(setupNodes.valueCollection()); @@ -451,6 +477,12 @@ private void trySyncing() } } + public void onUnavailable(long id) + { + unavailableGuilds.add(id); + log.debug("Guild with id {} is now marked unavailable. Total: {}", id, unavailableGuilds.size()); + } + public enum Status { INIT, 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 2814b61f7b..00d15a77fd 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/GuildSetupNode.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/GuildSetupNode.java @@ -25,6 +25,7 @@ import net.dv8tion.jda.api.audio.hooks.ConnectionListener; import net.dv8tion.jda.api.audio.hooks.ConnectionStatus; import net.dv8tion.jda.api.entities.VoiceChannel; +import net.dv8tion.jda.api.events.guild.GuildAvailableEvent; import net.dv8tion.jda.api.events.guild.GuildJoinEvent; import net.dv8tion.jda.api.events.guild.GuildReadyEvent; import net.dv8tion.jda.api.events.guild.UnavailableGuildJoinedEvent; @@ -55,17 +56,17 @@ public class GuildSetupNode private boolean requestedSync; boolean requestedChunk; - final boolean join; + final Type type; final boolean sync; boolean firedUnavailableJoin = false; boolean markedUnavailable = false; GuildSetupController.Status status = GuildSetupController.Status.INIT; - GuildSetupNode(long id, GuildSetupController controller, boolean join) + GuildSetupNode(long id, GuildSetupController controller, Type type) { this.id = id; this.controller = new UpstreamReference<>(controller); - this.join = join; + this.type = type; this.sync = controller.isClient(); } @@ -102,9 +103,14 @@ public int getCurrentMemberCount() return knownMembers.size(); } + public Type getType() + { + return type; + } + public boolean isJoin() { - return join; + return type == Type.JOIN; } public boolean isMarkedUnavailable() @@ -137,7 +143,7 @@ public String toString() "expectedMemberCount=" + expectedMemberCount + ", " + "requestedSync=" + requestedSync + ", " + "requestedChunk=" + requestedChunk + ", " + - "join=" + join + ", " + + "type=" + type + ", " + "sync=" + sync + ", " + "markedUnavailable=" + markedUnavailable + '}'; @@ -204,7 +210,7 @@ void handleReady(DataObject obj) } else { - getController().addGuildForSyncing(id, join); + getController().addGuildForSyncing(id, isJoin()); requestedSync = true; } } @@ -227,7 +233,7 @@ void handleCreate(DataObject obj) this.markedUnavailable = unavailable; if (unavailable) { - if (!firedUnavailableJoin && join) + if (!firedUnavailableJoin && isJoin()) { firedUnavailableJoin = true; JDAImpl api = getController().getJDA(); @@ -240,7 +246,7 @@ void handleCreate(DataObject obj) // We are using a client-account and joined a guild // in that case we need to sync before doing anything updateStatus(GuildSetupController.Status.SYNCING); - getController().addGuildForSyncing(id, join); + getController().addGuildForSyncing(id, isJoin()); requestedSync = true; return; } @@ -283,7 +289,7 @@ boolean handleMemberChunk(DataArray arr) members.put(id, obj); } - if (members.size() >= expectedMemberCount) + if (members.size() >= expectedMemberCount || !getController().getJDA().chunkGuild(id)) { completeSetup(); return false; @@ -385,25 +391,31 @@ private void completeSetup() for (TLongIterator it = removedMembers.iterator(); it.hasNext(); ) members.remove(it.next()); removedMembers.clear(); - GuildImpl guild = api.getEntityBuilder().createGuild(id, partialGuild, members); + GuildImpl guild = api.getEntityBuilder().createGuild(id, partialGuild, members, expectedMemberCount); updateAudioManagerReference(guild); - if (join) + switch (type) { + case AVAILABLE: + api.handleEvent(new GuildAvailableEvent(api, api.getResponseTotal(), guild)); + getController().remove(id); + break; + case JOIN: api.handleEvent(new GuildJoinEvent(api, api.getResponseTotal(), guild)); if (requestedChunk) getController().ready(id); else getController().remove(id); - } - else - { + break; + default: api.handleEvent(new GuildReadyEvent(api, api.getResponseTotal(), guild)); getController().ready(id); + break; } updateStatus(GuildSetupController.Status.READY); GuildSetupController.log.debug("Finished setup for guild {} firing cached events {}", id, cachedEvents.size()); api.getClient().handle(cachedEvents); api.getEventCache().playbackCache(EventCache.Type.GUILD, id); + guild.acknowledgeMembers(); } private void ensureMembers() @@ -412,10 +424,14 @@ private void ensureMembers() members = new TLongObjectHashMap<>(expectedMemberCount); removedMembers = new TLongHashSet(); DataArray memberArray = partialGuild.getArray("members"); - if (memberArray.length() < expectedMemberCount && !requestedChunk) + if (!getController().getJDA().chunkGuild(id)) + { + handleMemberChunk(memberArray); + } + else if (memberArray.length() < expectedMemberCount && !requestedChunk) { updateStatus(GuildSetupController.Status.CHUNKING); - getController().addGuildForChunking(id, join); + getController().addGuildForChunking(id, isJoin()); requestedChunk = true; } else if (handleMemberChunk(memberArray) && !requestedChunk) @@ -430,7 +446,7 @@ else if (handleMemberChunk(memberArray) && !requestedChunk) expectedMemberCount, memberArray.length(), members.size(), id); members.clear(); updateStatus(GuildSetupController.Status.CHUNKING); - getController().addGuildForChunking(id, join); + getController().addGuildForChunking(id, isJoin()); requestedChunk = true; } } @@ -480,4 +496,9 @@ private void updateAudioManagerReference(GuildImpl guild) audioManagerMap.put(id, newMng); } } + + public enum Type + { + INIT, JOIN, AVAILABLE + } } diff --git a/src/main/java/net/dv8tion/jda/internal/handle/GuildUpdateHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/GuildUpdateHandler.java index ba8c1f07f1..fd12c84356 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/GuildUpdateHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/GuildUpdateHandler.java @@ -93,16 +93,18 @@ protected Long handleInternally(DataObject content) if (ownerId != guild.getOwnerIdLong()) { + long oldOwnerId = guild.getOwnerIdLong(); Member oldOwner = guild.getOwner(); Member newOwner = guild.getMembersView().get(ownerId); if (newOwner == null) - WebSocketClient.LOG.warn("Received {} with owner not in cache. UserId: {} GuildId: {}", allContent.get("t"), ownerId, id); + WebSocketClient.LOG.debug("Received {} with owner not in cache. UserId: {} GuildId: {}", allContent.get("t"), ownerId, id); guild.setOwner(newOwner); guild.setOwnerId(ownerId); getJDA().handleEvent( new GuildUpdateOwnerEvent( getJDA(), responseNumber, - guild, oldOwner)); + guild, oldOwner, + oldOwnerId, ownerId)); } if (!Objects.equals(description, guild.getDescription())) { diff --git a/src/main/java/net/dv8tion/jda/internal/handle/MessageReactionHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/MessageReactionHandler.java index ceccd942d4..a730f4ab8c 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/MessageReactionHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/MessageReactionHandler.java @@ -16,10 +16,7 @@ package net.dv8tion.jda.internal.handle; -import net.dv8tion.jda.api.entities.Emote; -import net.dv8tion.jda.api.entities.MessageChannel; -import net.dv8tion.jda.api.entities.MessageReaction; -import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.*; import net.dv8tion.jda.api.events.message.guild.react.GuildMessageReactionAddEvent; import net.dv8tion.jda.api.events.message.guild.react.GuildMessageReactionRemoveEvent; import net.dv8tion.jda.api.events.message.priv.react.PrivateMessageReactionAddEvent; @@ -29,9 +26,14 @@ import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.internal.JDAImpl; import net.dv8tion.jda.internal.entities.EmoteImpl; +import net.dv8tion.jda.internal.entities.GuildImpl; +import net.dv8tion.jda.internal.entities.MemberImpl; import net.dv8tion.jda.internal.requests.WebSocketClient; import net.dv8tion.jda.internal.utils.JDALogger; +import java.util.Objects; +import java.util.Optional; + public class MessageReactionHandler extends SocketHandler { @@ -70,20 +72,39 @@ protected Long handleInternally(DataObject content) return null; } + Guild guild = getJDA().getGuildById(content.getUnsignedLong("guild_id", 0)); + MemberImpl member = null; + if (guild != null) + { + member = (MemberImpl) guild.getMemberById(userId); + // Attempt loading the member if possible + Optional memberJson = content.optObject("member"); + if (memberJson.isPresent()) // Check if we can load a member here + { + if (member == null || member.isIncomplete()) // do we need to load a member? + member = getJDA().getEntityBuilder().createMember((GuildImpl) guild, memberJson.get()); + } + if (member == null && add && guild.isLoaded()) + { + WebSocketClient.LOG.debug("Dropping reaction event for unknown member {}", content); + return null; + } + } + User user = getJDA().getUserById(userId); + if (user == null && member != null) + user = member.getUser(); // this happens when we have guild subscriptions disabled if (user == null) user = getJDA().getFakeUserMap().get(userId); if (user == null) { - if (!add) + if (add && guild != null) { - //This can be caused by a ban, we should just drop it in that case + getJDA().getEventCache().cache(EventCache.Type.USER, userId, responseNumber, allContent, this::handle); + EventCache.LOG.debug("Received a reaction for a user that JDA does not currently have cached. " + + "UserID: {} ChannelId: {} MessageId: {}", userId, channelId, messageId); return null; } - getJDA().getEventCache().cache(EventCache.Type.USER, userId, responseNumber, allContent, this::handle); - EventCache.LOG.debug("Received a reaction for a user that JDA does not currently have cached. " + - "UserID: {} ChannelId: {} MessageId: {}", userId, channelId, messageId); - return null; } MessageChannel channel = getJDA().getTextChannelById(channelId); @@ -125,16 +146,16 @@ protected Long handleInternally(DataObject content) { rEmote = MessageReaction.ReactionEmote.fromUnicode(emojiName, getJDA()); } - MessageReaction reaction = new MessageReaction(channel, rEmote, messageId, user.equals(getJDA().getSelfUser()), -1); + MessageReaction reaction = new MessageReaction(channel, rEmote, messageId, userId == getJDA().getSelfUser().getIdLong(), -1); if (add) - onAdd(reaction, user); + onAdd(reaction, user, member, userId); else - onRemove(reaction, user); + onRemove(reaction, user, member, userId); return null; } - private void onAdd(MessageReaction reaction, User user) + private void onAdd(MessageReaction reaction, User user, Member member, long userId) { JDAImpl jda = getJDA(); switch (reaction.getChannelType()) @@ -143,13 +164,13 @@ private void onAdd(MessageReaction reaction, User user) jda.handleEvent( new GuildMessageReactionAddEvent( jda, responseNumber, - user, reaction)); + Objects.requireNonNull(member), reaction)); break; case PRIVATE: jda.handleEvent( new PrivateMessageReactionAddEvent( jda, responseNumber, - user, reaction)); + user, reaction, userId)); break; case GROUP: WebSocketClient.LOG.debug("Received a reaction add for a group which should not be possible"); @@ -159,10 +180,10 @@ private void onAdd(MessageReaction reaction, User user) jda.handleEvent( new MessageReactionAddEvent( jda, responseNumber, - user, reaction)); + user, member, reaction, userId)); } - private void onRemove(MessageReaction reaction, User user) + private void onRemove(MessageReaction reaction, User user, Member member, long userId) { JDAImpl jda = getJDA(); switch (reaction.getChannelType()) @@ -171,13 +192,13 @@ private void onRemove(MessageReaction reaction, User user) jda.handleEvent( new GuildMessageReactionRemoveEvent( jda, responseNumber, - user, reaction)); + member, reaction, userId)); break; case PRIVATE: jda.handleEvent( new PrivateMessageReactionRemoveEvent( jda, responseNumber, - user, reaction)); + user, reaction, userId)); break; case GROUP: WebSocketClient.LOG.debug("Received a reaction remove for a group which should not be possible"); @@ -187,6 +208,6 @@ private void onRemove(MessageReaction reaction, User user) jda.handleEvent( new MessageReactionRemoveEvent( jda, responseNumber, - user, reaction)); + user, member, reaction, userId)); } } diff --git a/src/main/java/net/dv8tion/jda/internal/handle/PresenceUpdateHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/PresenceUpdateHandler.java index 4290a64391..ab07f044d6 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/PresenceUpdateHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/PresenceUpdateHandler.java @@ -20,7 +20,8 @@ import net.dv8tion.jda.api.entities.ClientType; import net.dv8tion.jda.api.events.user.UserActivityEndEvent; import net.dv8tion.jda.api.events.user.UserActivityStartEvent; -import net.dv8tion.jda.api.events.user.update.*; +import net.dv8tion.jda.api.events.user.update.UserUpdateActivityOrderEvent; +import net.dv8tion.jda.api.events.user.update.UserUpdateOnlineStatusEvent; import net.dv8tion.jda.api.utils.cache.CacheFlag; import net.dv8tion.jda.api.utils.data.DataArray; import net.dv8tion.jda.api.utils.data.DataObject; @@ -30,14 +31,16 @@ import net.dv8tion.jda.internal.entities.MemberImpl; import net.dv8tion.jda.internal.entities.UserImpl; import net.dv8tion.jda.internal.utils.Helpers; +import net.dv8tion.jda.internal.utils.JDALogger; +import org.slf4j.Logger; import java.util.ArrayList; import java.util.EnumSet; import java.util.List; -import java.util.Objects; public class PresenceUpdateHandler extends SocketHandler { + private static final Logger log = JDALogger.getLog(PresenceUpdateHandler.class); public PresenceUpdateHandler(JDAImpl api) { @@ -47,212 +50,185 @@ public PresenceUpdateHandler(JDAImpl api) @Override protected Long handleInternally(DataObject content) { - GuildImpl guild = null; + // Ignore events for relationships, presences are guild only to us + if (content.isNull("guild_id")) + { + log.debug("Received PRESENCE_UPDATE without guild_id. Ignoring event."); + return null; + } + //Do a pre-check to see if this is for a Guild, and if it is, if the guild is currently locked or not cached. - if (!content.isNull("guild_id")) + final long guildId = content.getLong("guild_id"); + if (getJDA().getGuildSetupController().isLocked(guildId)) + return guildId; + GuildImpl guild = (GuildImpl) getJDA().getGuildById(guildId); + if (guild == null) { - final long guildId = content.getLong("guild_id"); - if (getJDA().getGuildSetupController().isLocked(guildId)) - return guildId; - guild = (GuildImpl) getJDA().getGuildById(guildId); - if (guild == null) - { - getJDA().getEventCache().cache(EventCache.Type.GUILD, guildId, responseNumber, allContent, this::handle); - EventCache.LOG.debug("Received a PRESENCE_UPDATE for a guild that is not yet cached! " + - "GuildId: " + guildId + " UserId: " + content.getObject("user").get("id")); - return null; - } + getJDA().getEventCache().cache(EventCache.Type.GUILD, guildId, responseNumber, allContent, this::handle); + EventCache.LOG.debug("Received a PRESENCE_UPDATE for a guild that is not yet cached! GuildId:{} UserId: {}", + guildId, content.getObject("user").get("id")); + return null; } DataObject jsonUser = content.getObject("user"); final long userId = jsonUser.getLong("id"); UserImpl user = (UserImpl) getJDA().getUsersView().get(userId); - //If we do know about the user, lets update the user's specific info. - // Afterwards, we will see if we already have them cached in the specific guild - // or Relation. If not, we'll cache the OnlineStatus and Activity for later handling - // unless OnlineStatus is OFFLINE, in which case we probably received this event - // due to a User leaving a guild or no longer being a relation. - if (user != null) + // The user is not yet known to us, maybe due to lazy loading. Try creating it. + if (user == null) { - if (jsonUser.hasKey("username")) - { - String name = jsonUser.getString("username"); - String discriminator = jsonUser.get("discriminator").toString(); - String avatarId = jsonUser.getString("avatar", null); - String oldAvatar = user.getAvatarId(); + // If this presence update doesn't have a user or the status is offline we ignore it + if (jsonUser.isNull("username") || "offline".equals(content.get("status"))) + return null; + // We should have somewhat enough information to create this member, so lets do it! + user = (UserImpl) createMember(content, guildId, guild, jsonUser).getUser(); + } - if (!user.getName().equals(name)) - { - String oldUsername = user.getName(); - user.setName(name); - getJDA().handleEvent( - new UserUpdateNameEvent( - getJDA(), responseNumber, - user, oldUsername)); - } - if (!user.getDiscriminator().equals(discriminator)) - { - String oldDiscriminator = user.getDiscriminator(); - user.setDiscriminator(discriminator); - getJDA().handleEvent( - new UserUpdateDiscriminatorEvent( - getJDA(), responseNumber, - user, oldDiscriminator)); - } - if (!Objects.equals(avatarId, oldAvatar)) - { - String oldAvatarId = user.getAvatarId(); - user.setAvatarId(avatarId); - getJDA().handleEvent( - new UserUpdateAvatarEvent( - getJDA(), responseNumber, - user, oldAvatarId)); - } - } + if (jsonUser.hasKey("username")) + { + // username implies this is an update to a user - fire events and update properties + getJDA().getEntityBuilder().updateUser(user, jsonUser); + } - //Now that we've update the User's info, lets see if we need to set the specific Presence information. - // This is stored in the Member or Relation objects. - final DataArray activityArray = !getJDA().isCacheFlagSet(CacheFlag.ACTIVITY) || content.isNull("activities") ? null : content.getArray("activities"); - List newActivities = new ArrayList<>(); - boolean parsedGame = false; - try - { - if (activityArray != null) - { - for (int i = 0; i < activityArray.length(); i++) - newActivities.add(EntityBuilder.createActivity(activityArray.getObject(i))); - parsedGame = true; - } - } - catch (Exception ex) + //Now that we've update the User's info, lets see if we need to set the specific Presence information. + // This is stored in the Member objects. + //We set the activities to null to prevent parsing if the cache was disabled + final DataArray activityArray = !getJDA().isCacheFlagSet(CacheFlag.ACTIVITY) || content.isNull("activities") ? null : content.getArray("activities"); + List newActivities = new ArrayList<>(); + boolean parsedActivity = parseActivities(userId, activityArray, newActivities); + + MemberImpl member = (MemberImpl) guild.getMember(user); + //Create member from presence if not offline + if (member == null) + { + if (jsonUser.isNull("username") || "offline".equals(content.get("status"))) { - if (EntityBuilder.LOG.isDebugEnabled()) - EntityBuilder.LOG.warn("Encountered exception trying to parse a presence! UserID: {} JSON: {}", userId, activityArray, ex); - else - EntityBuilder.LOG.warn("Encountered exception trying to parse a presence! UserID: {} Message: {} Enable debug for details", userId, ex.getMessage()); + log.trace("Ignoring incomplete PRESENCE_UPDATE for member with id {} in guild with id {}", userId, guildId); + return null; } + member = createMember(content, guildId, guild, jsonUser); + } - OnlineStatus status = OnlineStatus.fromKey(content.getString("status")); - - //If we are in a Guild, then we will use Member. - // If we aren't we'll be dealing with the Relation system. - if (guild != null) - { - MemberImpl member = (MemberImpl) guild.getMember(user); + if (getJDA().isCacheFlagSet(CacheFlag.CLIENT_STATUS) && !content.isNull("client_status")) + handleClientStatus(content, member); - //If the Member is null, then User isn't in the Guild. - //This is either because this PRESENCE_UPDATE was received before the GUILD_MEMBER_ADD event - // or because a Member recently left and this PRESENCE_UPDATE came after the GUILD_MEMBER_REMOVE event. - //If it is because a Member recently left, then the status should be OFFLINE. As such, we will ignore - // the event if this is the case. If the status isn't OFFLINE, we will cache and use it when the - // Member object is setup during GUILD_MEMBER_ADD - if (member == null) - { - //Cache the presence and return to finish up. - if (status != OnlineStatus.OFFLINE) - { - guild.getCachedPresenceMap().put(userId, content); - return null; - } - } - else - { - if (getJDA().isCacheFlagSet(CacheFlag.CLIENT_STATUS) && !content.isNull("client_status")) - { - DataObject json = content.getObject("client_status"); - EnumSet types = EnumSet.of(ClientType.UNKNOWN); - for (String key : json.keys()) - { - ClientType type = ClientType.fromKey(key); - types.add(type); - String raw = String.valueOf(json.get(key)); - OnlineStatus clientStatus = OnlineStatus.fromKey(raw); - member.setOnlineStatus(type, clientStatus); - } - for (ClientType type : EnumSet.complementOf(types)) - member.setOnlineStatus(type, null); // set remaining types to offline - } - //The member is already cached, so modify the presence values and fire events as needed. - if (!member.getOnlineStatus().equals(status)) - { - OnlineStatus oldStatus = member.getOnlineStatus(); - member.setOnlineStatus(status); - getJDA().handleEvent( - new UserUpdateOnlineStatusEvent( - getJDA(), responseNumber, - user, guild, oldStatus)); - } - if (parsedGame) - { - List oldActivities = member.getActivities(); - boolean unorderedEquals = Helpers.deepEqualsUnordered(oldActivities, newActivities); - if (unorderedEquals) - { - boolean deepEquals = Helpers.deepEquals(oldActivities, newActivities); - if (!deepEquals) - { - member.setActivities(newActivities); - getJDA().handleEvent( - new UserUpdateActivityOrderEvent( - getJDA(), responseNumber, - oldActivities, member)); - } - } - else - { - member.setActivities(newActivities); - oldActivities = new ArrayList<>(oldActivities); // create modifiable copy - List startedActivities = new ArrayList<>(); - for (Activity activity : newActivities) - { - if (!oldActivities.remove(activity)) - startedActivities.add(activity); - } + // Check if activities changed + if (parsedActivity) + handleActivities(newActivities, member); - for (Activity activity : startedActivities) - { - getJDA().handleEvent( - new UserActivityStartEvent( - getJDA(), responseNumber, - member, activity)); - } + //The member is already cached, so modify the presence values and fire events as needed. + OnlineStatus status = OnlineStatus.fromKey(content.getString("status")); + if (!member.getOnlineStatus().equals(status)) + { + OnlineStatus oldStatus = member.getOnlineStatus(); + member.setOnlineStatus(status); + getJDA().handleEvent( + new UserUpdateOnlineStatusEvent( + getJDA(), responseNumber, + user, guild, oldStatus)); + } + return null; + } - for (Activity activity : oldActivities) - { - getJDA().handleEvent( - new UserActivityEndEvent( - getJDA(), responseNumber, - member, activity)); - } - } - } - } + private boolean parseActivities(long userId, DataArray activityArray, List newActivities) + { + boolean parsedActivity = false; + try + { + if (activityArray != null) + { + for (int i = 0; i < activityArray.length(); i++) + newActivities.add(EntityBuilder.createActivity(activityArray.getObject(i))); + parsedActivity = true; } - /* + } + catch (Exception ex) + { + if (EntityBuilder.LOG.isDebugEnabled()) + EntityBuilder.LOG.warn("Encountered exception trying to parse a presence! UserID: {} JSON: {}", userId, activityArray, ex); else + EntityBuilder.LOG.warn("Encountered exception trying to parse a presence! UserID: {} Message: {} Enable debug for details", userId, ex.getMessage()); + } + return parsedActivity; + } + + private MemberImpl createMember(DataObject content, long guildId, GuildImpl guild, DataObject jsonUser) + { + DataObject memberJson = DataObject.empty(); + + String nick = content.opt("nick").map(Object::toString).orElse(null); + DataArray roles = content.optArray("roles").orElse(null); + String onlineStatus = content.getString("status"); + // unfortunately this information is missing + String joinDate = content.getString("joined_at", null); + + memberJson.put("user", jsonUser) + .put("status", onlineStatus) + .put("roles", roles) + .put("nick", nick) + .put("joined_at", joinDate); + log.trace("Creating member from PRESENCE_UPDATE for userId: {} and guildId: {}", jsonUser.getUnsignedLong("id"), guild.getId()); + return getJDA().getEntityBuilder().createMember(guild, memberJson); + } + + private void handleActivities(List newActivities, MemberImpl member) + { + List oldActivities = member.getActivities(); + boolean unorderedEquals = Helpers.deepEqualsUnordered(oldActivities, newActivities); + if (unorderedEquals) + { + boolean deepEquals = Helpers.deepEquals(oldActivities, newActivities); + if (!deepEquals) { - //In this case, this PRESENCE_UPDATE is for a Relation. + member.setActivities(newActivities); + getJDA().handleEvent( + new UserUpdateActivityOrderEvent( + getJDA(), responseNumber, + oldActivities, member)); } - */ } else { - //In this case, we don't have the User cached, which means that we can't update the User's information. - // This is most likely because this PRESENCE_UPDATE came before the GUILD_MEMBER_ADD that would have added - // this User to our User cache. Or, it could have come after a GUILD_MEMBER_REMOVE that caused the User - // to be removed from JDA's central User cache because there were no more connected Guilds. If this is - // the case, then the OnlineStatus will be OFFLINE and we can ignore this event. - //Either way, we don't have the User cached so we need to cache the Presence information if - // the OnlineStatus is not OFFLINE. + member.setActivities(newActivities); + oldActivities = new ArrayList<>(oldActivities); // create modifiable copy + List startedActivities = new ArrayList<>(); + for (Activity activity : newActivities) + { + if (!oldActivities.remove(activity)) + startedActivities.add(activity); + } + + for (Activity activity : startedActivities) + { + getJDA().handleEvent( + new UserActivityStartEvent( + getJDA(), responseNumber, + member, activity)); + } - //If the OnlineStatus is OFFLINE, ignore the event and return. - OnlineStatus status = OnlineStatus.fromKey(content.getString("status")); + for (Activity activity : oldActivities) + { + getJDA().handleEvent( + new UserActivityEndEvent( + getJDA(), responseNumber, + member, activity)); + } + } + } - //If this was for a Guild, cache it in the Guild for later use in GUILD_MEMBER_ADD - if (status != OnlineStatus.OFFLINE && guild != null) - guild.getCachedPresenceMap().put(userId, content); + private void handleClientStatus(DataObject content, MemberImpl member) + { + DataObject json = content.getObject("client_status"); + EnumSet types = EnumSet.of(ClientType.UNKNOWN); + for (String key : json.keys()) + { + ClientType type = ClientType.fromKey(key); + types.add(type); + String raw = String.valueOf(json.get(key)); + OnlineStatus clientStatus = OnlineStatus.fromKey(raw); + member.setOnlineStatus(type, clientStatus); } - return null; + for (ClientType type : EnumSet.complementOf(types)) + member.setOnlineStatus(type, null); // set remaining types to offline } } diff --git a/src/main/java/net/dv8tion/jda/internal/handle/VoiceStateUpdateHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/VoiceStateUpdateHandler.java index 1c39ff8b74..382832035f 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/VoiceStateUpdateHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/VoiceStateUpdateHandler.java @@ -17,16 +17,20 @@ package net.dv8tion.jda.internal.handle; import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.events.guild.voice.*; import net.dv8tion.jda.api.hooks.VoiceDispatchInterceptor; +import net.dv8tion.jda.api.utils.cache.CacheFlag; import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.internal.JDAImpl; -import net.dv8tion.jda.internal.entities.GuildVoiceStateImpl; -import net.dv8tion.jda.internal.entities.MemberImpl; -import net.dv8tion.jda.internal.entities.VoiceChannelImpl; +import net.dv8tion.jda.internal.entities.*; import net.dv8tion.jda.internal.managers.AudioManagerImpl; +import net.dv8tion.jda.internal.utils.UnlockHook; +import net.dv8tion.jda.internal.utils.cache.MemberCacheViewImpl; +import net.dv8tion.jda.internal.utils.cache.SnowflakeCacheViewImpl; import java.util.Objects; +import java.util.Optional; public class VoiceStateUpdateHandler extends SocketHandler { @@ -75,24 +79,8 @@ private void handleGuildVoiceState(DataObject content) return; } - MemberImpl member = (MemberImpl) guild.getMemberById(userId); - if (member == null) - { - //Caching of this might not be valid. It is possible that we received this - // update due to this Member leaving the guild while still connected to a voice channel. - // In that case, we should not cache this because it could cause problems if they rejoined. - //However, we can't just ignore it completely because it could be a user that joined off of - // an invite to a VoiceChannel, so the GUILD_MEMBER_ADD and the VOICE_STATE_UPDATE may have - // come out of order. Not quite sure what to do. Going to cache for now however. - //At the worst, this will just cause a few events to fire with bad data if the member rejoins the guild if - // in fact the issue was that the VOICE_STATE_UPDATE was sent after they had left, however, by caching - // it we will preserve the integrity of the cache in the event that it was actually a mis-ordering of - // GUILD_MEMBER_ADD and VOICE_STATE_UPDATE. I'll take some bad-data events over an invalid cache. - long idHash = guildId ^ userId; - getJDA().getEventCache().cache(EventCache.Type.MEMBER, idHash, responseNumber, allContent, this::handle); - EventCache.LOG.debug("Received VOICE_STATE_UPDATE for a Member that has yet to be cached. HASH_ID: {} JSON: {}", idHash, content); - return; - } + MemberImpl member = getLazyMember(content, userId, (GuildImpl) guild, channelId != null); + if (member == null) return; GuildVoiceStateImpl vState = (GuildVoiceStateImpl) member.getVoiceState(); if (vState == null) @@ -191,4 +179,78 @@ else if (channel == null) getJDA().getDirectAudioController().update(guild, channel); } } + + private MemberImpl getLazyMember(DataObject content, long userId, GuildImpl guild, boolean connected) + { + // Check for existing member + Optional memberJson = content.optObject("member"); + MemberImpl member = (MemberImpl) guild.getMemberById(userId); + if (!memberJson.isPresent() || userId == getJDA().getSelfUser().getIdLong()) + return member; + + // Handle cache changes + boolean subscriptions = getJDA().isGuildSubscriptions(); + if (member == null) + { + if (subscriptions || (connected && getJDA().isCacheFlagSet(CacheFlag.VOICE_STATE))) + { + // this means either: + // - the member just connected to a voice channel, otherwise we would know about it already! + // - the member is not connect to a voice channel but we can load it here because subscriptions are enabled + member = loadMember(userId, guild, memberJson.get(), "Initializing"); + } + } + else + { + if (subscriptions && member.isIncomplete()) + { + // the member can be updated with new information that was missing before + member = loadMember(userId, guild, memberJson.get(), "Updating"); + } + else if (!subscriptions && !connected) + { + EntityBuilder.LOG.debug("Unloading member who just left a voice channel {}", memberJson); + // the member just disconnected from the voice channel - remove it from cache + unloadMember(userId, member); + return null; + } + } + return member; + } + + @SuppressWarnings("ConstantConditions") + private void unloadMember(long userId, MemberImpl member) + { + MemberCacheViewImpl membersView = member.getGuild().getMembersView(); + VoiceChannelImpl channelLeft = (VoiceChannelImpl) member.getVoiceState().getChannel(); + ((GuildVoiceStateImpl) member.getVoiceState()).setConnectedChannel(null); + if (channelLeft != null) + channelLeft.getConnectedMembersMap().remove(userId); + getJDA().handleEvent( + new GuildVoiceLeaveEvent( + getJDA(), responseNumber, + member, channelLeft)); + membersView.remove(userId); + User user = member.getUser(); + boolean dropUser = getJDA().getGuildsView().applyStream(stream -> stream.noneMatch(it -> it.isMember(user))); + if (dropUser) + getJDA().getUsersView().remove(userId); + } + + private MemberImpl loadMember(long userId, GuildImpl guild, DataObject memberJson, String comment) + { + EntityBuilder entityBuilder = getJDA().getEntityBuilder(); + MemberCacheViewImpl membersView = guild.getMembersView(); + SnowflakeCacheViewImpl usersView = getJDA().getUsersView(); + MemberImpl member; + EntityBuilder.LOG.debug("{} member from VOICE_STATE_UPDATE {}", comment, memberJson); + member = entityBuilder.createMember(guild, memberJson); + try (UnlockHook h1 = membersView.writeLock(); + UnlockHook h2 = usersView.writeLock()) + { + membersView.getMap().put(userId, member); + usersView.getMap().put(userId, member.getUser()); + } + return member; + } } 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 1f4fcfcc93..6a4a846c9b 100644 --- a/src/main/java/net/dv8tion/jda/internal/managers/AudioManagerImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/managers/AudioManagerImpl.java @@ -24,7 +24,6 @@ import net.dv8tion.jda.api.audio.hooks.ListenerProxy; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.VoiceChannel; -import net.dv8tion.jda.api.exceptions.GuildUnavailableException; import net.dv8tion.jda.api.exceptions.InsufficientPermissionException; import net.dv8tion.jda.api.managers.AudioManager; import net.dv8tion.jda.api.utils.MiscUtil; @@ -80,9 +79,6 @@ public void openAudioConnection(VoiceChannel channel) if (!getGuild().equals(channel.getGuild())) throw new IllegalArgumentException("The provided VoiceChannel is not a part of the Guild that this AudioManager handles." + "Please provide a VoiceChannel from the proper Guild"); - if (!getGuild().isAvailable()) - throw new GuildUnavailableException("Cannot open an Audio Connection with an unavailable guild. " + - "Please wait until this Guild is available to open a connection."); final Member self = getGuild().getSelfMember(); //if (!self.hasPermission(channel, Permission.VOICE_CONNECT)) // throw new InsufficientPermissionException(Permission.VOICE_CONNECT); 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 925abb9972..c9303db837 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/Route.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/Route.java @@ -107,6 +107,7 @@ public static class Guilds 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"); 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 89e8cacba6..cfe17ee0eb 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/WebSocketClient.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/WebSocketClient.java @@ -603,7 +603,8 @@ protected void sendIdentify() .put("token", getToken()) .put("properties", connectionProperties) .put("v", DISCORD_GATEWAY_VERSION) - .put("large_threshold", 250); + .put("guild_subscriptions", api.isGuildSubscriptions()) + .put("large_threshold", api.getLargeThreshold()); //Used to make the READY event be given // as compressed binary data when over a certain size. TY @ShadowLordAlpha //.put("compress", true); @@ -817,8 +818,11 @@ protected void onDispatch(DataObject raw) api.setStatus(JDA.Status.LOADING_SUBSYSTEMS); processingReady = true; handleIdentifyRateLimit = false; - sessionId = content.getString("session_id"); + // first handle the ready payload before applying the session id + // this prevents a possible race condition with the cache of the guild setup controller + // otherwise the audio connection requests that are currently pending might be removed in the process handlers.get("READY").handle(responseTotal, raw); + sessionId = content.getString("session_id"); break; case "RESUMED": sentAuthInfo = true; @@ -1110,7 +1114,7 @@ private SoftReference newDecompressBuffer() protected ConnectionRequest getNextAudioConnectRequest() { //Don't try to setup audio connections before JDA has finished loading. - if (!isReady()) + if (sessionId == null) return null; long now = System.currentTimeMillis(); @@ -1121,13 +1125,19 @@ protected ConnectionRequest getNextAudioConnectRequest() ConnectionRequest audioRequest = it.value(); if (audioRequest.getNextAttemptEpoch() < now) { - Guild guild = api.getGuildById(audioRequest.getGuildIdLong()); + // Check if the guild is ready + long guildId = audioRequest.getGuildIdLong(); + Guild guild = api.getGuildById(guildId); if (guild == null) { - it.remove(); - //if (listener != null) - // listener.onStatusChange(ConnectionStatus.DISCONNECTED_REMOVED_FROM_GUILD); - //already handled by event handling + // Not yet ready, check if the guild is known to this shard + GuildSetupController controller = api.getGuildSetupController(); + if (!controller.isKnown(guildId)) + { + // The guild is not tracked anymore -> we can't connect the audio channel + LOG.debug("Removing audio connection request because the guild has been removed. {}", audioRequest); + it.remove(); + } continue; } @@ -1269,7 +1279,7 @@ public void run(boolean isLast) throws InterruptedException return; try { - api.awaitStatus(JDA.Status.AWAITING_LOGIN_CONFIRMATION); + api.awaitStatus(JDA.Status.LOADING_SUBSYSTEMS, JDA.Status.RECONNECT_QUEUED); } catch (IllegalStateException ex) { @@ -1277,6 +1287,23 @@ public void run(boolean isLast) throws InterruptedException LOG.debug("Shutdown while trying to connect"); } } + + @Override + public int hashCode() + { + return Objects.hash("C", getJDA()); + } + + @Override + public boolean equals(Object obj) + { + if (obj == this) + return true; + if (!(obj instanceof StartingNode)) + return false; + StartingNode node = (StartingNode) obj; + return node.getJDA().equals(getJDA()); + } } protected class ReconnectNode extends ConnectNode @@ -1297,7 +1324,7 @@ public void run(boolean isLast) throws InterruptedException return; try { - api.awaitStatus(JDA.Status.AWAITING_LOGIN_CONFIRMATION); + api.awaitStatus(JDA.Status.LOADING_SUBSYSTEMS, JDA.Status.RECONNECT_QUEUED); } catch (IllegalStateException ex) { @@ -1305,5 +1332,22 @@ public void run(boolean isLast) throws InterruptedException LOG.debug("Shutdown while trying to reconnect"); } } + + @Override + public int hashCode() + { + return Objects.hash("R", getJDA()); + } + + @Override + public boolean equals(Object obj) + { + if (obj == this) + return true; + if (!(obj instanceof ReconnectNode)) + return false; + ReconnectNode node = (ReconnectNode) obj; + return node.getJDA().equals(getJDA()); + } } } 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 b1621ed79e..ae27e3c63e 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 @@ -32,6 +32,7 @@ public class MetaConfig private final EnumSet cacheFlags; private final boolean enableMDC; private final boolean useShutdownHook; + private final boolean guildSubscriptions; public MetaConfig( @Nullable ConcurrentMap mdcContextMap, @@ -44,6 +45,7 @@ public MetaConfig( else this.mdcContextMap = null; this.useShutdownHook = flags.contains(ConfigFlag.SHUTDOWN_HOOK); + this.guildSubscriptions = flags.contains(ConfigFlag.GUILD_SUBSCRIPTIONS); } @Nullable @@ -68,6 +70,11 @@ public boolean isUseShutdownHook() return useShutdownHook; } + public boolean isGuildSubscriptions() + { + return guildSubscriptions; + } + @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 ed2ad14fd1..f3c4175255 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 @@ -33,13 +33,14 @@ public class SessionConfig private final OkHttpClient httpClient; private final WebSocketFactory webSocketFactory; private final VoiceDispatchInterceptor interceptor; + private final int largeThreshold; private EnumSet flags; private int maxReconnectDelay; public SessionConfig( @Nullable SessionController sessionController, @Nullable OkHttpClient httpClient, @Nullable WebSocketFactory webSocketFactory, @Nullable VoiceDispatchInterceptor interceptor, - EnumSet flags, int maxReconnectDelay) + EnumSet flags, int maxReconnectDelay, int largeThreshold) { this.sessionController = sessionController == null ? new SessionControllerAdapter() : sessionController; this.httpClient = httpClient; @@ -47,6 +48,7 @@ public SessionConfig( this.interceptor = interceptor; this.flags = flags; this.maxReconnectDelay = maxReconnectDelay; + this.largeThreshold = largeThreshold; } public void setAutoReconnect(boolean autoReconnect) @@ -111,6 +113,11 @@ public int getMaxReconnectDelay() return maxReconnectDelay; } + public int getLargeThreshold() + { + return largeThreshold; + } + public EnumSet getFlags() { return flags; @@ -119,6 +126,6 @@ public EnumSet getFlags() @Nonnull public static SessionConfig getDefault() { - return new SessionConfig(null, null, null, null, ConfigFlag.getDefault(), 900); + return new SessionConfig(null, null, null, null, ConfigFlag.getDefault(), 900, 250); } } diff --git a/src/main/java/net/dv8tion/jda/internal/utils/config/flags/ConfigFlag.java b/src/main/java/net/dv8tion/jda/internal/utils/config/flags/ConfigFlag.java index a08c3597aa..5ee5602743 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/config/flags/ConfigFlag.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/config/flags/ConfigFlag.java @@ -26,7 +26,8 @@ public enum ConfigFlag BULK_DELETE_SPLIT(true), SHUTDOWN_HOOK(true), MDC_CONTEXT(true), - AUTO_RECONNECT(true); + AUTO_RECONNECT(true), + GUILD_SUBSCRIPTIONS(true); private final boolean isDefault; 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 20eefd1abb..984051fae6 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 @@ -39,9 +39,10 @@ public ShardingSessionConfig( @Nullable SessionController sessionController, @Nullable VoiceDispatchInterceptor interceptor, @Nullable OkHttpClient httpClient, @Nullable OkHttpClient.Builder httpClientBuilder, @Nullable WebSocketFactory webSocketFactory, @Nullable IAudioSendFactory audioSendFactory, - EnumSet flags, EnumSet shardingFlags, int maxReconnectDelay) + EnumSet flags, EnumSet shardingFlags, + int maxReconnectDelay, int largeThreshold) { - super(sessionController, httpClient, webSocketFactory, interceptor, flags, maxReconnectDelay); + super(sessionController, httpClient, webSocketFactory, interceptor, flags, maxReconnectDelay, largeThreshold); if (httpClient == null) this.builder = httpClientBuilder == null ? new OkHttpClient.Builder() : httpClientBuilder; else @@ -50,6 +51,11 @@ public ShardingSessionConfig( this.shardingFlags = shardingFlags; } + public SessionConfig toSessionConfig(OkHttpClient client) + { + return new SessionConfig(getSessionController(), client, getWebSocketFactory(), getVoiceDispatchInterceptor(), getFlags(), getMaxReconnectDelay(), getLargeThreshold()); + } + public EnumSet getShardingFlags() { return this.shardingFlags; @@ -70,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); + return new ShardingSessionConfig(null, null, null, null, null, null, ConfigFlag.getDefault(), ShardingConfigFlag.getDefault(), 900, 250); } }