diff --git a/api/src/main/java/com/discordsrv/api/reload/ReloadFlag.java b/api/src/main/java/com/discordsrv/api/reload/ReloadFlag.java index e4cb9cc1..4e470954 100644 --- a/api/src/main/java/com/discordsrv/api/reload/ReloadFlag.java +++ b/api/src/main/java/com/discordsrv/api/reload/ReloadFlag.java @@ -30,9 +30,11 @@ import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; import java.util.function.Predicate; +import java.util.stream.Collectors; public enum ReloadFlag { CONFIG(false), + CONFIG_UPGRADE(false), LINKED_ACCOUNT_PROVIDER(false), STORAGE(true), DISCORD_CONNECTION(DiscordSRVApi::isReady), @@ -41,8 +43,8 @@ public enum ReloadFlag { // Bukkit only TRANSLATIONS(false); - public static final Set ALL = Collections.unmodifiableSet( - new LinkedHashSet<>(Arrays.asList(values()))); + public static final Set LOAD = Collections.unmodifiableSet( + Arrays.stream(values()).filter(flag -> flag != ReloadFlag.CONFIG_UPGRADE).collect(Collectors.toSet())); public static final Set DEFAULT_FLAGS = Collections.unmodifiableSet( new LinkedHashSet<>(Collections.singletonList(CONFIG))); diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/BukkitDiscordSRV.java b/bukkit/src/main/java/com/discordsrv/bukkit/BukkitDiscordSRV.java index 7862a44e..e24abf77 100644 --- a/bukkit/src/main/java/com/discordsrv/bukkit/BukkitDiscordSRV.java +++ b/bukkit/src/main/java/com/discordsrv/bukkit/BukkitDiscordSRV.java @@ -94,13 +94,13 @@ public class BukkitDiscordSRV extends AbstractDiscordSRV configManager = connectionConfigManager(); + return configManager != null ? configManager.config() : null; } @Override @@ -449,7 +451,8 @@ public abstract class AbstractDiscordSRV< @Override public C config() { - return configManager().config(); + MainConfigManager configManager = configManager(); + return configManager != null ? configManager.config() : null; } @Override @@ -457,12 +460,17 @@ public abstract class AbstractDiscordSRV< @Override public MC messagesConfig(@Nullable Locale locale) { - MessagesConfigSingleManager manager = locale != null ? messagesConfigManager().getManager(locale) : null; + MessagesConfigManager configManager = messagesConfigManager(); + if (configManager == null) { + return null; + } + + MessagesConfigSingleManager manager = locale != null ? configManager.getManager(locale) : null; if (manager == null) { - manager = messagesConfigManager().getManager(defaultLocale()); + manager = configManager.getManager(defaultLocale()); } if (manager == null) { - manager = messagesConfigManager().getManager(Locale.US); + manager = configManager.getManager(Locale.US); } return manager.config(); } @@ -715,7 +723,7 @@ public abstract class AbstractDiscordSRV< } // Initial load - reload(ReloadFlag.ALL, true); + reload(ReloadFlag.LOAD, true); if (serverType() == ServerType.PROXY) { runServerStarted().get(); @@ -741,11 +749,17 @@ public abstract class AbstractDiscordSRV< logger().info("Reloading DiscordSRV..."); } - if (flags.contains(ReloadFlag.CONFIG)) { + boolean configUpgrade = flags.contains(ReloadFlag.CONFIG_UPGRADE); + if (flags.contains(ReloadFlag.CONFIG) || configUpgrade) { try { - connectionConfigManager().load(); - configManager().load(); - messagesConfigManager().load(); + AtomicBoolean anyMissingOptions = new AtomicBoolean(false); + connectionConfigManager().reload(configUpgrade, anyMissingOptions); + configManager().reload(configUpgrade, anyMissingOptions); + messagesConfigManager().reload(configUpgrade, anyMissingOptions); + + if (anyMissingOptions.get()) { + logger().info("Use \"/discordsrv reload config_upgrade\" to write the latest configuration"); + } channelConfig().reload(); createHttpClient(); diff --git a/common/src/main/java/com/discordsrv/common/command/game/commands/subcommand/reload/ReloadCommand.java b/common/src/main/java/com/discordsrv/common/command/game/commands/subcommand/reload/ReloadCommand.java index 9e40d0fc..ff4c36b3 100644 --- a/common/src/main/java/com/discordsrv/common/command/game/commands/subcommand/reload/ReloadCommand.java +++ b/common/src/main/java/com/discordsrv/common/command/game/commands/subcommand/reload/ReloadCommand.java @@ -155,7 +155,7 @@ public class ReloadCommand implements GameCommandExecutor, GameCommandSuggester if (discordSRV.status().isStartupError()) { // If startup error, use all flags parts.clear(); - flags.addAll(ReloadFlag.ALL); + flags.addAll(ReloadFlag.LOAD); } for (String part : parts) { @@ -190,7 +190,7 @@ public class ReloadCommand implements GameCommandExecutor, GameCommandSuggester String last = currentInput.substring(lastSpace); String beforeLastSpace = currentInput.substring(0, lastSpace); - List options = ReloadFlag.ALL.stream() + List options = Arrays.stream(ReloadFlag.values()) .map(flag -> flag.name().toLowerCase(Locale.ROOT)) .filter(flag -> flag.startsWith(last)) .collect(Collectors.toList()); diff --git a/common/src/main/java/com/discordsrv/common/config/configurate/manager/MessagesConfigManager.java b/common/src/main/java/com/discordsrv/common/config/configurate/manager/MessagesConfigManager.java index 8ca83e14..0b29b4a9 100644 --- a/common/src/main/java/com/discordsrv/common/config/configurate/manager/MessagesConfigManager.java +++ b/common/src/main/java/com/discordsrv/common/config/configurate/manager/MessagesConfigManager.java @@ -28,6 +28,7 @@ import com.discordsrv.common.exception.ConfigException; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Stream; public abstract class MessagesConfigManager { @@ -57,7 +58,7 @@ public abstract class MessagesConfigManager { return discordSRV.dataDirectory().resolve("messages"); } - public void load() throws ConfigException { + public void reload(boolean forceSave, AtomicBoolean anyMissingOptions) throws ConfigException { synchronized (configs) { configs.clear(); @@ -106,7 +107,7 @@ public abstract class MessagesConfigManager { } for (Map.Entry> entry : configs.entrySet()) { - entry.getValue().load(); + entry.getValue().reload(forceSave, anyMissingOptions); } } } diff --git a/common/src/main/java/com/discordsrv/common/config/configurate/manager/abstraction/ConfigManager.java b/common/src/main/java/com/discordsrv/common/config/configurate/manager/abstraction/ConfigManager.java index 1481dcb1..c8b2e2ea 100644 --- a/common/src/main/java/com/discordsrv/common/config/configurate/manager/abstraction/ConfigManager.java +++ b/common/src/main/java/com/discordsrv/common/config/configurate/manager/abstraction/ConfigManager.java @@ -22,13 +22,13 @@ import com.discordsrv.common.exception.ConfigException; import org.spongepowered.configurate.CommentedConfigurationNode; import org.spongepowered.configurate.loader.AbstractConfigurationLoader; +import java.util.concurrent.atomic.AtomicBoolean; + public interface ConfigManager { T createConfiguration(); T config(); - void load() throws ConfigException; - void reload() throws ConfigException; + void reload(boolean forceSave, AtomicBoolean anyMissingOptions) throws ConfigException; void save(AbstractConfigurationLoader loader) throws ConfigException; - void save() throws ConfigException; } diff --git a/common/src/main/java/com/discordsrv/common/config/configurate/manager/abstraction/ConfigurateConfigManager.java b/common/src/main/java/com/discordsrv/common/config/configurate/manager/abstraction/ConfigurateConfigManager.java index 53cb859c..d036217d 100644 --- a/common/src/main/java/com/discordsrv/common/config/configurate/manager/abstraction/ConfigurateConfigManager.java +++ b/common/src/main/java/com/discordsrv/common/config/configurate/manager/abstraction/ConfigurateConfigManager.java @@ -58,8 +58,10 @@ import org.spongepowered.configurate.yaml.YamlConfigurationLoader; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.lang.reflect.Type; +import java.nio.file.Files; import java.nio.file.Path; import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -385,56 +387,73 @@ public abstract class ConfigurateConfigManager defaultConfigClass = (Class) defaultConfig.getClass(); + try { - CommentedConfigurationNode node; - if (filePath().toFile().exists()) { - // Config file exists, load from that - node = loader().load(); + SAVE_OR_LOAD.set(true); - ConfigurationNode translation = getTranslation(); - if (translation != null) { - // Merge translation - node.mergeFrom(translation); - } + if (!Files.exists(filePath)) { + CommentedConfigurationNode node = getDefault(defaultConfig, false); + translate(node); - // Apply defaults that may not be there - node.mergeFrom(getDefault(defaultConfig, true)); - } else { - node = getDefault(defaultConfig, false); + configuration = objectMapper().get(defaultConfigClass).load(node); + + // TODO: v1 migration + + save(loader()); + return; } - try { - SAVE_OR_LOAD.set(true); - this.configuration = objectMapper() - .get((Class) defaultConfig.getClass()) - .load(node); - } finally { - SAVE_OR_LOAD.set(false); + // Load existing file & translate + CommentedConfigurationNode node = loader().load(); + + CommentedConfigurationNode defaultNode = getDefault(defaultConfig, true); + translate(defaultNode); + + // Log missing options, apply (missing) defaults + if (!forceSave) { + // Only log if it's not being force saved + checkIfValuesMissing(node, defaultNode, anyMissingOptions); + } + node.mergeFrom(defaultNode); + + configuration = objectMapper().get(defaultConfigClass).load(node); + if (forceSave) { + save(loader); } } catch (ConfigurateException e) { Class configClass = defaultConfig.getClass(); if (!configClass.isAnnotationPresent(ConfigSerializable.class)) { // Not very obvious and can easily happen - throw new ConfigException(configClass.getName() - + " is not annotated with @ConfigSerializable", e); + throw new ConfigException(configClass.getName() + " is not annotated with @ConfigSerializable", e); } throw new ConfigException("Failed to load configuration", e); + } finally { + SAVE_OR_LOAD.set(false); + } + } + + private void checkIfValuesMissing( + CommentedConfigurationNode node, + CommentedConfigurationNode defaultNode, + AtomicBoolean anyMissingOptions + ) throws ConfigurateException { + for (CommentedConfigurationNode child : defaultNode.childrenMap().values()) { + CommentedConfigurationNode value = node.node(child.key()); + if (value.virtual()) { + logger.warning("Missing option \"" + child.key() + "\" in " + fileName()); + anyMissingOptions.set(true); + continue; + } + + checkIfValuesMissing(value, child, anyMissingOptions); } } @@ -442,24 +461,16 @@ public abstract class ConfigurateConfigManager loader) throws ConfigException { try { + SAVE_OR_LOAD.set(true); CommentedConfigurationNode node = loader.createNode(); - save(configuration, (Class) configuration.getClass(), node); + + // Save configuration to the node + objectMapper().get((Class) configuration.getClass()).save(configuration, node); + + // Save the node to the provided loader loader.save(node); } catch (ConfigurateException e) { throw new ConfigException("Failed to load configuration", e); - } - } - - @Override - public void save() throws ConfigException { - LT loader = loader(); - save(loader); - } - - protected void save(T config, Class clazz, CommentedConfigurationNode node) throws SerializationException { - try { - SAVE_OR_LOAD.set(true); - objectMapper().get(clazz).save(config, node); } finally { SAVE_OR_LOAD.set(false); } diff --git a/common/src/main/java/com/discordsrv/common/config/configurate/manager/abstraction/TranslatedConfigManager.java b/common/src/main/java/com/discordsrv/common/config/configurate/manager/abstraction/TranslatedConfigManager.java index fe471176..2f5e36e9 100644 --- a/common/src/main/java/com/discordsrv/common/config/configurate/manager/abstraction/TranslatedConfigManager.java +++ b/common/src/main/java/com/discordsrv/common/config/configurate/manager/abstraction/TranslatedConfigManager.java @@ -20,8 +20,6 @@ package com.discordsrv.common.config.configurate.manager.abstraction; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.config.Config; -import com.discordsrv.common.exception.ConfigException; -import org.jetbrains.annotations.Nullable; import org.spongepowered.configurate.CommentedConfigurationNode; import org.spongepowered.configurate.ConfigurateException; import org.spongepowered.configurate.ConfigurationNode; @@ -56,13 +54,6 @@ public abstract class TranslatedConfigManager) config.getClass(), node); - translateNode(node, translation, comments); - } catch (ConfigurateException e) { - throw new ConfigException(e); - } + this.header = comments.node("$header").getString(); + translateNode(node, translation, comments); } private ConfigurationNode getTranslationRoot() throws ConfigurateException { diff --git a/common/src/main/java/com/discordsrv/common/config/helper/V1ConfigMigration.java b/common/src/main/java/com/discordsrv/common/config/helper/V1ConfigMigration.java new file mode 100644 index 00000000..46945d5b --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/config/helper/V1ConfigMigration.java @@ -0,0 +1,154 @@ +package com.discordsrv.common.config.helper; + +import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.abstraction.sync.enums.SyncDirection; +import com.discordsrv.common.config.connection.ConnectionConfig; +import com.discordsrv.common.config.main.ConsoleConfig; +import com.discordsrv.common.config.main.GroupSyncConfig; +import com.discordsrv.common.config.main.MainConfig; +import com.discordsrv.common.config.main.channels.DiscordToMinecraftChatConfig; +import com.discordsrv.common.config.main.channels.base.BaseChannelConfig; +import com.discordsrv.common.config.main.channels.base.ChannelConfig; +import org.spongepowered.configurate.ConfigurateException; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.serialize.SerializationException; +import org.spongepowered.configurate.yaml.YamlConfigurationLoader; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; + +public class V1ConfigMigration { + + private final DiscordSRV discordSRV; + private final ConfigurationNode config; + private final ConfigurationNode messages; + private final ConfigurationNode linking; + private final ConfigurationNode synchronization; + + public V1ConfigMigration(DiscordSRV discordSRV) { + this.discordSRV = discordSRV; + this.config = loadNode(discordSRV, "config.yml"); + this.messages = loadNode(discordSRV, "messages.yml"); + this.linking = loadNode(discordSRV, "linking.yml"); + this.synchronization = loadNode(discordSRV, "synchronization.yml"); + } + + private static ConfigurationNode loadNode(DiscordSRV discordSRV, String fileName) { + try { + Path path = discordSRV.dataDirectory().resolve(fileName); + if (!Files.exists(path)) { + return null; + } + return YamlConfigurationLoader.builder().path(path).build().load(); + } catch (ConfigurateException e) { + discordSRV.logger().warning("Failed to load v1 " + fileName + " for migration", e); + return null; + } + } + + public void migrate(MainConfig mainConfig) throws SerializationException { + mainConfig.channels.remove("global"); + BaseChannelConfig defaultChannel = mainConfig.channels.get("default"); + if (defaultChannel != null) { + defaultChannel.discordToMinecraft.enabled = config.node("DiscordChatChannelDiscordToMinecraft").getBoolean(true); + + String emojiBehaviour = config.node("DiscordChatChannelEmojiBehavior").getString(); + if ("show".equalsIgnoreCase(emojiBehaviour)) { + defaultChannel.discordToMinecraft.unicodeEmojiBehaviour = DiscordToMinecraftChatConfig.EmojiBehaviour.SHOW; + } else if ("hide".equalsIgnoreCase(emojiBehaviour)) { + defaultChannel.discordToMinecraft.unicodeEmojiBehaviour = DiscordToMinecraftChatConfig.EmojiBehaviour.HIDE; + } + + defaultChannel.discordToMinecraft.ignores.bots = config.node("DiscordChatChannelBlockBots").getBoolean(false); + defaultChannel.discordToMinecraft.ignores.webhooks = config.node("DiscordChatChannelWebhooks").getBoolean(true); + defaultChannel.discordToMinecraft.ignores.userBotAndWebhookIds.whitelist = false; + defaultChannel.discordToMinecraft.ignores.userBotAndWebhookIds.ids = config.node("DiscordChatChannelBlockedIds").getList(Long.class); + defaultChannel.discordToMinecraft.ignores.roleIds.whitelist = config.node("DiscordChatChannelBlockedRolesAsWhitelist").getBoolean(false); + defaultChannel.discordToMinecraft.ignores.roleIds.ids = config.node("DiscordChatChannelBlockedRolesIds").getList(Long.class); + + defaultChannel.minecraftToDiscord.enabled = config.node("DiscordChatChannelMinecraftToDiscord").getBoolean(true); + List allowedMentions = config.node("DiscordChatChannelAllowedMentions").getList(String.class, Collections.emptyList()); + defaultChannel.minecraftToDiscord.mentions.channels = allowedMentions.contains("channel"); + defaultChannel.minecraftToDiscord.mentions.everyone = allowedMentions.contains("everyone"); + defaultChannel.minecraftToDiscord.mentions.roles = allowedMentions.contains("role"); + defaultChannel.minecraftToDiscord.mentions.users = allowedMentions.contains("user"); + } + + config.node("Channels").childrenMap().forEach((key, value) -> { + String channelId = value.getString(); + if (!(key instanceof String) || channelId == null) { + return; + } + + ChannelConfig channelConfig = new ChannelConfig(); + channelConfig.destination.channelIds = Collections.singletonList(Long.parseUnsignedLong(channelId)); + channelConfig.destination.threads = Collections.emptyList(); + mainConfig.channels.put((String) key, channelConfig); + }); + + String consoleChannelId = config.node("DiscordConsoleChannelId").getString(""); + if (!consoleChannelId.replace("0", "").isEmpty()) { + ConsoleConfig consoleConfig = new ConsoleConfig(); + consoleConfig.channel.channelId = Long.parseUnsignedLong(consoleChannelId); + consoleConfig.appender.outputMode = config.node("DiscordConsoleChannelUseCodeBlocks").getBoolean() ? ConsoleConfig.OutputMode.DIFF : ConsoleConfig.OutputMode.PLAIN_CONTENT; + consoleConfig.appender.levels.levels = config.node("DiscordConsoleChannelLevels").getList(String.class, Collections.emptyList()) + .stream().map(level -> level.toUpperCase(Locale.ROOT)).collect(Collectors.toList()); + consoleConfig.appender.levels.blacklist = false; + + mainConfig.console.clear(); + mainConfig.console.add(consoleConfig); + } + + boolean toMinecraft = synchronization.node("BanSynchronizationDiscordToMinecraft").getBoolean(true); + boolean toDiscord = synchronization.node("BanSynchronizationMinecraftToDiscord").getBoolean(true); + SyncDirection banSyncDirection = SyncDirection.BIDIRECTIONAL; + if (toMinecraft && !toDiscord) { + banSyncDirection = SyncDirection.DISCORD_TO_MINECRAFT; + } else if (!toMinecraft && toDiscord) { + banSyncDirection = SyncDirection.MINECRAFT_TO_DISCORD; + } + + mainConfig.banSync.direction = banSyncDirection; + + + boolean minecraftIsTieBreaker = synchronization.node("GroupRoleSynchronizationMinecraftIsAuthoritative").getBoolean(true); + boolean oneWay = synchronization.node("GroupRoleSynchronizationOneWay").getBoolean(false); + SyncDirection groupSyncDirection = oneWay + ? (minecraftIsTieBreaker ? SyncDirection.MINECRAFT_TO_DISCORD : SyncDirection.DISCORD_TO_MINECRAFT) + : SyncDirection.BIDIRECTIONAL; + int groupSyncCycleTime = synchronization.node("GroupRoleSynchronizationCycleTime").getInt(); + + mainConfig.groupSync.pairs.clear(); + synchronization.node("GroupRoleSynchronizationGroupsAndRolesToSync").childrenMap().forEach((key, value) -> { + String roleId = value.getString(); + if (!(key instanceof String) || roleId == null) { + return; + } + + GroupSyncConfig.PairConfig pairConfig = new GroupSyncConfig.PairConfig(); + pairConfig.roleId = Long.parseUnsignedLong(roleId); + pairConfig.groupName = (String) key; + pairConfig.direction = groupSyncDirection; + pairConfig.timer.cycleTime = groupSyncCycleTime; + + mainConfig.groupSync.pairs.add(pairConfig); + }); + } + + public void migrate(ConnectionConfig connectionConfig) { + connectionConfig.bot.token = config.node("BotToken").getString(); + + if (!config.node("ProxyHost").getString("").endsWith("example.com")) { + connectionConfig.httpProxy.enabled = true; + connectionConfig.httpProxy.host = config.node("ProxyHost").getString(); + connectionConfig.httpProxy.port = config.node("ProxyPort").getInt(); + connectionConfig.httpProxy.basicAuth.enabled = true; + connectionConfig.httpProxy.basicAuth.username = config.node("ProxyUser").getString(); + connectionConfig.httpProxy.basicAuth.password = config.node("ProxyPassword").getString(); + } + } +} diff --git a/common/src/main/java/com/discordsrv/common/helper/TemporaryLocalData.java b/common/src/main/java/com/discordsrv/common/helper/TemporaryLocalData.java index 5644a2c0..ade7ab0f 100644 --- a/common/src/main/java/com/discordsrv/common/helper/TemporaryLocalData.java +++ b/common/src/main/java/com/discordsrv/common/helper/TemporaryLocalData.java @@ -92,12 +92,15 @@ public class TemporaryLocalData { return new Model(); } + Model model; try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(file))) { - return discordSRV.json().readValue(inputStream, Model.class); + model = discordSRV.json().readValue(inputStream, Model.class); } catch (IOException e) { discordSRV.logger().error("Failed to load temporary local data, resetting", e); return new Model(); } + + return model != null ? model : new Model(); } } @@ -111,6 +114,5 @@ public class TemporaryLocalData { */ public Map> consoleThreadRotationIds = new HashMap<>(); - } }