From 7a9c21c56d1dabadbee25c602997afd98a8e090e Mon Sep 17 00:00:00 2001 From: Vankka Date: Sun, 22 Jan 2023 22:20:36 +0200 Subject: [PATCH] Specifying intents, cache flags and member caching policies in modules --- .../com/discordsrv/api/DiscordSRVApi.java | 28 ++- .../connection/details/DiscordCacheFlag.java | 64 +++++++ .../DiscordConnectionDetails.java | 49 ++--- .../details/DiscordGatewayIntent.java | 88 +++++++++ .../details/DiscordMemberCachePolicy.java | 44 +++++ .../discordsrv/api/module/type/Module.java | 72 ++++++- .../discordsrv/common/AbstractDiscordSRV.java | 63 +++--- .../com/discordsrv/common/DiscordSRV.java | 19 +- .../discordsrv/common/ServerDiscordSRV.java | 3 +- .../common/channel/ChannelUpdaterModule.java | 2 +- .../command/game/GameCommandModule.java | 2 +- .../game/command/subcommand/DebugCommand.java | 3 +- .../command/subcommand/ReloadCommand.java | 52 ++++- .../common/config/main/MainConfig.java | 2 + .../config/main/MemberCachingConfig.java | 49 +++++ .../common/discord/api/DiscordAPIImpl.java | 7 +- .../connection/DiscordConnectionManager.java | 6 - .../connection/jda/JDAConnectionManager.java | 174 +++++++++++++---- .../details/DiscordConnectionDetailsImpl.java | 87 +++++---- .../common/function/CheckedRunnable.java | 4 +- .../common/groupsync/GroupSyncModule.java | 2 +- .../common/invite/DiscordInviteModule.java | 34 +++- .../discordsrv/common/linking/LinkStore.java | 2 +- .../discord/DiscordChatMessageModule.java | 19 ++ .../DiscordMessageMirroringModule.java | 17 ++ .../game/AbstractGameMessageModule.java | 10 + .../game/StartMessageModule.java | 29 +++ .../game/StopMessageModule.java | 28 +++ .../common/module/ModuleManager.java | 180 ++++++++++++++---- .../common/module/type/AbstractModule.java | 70 ++++++- .../common/module/type/ModuleDelegate.java | 40 +++- .../common/module/type/PluginIntegration.java | 5 + settings.gradle | 2 +- 33 files changed, 1039 insertions(+), 217 deletions(-) create mode 100644 api/src/main/java/com/discordsrv/api/discord/connection/details/DiscordCacheFlag.java rename api/src/main/java/com/discordsrv/api/discord/connection/{jda => details}/DiscordConnectionDetails.java (57%) create mode 100644 api/src/main/java/com/discordsrv/api/discord/connection/details/DiscordGatewayIntent.java create mode 100644 api/src/main/java/com/discordsrv/api/discord/connection/details/DiscordMemberCachePolicy.java create mode 100644 common/src/main/java/com/discordsrv/common/config/main/MemberCachingConfig.java diff --git a/api/src/main/java/com/discordsrv/api/DiscordSRVApi.java b/api/src/main/java/com/discordsrv/api/DiscordSRVApi.java index 78ca559d..71dcef4d 100644 --- a/api/src/main/java/com/discordsrv/api/DiscordSRVApi.java +++ b/api/src/main/java/com/discordsrv/api/DiscordSRVApi.java @@ -25,7 +25,7 @@ package com.discordsrv.api; import com.discordsrv.api.component.MinecraftComponentFactory; import com.discordsrv.api.discord.DiscordAPI; -import com.discordsrv.api.discord.connection.jda.DiscordConnectionDetails; +import com.discordsrv.api.discord.connection.details.DiscordConnectionDetails; import com.discordsrv.api.event.bus.EventBus; import com.discordsrv.api.placeholder.PlaceholderService; import com.discordsrv.api.player.DiscordSRVPlayer; @@ -38,6 +38,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; +import java.util.function.Predicate; /** * The DiscordSRV API. @@ -261,7 +262,7 @@ public interface DiscordSRVApi { CONFIG(false), LINKED_ACCOUNT_PROVIDER(false), STORAGE(true), - DISCORD_CONNECTION(true), + DISCORD_CONNECTION(DiscordSRVApi::isReady), MODULES(false), ; @@ -269,14 +270,31 @@ public interface DiscordSRVApi { public static final Set ALL = Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(values()))); public static final Set DEFAULT_FLAGS = Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(CONFIG, MODULES))); - private final boolean requiresConfirm; + private final Predicate requiresConfirm; ReloadFlag(boolean requiresConfirm) { + this(__ -> requiresConfirm); + } + + ReloadFlag(Predicate requiresConfirm) { this.requiresConfirm = requiresConfirm; } - public boolean requiresConfirm() { - return requiresConfirm; + public boolean requiresConfirm(DiscordSRVApi discordSRV) { + return requiresConfirm.test(discordSRV); + } + } + + interface ReloadResult { + + ReloadResult RESTART_REQUIRED = Results.RESTART_REQUIRED; + + String name(); + + enum Results implements ReloadResult { + + RESTART_REQUIRED + } } } diff --git a/api/src/main/java/com/discordsrv/api/discord/connection/details/DiscordCacheFlag.java b/api/src/main/java/com/discordsrv/api/discord/connection/details/DiscordCacheFlag.java new file mode 100644 index 00000000..49d339ab --- /dev/null +++ b/api/src/main/java/com/discordsrv/api/discord/connection/details/DiscordCacheFlag.java @@ -0,0 +1,64 @@ +/* + * This file is part of the DiscordSRV API, licensed under the MIT License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.discordsrv.api.discord.connection.details; + +import com.discordsrv.api.discord.entity.JDAEntity; +import net.dv8tion.jda.api.requests.GatewayIntent; +import net.dv8tion.jda.api.utils.cache.CacheFlag; + +public enum DiscordCacheFlag implements JDAEntity { + + ACTIVITY(CacheFlag.ACTIVITY), + VOICE_STATE(CacheFlag.VOICE_STATE), + EMOJI(CacheFlag.EMOJI), + STICKER(CacheFlag.STICKER), + CLIENT_STATUS(CacheFlag.CLIENT_STATUS), + MEMBER_OVERRIDES(CacheFlag.MEMBER_OVERRIDES), + ROLE_TAGS(CacheFlag.ROLE_TAGS), + FORUM_TAGS(CacheFlag.FORUM_TAGS), + ONLINE_STATUS(CacheFlag.ONLINE_STATUS), + SCHEDULED_EVENTS(CacheFlag.SCHEDULED_EVENTS), + + ; + + private final CacheFlag jda; + + DiscordCacheFlag(CacheFlag jda) { + this.jda = jda; + } + + public DiscordGatewayIntent requiredIntent() { + GatewayIntent intent = jda.getRequiredIntent(); + if (intent == null) { + return null; + } + + return DiscordGatewayIntent.getByJda(intent); + } + + @Override + public CacheFlag asJDA() { + return jda; + } +} diff --git a/api/src/main/java/com/discordsrv/api/discord/connection/jda/DiscordConnectionDetails.java b/api/src/main/java/com/discordsrv/api/discord/connection/details/DiscordConnectionDetails.java similarity index 57% rename from api/src/main/java/com/discordsrv/api/discord/connection/jda/DiscordConnectionDetails.java rename to api/src/main/java/com/discordsrv/api/discord/connection/details/DiscordConnectionDetails.java index 1641f7e8..0bc0face 100644 --- a/api/src/main/java/com/discordsrv/api/discord/connection/jda/DiscordConnectionDetails.java +++ b/api/src/main/java/com/discordsrv/api/discord/connection/details/DiscordConnectionDetails.java @@ -21,61 +21,46 @@ * SOFTWARE. */ -package com.discordsrv.api.discord.connection.jda; +package com.discordsrv.api.discord.connection.details; import com.discordsrv.api.DiscordSRVApi; import net.dv8tion.jda.api.requests.GatewayIntent; import net.dv8tion.jda.api.utils.cache.CacheFlag; import org.jetbrains.annotations.NotNull; -import java.util.Set; - /** - * A helper class to provide {@link GatewayIntent}s and {@link CacheFlag}s to the {@link net.dv8tion.jda.api.JDA} instance created by DiscordSRV during startup. + * A helper class to provide {@link DiscordGatewayIntent}s and {@link DiscordCacheFlag}s for the Discord connection. * @see DiscordSRVApi#discordConnectionDetails() */ @SuppressWarnings("unused") // API public interface DiscordConnectionDetails { /** - * If {@link #requestGatewayIntent(GatewayIntent, GatewayIntent...)}} and {@link #requestCacheFlag(CacheFlag, CacheFlag...)} can be used. - * @return true, if {@link GatewayIntent}s and {@link CacheFlag} will be accepted - */ - boolean readyToTakeDetails(); - - /** - * The current gateway intents. - * @return the current set of gateway intents - */ - @NotNull - Set getGatewayIntents(); - - /** - * Requests that the provided {@link GatewayIntent}s be passed to {@link net.dv8tion.jda.api.JDA}. + * Requests that the provided {@link DiscordGatewayIntent}s be passed to the Discord connection. * * @param gatewayIntent the first gateway intent to add * @param gatewayIntents more gateway intents - * @throws IllegalStateException if DiscordSRV is already connecting/connected to Discord - * @see #readyToTakeDetails() + * @return {@code true} if the Discord connection is yet to be created and the intent will become active once it is */ - void requestGatewayIntent(@NotNull GatewayIntent gatewayIntent, @NotNull GatewayIntent... gatewayIntents); + boolean requestGatewayIntent(@NotNull DiscordGatewayIntent gatewayIntent, @NotNull DiscordGatewayIntent... gatewayIntents); /** - * The current cache flags. - * @return the current set of cache flags - */ - @NotNull - Set getCacheFlags(); - - /** - * Requests that the provided {@link CacheFlag} be passed to {@link net.dv8tion.jda.api.JDA}. + * Requests that the provided {@link DiscordCacheFlag}s be passed to the Discord connection. * * @param cacheFlag the first cache flag * @param cacheFlags more cache flags - * @throws IllegalStateException if DiscordSRV is already connecting/connected to Discord + * @return {@code true} if the Discord connection is yet to be created and the intent will become active once it is * @throws IllegalArgumentException if one of the requested {@link CacheFlag}s requires a {@link GatewayIntent} that hasn't been requested - * @see #readyToTakeDetails() */ - void requestCacheFlag(@NotNull CacheFlag cacheFlag, @NotNull CacheFlag... cacheFlags); + boolean requestCacheFlag(@NotNull DiscordCacheFlag cacheFlag, @NotNull DiscordCacheFlag... cacheFlags); + + /** + * Requests that the provided {@link DiscordMemberCachePolicy}s be passed to the Discord connection. + * + * @param memberCachePolicy the first member cache policy + * @param memberCachePolicies more member cache policies + * @return {@code true} if the Discord connection is yet to be created and the intent will become active once it is + */ + boolean requestMemberCachePolicy(@NotNull DiscordMemberCachePolicy memberCachePolicy, @NotNull DiscordMemberCachePolicy... memberCachePolicies); } diff --git a/api/src/main/java/com/discordsrv/api/discord/connection/details/DiscordGatewayIntent.java b/api/src/main/java/com/discordsrv/api/discord/connection/details/DiscordGatewayIntent.java new file mode 100644 index 00000000..d764a40c --- /dev/null +++ b/api/src/main/java/com/discordsrv/api/discord/connection/details/DiscordGatewayIntent.java @@ -0,0 +1,88 @@ +/* + * This file is part of the DiscordSRV API, licensed under the MIT License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.discordsrv.api.discord.connection.details; + +import com.discordsrv.api.discord.entity.JDAEntity; +import net.dv8tion.jda.api.requests.GatewayIntent; + +public enum DiscordGatewayIntent implements JDAEntity { + + GUILD_MEMBERS(GatewayIntent.GUILD_MEMBERS, "Server Members Intent"), + GUILD_BANS(GatewayIntent.GUILD_BANS), + GUILD_EMOJIS_AND_STICKERS(GatewayIntent.GUILD_EMOJIS_AND_STICKERS), + GUILD_WEBHOOKS(GatewayIntent.GUILD_WEBHOOKS), + GUILD_INVITES(GatewayIntent.GUILD_INVITES), + GUILD_VOICE_STATES(GatewayIntent.GUILD_VOICE_STATES), + GUILD_PRESENCES(GatewayIntent.GUILD_PRESENCES, "Presence Intent"), + GUILD_MESSAGES(GatewayIntent.GUILD_MESSAGES), + GUILD_MESSAGE_REACTIONS(GatewayIntent.GUILD_MESSAGE_REACTIONS), + GUILD_MESSAGE_TYPING(GatewayIntent.GUILD_MESSAGE_TYPING), + DIRECT_MESSAGES(GatewayIntent.DIRECT_MESSAGES), + DIRECT_MESSAGE_REACTIONS(GatewayIntent.DIRECT_MESSAGE_REACTIONS), + DIRECT_MESSAGE_TYPING(GatewayIntent.DIRECT_MESSAGE_TYPING), + MESSAGE_CONTENT(GatewayIntent.MESSAGE_CONTENT, "Message Content Intent"), + SCHEDULED_EVENTS(GatewayIntent.SCHEDULED_EVENTS), + + ; + + static DiscordGatewayIntent getByJda(GatewayIntent jda) { + for (DiscordGatewayIntent value : values()) { + if (value.asJDA() == jda) { + return value; + } + } + throw new IllegalArgumentException("This intent does not have a "); + } + + private final GatewayIntent jda; + private final String portalName; + private final boolean privileged; + + DiscordGatewayIntent(GatewayIntent jda) { + this(jda, null, false); + } + + DiscordGatewayIntent(GatewayIntent jda, String portalName) { + this(jda, portalName, true); + } + + DiscordGatewayIntent(GatewayIntent jda, String portalName, boolean privileged) { + this.jda = jda; + this.portalName = portalName; + this.privileged = privileged; + } + + public String portalName() { + return portalName; + } + + public boolean privileged() { + return privileged; + } + + @Override + public GatewayIntent asJDA() { + return jda; + } +} diff --git a/api/src/main/java/com/discordsrv/api/discord/connection/details/DiscordMemberCachePolicy.java b/api/src/main/java/com/discordsrv/api/discord/connection/details/DiscordMemberCachePolicy.java new file mode 100644 index 00000000..57a191c5 --- /dev/null +++ b/api/src/main/java/com/discordsrv/api/discord/connection/details/DiscordMemberCachePolicy.java @@ -0,0 +1,44 @@ +/* + * This file is part of the DiscordSRV API, licensed under the MIT License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.discordsrv.api.discord.connection.details; + +import com.discordsrv.api.DiscordSRVApi; +import com.discordsrv.api.discord.entity.guild.DiscordGuildMember; +import com.discordsrv.api.profile.IProfile; + +/** + * Represents a Discord member caching policy, a function which dictates if a given {@link DiscordGuildMember} should be cached. + */ +@FunctionalInterface +public interface DiscordMemberCachePolicy { + + DiscordMemberCachePolicy ALL = member -> true; + DiscordMemberCachePolicy LINKED = member -> DiscordSRVApi.optional() + .map(api -> api.profileManager().getProfile(member.getUser().getId())) + .map(IProfile::isLinked).orElse(false); + DiscordMemberCachePolicy VOICE = member -> member.asJDA().getVoiceState() != null; + DiscordMemberCachePolicy OWNER = member -> member.asJDA().isOwner(); + + boolean isCached(DiscordGuildMember member); +} diff --git a/api/src/main/java/com/discordsrv/api/module/type/Module.java b/api/src/main/java/com/discordsrv/api/module/type/Module.java index 807a7670..703d214b 100644 --- a/api/src/main/java/com/discordsrv/api/module/type/Module.java +++ b/api/src/main/java/com/discordsrv/api/module/type/Module.java @@ -23,12 +23,57 @@ package com.discordsrv.api.module.type; +import com.discordsrv.api.DiscordSRVApi; +import com.discordsrv.api.discord.connection.details.DiscordCacheFlag; +import com.discordsrv.api.discord.connection.details.DiscordGatewayIntent; +import com.discordsrv.api.discord.connection.details.DiscordMemberCachePolicy; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + public interface Module { + /** + * Determines if this {@link Module} should be enabled at the instant this method is called, this will be used + * to determine when modules should be enabled or disabled when DiscordSRV enabled, disables and reloads. + * @return the current enabled status the module should be in currently + */ default boolean isEnabled() { return true; } + /** + * Provides a {@link Collection} of {@link DiscordGatewayIntent}s that are required for this {@link Module}. + * @return the collection of gateway intents required by this module at the time this method is called + */ + @NotNull + default Collection requiredIntents() { + return Collections.emptyList(); + } + + /** + * Provides a {@link Collection} of {@link DiscordCacheFlag}s that are required for this {@link Module}. + * {@link DiscordGatewayIntent}s required by the cache flags will be required automatically. + * @return the collection of cache flags required by this module at the time this method is called + */ + @NotNull + default Collection requiredCacheFlags() { + return Collections.emptyList(); + } + + /** + * Provides a {@link Collection} of {@link DiscordMemberCachePolicy DiscordMemberCachePolicies} that are required for this {@link Module}, + * if a policy other than {@link DiscordMemberCachePolicy#OWNER} or {@link DiscordMemberCachePolicy#VOICE} is provided the {@link DiscordGatewayIntent#GUILD_MEMBERS} intent will be required automatically. + * @return the collection of member caching policies required by this module at the time this method is called + */ + @NotNull + default Collection requiredMemberCachingPolicies() { + return Collections.emptyList(); + } + /** * Returns the priority of this Module given the lookup type. * @param type the type being looked up this could be an interface @@ -39,14 +84,39 @@ public interface Module { return 0; } + /** + * Determines the order which this module should shut down in compared to other modules. + * @return the shutdown order of this module, higher values will be shut down first. The default is the same as {@link #priority(Class)} with the type of the class. + */ default int shutdownOrder() { return priority(getClass()); } + /** + * Called by DiscordSRV to enable this module. Calls {@link #reload()} if not implemented. + */ default void enable() { reload(); } + /** + * Called by DiscordSRV to disable this module. + */ default void disable() {} - default void reload() {} + + /** + * Called by DiscordSRV to reload this module. This is called to enable the module as well unless {@link #enable()} is overridden and does not call super. + * Use {@link #reloadNoResult()} if you don't wish to provide any result. + * @return the result(s) that occurred during this reload, if any. May be {@code null}. + */ + @Nullable + default Set reload() { + reloadNoResult(); + return null; + } + + /** + * An alternative to {@link #reload()}, which returns {@code void} instead of results. This method will not be called if {@link #reload()} is overridden! + */ + default void reloadNoResult() {} } diff --git a/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java b/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java index f51c6416..96d11170 100644 --- a/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java +++ b/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java @@ -18,7 +18,6 @@ package com.discordsrv.common; -import com.discordsrv.api.discord.connection.jda.DiscordConnectionDetails; import com.discordsrv.api.event.events.lifecycle.DiscordSRVConnectedEvent; import com.discordsrv.api.event.events.lifecycle.DiscordSRVReadyEvent; import com.discordsrv.api.event.events.lifecycle.DiscordSRVReloadedEvent; @@ -92,11 +91,8 @@ import java.io.InputStream; import java.lang.reflect.Constructor; import java.net.URL; import java.nio.file.Path; -import java.util.Locale; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; +import java.util.*; +import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.ReentrantLock; import java.util.jar.Attributes; @@ -120,7 +116,7 @@ public abstract class AbstractDiscordSRV constructor = clazz.getConstructors()[0]; module = constructor.newInstance(this); } catch (Throwable e) { - moduleManager.logger().debug("Failed to load integration: " + className, e); + String suffix = ""; + if (e instanceof LinkageError || e instanceof ClassNotFoundException) { + suffix = " (Integration likely not installed or using wrong version)"; + } + moduleManager.logger().debug("Failed to load integration: " + className + suffix, e); return; } moduleManager.registerModule(this, d -> (AbstractModule) module); @@ -396,6 +396,11 @@ public abstract class AbstractDiscordSRV invokeLifecycle(CheckedRunnable runnable) { + protected CompletableFuture invokeLifecycle(CheckedRunnable runnable) { return invokeLifecycle(() -> { try { lifecycleLock.lock(); @@ -454,21 +459,22 @@ public abstract class AbstractDiscordSRV invokeLifecycle(CheckedRunnable runnable, String message, boolean enable) { - return CompletableFuture.runAsync(() -> { + protected CompletableFuture invokeLifecycle(CheckedRunnable runnable, String message, boolean enable) { + return CompletableFuture.supplyAsync(() -> { if (status().isShutdown()) { // Already shutdown/shutting down, don't bother - return; + return null; } try { - runnable.run(); + return runnable.run(); } catch (Throwable t) { if (status().isShutdown() && t instanceof NoClassDefFoundError) { // Already shutdown, ignore errors for classes that already got unloaded - return; + return null; } if (enable) { setStatus(Status.FAILED_TO_START); @@ -476,6 +482,7 @@ public abstract class AbstractDiscordSRV invokeReload(Set flags, boolean silent) { + public final CompletableFuture> invokeReload(Set flags, boolean silent) { return invokeLifecycle(() -> reload(flags, silent), "Failed to reload", false); } @@ -573,7 +581,7 @@ public abstract class AbstractDiscordSRV flags, boolean initial) throws Throwable { + protected List reload(Set flags, boolean initial) throws Throwable { if (!initial) { logger().info("Reloading DiscordSRV..."); } @@ -595,13 +603,13 @@ public abstract class AbstractDiscordSRV don't continue - return; + return Collections.singletonList(ReloadResults.DISCORD_CONNECTION_FAILED); } } } @@ -699,13 +710,17 @@ public abstract class AbstractDiscordSRV results = new ArrayList<>(); if (flags.contains(ReloadFlag.MODULES)) { - moduleManager.reload(); + results.addAll(moduleManager.reload()); } if (!initial) { eventBus().publish(new DiscordSRVReloadedEvent(flags)); logger().info("Reload complete."); } + + results.add(ReloadResults.SUCCESS); + return results; } } diff --git a/common/src/main/java/com/discordsrv/common/DiscordSRV.java b/common/src/main/java/com/discordsrv/common/DiscordSRV.java index 50b4f9e9..b59337c9 100644 --- a/common/src/main/java/com/discordsrv/common/DiscordSRV.java +++ b/common/src/main/java/com/discordsrv/common/DiscordSRV.java @@ -34,9 +34,11 @@ import com.discordsrv.common.debug.data.VersionInfo; import com.discordsrv.common.dependency.DiscordSRVDependencyManager; import com.discordsrv.common.discord.api.DiscordAPIImpl; import com.discordsrv.common.discord.connection.jda.JDAConnectionManager; +import com.discordsrv.common.discord.details.DiscordConnectionDetailsImpl; import com.discordsrv.common.linking.LinkProvider; import com.discordsrv.common.logging.Logger; import com.discordsrv.common.logging.impl.DiscordSRVLogger; +import com.discordsrv.common.module.ModuleManager; import com.discordsrv.common.module.type.AbstractModule; import com.discordsrv.common.placeholder.PlaceholderServiceImpl; import com.discordsrv.common.player.provider.AbstractPlayerProvider; @@ -52,6 +54,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.nio.file.Path; +import java.util.List; import java.util.Locale; import java.util.Set; import java.util.concurrent.CompletableFuture; @@ -112,11 +115,14 @@ public interface DiscordSRV extends DiscordSRVApi { // Internal JDAConnectionManager discordConnectionManager(); + @NotNull DiscordConnectionDetailsImpl discordConnectionDetails(); + // Modules @Nullable T getModule(Class moduleType); void registerModule(AbstractModule module); void unregisterModule(AbstractModule module); + ModuleManager moduleManager(); Locale locale(); @@ -139,6 +145,17 @@ public interface DiscordSRV extends DiscordSRVApi { // Lifecycle CompletableFuture invokeEnable(); CompletableFuture invokeDisable(); - CompletableFuture invokeReload(Set flags, boolean silent); + CompletableFuture> invokeReload(Set flags, boolean silent); + + enum ReloadResults implements ReloadResult { + + // Internal reasons + SUCCESS, + SECURITY_FAILED, + STORAGE_CONNECTION_FAILED, + DISCORD_CONNECTION_RELOAD_REQUIRED, + DISCORD_CONNECTION_FAILED + + } } diff --git a/common/src/main/java/com/discordsrv/common/ServerDiscordSRV.java b/common/src/main/java/com/discordsrv/common/ServerDiscordSRV.java index 5f811690..f68a830d 100644 --- a/common/src/main/java/com/discordsrv/common/ServerDiscordSRV.java +++ b/common/src/main/java/com/discordsrv/common/ServerDiscordSRV.java @@ -51,9 +51,10 @@ public abstract class ServerDiscordSRV invokeServerStarted() { return invokeLifecycle(() -> { if (status().isShutdown()) { - return; + return null; } this.serverStarted(); + return null; }); } diff --git a/common/src/main/java/com/discordsrv/common/channel/ChannelUpdaterModule.java b/common/src/main/java/com/discordsrv/common/channel/ChannelUpdaterModule.java index 70afae30..3dfc9520 100644 --- a/common/src/main/java/com/discordsrv/common/channel/ChannelUpdaterModule.java +++ b/common/src/main/java/com/discordsrv/common/channel/ChannelUpdaterModule.java @@ -45,7 +45,7 @@ public class ChannelUpdaterModule extends AbstractModule { } @Override - public void reload() { + public void reloadNoResult() { futures.forEach(future -> future.cancel(false)); futures.clear(); diff --git a/common/src/main/java/com/discordsrv/common/command/game/GameCommandModule.java b/common/src/main/java/com/discordsrv/common/command/game/GameCommandModule.java index daa4f0af..40eee21c 100644 --- a/common/src/main/java/com/discordsrv/common/command/game/GameCommandModule.java +++ b/common/src/main/java/com/discordsrv/common/command/game/GameCommandModule.java @@ -47,7 +47,7 @@ public class GameCommandModule extends AbstractModule { } @Override - public void reload() { + public void reloadNoResult() { CommandConfig config = discordSRV.config().command; if (config == null) { return; diff --git a/common/src/main/java/com/discordsrv/common/command/game/command/subcommand/DebugCommand.java b/common/src/main/java/com/discordsrv/common/command/game/command/subcommand/DebugCommand.java index 03f4d5d9..ce32ee60 100644 --- a/common/src/main/java/com/discordsrv/common/command/game/command/subcommand/DebugCommand.java +++ b/common/src/main/java/com/discordsrv/common/command/game/command/subcommand/DebugCommand.java @@ -27,6 +27,7 @@ import com.discordsrv.common.debug.DebugReport; import com.discordsrv.common.paste.Paste; import com.discordsrv.common.paste.PasteService; import com.discordsrv.common.paste.service.AESEncryptedPasteService; +import com.discordsrv.common.paste.service.BytebinPasteService; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.format.NamedTextColor; @@ -67,7 +68,7 @@ public class DebugCommand implements GameCommandExecutor { public DebugCommand(DiscordSRV discordSRV) { this.discordSRV = discordSRV; - this.pasteService = new AESEncryptedPasteService(null /* TODO: tbd */, 128); + this.pasteService = new AESEncryptedPasteService(new BytebinPasteService(discordSRV, "https://bytebin.lucko.me") /* TODO: final store tbd */, 128); } @Override diff --git a/common/src/main/java/com/discordsrv/common/command/game/command/subcommand/ReloadCommand.java b/common/src/main/java/com/discordsrv/common/command/game/command/subcommand/ReloadCommand.java index 9fdb41ae..778f0d41 100644 --- a/common/src/main/java/com/discordsrv/common/command/game/command/subcommand/ReloadCommand.java +++ b/common/src/main/java/com/discordsrv/common/command/game/command/subcommand/ReloadCommand.java @@ -18,13 +18,17 @@ package com.discordsrv.common.command.game.command.subcommand; +import com.discordsrv.api.DiscordSRVApi; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.command.game.abstraction.GameCommand; import com.discordsrv.common.command.game.abstraction.GameCommandArguments; import com.discordsrv.common.command.game.abstraction.GameCommandExecutor; import com.discordsrv.common.command.game.abstraction.GameCommandSuggester; import com.discordsrv.common.command.game.sender.ICommandSender; +import com.discordsrv.common.player.IPlayer; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextDecoration; @@ -75,7 +79,7 @@ public class ReloadCommand implements GameCommandExecutor, GameCommandSuggester return; } - discordSRV.invokeReload(flags, false).whenComplete((v, t) -> { + discordSRV.invokeReload(flags, false).whenComplete((results, t) -> { if (t != null) { discordSRV.logger().error("Failed to reload", t); sender.sendMessage( @@ -83,8 +87,42 @@ public class ReloadCommand implements GameCommandExecutor, GameCommandSuggester .append(Component.text("Reload failed.", NamedTextColor.DARK_RED, TextDecoration.BOLD)) .append(Component.text("Please check the server console/log for more details.")) ); - } else { - sender.sendMessage(Component.text("Reload successful", NamedTextColor.GRAY)); + return; + } + + for (DiscordSRV.ReloadResult result : results) { + String res = result.name(); + if (res.equals(DiscordSRV.ReloadResults.SECURITY_FAILED.name())) { + sender.sendMessage(Component.text( + "DiscordSRV is disabled due to a security check failure. " + + "Please check console for more details", NamedTextColor.DARK_RED)); + } else if (res.equals(DiscordSRV.ReloadResults.SUCCESS.name())) { + sender.sendMessage(Component.text("Reload successful", NamedTextColor.GRAY)); + } else if (res.equals(DiscordSRV.ReloadResults.RESTART_REQUIRED.name())) { + sender.sendMessage(Component.text("Some changes require a server restart")); + } else if (res.equals(DiscordSRV.ReloadResults.STORAGE_CONNECTION_FAILED.name())) { + sender.sendMessage(Component.text("Storage connection failed, please check console for details.", NamedTextColor.RED)); + } else if (res.equals(DiscordSRV.ReloadResults.DISCORD_CONNECTION_FAILED.name())) { + sender.sendMessage(Component.text("Discord connection failed, please check console for details.", NamedTextColor.RED)); + } else if (res.equals(DiscordSRV.ReloadResults.DISCORD_CONNECTION_RELOAD_REQUIRED.name())) { + String command = "discordsrv reload " + DiscordSRVApi.ReloadFlag.DISCORD_CONNECTION.name().toLowerCase(Locale.ROOT) + " -confirm"; + Component child; + if (sender instanceof IPlayer) { + child = Component.text("[Click to reload Discord connection]", NamedTextColor.DARK_RED) + .clickEvent(ClickEvent.runCommand("/" + command)) + .hoverEvent(HoverEvent.showText(Component.text("/" + command))); + } else { + child = Component.text("Run ", NamedTextColor.DARK_RED) + .append(Component.text(command, NamedTextColor.GRAY)) + .append(Component.text(" to reload the Discord connection")); + } + + sender.sendMessage( + Component.text() + .append(Component.text("Some changes require a Discord connection reload. ", NamedTextColor.GRAY)) + .append(child) + ); + } } }); } @@ -103,10 +141,16 @@ public class ReloadCommand implements GameCommandExecutor, GameCommandSuggester boolean confirm = parts.remove("-confirm"); Set flags = new LinkedHashSet<>(); + if (discordSRV.status().isStartupError()) { + // If startup error, use all flags + parts.clear(); + flags.addAll(DiscordSRVApi.ReloadFlag.ALL); + } + for (String part : parts) { try { DiscordSRV.ReloadFlag flag = DiscordSRV.ReloadFlag.valueOf(part.toUpperCase(Locale.ROOT)); - if (flag.requiresConfirm() && !confirm) { + if (flag.requiresConfirm(discordSRV) && !confirm) { dangerousFlags.set(true); sender.sendMessage( Component.text("Reloading ", NamedTextColor.RED) diff --git a/common/src/main/java/com/discordsrv/common/config/main/MainConfig.java b/common/src/main/java/com/discordsrv/common/config/main/MainConfig.java index 1281fcf1..f16e6526 100644 --- a/common/src/main/java/com/discordsrv/common/config/main/MainConfig.java +++ b/common/src/main/java/com/discordsrv/common/config/main/MainConfig.java @@ -45,6 +45,8 @@ public class MainConfig implements Config { public LinkedAccountConfig linkedAccounts = new LinkedAccountConfig(); + public MemberCachingConfig memberCaching = new MemberCachingConfig(); + public List channelUpdaters = new ArrayList<>(Collections.singletonList(new ChannelUpdaterConfig())); @Comment("Configuration options for group-role synchronization") diff --git a/common/src/main/java/com/discordsrv/common/config/main/MemberCachingConfig.java b/common/src/main/java/com/discordsrv/common/config/main/MemberCachingConfig.java new file mode 100644 index 00000000..9cea3fd8 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/config/main/MemberCachingConfig.java @@ -0,0 +1,49 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.config.main; + +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; + +import java.util.ArrayList; +import java.util.List; + +@ConfigSerializable +public class MemberCachingConfig { + + @Comment("If linked users' members should be cached, this requires the \"Server Members Intent\"") + public boolean linkedUsers = true; + + @Comment("If all members should be cached") + public boolean all = false; + + @Comment("If members should be cached at startup, this requires the \"Server Members Intent\"") + public boolean chunk = false; + + @Comment("Filter for which servers should be chunked") + public GuildFilter chunkingServerFilter = new GuildFilter(); + + @ConfigSerializable + public static class GuildFilter { + + @Comment("If the ids option acts as a blacklist, otherwise it is a whitelist") + public boolean blacklist = true; + public List ids = new ArrayList<>(); + } +} diff --git a/common/src/main/java/com/discordsrv/common/discord/api/DiscordAPIImpl.java b/common/src/main/java/com/discordsrv/common/discord/api/DiscordAPIImpl.java index 767b027a..181dceaa 100644 --- a/common/src/main/java/com/discordsrv/common/discord/api/DiscordAPIImpl.java +++ b/common/src/main/java/com/discordsrv/common/discord/api/DiscordAPIImpl.java @@ -21,6 +21,7 @@ package com.discordsrv.common.discord.api; import club.minnced.discord.webhook.WebhookClient; import club.minnced.discord.webhook.WebhookClientBuilder; import com.discordsrv.api.discord.DiscordAPI; +import com.discordsrv.api.discord.connection.details.DiscordGatewayIntent; import com.discordsrv.api.discord.connection.jda.errorresponse.ErrorCallbackContext; import com.discordsrv.api.discord.entity.DiscordUser; import com.discordsrv.api.discord.entity.channel.*; @@ -54,7 +55,6 @@ import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; import net.dv8tion.jda.api.exceptions.ErrorResponseException; import net.dv8tion.jda.api.requests.ErrorResponse; -import net.dv8tion.jda.api.requests.GatewayIntent; import org.checkerframework.checker.index.qual.NonNegative; import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.NotNull; @@ -444,9 +444,8 @@ public class DiscordAPIImpl implements DiscordAPI { @Override public boolean isUserCachingEnabled() { - return discordSRV.discordConnectionDetails() - .getGatewayIntents() - .contains(GatewayIntent.GUILD_MEMBERS); + return discordSRV.discordConnectionManager().getIntents() + .contains(DiscordGatewayIntent.GUILD_MEMBERS); } @Override diff --git a/common/src/main/java/com/discordsrv/common/discord/connection/DiscordConnectionManager.java b/common/src/main/java/com/discordsrv/common/discord/connection/DiscordConnectionManager.java index b2d5ce24..b43e2030 100644 --- a/common/src/main/java/com/discordsrv/common/discord/connection/DiscordConnectionManager.java +++ b/common/src/main/java/com/discordsrv/common/discord/connection/DiscordConnectionManager.java @@ -38,12 +38,6 @@ public interface DiscordConnectionManager { @Nullable JDA instance(); - /** - * Are gateway intents and cache flags accepted. - * @return true for yes - */ - boolean areDetailsAccepted(); - /** * Attempts to connect to Discord. * @return a {@link CompletableFuture} diff --git a/common/src/main/java/com/discordsrv/common/discord/connection/jda/JDAConnectionManager.java b/common/src/main/java/com/discordsrv/common/discord/connection/jda/JDAConnectionManager.java index 230e38e6..9f07c8b6 100644 --- a/common/src/main/java/com/discordsrv/common/discord/connection/jda/JDAConnectionManager.java +++ b/common/src/main/java/com/discordsrv/common/discord/connection/jda/JDAConnectionManager.java @@ -18,9 +18,13 @@ package com.discordsrv.common.discord.connection.jda; -import com.discordsrv.api.discord.connection.jda.DiscordConnectionDetails; +import com.discordsrv.api.DiscordSRVApi; +import com.discordsrv.api.discord.connection.details.DiscordCacheFlag; +import com.discordsrv.api.discord.connection.details.DiscordGatewayIntent; +import com.discordsrv.api.discord.connection.details.DiscordMemberCachePolicy; import com.discordsrv.api.discord.connection.jda.errorresponse.ErrorCallbackContext; import com.discordsrv.api.discord.entity.DiscordUser; +import com.discordsrv.api.discord.entity.guild.DiscordGuildMember; import com.discordsrv.api.event.bus.EventPriority; import com.discordsrv.api.event.bus.Subscribe; import com.discordsrv.api.event.events.lifecycle.DiscordSRVShuttingDownEvent; @@ -29,9 +33,13 @@ import com.discordsrv.api.placeholder.PlaceholderLookupResult; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.config.connection.BotConfig; import com.discordsrv.common.config.connection.ConnectionConfig; +import com.discordsrv.common.config.main.MemberCachingConfig; +import com.discordsrv.common.debug.DebugGenerateEvent; +import com.discordsrv.common.debug.file.TextDebugFile; import com.discordsrv.common.discord.api.DiscordAPIImpl; import com.discordsrv.common.discord.api.entity.message.ReceivedDiscordMessageImpl; import com.discordsrv.common.discord.connection.DiscordConnectionManager; +import com.discordsrv.common.discord.details.DiscordConnectionDetailsImpl; import com.discordsrv.common.logging.Logger; import com.discordsrv.common.logging.NamedLogger; import com.discordsrv.common.scheduler.Scheduler; @@ -52,28 +60,24 @@ import net.dv8tion.jda.api.exceptions.InvalidTokenException; import net.dv8tion.jda.api.exceptions.RateLimitedException; import net.dv8tion.jda.api.requests.*; import net.dv8tion.jda.api.utils.ChunkingFilter; -import net.dv8tion.jda.api.utils.MemberCachePolicy; +import net.dv8tion.jda.api.utils.cache.CacheFlag; import net.dv8tion.jda.api.utils.messages.MessageRequest; import net.dv8tion.jda.internal.entities.ReceivedMessage; import net.dv8tion.jda.internal.hooks.EventManagerProxy; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.jetbrains.annotations.NotNull; import java.io.InterruptedIOException; -import java.util.*; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; public class JDAConnectionManager implements DiscordConnectionManager { - private static final Map PRIVILEGED_INTENTS = new HashMap<>(); - - static { - PRIVILEGED_INTENTS.put(GatewayIntent.GUILD_MEMBERS, "Server Members Intent"); - PRIVILEGED_INTENTS.put(GatewayIntent.GUILD_PRESENCES, "Presence Intent"); - PRIVILEGED_INTENTS.put(GatewayIntent.MESSAGE_CONTENT, "Message Content Intent"); - } - private final DiscordSRV discordSRV; private final FailureCallback failureCallback; private Future failureCallbackFuture; @@ -82,7 +86,11 @@ public class JDAConnectionManager implements DiscordConnectionManager { private CompletableFuture connectionFuture; private JDA instance; - private boolean detailsAccepted = true; + + // Currently used intents & cache flags + private final Set intents = new HashSet<>(); + private final Set cacheFlags = new HashSet<>(); + private final Set memberCachePolicies = new HashSet<>(); // Bot owner details private final Timeout botOwnerTimeout = new Timeout(5, TimeUnit.MINUTES); @@ -108,14 +116,21 @@ public class JDAConnectionManager implements DiscordConnectionManager { discordSRV.eventBus().subscribe(this); } - @Override - public JDA instance() { - return instance; + public Set getIntents() { + return intents; + } + + public Set getCacheFlags() { + return cacheFlags; + } + + public Set getMemberCachePolicies() { + return memberCachePolicies; } @Override - public boolean areDetailsAccepted() { - return detailsAccepted; + public JDA instance() { + return instance; } private void checkDefaultFailureCallback() { @@ -141,10 +156,10 @@ public class JDAConnectionManager implements DiscordConnectionManager { DiscordSRV.Status newStatus; if (ordinal < JDA.Status.CONNECTED.ordinal()) { newStatus = DiscordSRV.Status.ATTEMPTING_TO_CONNECT; - } else if (ordinal < JDA.Status.SHUTTING_DOWN.ordinal()) { - newStatus = DiscordSRV.Status.CONNECTED; - } else { + } else if (status == JDA.Status.DISCONNECTED || ordinal >= JDA.Status.SHUTTING_DOWN.ordinal()) { newStatus = DiscordSRV.Status.FAILED_TO_CONNECT; + } else { + newStatus = DiscordSRV.Status.CONNECTED; } discordSRV.setStatus(newStatus); } @@ -174,6 +189,37 @@ public class JDAConnectionManager implements DiscordConnectionManager { return discordSRV.discordAPI(); } + @Subscribe + public void onDebugGenerate(DebugGenerateEvent event) { + + StringBuilder builder = new StringBuilder(); + builder.append("Intents: ").append(intents); + builder.append("\nCache Flags: ").append(cacheFlags); + builder.append("\nMember Caching Policies: ").append(memberCachePolicies.size()); + + if (instance != null) { + CompletableFuture restPingFuture = instance.getRestPing().timeout(5, TimeUnit.SECONDS).submit(); + builder.append("\nGateway Ping: ").append(instance.getGatewayPing()).append("ms"); + + String restPing; + try { + restPing = restPingFuture.get() + "ms"; + } catch (ExecutionException e) { + if (e.getCause() instanceof TimeoutException) { + restPing = ">5s"; + } else { + restPing = ExceptionUtils.getMessage(e); + } + } catch (Throwable t) { + restPing = ExceptionUtils.getMessage(t); + } + + builder.append("\nRest Ping: ").append(restPing); + } + + event.addFile(new TextDebugFile("jda_connection_manager.txt", builder)); + } + @Subscribe(priority = EventPriority.EARLIEST) public void onPlaceholderLookup(PlaceholderLookupEvent event) { if (event.isProcessed()) { @@ -232,10 +278,7 @@ public class JDAConnectionManager implements DiscordConnectionManager { } private void connectInternal() { - discordSRV.discordConnectionDetails().requestGatewayIntent(GatewayIntent.GUILD_MESSAGES); // TODO: figure out how DiscordSRV required intents are going to work - discordSRV.discordConnectionDetails().requestGatewayIntent(GatewayIntent.GUILD_MEMBERS); // TODO: figure out how DiscordSRV required intents are going to work - detailsAccepted = false; - + discordSRV.setStatus(DiscordSRVApi.Status.ATTEMPTING_TO_CONNECT); this.gatewayPool = new ScheduledThreadPoolExecutor( 1, r -> new Thread(r, Scheduler.THREAD_NAME_PREFIX + "JDA Gateway") @@ -252,15 +295,70 @@ public class JDAConnectionManager implements DiscordConnectionManager { ); BotConfig botConfig = discordSRV.connectionConfig().bot; - DiscordConnectionDetails connectionDetails = discordSRV.discordConnectionDetails(); - Set intents = connectionDetails.getGatewayIntents(); - boolean membersIntent = intents.contains(GatewayIntent.GUILD_MEMBERS); + MemberCachingConfig memberCachingConfig = discordSRV.config().memberCaching; + DiscordConnectionDetailsImpl connectionDetails = discordSRV.discordConnectionDetails(); + + Set intents = new LinkedHashSet<>(); + this.intents.clear(); + this.intents.addAll(connectionDetails.getGatewayIntents()); + this.intents.forEach(intent -> intents.add(intent.asJDA())); + + Set cacheFlags = new LinkedHashSet<>(); + this.cacheFlags.clear(); + this.cacheFlags.addAll(connectionDetails.getCacheFlags()); + this.cacheFlags.forEach(flag -> { + cacheFlags.add(flag.asJDA()); + DiscordGatewayIntent intent = flag.requiredIntent(); + if (intent != null) { + intents.add(intent.asJDA()); + } + }); + + this.memberCachePolicies.clear(); + this.memberCachePolicies.addAll(connectionDetails.getMemberCachePolicies()); + if (memberCachingConfig.all || this.memberCachePolicies.contains(DiscordMemberCachePolicy.ALL)) { + this.memberCachePolicies.clear(); + this.memberCachePolicies.add(DiscordMemberCachePolicy.ALL); + } else if (memberCachingConfig.linkedUsers) { + this.memberCachePolicies.add(DiscordMemberCachePolicy.LINKED); + } + for (DiscordMemberCachePolicy policy : this.memberCachePolicies) { + if (policy != DiscordMemberCachePolicy.OWNER && policy != DiscordMemberCachePolicy.VOICE) { + this.intents.add(DiscordGatewayIntent.GUILD_MEMBERS); + break; + } + } + + ChunkingFilter chunkingFilter; + if (memberCachingConfig.chunk) { + MemberCachingConfig.GuildFilter servers = memberCachingConfig.chunkingServerFilter; + long[] ids = servers.ids.stream().mapToLong(l -> l).toArray(); + if (servers.blacklist) { + chunkingFilter = ChunkingFilter.exclude(ids); + } else { + chunkingFilter = ChunkingFilter.include(ids); + } + } else { + chunkingFilter = ChunkingFilter.NONE; + } // Start with everything disabled & enable stuff that we actually need JDABuilder jdaBuilder = JDABuilder.createLight(botConfig.token, intents); - jdaBuilder.enableCache(connectionDetails.getCacheFlags()); - jdaBuilder.setMemberCachePolicy(membersIntent ? MemberCachePolicy.ALL : MemberCachePolicy.OWNER); - jdaBuilder.setChunkingFilter(membersIntent ? ChunkingFilter.ALL : ChunkingFilter.NONE); + jdaBuilder.enableCache(cacheFlags); + jdaBuilder.setMemberCachePolicy(member -> { + if (this.memberCachePolicies.isEmpty()) { + return false; + } + + DiscordGuildMember guildMember = api().getGuildMember(member); + for (DiscordMemberCachePolicy memberCachePolicy : this.memberCachePolicies) { + if (memberCachePolicy.isCached(guildMember)) { + return true; + } + } + return false; + }); + jdaBuilder.setChunkingFilter(chunkingFilter); // We shut down JDA ourselves. Doing it at the JVM's shutdown may cause errors due to classloading jdaBuilder.setEnableShutdownHook(false); @@ -310,7 +408,6 @@ public class JDAConnectionManager implements DiscordConnectionManager { @SuppressWarnings("BusyWait") private void shutdownInternal(long timeoutMillis) { - detailsAccepted = true; if (instance == null) { shutdownExecutors(); return; @@ -413,15 +510,15 @@ public class JDAConnectionManager implements DiscordConnectionManager { if (closeCode == null) { return false; } else if (closeCode == CloseCode.DISALLOWED_INTENTS) { - Set intents = discordSRV.discordConnectionDetails().getGatewayIntents(); + Set intents = getIntents(); discordSRV.logger().error("+-------------------------------------->"); discordSRV.logger().error("| Failed to connect to Discord:"); discordSRV.logger().error("|"); discordSRV.logger().error("| The Discord bot is lacking one or more"); discordSRV.logger().error("| privileged intents listed below"); discordSRV.logger().error("|"); - for (GatewayIntent intent : intents) { - String displayName = PRIVILEGED_INTENTS.get(intent); + for (DiscordGatewayIntent intent : intents) { + String displayName = intent.portalName(); if (displayName != null) { discordSRV.logger().error("| " + displayName); } @@ -434,8 +531,9 @@ public class JDAConnectionManager implements DiscordConnectionManager { discordSRV.logger().error("| Discord user who created the bot"); discordSRV.logger().error("| 3. Go to the \"Bot\" tab"); discordSRV.logger().error("| 4. Make sure the intents listed above are all enabled"); - discordSRV.logger().error("| 5. "); // TODO + discordSRV.logger().error("| 5. Run the \"/discordsrv reload config discord_connection\" command"); discordSRV.logger().error("+-------------------------------------->"); + discordSRV.setStatus(DiscordSRVApi.Status.FAILED_TO_CONNECT); return true; } else if (closeCode == CloseCode.AUTHENTICATION_FAILED) { invalidToken(); @@ -453,9 +551,15 @@ public class JDAConnectionManager implements DiscordConnectionManager { discordSRV.logger().error("|"); discordSRV.logger().error("| You can get the token for your bot from:"); discordSRV.logger().error("| https://discord.com/developers/applications"); + discordSRV.logger().error("| by selecting the application, going to the \"Bot\" tab"); + discordSRV.logger().error("| and clicking on \"Reset Token\""); discordSRV.logger().error("| - Keep in mind the bot is only visible to"); discordSRV.logger().error("| the Discord user that created the bot"); + discordSRV.logger().error("|"); + discordSRV.logger().error("| Once the token is corrected in the " + ConnectionConfig.FILE_NAME); + discordSRV.logger().error("| Run the \"/discordsrv reload config discord_connection\" command"); discordSRV.logger().error("+------------------------------>"); + discordSRV.setStatus(DiscordSRVApi.Status.FAILED_TO_CONNECT); } private class FailureCallback implements Consumer { diff --git a/common/src/main/java/com/discordsrv/common/discord/details/DiscordConnectionDetailsImpl.java b/common/src/main/java/com/discordsrv/common/discord/details/DiscordConnectionDetailsImpl.java index fbe096a2..f61e47a7 100644 --- a/common/src/main/java/com/discordsrv/common/discord/details/DiscordConnectionDetailsImpl.java +++ b/common/src/main/java/com/discordsrv/common/discord/details/DiscordConnectionDetailsImpl.java @@ -18,11 +18,12 @@ package com.discordsrv.common.discord.details; -import com.discordsrv.api.discord.connection.jda.DiscordConnectionDetails; +import com.discordsrv.api.discord.connection.details.DiscordCacheFlag; +import com.discordsrv.api.discord.connection.details.DiscordConnectionDetails; +import com.discordsrv.api.discord.connection.details.DiscordGatewayIntent; +import com.discordsrv.api.discord.connection.details.DiscordMemberCachePolicy; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.exception.util.ExceptionUtil; -import net.dv8tion.jda.api.requests.GatewayIntent; -import net.dv8tion.jda.api.utils.cache.CacheFlag; import org.jetbrains.annotations.NotNull; import java.util.*; @@ -30,58 +31,52 @@ import java.util.*; public class DiscordConnectionDetailsImpl implements DiscordConnectionDetails { private final DiscordSRV discordSRV; - private final Set gatewayIntents = new HashSet<>(); - private final Set cacheFlags = new HashSet<>(); + private final Set gatewayIntents = new HashSet<>(); + private final Set cacheFlags = new HashSet<>(); + private final Set memberCachePolicies = new HashSet<>(); public DiscordConnectionDetailsImpl(DiscordSRV discordSRV) { this.discordSRV = discordSRV; + this.memberCachePolicies.add(DiscordMemberCachePolicy.OWNER); + } + + private boolean isStatus() { + return discordSRV.status() == DiscordSRV.Status.INITIALIZED + || discordSRV.status() == DiscordSRV.Status.ATTEMPTING_TO_CONNECT; + } + + public @NotNull Set getGatewayIntents() { + Set intents = new HashSet<>(gatewayIntents); + intents.addAll(discordSRV.moduleManager().requiredIntents()); + return intents; } @Override - public boolean readyToTakeDetails() { - return discordSRV.discordConnectionManager().areDetailsAccepted(); - } - - private void check() { - if (!readyToTakeDetails()) { - throw new IllegalStateException("Too late. Please use DiscordConnectionDetails#readyToTakeDetails " + - "to check if the method can be used"); - } - } - - @Override - public @NotNull Set getGatewayIntents() { - return gatewayIntents; - } - - @Override - public void requestGatewayIntent(@NotNull GatewayIntent gatewayIntent, GatewayIntent... gatewayIntents) { - check(); - - List intents = new ArrayList<>(Collections.singleton(gatewayIntent)); + public boolean requestGatewayIntent(@NotNull DiscordGatewayIntent gatewayIntent, DiscordGatewayIntent... gatewayIntents) { + List intents = new ArrayList<>(Collections.singleton(gatewayIntent)); intents.addAll(Arrays.asList(gatewayIntents)); this.gatewayIntents.addAll(intents); + return isStatus(); + } + + public @NotNull Set getCacheFlags() { + Set flags = new HashSet<>(cacheFlags); + flags.addAll(discordSRV.moduleManager().requiredCacheFlags()); + return flags; } @Override - public @NotNull Set getCacheFlags() { - return cacheFlags; - } - - @Override - public void requestCacheFlag(@NotNull CacheFlag cacheFlag, CacheFlag... cacheFlags) { - check(); - - List flags = new ArrayList<>(Collections.singleton(cacheFlag)); + public boolean requestCacheFlag(@NotNull DiscordCacheFlag cacheFlag, DiscordCacheFlag... cacheFlags) { + List flags = new ArrayList<>(Collections.singleton(cacheFlag)); flags.addAll(Arrays.asList(cacheFlags)); List suppressed = new ArrayList<>(); - for (CacheFlag flag : flags) { - GatewayIntent requiredIntent = flag.getRequiredIntent(); + for (DiscordCacheFlag flag : flags) { + DiscordGatewayIntent requiredIntent = flag.requiredIntent(); if (requiredIntent != null && !gatewayIntents.contains(requiredIntent)) { suppressed.add(ExceptionUtil.minifyException(new IllegalArgumentException("CacheFlag " - + flag.getRequiredIntent().name() + " requires GatewayIntent " + requiredIntent.name()))); + + requiredIntent.name() + " requires GatewayIntent " + requiredIntent.name()))); } } @@ -92,5 +87,21 @@ public class DiscordConnectionDetailsImpl implements DiscordConnectionDetails { } this.cacheFlags.addAll(flags); + return isStatus(); + } + + public @NotNull Set getMemberCachePolicies() { + Set policies = new HashSet<>(memberCachePolicies); + policies.addAll(discordSRV.moduleManager().requiredMemberCachePolicies()); + return policies; + } + + @Override + public boolean requestMemberCachePolicy(@NotNull DiscordMemberCachePolicy memberCachePolicy, @NotNull DiscordMemberCachePolicy... memberCachePolicies) { + List policies = new ArrayList<>(Collections.singleton(memberCachePolicy)); + policies.addAll(Arrays.asList(memberCachePolicies)); + + this.memberCachePolicies.addAll(policies); + return isStatus(); } } diff --git a/common/src/main/java/com/discordsrv/common/function/CheckedRunnable.java b/common/src/main/java/com/discordsrv/common/function/CheckedRunnable.java index d39ba4d4..c347b217 100644 --- a/common/src/main/java/com/discordsrv/common/function/CheckedRunnable.java +++ b/common/src/main/java/com/discordsrv/common/function/CheckedRunnable.java @@ -19,7 +19,7 @@ package com.discordsrv.common.function; @FunctionalInterface -public interface CheckedRunnable { +public interface CheckedRunnable { - void run() throws Throwable; + T run() throws Throwable; } diff --git a/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncModule.java b/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncModule.java index 378ff6ba..839d9d55 100644 --- a/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncModule.java +++ b/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncModule.java @@ -69,7 +69,7 @@ public class GroupSyncModule extends AbstractModule { } @Override - public void reload() { + public void reloadNoResult() { synchronized (pairs) { pairs.values().forEach(future -> { if (future != null) { diff --git a/common/src/main/java/com/discordsrv/common/invite/DiscordInviteModule.java b/common/src/main/java/com/discordsrv/common/invite/DiscordInviteModule.java index 921a1028..cdae1b80 100644 --- a/common/src/main/java/com/discordsrv/common/invite/DiscordInviteModule.java +++ b/common/src/main/java/com/discordsrv/common/invite/DiscordInviteModule.java @@ -18,7 +18,9 @@ package com.discordsrv.common.invite; +import com.discordsrv.api.discord.connection.details.DiscordGatewayIntent; import com.discordsrv.api.discord.connection.jda.errorresponse.ErrorCallbackContext; +import com.discordsrv.api.event.bus.Subscribe; import com.discordsrv.api.placeholder.FormattedText; import com.discordsrv.api.placeholder.annotation.Placeholder; import com.discordsrv.common.DiscordSRV; @@ -29,7 +31,13 @@ import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.channel.attribute.IInviteContainer; +import net.dv8tion.jda.api.events.guild.invite.GuildInviteDeleteEvent; +import net.dv8tion.jda.api.events.guild.update.GuildUpdateVanityCodeEvent; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import java.util.Collection; +import java.util.Collections; import java.util.List; public class DiscordInviteModule extends AbstractModule { @@ -43,7 +51,29 @@ public class DiscordInviteModule extends AbstractModule { } @Override - public void reload() { + public @NotNull Collection requiredIntents() { + DiscordInviteConfig config = discordSRV.config().invite; + if (StringUtils.isNotEmpty(config.inviteUrl)) { + return Collections.emptyList(); + } + + return Collections.singleton(DiscordGatewayIntent.GUILD_INVITES); + } + + @Subscribe + public void onGuildInviteDelete(GuildInviteDeleteEvent event) { + if (invite.equals(event.getUrl())) { + reload(); + } + } + + @Subscribe + public void onGuildUpdateVanityCode(GuildUpdateVanityCodeEvent event) { + reload(); + } + + @Override + public void reloadNoResult() { JDA jda = discordSRV.jda(); if (jda == null) { return; @@ -53,7 +83,7 @@ public class DiscordInviteModule extends AbstractModule { // Manual String invite = config.inviteUrl; - if (invite != null && !invite.isEmpty()) { + if (StringUtils.isNotEmpty(invite)) { this.invite = invite; return; } diff --git a/common/src/main/java/com/discordsrv/common/linking/LinkStore.java b/common/src/main/java/com/discordsrv/common/linking/LinkStore.java index 3785e5cd..29a2952e 100644 --- a/common/src/main/java/com/discordsrv/common/linking/LinkStore.java +++ b/common/src/main/java/com/discordsrv/common/linking/LinkStore.java @@ -23,7 +23,7 @@ import org.jetbrains.annotations.NotNull; import java.util.UUID; import java.util.concurrent.CompletableFuture; -public interface LinkStore { +public interface LinkStore extends LinkProvider { CompletableFuture createLink(@NotNull UUID playerUUID, long userId); diff --git a/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordChatMessageModule.java b/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordChatMessageModule.java index ad16f330..df3d348e 100644 --- a/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordChatMessageModule.java +++ b/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordChatMessageModule.java @@ -21,6 +21,7 @@ package com.discordsrv.common.messageforwarding.discord; import com.discordsrv.api.channel.GameChannel; import com.discordsrv.api.component.GameTextBuilder; import com.discordsrv.api.component.MinecraftComponent; +import com.discordsrv.api.discord.connection.details.DiscordGatewayIntent; import com.discordsrv.api.discord.entity.DiscordUser; import com.discordsrv.api.discord.entity.channel.DiscordMessageChannel; import com.discordsrv.api.discord.entity.guild.DiscordGuildMember; @@ -39,7 +40,10 @@ import com.discordsrv.common.function.OrDefault; import com.discordsrv.common.logging.NamedLogger; import com.discordsrv.common.module.type.AbstractModule; import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.NotNull; +import java.util.Arrays; +import java.util.Collection; import java.util.Map; public class DiscordChatMessageModule extends AbstractModule { @@ -48,6 +52,21 @@ public class DiscordChatMessageModule extends AbstractModule { super(discordSRV, new NamedLogger(discordSRV, "DISCORD_TO_MINECRAFT")); } + @Override + public boolean isEnabled() { + for (OrDefault config : discordSRV.channelConfig().getAllChannels()) { + if (config.map(cfg -> cfg.discordToMinecraft).get(cfg -> cfg.enabled, false)) { + return true; + } + } + return false; + } + + @Override + public @NotNull Collection requiredIntents() { + return Arrays.asList(DiscordGatewayIntent.GUILD_MESSAGES, DiscordGatewayIntent.MESSAGE_CONTENT); + } + @Subscribe public void onDiscordMessageReceived(DiscordMessageReceiveEvent event) { if (!discordSRV.isReady() || event.getMessage().isFromSelf() diff --git a/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordMessageMirroringModule.java b/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordMessageMirroringModule.java index 9ddd80a0..28250eeb 100644 --- a/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordMessageMirroringModule.java +++ b/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordMessageMirroringModule.java @@ -19,6 +19,7 @@ package com.discordsrv.common.messageforwarding.discord; import com.discordsrv.api.channel.GameChannel; +import com.discordsrv.api.discord.connection.details.DiscordGatewayIntent; import com.discordsrv.api.discord.entity.DiscordUser; import com.discordsrv.api.discord.entity.channel.DiscordGuildMessageChannel; import com.discordsrv.api.discord.entity.channel.DiscordMessageChannel; @@ -47,6 +48,7 @@ import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; import org.apache.commons.lang3.tuple.Pair; +import org.jetbrains.annotations.NotNull; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -67,6 +69,21 @@ public class DiscordMessageMirroringModule extends AbstractModule { .build(); } + @Override + public boolean isEnabled() { + for (OrDefault config : discordSRV.channelConfig().getAllChannels()) { + if (config.map(cfg -> cfg.mirroring).get(cfg -> cfg.enabled, false)) { + return true; + } + } + return false; + } + + @Override + public @NotNull Collection requiredIntents() { + return Arrays.asList(DiscordGatewayIntent.GUILD_MESSAGES, DiscordGatewayIntent.MESSAGE_CONTENT); + } + @Subscribe public void onDiscordChatMessageProcessing(DiscordChatMessageProcessingEvent event) { if (checkCancellation(event)) { diff --git a/common/src/main/java/com/discordsrv/common/messageforwarding/game/AbstractGameMessageModule.java b/common/src/main/java/com/discordsrv/common/messageforwarding/game/AbstractGameMessageModule.java index ccbde0b6..ac915fb6 100644 --- a/common/src/main/java/com/discordsrv/common/messageforwarding/game/AbstractGameMessageModule.java +++ b/common/src/main/java/com/discordsrv/common/messageforwarding/game/AbstractGameMessageModule.java @@ -55,6 +55,16 @@ public abstract class AbstractGameMessageModule channelConfig : discordSRV.channelConfig().getAllChannels()) { + if (mapConfig(channelConfig).get(IMessageConfig::enabled, false)) { + return true; + } + } + return false; + } + public OrDefault mapConfig(E event, OrDefault channelConfig) { return mapConfig(channelConfig); } diff --git a/common/src/main/java/com/discordsrv/common/messageforwarding/game/StartMessageModule.java b/common/src/main/java/com/discordsrv/common/messageforwarding/game/StartMessageModule.java index d5b1f40c..6f112497 100644 --- a/common/src/main/java/com/discordsrv/common/messageforwarding/game/StartMessageModule.java +++ b/common/src/main/java/com/discordsrv/common/messageforwarding/game/StartMessageModule.java @@ -18,12 +18,21 @@ package com.discordsrv.common.messageforwarding.game; +import com.discordsrv.api.discord.entity.channel.DiscordMessageChannel; +import com.discordsrv.api.discord.entity.message.ReceivedDiscordMessage; import com.discordsrv.api.discord.entity.message.ReceivedDiscordMessageCluster; +import com.discordsrv.api.discord.entity.message.SendableDiscordMessage; import com.discordsrv.api.event.events.message.receive.game.AbstractGameMessageReceiveEvent; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.config.main.channels.StartMessageConfig; import com.discordsrv.common.config.main.channels.base.BaseChannelConfig; import com.discordsrv.common.function.OrDefault; +import com.discordsrv.common.player.IPlayer; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; public class StartMessageModule extends AbstractGameMessageModule { @@ -31,6 +40,11 @@ public class StartMessageModule extends AbstractGameMessageModule mapConfig(OrDefault channelConfig) { return channelConfig.map(cfg -> cfg.startMessage); @@ -39,6 +53,21 @@ public class StartMessageModule extends AbstractGameMessageModule, DiscordMessageChannel> sendMessageToChannels( + OrDefault config, + SendableDiscordMessage.Builder format, + List channels, + String message, + IPlayer player, + Object... context + ) { + if (!config.get(cfg -> cfg.enabled, false)) { + return Collections.emptyMap(); + } + return super.sendMessageToChannels(config, format, channels, message, player, context); + } + @Override public void enable() { process(null, null, null); diff --git a/common/src/main/java/com/discordsrv/common/messageforwarding/game/StopMessageModule.java b/common/src/main/java/com/discordsrv/common/messageforwarding/game/StopMessageModule.java index 6ffe7d5d..e89c1e40 100644 --- a/common/src/main/java/com/discordsrv/common/messageforwarding/game/StopMessageModule.java +++ b/common/src/main/java/com/discordsrv/common/messageforwarding/game/StopMessageModule.java @@ -18,13 +18,21 @@ package com.discordsrv.common.messageforwarding.game; +import com.discordsrv.api.discord.entity.channel.DiscordMessageChannel; +import com.discordsrv.api.discord.entity.message.ReceivedDiscordMessage; import com.discordsrv.api.discord.entity.message.ReceivedDiscordMessageCluster; +import com.discordsrv.api.discord.entity.message.SendableDiscordMessage; import com.discordsrv.api.event.events.message.receive.game.AbstractGameMessageReceiveEvent; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.config.main.channels.StopMessageConfig; import com.discordsrv.common.config.main.channels.base.BaseChannelConfig; import com.discordsrv.common.function.OrDefault; +import com.discordsrv.common.player.IPlayer; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -35,6 +43,11 @@ public class StopMessageModule extends AbstractGameMessageModule mapConfig(OrDefault channelConfig) { return channelConfig.map(cfg -> cfg.stopMessage); @@ -43,6 +56,21 @@ public class StopMessageModule extends AbstractGameMessageModule, DiscordMessageChannel> sendMessageToChannels( + OrDefault config, + SendableDiscordMessage.Builder format, + List channels, + String message, + IPlayer player, + Object... context + ) { + if (!config.get(cfg -> cfg.enabled, false)) { + return Collections.emptyMap(); + } + return super.sendMessageToChannels(config, format, channels, message, player, context); + } + @Override public void disable() { try { diff --git a/common/src/main/java/com/discordsrv/common/module/ModuleManager.java b/common/src/main/java/com/discordsrv/common/module/ModuleManager.java index 1087df03..c7aed952 100644 --- a/common/src/main/java/com/discordsrv/common/module/ModuleManager.java +++ b/common/src/main/java/com/discordsrv/common/module/ModuleManager.java @@ -18,6 +18,10 @@ package com.discordsrv.common.module; +import com.discordsrv.api.DiscordSRVApi; +import com.discordsrv.api.discord.connection.details.DiscordCacheFlag; +import com.discordsrv.api.discord.connection.details.DiscordGatewayIntent; +import com.discordsrv.api.discord.connection.details.DiscordMemberCachePolicy; import com.discordsrv.api.event.bus.EventPriority; import com.discordsrv.api.event.bus.Subscribe; import com.discordsrv.api.event.events.lifecycle.DiscordSRVShuttingDownEvent; @@ -25,18 +29,18 @@ import com.discordsrv.api.module.type.Module; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.debug.DebugGenerateEvent; import com.discordsrv.common.debug.file.TextDebugFile; +import com.discordsrv.common.discord.connection.jda.JDAConnectionManager; import com.discordsrv.common.function.CheckedFunction; import com.discordsrv.common.logging.Logger; import com.discordsrv.common.logging.NamedLogger; import com.discordsrv.common.module.type.AbstractModule; import com.discordsrv.common.module.type.ModuleDelegate; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArraySet; +import java.util.function.BiConsumer; +import java.util.function.Function; public class ModuleManager { @@ -56,6 +60,37 @@ public class ModuleManager { return logger; } + private Set getModuleDetails(Function> detailFunction, BiConsumer, Collection> setRequested) { + Set details = new HashSet<>(); + for (Module module : modules) { + try { + if (!module.isEnabled()) { + continue; + } + + Collection values = detailFunction.apply(module); + details.addAll(values); + + setRequested.accept(getAbstract(module), values); + } catch (Throwable t) { + logger.debug("Failed to get details from " + module.getClass(), t); + } + } + return details; + } + + public Set requiredIntents() { + return getModuleDetails(Module::requiredIntents, AbstractModule::setRequestedIntents); + } + + public Set requiredCacheFlags() { + return getModuleDetails(Module::requiredCacheFlags, AbstractModule::setRequestedCacheFlags); + } + + public Set requiredMemberCachePolicies() { + return getModuleDetails(Module::requiredMemberCachingPolicies, (mod, result) -> mod.setRequestedMemberCachePolicies(result.size())); + } + @SuppressWarnings("unchecked") public T getModule(Class moduleType) { return (T) moduleLookupTable.computeIfAbsent(moduleType.getName(), key -> { @@ -94,18 +129,32 @@ public class ModuleManager { throw new IllegalArgumentException("Cannot register a delegate"); } - AbstractModule abstractModule = getAbstract(module); - - logger.debug(module + " registered"); this.modules.add(module); this.moduleLookupTable.put(module.getClass().getName(), module); - if (discordSRV.config() != null) { - // Check if config is ready, if it is already we'll enable the module - enable(abstractModule); + logger.debug(module + " registered"); + + if (discordSRV.isReady()) { + // Check if Discord connection is ready, if it is already we'll enable the module + enable(getAbstract(module)); } } + public void unregister(Module module) { + if (module instanceof ModuleDelegate) { + throw new IllegalArgumentException("Cannot unregister a delegate"); + } + + // Disable if needed + disable(getAbstract(module)); + + this.modules.remove(module); + this.moduleLookupTable.values().removeIf(mod -> mod == module); + this.delegates.remove(module); + + logger.debug(module + " unregistered"); + } + private void enable(AbstractModule module) { try { if (module.enableModule()) { @@ -116,28 +165,13 @@ public class ModuleManager { } } - public void unregister(Module module) { - if (module instanceof ModuleDelegate) { - throw new IllegalArgumentException("Cannot unregister a delegate"); - } - - if (getAbstract(module).hasBeenEnabled()) { - disable(module); - } - - this.modules.remove(module); - this.moduleLookupTable.values().removeIf(mod -> mod == module); - this.delegates.remove(module); - } - - private void disable(Module module) { - AbstractModule abstractModule = getAbstract(module); + private void disable(AbstractModule module) { try { - logger.debug(module + " disabling"); - discordSRV.eventBus().unsubscribe(abstractModule); - abstractModule.disable(); + if (module.disableModule()) { + logger.debug(module + " disabled"); + } } catch (Throwable t) { - discordSRV.logger().error("Failed to disable " + abstractModule.toString(), t); + discordSRV.logger().error("Failed to disable " + module.getClass().getSimpleName(), t); } } @@ -145,22 +179,64 @@ public class ModuleManager { public void onShuttingDown(DiscordSRVShuttingDownEvent event) { modules.stream() .sorted((m1, m2) -> Integer.compare(m2.shutdownOrder(), m1.shutdownOrder())) - .forEachOrdered(this::disable); + .forEachOrdered(module -> disable(getAbstract(module))); } - public void reload() { + public List reload() { + JDAConnectionManager connectionManager = discordSRV.discordConnectionManager(); + + Set reloadResults = new HashSet<>(); for (Module module : modules) { AbstractModule abstractModule = getAbstract(module); - // Check if the module needs to be enabled due to reload - enable(abstractModule); + boolean fail = false; + if (abstractModule.isEnabled()) { + for (DiscordGatewayIntent requiredIntent : abstractModule.getRequestedIntents()) { + if (!connectionManager.getIntents().contains(requiredIntent)) { + fail = true; + logger().warning("Missing gateway intent " + requiredIntent.name() + " for module " + module.getClass().getSimpleName()); + } + } + for (DiscordCacheFlag requiredCacheFlag : abstractModule.getRequestedCacheFlags()) { + if (!connectionManager.getCacheFlags().contains(requiredCacheFlag)) { + fail = true; + logger().warning("Missing cache flag " + requiredCacheFlag.name() + " for module " + module.getClass().getSimpleName()); + } + } + } + + if (fail) { + reloadResults.add(DiscordSRV.ReloadResults.DISCORD_CONNECTION_RELOAD_REQUIRED); + } + + // Check if the module needs to be enabled or disabled + if (!fail) { + enable(abstractModule); + } + if (!abstractModule.isEnabled()) { + disable(abstractModule); + } try { - abstractModule.reload(); + Set results = abstractModule.reload(); + if (results != null) { + reloadResults.addAll(results); + } } catch (Throwable t) { discordSRV.logger().error("Failed to reload " + module.getClass().getSimpleName(), t); } } + + List results = new ArrayList<>(); + + List validResults = Arrays.asList(DiscordSRVApi.ReloadResult.Results.values()); + for (DiscordSRVApi.ReloadResult reloadResult : reloadResults) { + if (validResults.contains(reloadResult)) { + results.add(reloadResult); + } + } + + return results; } @Subscribe @@ -170,24 +246,44 @@ public class ModuleManager { builder.append("Enabled modules:"); List disabled = new ArrayList<>(); for (Module module : modules) { - AbstractModule abstractModule = getAbstract(module); - - if (!abstractModule.isEnabled()) { - disabled.add(abstractModule); + if (!getAbstract(module).isEnabled()) { + disabled.add(module); continue; } - appendModule(builder, abstractModule); + + appendModule(builder, module, true); } builder.append("\n\nDisabled modules:"); for (Module module : disabled) { - appendModule(builder, module); + appendModule(builder, module, false); } event.addFile(new TextDebugFile("modules.txt", builder)); } - private void appendModule(StringBuilder builder, Module module) { + private void appendModule(StringBuilder builder, Module module, boolean extra) { builder.append('\n').append(module.getClass().getName()); + + if (!extra) { + return; + } + + AbstractModule mod = getAbstract(module); + + List intents = mod.getRequestedIntents(); + if (!intents.isEmpty()) { + builder.append("\n Intents: ").append(intents); + } + + List cacheFlags = mod.getRequestedCacheFlags(); + if (!cacheFlags.isEmpty()) { + builder.append("\n Cache Flags: ").append(cacheFlags); + } + + int memberCachePolicies = mod.getRequestedMemberCachePolicies(); + if (memberCachePolicies != 0) { + builder.append("\n Member Cache Policies: ").append(memberCachePolicies); + } } } diff --git a/common/src/main/java/com/discordsrv/common/module/type/AbstractModule.java b/common/src/main/java/com/discordsrv/common/module/type/AbstractModule.java index ac853ccd..08d544d0 100644 --- a/common/src/main/java/com/discordsrv/common/module/type/AbstractModule.java +++ b/common/src/main/java/com/discordsrv/common/module/type/AbstractModule.java @@ -18,6 +18,8 @@ package com.discordsrv.common.module.type; +import com.discordsrv.api.discord.connection.details.DiscordCacheFlag; +import com.discordsrv.api.discord.connection.details.DiscordGatewayIntent; import com.discordsrv.api.event.events.Cancellable; import com.discordsrv.api.event.events.Processable; import com.discordsrv.api.module.type.Module; @@ -25,12 +27,20 @@ import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.event.util.EventUtil; import com.discordsrv.common.logging.Logger; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + public abstract class AbstractModule
implements Module { protected final DT discordSRV; private final Logger logger; private boolean hasBeenEnabled = false; + private final List requestedIntents = new ArrayList<>(); + private final List requestedCacheFlags = new ArrayList<>(); + private int requestedMemberCachePolicies = 0; + public AbstractModule(DT discordSRV) { this(discordSRV, discordSRV.logger()); } @@ -40,14 +50,27 @@ public abstract class AbstractModule
implements Module { this.logger = logger; } + @Override + public String toString() { + return getClass().getName(); + } + + // Utility + public final Logger logger() { return logger; } - public final boolean hasBeenEnabled() { - return hasBeenEnabled; + protected final boolean checkProcessor(Processable event) { + return EventUtil.checkProcessor(discordSRV, event, logger()); } + protected final boolean checkCancellation(Cancellable event) { + return EventUtil.checkCancellation(discordSRV, event, logger()); + } + + // Internal + public final boolean enableModule() { if (hasBeenEnabled || !isEnabled()) { return false; @@ -63,17 +86,44 @@ public abstract class AbstractModule
implements Module { return true; } - @Override - public String toString() { - return getClass().getName(); + public final boolean disableModule() { + if (!hasBeenEnabled) { + return false; + } + + disable(); + hasBeenEnabled = false; + + try { + discordSRV.eventBus().unsubscribe(this); + // Ignore not having listener methods exception + } catch (IllegalArgumentException ignored) {} + return true; } - // Utility - protected final boolean checkProcessor(Processable event) { - return EventUtil.checkProcessor(discordSRV, event, logger()); + public final void setRequestedIntents(Collection intents) { + this.requestedIntents.clear(); + this.requestedIntents.addAll(intents); } - protected final boolean checkCancellation(Cancellable event) { - return EventUtil.checkCancellation(discordSRV, event, logger()); + public final List getRequestedIntents() { + return requestedIntents; + } + + public final void setRequestedCacheFlags(Collection cacheFlags) { + this.requestedCacheFlags.clear(); + this.requestedCacheFlags.addAll(cacheFlags); + } + + public final List getRequestedCacheFlags() { + return requestedCacheFlags; + } + + public final void setRequestedMemberCachePolicies(int amount) { + this.requestedMemberCachePolicies = amount; + } + + public final int getRequestedMemberCachePolicies() { + return requestedMemberCachePolicies; } } diff --git a/common/src/main/java/com/discordsrv/common/module/type/ModuleDelegate.java b/common/src/main/java/com/discordsrv/common/module/type/ModuleDelegate.java index e8b62fb6..c4d38848 100644 --- a/common/src/main/java/com/discordsrv/common/module/type/ModuleDelegate.java +++ b/common/src/main/java/com/discordsrv/common/module/type/ModuleDelegate.java @@ -18,9 +18,17 @@ package com.discordsrv.common.module.type; +import com.discordsrv.api.DiscordSRVApi; +import com.discordsrv.api.discord.connection.details.DiscordCacheFlag; +import com.discordsrv.api.discord.connection.details.DiscordGatewayIntent; +import com.discordsrv.api.discord.connection.details.DiscordMemberCachePolicy; import com.discordsrv.api.module.type.Module; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.logging.NamedLogger; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.Set; public class ModuleDelegate extends AbstractModule { @@ -36,14 +44,39 @@ public class ModuleDelegate extends AbstractModule { return module.isEnabled(); } + @Override + public @NotNull Collection requiredIntents() { + return module.requiredIntents(); + } + + @Override + public @NotNull Collection requiredCacheFlags() { + return module.requiredCacheFlags(); + } + + @Override + public @NotNull Collection requiredMemberCachingPolicies() { + return module.requiredMemberCachingPolicies(); + } + + @Override + public int priority(Class type) { + return module.priority(type); + } + + @Override + public int shutdownOrder() { + return module.shutdownOrder(); + } + @Override public void enable() { module.enable(); } @Override - public void reload() { - module.reload(); + public Set reload() { + return module.reload(); } @Override @@ -53,7 +86,6 @@ public class ModuleDelegate extends AbstractModule { @Override public String toString() { - String original = super.toString(); - return original.substring(0, original.length() - 1) + ",module=" + module.getClass().getName() + "(" + module + ")}"; + return super.toString() + "{module=" + module.getClass().getName() + "(" + module + ")}"; } } diff --git a/common/src/main/java/com/discordsrv/common/module/type/PluginIntegration.java b/common/src/main/java/com/discordsrv/common/module/type/PluginIntegration.java index 59d355ca..c458dd67 100644 --- a/common/src/main/java/com/discordsrv/common/module/type/PluginIntegration.java +++ b/common/src/main/java/com/discordsrv/common/module/type/PluginIntegration.java @@ -19,6 +19,7 @@ package com.discordsrv.common.module.type; import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.logging.Logger; import javax.annotation.OverridingMethodsMustInvokeSuper; @@ -28,6 +29,10 @@ public abstract class PluginIntegration
extends AbstractM super(discordSRV); } + public PluginIntegration(DT discordSRV, Logger logger) { + super(discordSRV, logger); + } + @Override @OverridingMethodsMustInvokeSuper public boolean isEnabled() { diff --git a/settings.gradle b/settings.gradle index 6c93ba8d..66d0d774 100644 --- a/settings.gradle +++ b/settings.gradle @@ -33,7 +33,7 @@ dependencyResolutionManagement { library('velocity', 'com.velocitypowered', 'velocity-api').version('3.0.0') // DependencyDownload - version('dependencydownload', '1.2.2-SNAPSHOT') + version('dependencydownload', '1.3.1-SNAPSHOT') plugin('dependencydownload-plugin', 'dev.vankka.dependencydownload.plugin').versionRef('dependencydownload') library('dependencydownload-runtime', 'dev.vankka', 'dependencydownload-runtime').versionRef('dependencydownload') library('dependencydownload-jarinjar-bootstrap', 'dev.vankka', 'dependencydownload-jarinjar-bootstrap').versionRef('dependencydownload')