diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/integration/VaultIntegration.java b/bukkit/src/main/java/com/discordsrv/bukkit/integration/VaultIntegration.java index 4a13cc6d..40f49be3 100644 --- a/bukkit/src/main/java/com/discordsrv/bukkit/integration/VaultIntegration.java +++ b/bukkit/src/main/java/com/discordsrv/bukkit/integration/VaultIntegration.java @@ -31,6 +31,9 @@ import org.bukkit.plugin.RegisteredServiceProvider; import org.bukkit.plugin.ServicesManager; import org.jetbrains.annotations.Nullable; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -127,6 +130,12 @@ public class VaultIntegration extends PluginIntegration implem return discordSRV.plugin().getServer().getOfflinePlayer(player); } + @Override + public List getGroups() { + String[] groups = permission.getGroups(); + return groups != null ? Arrays.asList(groups) : Collections.emptyList(); + } + @Override public CompletableFuture hasGroup(UUID player, String groupName, boolean includeInherited) { if (permission == null || !permission.isEnabled() || !permission.hasGroupSupport()) { diff --git a/common/build.gradle b/common/build.gradle index 13ed2fbe..cb877fa4 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -40,6 +40,9 @@ dependencies { runtimeDownloadApi 'org.spongepowered:configurate-yaml:' + rootProject.configurateVersion runtimeDownloadApi 'org.spongepowered:configurate-hocon:' + rootProject.configurateVersion + // Jackson (transitive in :api) + compileOnly 'com.fasterxml.jackson.core:jackson-databind:2.13.1' + // Logging compileOnlyApi project(':common:common-slf4j-hack') compileOnly 'org.apache.logging.log4j:log4j-core:2.0-beta9' diff --git a/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java b/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java index 0de89b2d..4e038ac3 100644 --- a/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java +++ b/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java @@ -101,8 +101,8 @@ public abstract class AbstractDiscordSRV. + */ + +package com.discordsrv.common.debug; + +import com.discordsrv.api.event.events.Event; +import com.discordsrv.common.debug.file.DebugFile; + +public class DebugGenerateEvent implements Event { + + private final DebugReport report; + + public DebugGenerateEvent(DebugReport report) { + this.report = report; + } + + public void addFile(DebugFile file) { + report.addFile(file); + } +} diff --git a/common/src/main/java/com/discordsrv/common/debug/DebugReport.java b/common/src/main/java/com/discordsrv/common/debug/DebugReport.java new file mode 100644 index 00000000..fa32b983 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/debug/DebugReport.java @@ -0,0 +1,110 @@ +/* + * 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.debug; + +import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.debug.file.DebugFile; +import com.discordsrv.common.debug.file.KeyValueDebugFile; +import com.discordsrv.common.debug.file.TextDebugFile; +import com.discordsrv.common.plugin.Plugin; +import org.apache.commons.lang3.exception.ExceptionUtils; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class DebugReport { + + private final List files = new ArrayList<>(); + private final DiscordSRV discordSRV; + + public DebugReport(DiscordSRV discordSRV) { + this.discordSRV = discordSRV; + } + + public void generate() { + discordSRV.eventBus().publish(new DebugGenerateEvent(this)); + + addFile(environment()); + addFile(plugins()); + for (Path debugLog : discordSRV.logger().getDebugLogs()) { + addFile(readFile(1, debugLog)); + } + } + + public void addFile(DebugFile file) { + files.add(file); + } + + public List getFiles() { + return files; + } + + private DebugFile environment() { + Map values = new LinkedHashMap<>(); + values.put("discordSRV", discordSRV.getClass().getSimpleName()); + values.put("version", discordSRV.version()); + values.put("status", discordSRV.status().name()); + values.put("jdaStatus", discordSRV.jda().map(jda -> jda.getStatus().name()).orElse("JDA null")); + values.put("platformLogger", discordSRV.platformLogger().getClass().getName()); + values.put("onlineMode", discordSRV.onlineMode().name()); + + values.put("javaVersion", System.getProperty("java.version")); + values.put("javaVendor", System.getProperty("java.vendor") + + " (" + System.getProperty("java.vendor.url") + ")"); + + values.put("operatingSystem", System.getProperty("os.name") + + " " + System.getProperty("os.version") + + " (" + System.getProperty("os.arch") + ")"); + + return new KeyValueDebugFile(10, "environment.json", values); + } + + private DebugFile plugins() { + List plugins = discordSRV.pluginManager().getPlugins(); + StringBuilder builder = new StringBuilder("Plugins (" + plugins.size() + "):\n"); + + for (Plugin plugin : plugins) { + builder.append('\n') + .append(plugin.name()) + .append(" v").append(plugin.version()) + .append(" ").append(plugin.authors()); + } + return new TextDebugFile(5, "plugins.txt", builder.toString()); + } + + private DebugFile readFile(int order, Path file) { + String fileName = file.getFileName().toString(); + if (!Files.exists(file)) { + return new TextDebugFile(order, fileName, "File does not exist"); + } + + try { + List lines = Files.readAllLines(file, StandardCharsets.UTF_8); + return new TextDebugFile(order, fileName, String.join("\n", lines)); + } catch (IOException e) { + return new TextDebugFile(order, fileName, ExceptionUtils.getStackTrace(e)); + } + } +} diff --git a/common/src/main/java/com/discordsrv/common/debug/file/DebugFile.java b/common/src/main/java/com/discordsrv/common/debug/file/DebugFile.java new file mode 100644 index 00000000..1d199c89 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/debug/file/DebugFile.java @@ -0,0 +1,26 @@ +/* + * 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.debug.file; + +public interface DebugFile { + + int order(); + String name(); + String content(); +} diff --git a/common/src/main/java/com/discordsrv/common/debug/file/KeyValueDebugFile.java b/common/src/main/java/com/discordsrv/common/debug/file/KeyValueDebugFile.java new file mode 100644 index 00000000..ac5d3227 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/debug/file/KeyValueDebugFile.java @@ -0,0 +1,59 @@ +/* + * 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.debug.file; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.exception.ExceptionUtils; + +import java.util.Map; + +public class KeyValueDebugFile implements DebugFile { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private final int order; + private final String name; + private final Map values; + + public KeyValueDebugFile(int order, String name, Map values) { + this.order = order; + this.name = name; + this.values = values; + } + + @Override + public int order() { + return order; + } + + @Override + public String name() { + return name; + } + + @Override + public String content() { + try { + return OBJECT_MAPPER.writeValueAsString(values); + } catch (JsonProcessingException e) { + return ExceptionUtils.getStackTrace(e); + } + } +} diff --git a/common/src/main/java/com/discordsrv/common/debug/file/TextDebugFile.java b/common/src/main/java/com/discordsrv/common/debug/file/TextDebugFile.java new file mode 100644 index 00000000..4e78aa6e --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/debug/file/TextDebugFile.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.debug.file; + +public class TextDebugFile implements DebugFile { + + private final int order; + private final String name; + private final String content; + + public TextDebugFile(String name, CharSequence content) { + this(0, name, content); + } + + public TextDebugFile(int order, String name, CharSequence content) { + this.order = order; + this.name = name; + this.content = content.toString(); + } + + public int order() { + return order; + } + + public String name() { + return name; + } + + public String content() { + return content; + } + +} diff --git a/common/src/main/java/com/discordsrv/common/event/bus/EventBusImpl.java b/common/src/main/java/com/discordsrv/common/event/bus/EventBusImpl.java index d9990ad9..50c8ad58 100644 --- a/common/src/main/java/com/discordsrv/common/event/bus/EventBusImpl.java +++ b/common/src/main/java/com/discordsrv/common/event/bus/EventBusImpl.java @@ -27,6 +27,8 @@ import com.discordsrv.api.event.events.Cancellable; import com.discordsrv.api.event.events.Event; import com.discordsrv.api.event.events.Processable; import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.debug.DebugGenerateEvent; +import com.discordsrv.common.debug.file.TextDebugFile; import com.discordsrv.common.exception.InvalidListenerMethodException; import com.discordsrv.common.logging.Logger; import com.discordsrv.common.logging.NamedLogger; @@ -60,6 +62,7 @@ public class EventBusImpl implements EventBus { public EventBusImpl(DiscordSRV discordSRV) { this.discordSRV = discordSRV; this.logger = new NamedLogger(discordSRV, "EVENT_BUS"); + subscribe(this); } @Override @@ -234,4 +237,32 @@ public class EventBusImpl implements EventBus { state.getValue().remove(); } } + + @Subscribe + public void onDebugGenerate(DebugGenerateEvent event) { + StringBuilder builder = new StringBuilder("Registered listeners (" + listeners.size() + "/" + allListeners.size() + "):\n"); + + for (Map.Entry> entry : listeners.entrySet()) { + Object listener = entry.getKey(); + List eventListeners = entry.getValue(); + builder.append('\n') + .append(listener) + .append(" (") + .append(listener.getClass().getName()) + .append(") [") + .append(eventListeners.size()) + .append("]\n"); + for (EventListenerImpl eventListener : eventListeners) { + builder.append(" - ") + .append(eventListener.eventClass().getName()) + .append(": ") + .append(eventListener.methodName()) + .append(" @ ") + .append(eventListener.priority().name()) + .append('\n'); + } + } + + event.addFile(new TextDebugFile("event-bus.txt", builder)); + } } 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 253c5f2a..d59177aa 100644 --- a/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncModule.java +++ b/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncModule.java @@ -25,6 +25,8 @@ import com.discordsrv.api.discord.events.member.role.DiscordMemberRoleRemoveEven import com.discordsrv.api.event.bus.Subscribe; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.config.main.GroupSyncConfig; +import com.discordsrv.common.debug.DebugGenerateEvent; +import com.discordsrv.common.debug.file.TextDebugFile; import com.discordsrv.common.future.util.CompletableFutureUtil; import com.discordsrv.common.groupsync.enums.GroupSyncCause; import com.discordsrv.common.groupsync.enums.GroupSyncDirection; @@ -115,6 +117,36 @@ public class GroupSyncModule extends AbstractModule { } } + // Debug + + @Subscribe + public void onDebugGenerate(DebugGenerateEvent event) { + StringBuilder builder = new StringBuilder("Active pairs:"); + + for (Map.Entry> entry : pairs.entrySet()) { + GroupSyncConfig.PairConfig pair = entry.getKey(); + builder.append("\n- ").append(pair) + .append(" (tie-breaker: ").append(pair.tieBreaker()) + .append(", direction: ").append(pair.direction()) + .append(", server context: ").append(pair.serverContext).append(")"); + if (entry.getValue() != null) { + builder.append(" [Timed]"); + } + } + + PermissionDataProvider.Groups groups = getPermissionProvider(); + if (groups != null) { + builder.append("\n\nAvailable groups (").append(groups.getClass().getName()).append("):"); + + for (String group : groups.getGroups()) { + builder.append("\n- ").append(group); + } + } else { + builder.append("\n\nNo permission provider available"); + } + event.addFile(new TextDebugFile("group-sync.txt", builder)); + } + private void logSummary( UUID player, GroupSyncCause cause, @@ -146,6 +178,15 @@ public class GroupSyncModule extends AbstractModule { return groupsContext == null ? discordSRV.getModule(PermissionDataProvider.Groups.class) : groupsContext; } + private boolean noPermissionProvider() { + PermissionDataProvider.Groups groups = getPermissionProvider(); + return groups == null || !groups.isEnabled(); + } + + private boolean supportsOffline() { + return getPermissionProvider().supportsOffline(); + } + private CompletableFuture hasGroup( UUID player, String groupName, @@ -209,6 +250,10 @@ public class GroupSyncModule extends AbstractModule { } public void resync(UUID player, long userId, GroupSyncCause cause) { + if (noPermissionProvider() || (!discordSRV.playerProvider().player(player).isPresent() && !supportsOffline())) { + return; + } + Map> futures = new LinkedHashMap<>(); for (GroupSyncConfig.PairConfig pair : pairs.keySet()) { futures.put(pair, resyncPair(pair, player, userId)); @@ -218,6 +263,10 @@ public class GroupSyncModule extends AbstractModule { } private void resyncPair(GroupSyncConfig.PairConfig pair, GroupSyncCause cause) { + if (noPermissionProvider()) { + return; + } + for (IPlayer player : discordSRV.playerProvider().allPlayers()) { UUID uuid = player.uniqueId(); Long userId = getLinkedAccount(uuid); @@ -352,6 +401,10 @@ public class GroupSyncModule extends AbstractModule { } private void roleChanged(long userId, long roleId, boolean remove) { + if (noPermissionProvider()) { + return; + } + if (checkExpectation(expectedDiscordChanges, userId, roleId, remove)) { return; } @@ -405,6 +458,10 @@ public class GroupSyncModule extends AbstractModule { } private void groupChanged(UUID player, String groupName, @Nullable Set serverContext, GroupSyncCause cause, boolean remove) { + if (noPermissionProvider()) { + return; + } + if (cause.isDiscordSRVCanCause() && checkExpectation(expectedMinecraftChanges, player, groupName, remove)) { return; } diff --git a/common/src/main/java/com/discordsrv/common/integration/LuckPermsIntegration.java b/common/src/main/java/com/discordsrv/common/integration/LuckPermsIntegration.java index 68827976..73fa3f67 100644 --- a/common/src/main/java/com/discordsrv/common/integration/LuckPermsIntegration.java +++ b/common/src/main/java/com/discordsrv/common/integration/LuckPermsIntegration.java @@ -52,6 +52,7 @@ import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.function.BiFunction; import java.util.function.Consumer; +import java.util.stream.Collectors; public class LuckPermsIntegration extends PluginIntegration implements PermissionDataProvider.All { @@ -241,4 +242,10 @@ public class LuckPermsIntegration extends PluginIntegration implemen } } + @Override + public List getGroups() { + return luckPerms.getGroupManager().getLoadedGroups().stream() + .map(Group::getName) + .collect(Collectors.toList()); + } } 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 dcec3bc9..7f636962 100644 --- a/common/src/main/java/com/discordsrv/common/module/ModuleManager.java +++ b/common/src/main/java/com/discordsrv/common/module/ModuleManager.java @@ -22,10 +22,14 @@ import com.discordsrv.api.event.bus.EventPriority; import com.discordsrv.api.event.bus.Subscribe; import com.discordsrv.api.event.events.lifecycle.DiscordSRVReloadEvent; import com.discordsrv.api.event.events.lifecycle.DiscordSRVShuttingDownEvent; -import com.discordsrv.common.DiscordSRV; -import com.discordsrv.common.module.type.AbstractModule; 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.module.type.AbstractModule; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -33,20 +37,21 @@ import java.util.concurrent.CopyOnWriteArraySet; public class ModuleManager { - private final Set> modules = new CopyOnWriteArraySet<>(); - private final Map> moduleLookupTable = new ConcurrentHashMap<>(); + private final Set modules = new CopyOnWriteArraySet<>(); + private final Map moduleLookupTable = new ConcurrentHashMap<>(); private final DiscordSRV discordSRV; public ModuleManager(DiscordSRV discordSRV) { this.discordSRV = discordSRV; + discordSRV.eventBus().subscribe(this); } @SuppressWarnings("unchecked") public T getModule(Class moduleType) { return (T) moduleLookupTable.computeIfAbsent(moduleType.getName(), key -> { - AbstractModule bestCandidate = null; + Module bestCandidate = null; int bestCandidatePriority = Integer.MIN_VALUE; - for (AbstractModule module : modules) { + for (Module module : modules) { int priority; if (moduleType.isAssignableFrom(module.getClass()) && ((priority = module.priority(moduleType)) > bestCandidatePriority)) { bestCandidate = module; @@ -61,25 +66,29 @@ public class ModuleManager { this.modules.add(module); this.moduleLookupTable.put(module.getClass().getName(), module); - enable(module); + enable(module, true); } - private void enable(AbstractModule module) { + private void enable(Module module, boolean enableNonAbstract) { try { - module.enableModule(); + if (module instanceof AbstractModule) { + ((AbstractModule) module).enableModule(); + } else if (enableNonAbstract) { + module.enable(); + } } catch (Throwable t) { discordSRV.logger().error("Failed to enable " + module.getClass().getSimpleName(), t); } } - public void unregister(AbstractModule module) { + public void unregister(Module module) { this.modules.remove(module); this.moduleLookupTable.values().removeIf(mod -> mod == module); disable(module); } - private void disable(AbstractModule module) { + private void disable(Module module) { try { module.disable(); } catch (Throwable t) { @@ -89,16 +98,16 @@ public class ModuleManager { @Subscribe(priority = EventPriority.EARLY) public void onShuttingDown(DiscordSRVShuttingDownEvent event) { - for (AbstractModule module : modules) { + for (Module module : modules) { unregister(module); } } @Subscribe(priority = EventPriority.EARLY) public void onReload(DiscordSRVReloadEvent event) { - for (AbstractModule module : modules) { + for (Module module : modules) { // Check if the module needs to be enabled due to reload - enable(module); + enable(module, false); try { module.reload(); @@ -107,4 +116,30 @@ public class ModuleManager { } } } + + @Subscribe + public void onDebugGenerate(DebugGenerateEvent event) { + StringBuilder builder = new StringBuilder(); + + builder.append("Enabled modules:"); + List disabled = new ArrayList<>(); + for (Module module : modules) { + if (!module.isEnabled()) { + disabled.add(module); + continue; + } + appendModule(builder, module); + } + + builder.append("\n\nDisabled modules:"); + for (Module module : disabled) { + appendModule(builder, module); + } + + event.addFile(new TextDebugFile("modules.txt", builder)); + } + + private void appendModule(StringBuilder builder, Module module) { + builder.append('\n').append(module.getClass().getName()); + } } diff --git a/common/src/main/java/com/discordsrv/common/module/type/PermissionDataProvider.java b/common/src/main/java/com/discordsrv/common/module/type/PermissionDataProvider.java index 9908605c..17239bdc 100644 --- a/common/src/main/java/com/discordsrv/common/module/type/PermissionDataProvider.java +++ b/common/src/main/java/com/discordsrv/common/module/type/PermissionDataProvider.java @@ -21,6 +21,7 @@ package com.discordsrv.common.module.type; import com.discordsrv.api.module.type.Module; import org.jetbrains.annotations.Nullable; +import java.util.List; import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -33,6 +34,7 @@ public interface PermissionDataProvider extends Module { interface All extends Basic, Meta, GroupsContext {} interface Groups extends PermissionDataProvider { + List getGroups(); CompletableFuture hasGroup(UUID player, String groupName, boolean includeInherited); CompletableFuture addGroup(UUID player, String groupName); CompletableFuture removeGroup(UUID player, String groupName);