Only save new config options with new reload flag, add initial v1 config migration

This commit is contained in:
Vankka 2024-12-28 23:22:21 +02:00
parent 62fe2d36d0
commit c54f677761
No known key found for this signature in database
GPG Key ID: 62E48025ED4E7EBB
10 changed files with 263 additions and 110 deletions

View File

@ -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<ReloadFlag> ALL = Collections.unmodifiableSet(
new LinkedHashSet<>(Arrays.asList(values())));
public static final Set<ReloadFlag> LOAD = Collections.unmodifiableSet(
Arrays.stream(values()).filter(flag -> flag != ReloadFlag.CONFIG_UPGRADE).collect(Collectors.toSet()));
public static final Set<ReloadFlag> DEFAULT_FLAGS = Collections.unmodifiableSet(
new LinkedHashSet<>(Collections.singletonList(CONFIG)));

View File

@ -94,13 +94,13 @@ public class BukkitDiscordSRV extends AbstractDiscordSRV<DiscordSRVBukkitBootstr
this.playerProvider = new BukkitPlayerProvider(this);
this.pluginManager = new BukkitPluginManager(this);
load();
// Config
this.connectionConfigManager = new BukkitConnectionConfigManager(this);
this.configManager = new BukkitConfigManager(this);
this.messagesConfigManager = new BukkitMessagesConfigManager(this);
load();
this.autoCompleteHelper = new BukkitGameCommandExecutionHelper(this);
}

View File

@ -112,6 +112,7 @@ import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
@ -441,7 +442,8 @@ public abstract class AbstractDiscordSRV<
@Override
public CC connectionConfig() {
return connectionConfigManager().config();
ConnectionConfigManager<CC> 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<C> 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<MC> manager = locale != null ? messagesConfigManager().getManager(locale) : null;
MessagesConfigManager<MC> configManager = messagesConfigManager();
if (configManager == null) {
return null;
}
MessagesConfigSingleManager<MC> 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();

View File

@ -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<String> options = ReloadFlag.ALL.stream()
List<String> options = Arrays.stream(ReloadFlag.values())
.map(flag -> flag.name().toLowerCase(Locale.ROOT))
.filter(flag -> flag.startsWith(last))
.collect(Collectors.toList());

View File

@ -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<C extends MessagesConfig> {
@ -57,7 +58,7 @@ public abstract class MessagesConfigManager<C extends MessagesConfig> {
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<C extends MessagesConfig> {
}
for (Map.Entry<Locale, MessagesConfigSingleManager<C>> entry : configs.entrySet()) {
entry.getValue().load();
entry.getValue().reload(forceSave, anyMissingOptions);
}
}
}

View File

@ -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> {
T createConfiguration();
T config();
void load() throws ConfigException;
void reload() throws ConfigException;
void reload(boolean forceSave, AtomicBoolean anyMissingOptions) throws ConfigException;
void save(AbstractConfigurationLoader<CommentedConfigurationNode> loader) throws ConfigException;
void save() throws ConfigException;
}

View File

@ -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<T, LT extends AbstractConfigurati
return node;
}
@Nullable
protected ConfigurationNode getTranslation() throws ConfigurateException {
return null;
}
protected void translate(CommentedConfigurationNode node) throws ConfigurateException {}
@SuppressWarnings("unchecked") // Cast to generic
@Override
public void load() throws ConfigException {
reload();
save();
}
@SuppressWarnings("unchecked")
@Override
public void reload() throws ConfigException {
public void reload(boolean forceSave, AtomicBoolean anyMissingOptions) throws ConfigException {
T defaultConfig = createConfiguration();
Class<T> defaultConfigClass = (Class<T>) 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<T>) 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<T, LT extends AbstractConfigurati
@Override
public void save(AbstractConfigurationLoader<CommentedConfigurationNode> loader) throws ConfigException {
try {
SAVE_OR_LOAD.set(true);
CommentedConfigurationNode node = loader.createNode();
save(configuration, (Class<T>) configuration.getClass(), node);
// Save configuration to the node
objectMapper().get((Class<T>) 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<T> clazz, CommentedConfigurationNode node) throws SerializationException {
try {
SAVE_OR_LOAD.set(true);
objectMapper().get(clazz).save(config, node);
} finally {
SAVE_OR_LOAD.set(false);
}

View File

@ -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<T extends Config, LT extends Abstr
return discordSRV.defaultLocale();
}
@Override
public void load() throws ConfigException {
super.reload();
translate();
super.save();
}
@Override
protected String header() {
if (header != null) {
@ -72,41 +63,19 @@ public abstract class TranslatedConfigManager<T extends Config, LT extends Abstr
}
@Override
protected @Nullable ConfigurationNode getTranslation() throws ConfigurateException {
ConfigurationNode translation = getTranslationRoot();
if (translation == null) {
return null;
}
translation = translation.copy();
translation.node("_comments").set(null);
return translation;
}
protected void translate(CommentedConfigurationNode node) throws ConfigurateException {
String fileName = fileName();
@SuppressWarnings("unchecked")
public void translate() throws ConfigException {
T config = config();
if (config == null) {
ConfigurationNode translationRoot = getTranslationRoot();
if (translationRoot == null) {
return;
}
try {
ConfigurationNode translationRoot = getTranslationRoot();
if (translationRoot == null) {
return;
}
ConfigurationNode translation = translationRoot.node(fileName);
ConfigurationNode comments = translationRoot.node(fileName + "_comments");
String fileIdentifier = config.getFileName();
ConfigurationNode translation = translationRoot.node(fileIdentifier);
ConfigurationNode comments = translationRoot.node(fileIdentifier + "_comments");
CommentedConfigurationNode node = loader().createNode();
this.header = comments.node("$header").getString();
save(config, (Class<T>) 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 {

View File

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

View File

@ -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<String, List<Long>> consoleThreadRotationIds = new HashMap<>();
}
}