Specifying intents, cache flags and member caching policies in modules

This commit is contained in:
Vankka 2023-01-22 22:20:36 +02:00
parent 1e8ce58b79
commit 7a9c21c56d
No known key found for this signature in database
GPG Key ID: 6E50CB7A29B96AD0
33 changed files with 1039 additions and 217 deletions

View File

@ -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<ReloadFlag> ALL = Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(values())));
public static final Set<ReloadFlag> DEFAULT_FLAGS = Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(CONFIG, MODULES)));
private final boolean requiresConfirm;
private final Predicate<DiscordSRVApi> requiresConfirm;
ReloadFlag(boolean requiresConfirm) {
this(__ -> requiresConfirm);
}
ReloadFlag(Predicate<DiscordSRVApi> 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
}
}
}

View File

@ -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<CacheFlag> {
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;
}
}

View File

@ -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<GatewayIntent> 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<CacheFlag> 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);
}

View File

@ -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<GatewayIntent> {
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;
}
}

View File

@ -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);
}

View File

@ -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<DiscordGatewayIntent> 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<DiscordCacheFlag> 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<DiscordMemberCachePolicy> 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<DiscordSRVApi.ReloadResult> reload() {
reloadNoResult();
return null;
}
/**
* An alternative to {@link #reload()}, which returns {@code void} instead of results. This method will <b>not</b> be called if {@link #reload()} is overridden!
*/
default void reloadNoResult() {}
}

View File

@ -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<B extends IBootstrap, C extends MainCon
private PlaceholderServiceImpl placeholderService;
private ComponentFactory componentFactory;
private DiscordAPIImpl discordAPI;
private DiscordConnectionDetails discordConnectionDetails;
private DiscordConnectionDetailsImpl discordConnectionDetails;
// DiscordSRV
protected final B bootstrap;
@ -189,9 +185,9 @@ public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainCon
|| host.endsWith("discord.gg");
String userAgent = isDiscord
? "DiscordBot (https://github.com/DiscordSRV/DiscordSRV, " + versionInfo() + ")"
? "DiscordBot (https://github.com/DiscordSRV/DiscordSRV, " + versionInfo().version() + ")"
+ " (" + JDAInfo.GITHUB + ", " + JDAInfo.VERSION + ")"
: "DiscordSRV/" + versionInfo();
: "DiscordSRV/" + versionInfo().version();
return chain.proceed(
original.newBuilder()
@ -283,7 +279,7 @@ public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainCon
}
@Override
public final @NotNull DiscordConnectionDetails discordConnectionDetails() {
public final @NotNull DiscordConnectionDetailsImpl discordConnectionDetails() {
return discordConnectionDetails;
}
@ -385,7 +381,11 @@ public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainCon
Constructor<?> 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<B extends IBootstrap, C extends MainCon
moduleManager.unregister(module);
}
@Override
public ModuleManager moduleManager() {
return moduleManager;
}
@Override
public Locale locale() {
// TODO: config
@ -446,7 +451,7 @@ public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainCon
// Lifecycle
protected CompletableFuture<Void> invokeLifecycle(CheckedRunnable runnable) {
protected CompletableFuture<Void> invokeLifecycle(CheckedRunnable<?> runnable) {
return invokeLifecycle(() -> {
try {
lifecycleLock.lock();
@ -454,21 +459,22 @@ public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainCon
} finally {
lifecycleLock.unlock();
}
return null;
}, "Failed to enable", true);
}
protected CompletableFuture<Void> invokeLifecycle(CheckedRunnable runnable, String message, boolean enable) {
return CompletableFuture.runAsync(() -> {
protected <T> CompletableFuture<T> invokeLifecycle(CheckedRunnable<T> 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<B extends IBootstrap, C extends MainCon
}
logger().error(message, t);
}
return null;
}, scheduler().executorService());
}
@ -485,6 +492,7 @@ public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainCon
this.enable();
waitForStatus(Status.CONNECTED);
eventBus().publish(new DiscordSRVReadyEvent());
return null;
});
}
@ -498,7 +506,7 @@ public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainCon
}
@Override
public final CompletableFuture<Void> invokeReload(Set<ReloadFlag> flags, boolean silent) {
public final CompletableFuture<List<ReloadResult>> invokeReload(Set<ReloadFlag> flags, boolean silent) {
return invokeLifecycle(() -> reload(flags, silent), "Failed to reload", false);
}
@ -573,7 +581,7 @@ public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainCon
}
@OverridingMethodsMustInvokeSuper
protected void reload(Set<ReloadFlag> flags, boolean initial) throws Throwable {
protected List<ReloadResult> reload(Set<ReloadFlag> flags, boolean initial) throws Throwable {
if (!initial) {
logger().info("Reloading DiscordSRV...");
}
@ -595,13 +603,13 @@ public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainCon
if (updateConfig.security.enabled) {
if (updateChecker.isSecurityFailed()) {
// Security has already failed
return;
return Collections.singletonList(ReloadResults.SECURITY_FAILED);
}
if (initial && !updateChecker.check(true)) {
// Security failed cancel startup & shutdown
invokeDisable();
return;
return Collections.singletonList(ReloadResults.SECURITY_FAILED);
}
} else if (initial) {
// Not using security, run update check off thread
@ -670,7 +678,7 @@ public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainCon
e.log(this);
logger().error("Failed to connect to storage");
setStatus(Status.FAILED_TO_START);
return;
return Collections.singletonList(ReloadResults.STORAGE_CONNECTION_FAILED);
}
}
@ -683,6 +691,9 @@ public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainCon
}
if (!initial) {
waitForStatus(Status.CONNECTED, 20, TimeUnit.SECONDS);
if (status() != Status.CONNECTED) {
return Collections.singletonList(ReloadResults.DISCORD_CONNECTION_FAILED);
}
} else {
JDA jda = jda();
if (jda != null) {
@ -690,7 +701,7 @@ public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainCon
jda.awaitReady();
} catch (IllegalStateException ignored) {
// JDA shutdown -> don't continue
return;
return Collections.singletonList(ReloadResults.DISCORD_CONNECTION_FAILED);
}
}
}
@ -699,13 +710,17 @@ public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainCon
}
}
List<ReloadResult> 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;
}
}

View File

@ -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 extends Module> T getModule(Class<T> moduleType);
void registerModule(AbstractModule<?> module);
void unregisterModule(AbstractModule<?> module);
ModuleManager moduleManager();
Locale locale();
@ -139,6 +145,17 @@ public interface DiscordSRV extends DiscordSRVApi {
// Lifecycle
CompletableFuture<Void> invokeEnable();
CompletableFuture<Void> invokeDisable();
CompletableFuture<Void> invokeReload(Set<ReloadFlag> flags, boolean silent);
CompletableFuture<List<ReloadResult>> invokeReload(Set<ReloadFlag> flags, boolean silent);
enum ReloadResults implements ReloadResult {
// Internal reasons
SUCCESS,
SECURITY_FAILED,
STORAGE_CONNECTION_FAILED,
DISCORD_CONNECTION_RELOAD_REQUIRED,
DISCORD_CONNECTION_FAILED
}
}

View File

@ -51,9 +51,10 @@ public abstract class ServerDiscordSRV<B extends IBootstrap, C extends MainConfi
public final CompletableFuture<Void> invokeServerStarted() {
return invokeLifecycle(() -> {
if (status().isShutdown()) {
return;
return null;
}
this.serverStarted();
return null;
});
}

View File

@ -45,7 +45,7 @@ public class ChannelUpdaterModule extends AbstractModule<DiscordSRV> {
}
@Override
public void reload() {
public void reloadNoResult() {
futures.forEach(future -> future.cancel(false));
futures.clear();

View File

@ -47,7 +47,7 @@ public class GameCommandModule extends AbstractModule<DiscordSRV> {
}
@Override
public void reload() {
public void reloadNoResult() {
CommandConfig config = discordSRV.config().command;
if (config == null) {
return;

View File

@ -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

View File

@ -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<DiscordSRV.ReloadFlag> 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)

View File

@ -45,6 +45,8 @@ public class MainConfig implements Config {
public LinkedAccountConfig linkedAccounts = new LinkedAccountConfig();
public MemberCachingConfig memberCaching = new MemberCachingConfig();
public List<ChannelUpdaterConfig> channelUpdaters = new ArrayList<>(Collections.singletonList(new ChannelUpdaterConfig()));
@Comment("Configuration options for group-role synchronization")

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Long> ids = new ArrayList<>();
}
}

View File

@ -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

View File

@ -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}

View File

@ -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<GatewayIntent, String> 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<Void> connectionFuture;
private JDA instance;
private boolean detailsAccepted = true;
// Currently used intents & cache flags
private final Set<DiscordGatewayIntent> intents = new HashSet<>();
private final Set<DiscordCacheFlag> cacheFlags = new HashSet<>();
private final Set<DiscordMemberCachePolicy> 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<DiscordGatewayIntent> getIntents() {
return intents;
}
public Set<DiscordCacheFlag> getCacheFlags() {
return cacheFlags;
}
public Set<DiscordMemberCachePolicy> 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<Long> 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<GatewayIntent> intents = connectionDetails.getGatewayIntents();
boolean membersIntent = intents.contains(GatewayIntent.GUILD_MEMBERS);
MemberCachingConfig memberCachingConfig = discordSRV.config().memberCaching;
DiscordConnectionDetailsImpl connectionDetails = discordSRV.discordConnectionDetails();
Set<GatewayIntent> intents = new LinkedHashSet<>();
this.intents.clear();
this.intents.addAll(connectionDetails.getGatewayIntents());
this.intents.forEach(intent -> intents.add(intent.asJDA()));
Set<CacheFlag> 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<GatewayIntent> intents = discordSRV.discordConnectionDetails().getGatewayIntents();
Set<DiscordGatewayIntent> 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<Throwable> {

View File

@ -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<GatewayIntent> gatewayIntents = new HashSet<>();
private final Set<CacheFlag> cacheFlags = new HashSet<>();
private final Set<DiscordGatewayIntent> gatewayIntents = new HashSet<>();
private final Set<DiscordCacheFlag> cacheFlags = new HashSet<>();
private final Set<DiscordMemberCachePolicy> 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<DiscordGatewayIntent> getGatewayIntents() {
Set<DiscordGatewayIntent> 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<GatewayIntent> getGatewayIntents() {
return gatewayIntents;
}
@Override
public void requestGatewayIntent(@NotNull GatewayIntent gatewayIntent, GatewayIntent... gatewayIntents) {
check();
List<GatewayIntent> intents = new ArrayList<>(Collections.singleton(gatewayIntent));
public boolean requestGatewayIntent(@NotNull DiscordGatewayIntent gatewayIntent, DiscordGatewayIntent... gatewayIntents) {
List<DiscordGatewayIntent> intents = new ArrayList<>(Collections.singleton(gatewayIntent));
intents.addAll(Arrays.asList(gatewayIntents));
this.gatewayIntents.addAll(intents);
return isStatus();
}
public @NotNull Set<DiscordCacheFlag> getCacheFlags() {
Set<DiscordCacheFlag> flags = new HashSet<>(cacheFlags);
flags.addAll(discordSRV.moduleManager().requiredCacheFlags());
return flags;
}
@Override
public @NotNull Set<CacheFlag> getCacheFlags() {
return cacheFlags;
}
@Override
public void requestCacheFlag(@NotNull CacheFlag cacheFlag, CacheFlag... cacheFlags) {
check();
List<CacheFlag> flags = new ArrayList<>(Collections.singleton(cacheFlag));
public boolean requestCacheFlag(@NotNull DiscordCacheFlag cacheFlag, DiscordCacheFlag... cacheFlags) {
List<DiscordCacheFlag> flags = new ArrayList<>(Collections.singleton(cacheFlag));
flags.addAll(Arrays.asList(cacheFlags));
List<Throwable> 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<DiscordMemberCachePolicy> getMemberCachePolicies() {
Set<DiscordMemberCachePolicy> policies = new HashSet<>(memberCachePolicies);
policies.addAll(discordSRV.moduleManager().requiredMemberCachePolicies());
return policies;
}
@Override
public boolean requestMemberCachePolicy(@NotNull DiscordMemberCachePolicy memberCachePolicy, @NotNull DiscordMemberCachePolicy... memberCachePolicies) {
List<DiscordMemberCachePolicy> policies = new ArrayList<>(Collections.singleton(memberCachePolicy));
policies.addAll(Arrays.asList(memberCachePolicies));
this.memberCachePolicies.addAll(policies);
return isStatus();
}
}

View File

@ -19,7 +19,7 @@
package com.discordsrv.common.function;
@FunctionalInterface
public interface CheckedRunnable {
public interface CheckedRunnable<T> {
void run() throws Throwable;
T run() throws Throwable;
}

View File

@ -69,7 +69,7 @@ public class GroupSyncModule extends AbstractModule<DiscordSRV> {
}
@Override
public void reload() {
public void reloadNoResult() {
synchronized (pairs) {
pairs.values().forEach(future -> {
if (future != null) {

View File

@ -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<DiscordSRV> {
@ -43,7 +51,29 @@ public class DiscordInviteModule extends AbstractModule<DiscordSRV> {
}
@Override
public void reload() {
public @NotNull Collection<DiscordGatewayIntent> 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<DiscordSRV> {
// Manual
String invite = config.inviteUrl;
if (invite != null && !invite.isEmpty()) {
if (StringUtils.isNotEmpty(invite)) {
this.invite = invite;
return;
}

View File

@ -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<Void> createLink(@NotNull UUID playerUUID, long userId);

View File

@ -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<DiscordSRV> {
@ -48,6 +52,21 @@ public class DiscordChatMessageModule extends AbstractModule<DiscordSRV> {
super(discordSRV, new NamedLogger(discordSRV, "DISCORD_TO_MINECRAFT"));
}
@Override
public boolean isEnabled() {
for (OrDefault<BaseChannelConfig> config : discordSRV.channelConfig().getAllChannels()) {
if (config.map(cfg -> cfg.discordToMinecraft).get(cfg -> cfg.enabled, false)) {
return true;
}
}
return false;
}
@Override
public @NotNull Collection<DiscordGatewayIntent> requiredIntents() {
return Arrays.asList(DiscordGatewayIntent.GUILD_MESSAGES, DiscordGatewayIntent.MESSAGE_CONTENT);
}
@Subscribe
public void onDiscordMessageReceived(DiscordMessageReceiveEvent event) {
if (!discordSRV.isReady() || event.getMessage().isFromSelf()

View File

@ -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<DiscordSRV> {
.build();
}
@Override
public boolean isEnabled() {
for (OrDefault<BaseChannelConfig> config : discordSRV.channelConfig().getAllChannels()) {
if (config.map(cfg -> cfg.mirroring).get(cfg -> cfg.enabled, false)) {
return true;
}
}
return false;
}
@Override
public @NotNull Collection<DiscordGatewayIntent> requiredIntents() {
return Arrays.asList(DiscordGatewayIntent.GUILD_MESSAGES, DiscordGatewayIntent.MESSAGE_CONTENT);
}
@Subscribe
public void onDiscordChatMessageProcessing(DiscordChatMessageProcessingEvent event) {
if (checkCancellation(event)) {

View File

@ -55,6 +55,16 @@ public abstract class AbstractGameMessageModule<T extends IMessageConfig, E exte
super(discordSRV, new NamedLogger(discordSRV, loggerName));
}
@Override
public boolean isEnabled() {
for (OrDefault<BaseChannelConfig> channelConfig : discordSRV.channelConfig().getAllChannels()) {
if (mapConfig(channelConfig).get(IMessageConfig::enabled, false)) {
return true;
}
}
return false;
}
public OrDefault<T> mapConfig(E event, OrDefault<BaseChannelConfig> channelConfig) {
return mapConfig(channelConfig);
}

View File

@ -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<StartMessageConfig, AbstractGameMessageReceiveEvent> {
@ -31,6 +40,11 @@ public class StartMessageModule extends AbstractGameMessageModule<StartMessageCo
super(discordSRV, "START_MESSAGE");
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public OrDefault<StartMessageConfig> mapConfig(OrDefault<BaseChannelConfig> channelConfig) {
return channelConfig.map(cfg -> cfg.startMessage);
@ -39,6 +53,21 @@ public class StartMessageModule extends AbstractGameMessageModule<StartMessageCo
@Override
public void postClusterToEventBus(ReceivedDiscordMessageCluster cluster) {}
@Override
public Map<CompletableFuture<ReceivedDiscordMessage>, DiscordMessageChannel> sendMessageToChannels(
OrDefault<StartMessageConfig> config,
SendableDiscordMessage.Builder format,
List<DiscordMessageChannel> 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);

View File

@ -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<StopMessageConf
super(discordSRV, "START_MESSAGE");
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public OrDefault<StopMessageConfig> mapConfig(OrDefault<BaseChannelConfig> channelConfig) {
return channelConfig.map(cfg -> cfg.stopMessage);
@ -43,6 +56,21 @@ public class StopMessageModule extends AbstractGameMessageModule<StopMessageConf
@Override
public void postClusterToEventBus(ReceivedDiscordMessageCluster cluster) {}
@Override
public Map<CompletableFuture<ReceivedDiscordMessage>, DiscordMessageChannel> sendMessageToChannels(
OrDefault<StopMessageConfig> config,
SendableDiscordMessage.Builder format,
List<DiscordMessageChannel> 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 {

View File

@ -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 <T> Set<T> getModuleDetails(Function<Module, Collection<T>> detailFunction, BiConsumer<AbstractModule<?>, Collection<T>> setRequested) {
Set<T> details = new HashSet<>();
for (Module module : modules) {
try {
if (!module.isEnabled()) {
continue;
}
Collection<T> 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<DiscordGatewayIntent> requiredIntents() {
return getModuleDetails(Module::requiredIntents, AbstractModule::setRequestedIntents);
}
public Set<DiscordCacheFlag> requiredCacheFlags() {
return getModuleDetails(Module::requiredCacheFlags, AbstractModule::setRequestedCacheFlags);
}
public Set<DiscordMemberCachePolicy> requiredMemberCachePolicies() {
return getModuleDetails(Module::requiredMemberCachingPolicies, (mod, result) -> mod.setRequestedMemberCachePolicies(result.size()));
}
@SuppressWarnings("unchecked")
public <T extends Module> T getModule(Class<T> 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<DiscordSRV.ReloadResult> reload() {
JDAConnectionManager connectionManager = discordSRV.discordConnectionManager();
Set<DiscordSRVApi.ReloadResult> 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<DiscordSRVApi.ReloadResult> results = abstractModule.reload();
if (results != null) {
reloadResults.addAll(results);
}
} catch (Throwable t) {
discordSRV.logger().error("Failed to reload " + module.getClass().getSimpleName(), t);
}
}
List<DiscordSRVApi.ReloadResult> results = new ArrayList<>();
List<DiscordSRV.ReloadResult> 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<Module> 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<DiscordGatewayIntent> intents = mod.getRequestedIntents();
if (!intents.isEmpty()) {
builder.append("\n Intents: ").append(intents);
}
List<DiscordCacheFlag> 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);
}
}
}

View File

@ -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<DT extends DiscordSRV> implements Module {
protected final DT discordSRV;
private final Logger logger;
private boolean hasBeenEnabled = false;
private final List<DiscordGatewayIntent> requestedIntents = new ArrayList<>();
private final List<DiscordCacheFlag> requestedCacheFlags = new ArrayList<>();
private int requestedMemberCachePolicies = 0;
public AbstractModule(DT discordSRV) {
this(discordSRV, discordSRV.logger());
}
@ -40,14 +50,27 @@ public abstract class AbstractModule<DT extends DiscordSRV> 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<DT extends DiscordSRV> 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<DiscordGatewayIntent> intents) {
this.requestedIntents.clear();
this.requestedIntents.addAll(intents);
}
protected final boolean checkCancellation(Cancellable event) {
return EventUtil.checkCancellation(discordSRV, event, logger());
public final List<DiscordGatewayIntent> getRequestedIntents() {
return requestedIntents;
}
public final void setRequestedCacheFlags(Collection<DiscordCacheFlag> cacheFlags) {
this.requestedCacheFlags.clear();
this.requestedCacheFlags.addAll(cacheFlags);
}
public final List<DiscordCacheFlag> getRequestedCacheFlags() {
return requestedCacheFlags;
}
public final void setRequestedMemberCachePolicies(int amount) {
this.requestedMemberCachePolicies = amount;
}
public final int getRequestedMemberCachePolicies() {
return requestedMemberCachePolicies;
}
}

View File

@ -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<DiscordSRV> {
@ -36,14 +44,39 @@ public class ModuleDelegate extends AbstractModule<DiscordSRV> {
return module.isEnabled();
}
@Override
public @NotNull Collection<DiscordGatewayIntent> requiredIntents() {
return module.requiredIntents();
}
@Override
public @NotNull Collection<DiscordCacheFlag> requiredCacheFlags() {
return module.requiredCacheFlags();
}
@Override
public @NotNull Collection<DiscordMemberCachePolicy> 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<DiscordSRVApi.ReloadResult> reload() {
return module.reload();
}
@Override
@ -53,7 +86,6 @@ public class ModuleDelegate extends AbstractModule<DiscordSRV> {
@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 + ")}";
}
}

View File

@ -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<DT extends DiscordSRV> extends AbstractM
super(discordSRV);
}
public PluginIntegration(DT discordSRV, Logger logger) {
super(discordSRV, logger);
}
@Override
@OverridingMethodsMustInvokeSuper
public boolean isEnabled() {

View File

@ -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')