Update checker

This commit is contained in:
Vankka 2022-12-27 22:46:28 +02:00
parent bab1d4a765
commit 29f48944d3
No known key found for this signature in database
GPG Key ID: 6E50CB7A29B96AD0
15 changed files with 600 additions and 16 deletions

View File

@ -31,6 +31,7 @@ import com.discordsrv.api.discord.entity.channel.DiscordTextChannel;
import com.discordsrv.api.discord.entity.guild.DiscordGuild; import com.discordsrv.api.discord.entity.guild.DiscordGuild;
import com.discordsrv.api.discord.entity.guild.DiscordGuildMember; import com.discordsrv.api.discord.entity.guild.DiscordGuildMember;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable; import org.jetbrains.annotations.Unmodifiable;
import java.util.List; import java.util.List;
@ -43,10 +44,11 @@ import java.util.concurrent.CompletableFuture;
public interface ReceivedDiscordMessage extends Snowflake { public interface ReceivedDiscordMessage extends Snowflake {
/** /**
* Gets the content of this message. * Gets the content of this message. This will return {@code null} if the bot doesn't have the MESSAGE_CONTENT intent,
* @return the message content * and this message was not from the bot, did not mention the bot and was not a direct message.
* @return the message content or {@code null}
*/ */
@NotNull @Nullable
String getContent(); String getContent();
/** /**

View File

@ -33,6 +33,7 @@ import com.discordsrv.common.channel.GlobalChannelLookupModule;
import com.discordsrv.common.command.game.GameCommandModule; import com.discordsrv.common.command.game.GameCommandModule;
import com.discordsrv.common.component.ComponentFactory; import com.discordsrv.common.component.ComponentFactory;
import com.discordsrv.common.config.connection.ConnectionConfig; import com.discordsrv.common.config.connection.ConnectionConfig;
import com.discordsrv.common.config.connection.UpdateConfig;
import com.discordsrv.common.config.main.LinkedAccountConfig; import com.discordsrv.common.config.main.LinkedAccountConfig;
import com.discordsrv.common.config.main.MainConfig; import com.discordsrv.common.config.main.MainConfig;
import com.discordsrv.common.config.manager.ConnectionConfigManager; import com.discordsrv.common.config.manager.ConnectionConfigManager;
@ -71,6 +72,8 @@ import com.discordsrv.common.placeholder.result.ComponentResultStringifier;
import com.discordsrv.common.profile.ProfileManager; import com.discordsrv.common.profile.ProfileManager;
import com.discordsrv.common.storage.Storage; import com.discordsrv.common.storage.Storage;
import com.discordsrv.common.storage.StorageType; import com.discordsrv.common.storage.StorageType;
import com.discordsrv.common.update.UpdateChecker;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDAInfo; import net.dv8tion.jda.api.JDAInfo;
@ -133,12 +136,15 @@ public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainCon
private LinkProvider linkProvider; private LinkProvider linkProvider;
// Version // Version
private UpdateChecker updateChecker;
private String version; private String version;
private String gitRevision; private String gitRevision;
private String gitBranch; private String gitBranch;
private OkHttpClient httpClient; private OkHttpClient httpClient;
private final ObjectMapper objectMapper = new ObjectMapper(); private final ObjectMapper objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// Internal // Internal
private final ReentrantLock lifecycleLock = new ReentrantLock(); private final ReentrantLock lifecycleLock = new ReentrantLock();
@ -165,6 +171,7 @@ public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainCon
this.discordConnectionDetails = new DiscordConnectionDetailsImpl(this); this.discordConnectionDetails = new DiscordConnectionDetailsImpl(this);
this.discordConnectionManager = new JDAConnectionManager(this); this.discordConnectionManager = new JDAConnectionManager(this);
this.channelConfig = new ChannelConfigHelper(this); this.channelConfig = new ChannelConfigHelper(this);
this.updateChecker = new UpdateChecker(this);
readManifest(); readManifest();
Dispatcher dispatcher = new Dispatcher(); Dispatcher dispatcher = new Dispatcher();
@ -587,6 +594,27 @@ public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainCon
channelConfig().reload(); channelConfig().reload();
} }
// Update check
UpdateConfig updateConfig = connectionConfig().update;
if (updateConfig.security.enabled) {
if (updateChecker.isSecurityFailed()) {
// Security has already failed
return;
}
if (initial && !updateChecker.check(true)) {
// Security failed cancel startup & shutdown
invokeDisable();
return;
}
} else if (initial) {
// Not using security, run update check off thread
scheduler().run(() -> updateChecker.check(true));
}
if (initial) {
scheduler().runAtFixedRate(() -> updateChecker.check(false), 6L, TimeUnit.HOURS);
}
if (flags.contains(ReloadFlag.LINKED_ACCOUNT_PROVIDER)) { if (flags.contains(ReloadFlag.LINKED_ACCOUNT_PROVIDER)) {
LinkedAccountConfig linkedAccountConfig = config().linkedAccounts; LinkedAccountConfig linkedAccountConfig = config().linkedAccounts;
if (linkedAccountConfig != null && linkedAccountConfig.enabled) { if (linkedAccountConfig != null && linkedAccountConfig.enabled) {

View File

@ -0,0 +1,31 @@
/*
* 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.connection;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
@ConfigSerializable
public class BotConfig {
@Comment("The Discord bot token from https://discord.com/developers/applications\n"
+ "Requires a connection to: discord.com, gateway.discord.gg, cdn.discordapp.com")
public String token = "Token here";
}

View File

@ -20,7 +20,6 @@ package com.discordsrv.common.config.connection;
import com.discordsrv.common.config.Config; import com.discordsrv.common.config.Config;
import org.spongepowered.configurate.objectmapping.ConfigSerializable; import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
@ConfigSerializable @ConfigSerializable
public class ConnectionConfig implements Config { public class ConnectionConfig implements Config {
@ -32,15 +31,9 @@ public class ConnectionConfig implements Config {
return FILE_NAME; return FILE_NAME;
} }
public Bot bot = new Bot(); public BotConfig bot = new BotConfig();
public StorageConfig storage = new StorageConfig(); public StorageConfig storage = new StorageConfig();
@ConfigSerializable public UpdateConfig update = new UpdateConfig();
public static class Bot {
@Comment("Don't know what this is? Neither do I")
public String token = "Token here";
}
} }

View File

@ -0,0 +1,82 @@
/*
* 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.connection;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
import org.spongepowered.configurate.objectmapping.meta.Setting;
@ConfigSerializable
public class UpdateConfig {
@Setting(value = "notification-enabled")
@Comment("On/off for notifications when a new version of DiscordSRV is available")
public boolean notificationEnabled = true;
@Setting(value = "notification-ingame")
@Comment("If players with the discordsrv.updatenotification permission should receive\n"
+ "a update notification upon joining if there is a update available")
public boolean notificationInGame = true;
@Setting(value = "enable-first-party-api-for-notifications")
@Comment("Weather the DiscordSRV download API should be used for update checks\n"
+ "Requires a connection to: download.discordsrv.com")
public boolean firstPartyNotification = true;
@Setting(value = "github")
public GitHub github = new GitHub();
@Setting(value = "security")
public Security security = new Security();
@ConfigSerializable
public static class GitHub {
@Setting(value = "enabled")
@Comment("Weather the GitHub API should be used for update checks\n"
+ "This will be the secondary API if both first party and GitHub sources are enabled\n"
+ "Requires a connection to: api.github.com")
public boolean enabled = true;
@Setting(value = "api-token")
@Comment("The GitHub API token used for authenticating to the GitHub api,\n"
+ "if this isn't specified the API will be used 'anonymously'\n"
+ "The token only requires read permission to DiscordSRV/DiscordSRV releases, workflows and commits")
public String apiToken = "";
}
@ConfigSerializable
public static class Security {
@Setting(value = "enabled")
@Comment("Uses the DiscordSRV download API to check if the version of DiscordSRV\n"
+ "being used is vulnerable to known vulnerabilities, disabling the plugin if it is.\n"
+ "Requires a connection to: download.discordsrv.com\n"
+ "\n"
+ "WARNING! DO NOT TURN THIS OFF UNLESS YOU KNOW WHAT YOU'RE DOING AND STAY UP-TO-DATE")
public boolean enabled = true;
@Setting(value = "force")
@Comment("If the security check needs to be completed for DiscordSRV to enable,\n"
+ "if the security check fails, DiscordSRV will be disabled if this option is set to true")
public boolean force = false;
}
}

View File

@ -22,6 +22,7 @@ import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.config.connection.ConnectionConfig; import com.discordsrv.common.config.connection.ConnectionConfig;
import com.discordsrv.common.config.manager.loader.YamlConfigLoaderProvider; import com.discordsrv.common.config.manager.loader.YamlConfigLoaderProvider;
import com.discordsrv.common.config.manager.manager.TranslatedConfigManager; import com.discordsrv.common.config.manager.manager.TranslatedConfigManager;
import org.spongepowered.configurate.ConfigurationOptions;
import org.spongepowered.configurate.yaml.YamlConfigurationLoader; import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
public abstract class ConnectionConfigManager<C extends ConnectionConfig> public abstract class ConnectionConfigManager<C extends ConnectionConfig>
@ -32,6 +33,18 @@ public abstract class ConnectionConfigManager<C extends ConnectionConfig>
super(discordSRV); super(discordSRV);
} }
@Override
public ConfigurationOptions defaultOptions() {
return super.defaultOptions()
.header("DiscordSRV's configuration file for connections to different external services.\n"
+ "This file is intended to contain connection details to services in order to keep them out of the config.yml\n"
+ "and to serve as a easy way to identify and control what external connections are being used.\n"
+ "\n"
+ "All domains listed as \"Requires a connection to\" require port 443 (https/wss) unless otherwise specified\n"
+ "\n"
+ " ABSOLUTELY DO NOT SEND THIS FILE TO ANYONE - IT ONLY CONTAINS SECRETS\n");
}
@Override @Override
protected String fileName() { protected String fileName() {
return ConnectionConfig.FILE_NAME; return ConnectionConfig.FILE_NAME;

View File

@ -25,6 +25,7 @@ import org.jetbrains.annotations.Nullable;
import org.spongepowered.configurate.CommentedConfigurationNode; import org.spongepowered.configurate.CommentedConfigurationNode;
import org.spongepowered.configurate.ConfigurateException; import org.spongepowered.configurate.ConfigurateException;
import org.spongepowered.configurate.ConfigurationNode; import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.ConfigurationOptions;
import org.spongepowered.configurate.loader.AbstractConfigurationLoader; import org.spongepowered.configurate.loader.AbstractConfigurationLoader;
import org.spongepowered.configurate.serialize.SerializationException; import org.spongepowered.configurate.serialize.SerializationException;
import org.spongepowered.configurate.yaml.YamlConfigurationLoader; import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
@ -37,6 +38,8 @@ import java.util.List;
public abstract class TranslatedConfigManager<T extends Config, LT extends AbstractConfigurationLoader<CommentedConfigurationNode>> public abstract class TranslatedConfigManager<T extends Config, LT extends AbstractConfigurationLoader<CommentedConfigurationNode>>
extends ConfigurateConfigManager<T, LT> { extends ConfigurateConfigManager<T, LT> {
private String header;
public TranslatedConfigManager(DiscordSRV discordSRV) { public TranslatedConfigManager(DiscordSRV discordSRV) {
super(discordSRV); super(discordSRV);
} }
@ -48,6 +51,15 @@ public abstract class TranslatedConfigManager<T extends Config, LT extends Abstr
super.save(); super.save();
} }
@Override
public ConfigurationOptions defaultOptions() {
ConfigurationOptions options = super.defaultOptions();
if (header != null) {
options = options.header(header);
}
return options;
}
@Override @Override
protected @Nullable ConfigurationNode getTranslation() throws ConfigurateException { protected @Nullable ConfigurationNode getTranslation() throws ConfigurateException {
ConfigurationNode translation = getTranslationRoot(); ConfigurationNode translation = getTranslationRoot();
@ -77,6 +89,8 @@ public abstract class TranslatedConfigManager<T extends Config, LT extends Abstr
ConfigurationNode comments = translationRoot.node(fileIdentifier + "_comments"); ConfigurationNode comments = translationRoot.node(fileIdentifier + "_comments");
CommentedConfigurationNode node = loader().createNode(); CommentedConfigurationNode node = loader().createNode();
this.header = comments.node("$header").getString();
save(config, (Class<T>) config.getClass(), node); save(config, (Class<T>) config.getClass(), node);
translateNode(node, translation, comments); translateNode(node, translation, comments);
} catch (ConfigurateException e) { } catch (ConfigurateException e) {

View File

@ -27,6 +27,7 @@ import com.discordsrv.api.event.events.lifecycle.DiscordSRVShuttingDownEvent;
import com.discordsrv.api.event.events.placeholder.PlaceholderLookupEvent; import com.discordsrv.api.event.events.placeholder.PlaceholderLookupEvent;
import com.discordsrv.api.placeholder.PlaceholderLookupResult; import com.discordsrv.api.placeholder.PlaceholderLookupResult;
import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.config.connection.BotConfig;
import com.discordsrv.common.config.connection.ConnectionConfig; import com.discordsrv.common.config.connection.ConnectionConfig;
import com.discordsrv.common.discord.api.DiscordAPIImpl; import com.discordsrv.common.discord.api.DiscordAPIImpl;
import com.discordsrv.common.discord.api.entity.message.ReceivedDiscordMessageImpl; import com.discordsrv.common.discord.api.entity.message.ReceivedDiscordMessageImpl;
@ -101,6 +102,9 @@ public class JDAConnectionManager implements DiscordConnectionManager {
// Disable all mentions by default for safety // Disable all mentions by default for safety
MessageRequest.setDefaultMentions(Collections.emptyList()); MessageRequest.setDefaultMentions(Collections.emptyList());
// Disable this warning (that doesn't even have a stacktrace)
Message.suppressContentIntentWarning();
discordSRV.eventBus().subscribe(this); discordSRV.eventBus().subscribe(this);
} }
@ -247,7 +251,7 @@ public class JDAConnectionManager implements DiscordConnectionManager {
TimeUnit.SECONDS TimeUnit.SECONDS
); );
ConnectionConfig.Bot botConfig = discordSRV.connectionConfig().bot; BotConfig botConfig = discordSRV.connectionConfig().bot;
DiscordConnectionDetails connectionDetails = discordSRV.discordConnectionDetails(); DiscordConnectionDetails connectionDetails = discordSRV.discordConnectionDetails();
Set<GatewayIntent> intents = connectionDetails.getGatewayIntents(); Set<GatewayIntent> intents = connectionDetails.getGatewayIntents();
boolean membersIntent = intents.contains(GatewayIntent.GUILD_MEMBERS); boolean membersIntent = intents.contains(GatewayIntent.GUILD_MEMBERS);

View File

@ -100,6 +100,12 @@ public class DiscordSRVLogger implements Logger {
@Override @Override
public void log(@Nullable String loggerName, @NotNull LogLevel logLevel, @Nullable String message, @Nullable Throwable throwable) { public void log(@Nullable String loggerName, @NotNull LogLevel logLevel, @Nullable String message, @Nullable Throwable throwable) {
if (throwable != null && throwable.getMessage() != null
&& (throwable.getStackTrace() == null || throwable.getStackTrace().length == 0)) {
// Empty stack trace
message = (message != null ? message + ": " : "") + throwable.getMessage();
throwable = null;
}
if (throwable instanceof InsufficientPermissionException) { if (throwable instanceof InsufficientPermissionException) {
Permission permission = ((InsufficientPermissionException) throwable).getPermission(); Permission permission = ((InsufficientPermissionException) throwable).getPermission();
String msg = "The bot is missing the \"" + permission.getName() + "\" permission"; String msg = "The bot is missing the \"" + permission.getName() + "\" permission";

View File

@ -83,7 +83,7 @@ public interface Scheduler {
ScheduledFuture<?> runLater(Runnable task, long timeMillis); ScheduledFuture<?> runLater(Runnable task, long timeMillis);
/** /**
* Schedules the given task without any initial delay. * Schedules the given task at the given rate.
* *
* @param task the task * @param task the task
* @param rate the rate in the given unit * @param rate the rate in the given unit
@ -91,7 +91,7 @@ public interface Scheduler {
*/ */
@ApiStatus.NonExtendable @ApiStatus.NonExtendable
default ScheduledFuture<?> runAtFixedRate(@NotNull Runnable task, long rate, @NotNull TimeUnit unit) { default ScheduledFuture<?> runAtFixedRate(@NotNull Runnable task, long rate, @NotNull TimeUnit unit) {
return runAtFixedRate(task, 0, rate, unit); return runAtFixedRate(task, rate, rate, unit);
} }
/** /**

View File

@ -0,0 +1,313 @@
/*
* 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.update;
import com.discordsrv.api.event.bus.Subscribe;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.config.connection.ConnectionConfig;
import com.discordsrv.common.config.connection.UpdateConfig;
import com.discordsrv.common.exception.MessageException;
import com.discordsrv.common.logging.NamedLogger;
import com.discordsrv.common.player.IPlayer;
import com.discordsrv.common.player.event.PlayerConnectedEvent;
import com.discordsrv.common.update.github.GitHubCompareResponse;
import com.discordsrv.common.update.github.GithubRelease;
import com.fasterxml.jackson.core.type.TypeReference;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class UpdateChecker {
private static final String USER_DOWNLOAD_URL = "https://discordsrv.com/download";
private static final String GITHUB_REPOSITORY = "DiscordSRV/DiscordSRV";
private static final String GITHUB_DEV_BRANCH = "master";
private static final String DOWNLOAD_SERVICE_HOST = "https://download.discordsrv.com";
private static final String DOWNLOAD_SERVICE_SNAPSHOT_CHANNEL = "snapshot";
private static final String DOWNLOAD_SERVICE_RELEASE_CHANNEL = "release";
private static final String GITHUB_API_HOST = "https://api.github.com";
private final DiscordSRV discordSRV;
private final NamedLogger logger;
private boolean securityFailed = false;
private VersionCheck latestCheck;
private VersionCheck loggedCheck;
public UpdateChecker(DiscordSRV discordSRV) {
this.discordSRV = discordSRV;
this.logger = new NamedLogger(discordSRV, "UPDATES");
discordSRV.eventBus().subscribe(this);
}
public boolean isSecurityFailed() {
return securityFailed;
}
/**
* @return if enabling is permitted
*/
public boolean check(boolean logUpToDate) {
UpdateConfig config = discordSRV.connectionConfig().update;
boolean isSnapshot = discordSRV.version().endsWith("-SNAPSHOT");
boolean isSecurity = config.security.enabled;
boolean isFirstPartyNotification = config.firstPartyNotification;
boolean isNotification = config.notificationEnabled;
if (!isSecurity && !isNotification) {
// Nothing to do
return true;
}
if (isFirstPartyNotification || isSecurity) {
VersionCheck check = null;
try {
check = checkFirstParty(isSnapshot);
if (check == null && isSecurity) {
securityFailed = true;
return false;
}
} catch (Throwable t) {
List<String> failedThings = new ArrayList<>(2);
if (isSecurity) {
failedThings.add("perform version security check");
}
if (isFirstPartyNotification) {
failedThings.add("check for updates");
}
logger.warning("Failed to " + String.join(" and ", failedThings)
+ " from the first party API", t);
if (config.security.force) {
logger.error("Security check is required (as configured in " + ConnectionConfig.FILE_NAME + ")"
+ ", startup will be cancelled.");
securityFailed = true;
return false;
}
}
if (isNotification && isFirstPartyNotification
&& check != null && check.status != VersionCheck.Status.UNKNOWN) {
latestCheck = check;
log(check, logUpToDate);
return true;
}
}
if (isNotification && config.github.enabled) {
VersionCheck check = null;
try {
check = checkGitHub(isSnapshot);
} catch (Throwable t) {
logger.warning("Failed to check for updates from GitHub", t);
}
if (check != null && check.status != VersionCheck.Status.UNKNOWN) {
latestCheck = check;
log(check, logUpToDate);
return true;
} else {
discordSRV.logger().error("Update check" + (isFirstPartyNotification ? "s" : "")
+ " failed: unknown version");
}
}
return true;
}
private ResponseBody checkResponse(Request request, Response response, ResponseBody responseBody) throws IOException {
if (responseBody == null || !response.isSuccessful()) {
String responseString = responseBody == null
? "response body is null"
: StringUtils.substring(responseBody.string(), 0, 500);
throw new MessageException("Request to " + request.url().host() + " failed: " + response.code() + ": " + responseString);
}
return responseBody;
}
/**
* @return {@code null} for preventing shutdown
*/
private VersionCheck checkFirstParty(boolean isSnapshot) throws IOException {
Request request = new Request.Builder()
.url(DOWNLOAD_SERVICE_HOST + "/v2/" + GITHUB_REPOSITORY
+ "/" + (isSnapshot ? DOWNLOAD_SERVICE_SNAPSHOT_CHANNEL : DOWNLOAD_SERVICE_RELEASE_CHANNEL)
+ "/version-check/" + (isSnapshot ? discordSRV.gitRevision() : discordSRV.version()))
.get().build();
String responseString;
try (Response response = discordSRV.httpClient().newCall(request).execute()) {
ResponseBody responseBody = checkResponse(request, response, response.body());
responseString = responseBody.string();
}
VersionCheck versionCheck = discordSRV.json().readValue(responseString, VersionCheck.class);
if (versionCheck == null) {
throw new MessageException("Failed to parse " + request.url() + " response body: " + StringUtils.substring(responseString, 0, 500));
}
boolean insecure = versionCheck.insecure;
List<String> securityIssues = versionCheck.securityIssues;
if (insecure) {
logger.error("This version of DiscordSRV is insecure, security issues are listed below, startup will be cancelled.");
for (String securityIssue : versionCheck.securityIssues) {
logger.error(securityIssue);
}
// Block startup
return null;
} else if (securityIssues != null && !securityIssues.isEmpty()) {
logger.warning("There are security warnings for this version of DiscordSRV, listed below");
for (String securityIssue : versionCheck.securityIssues) {
logger.warning(securityIssue);
}
}
return versionCheck;
}
private VersionCheck checkGitHub(boolean isSnapshot) throws IOException {
if (isSnapshot) {
Request request = new Request.Builder()
.url(GITHUB_API_HOST + "/repos/" + GITHUB_REPOSITORY + "/compare/"
+ GITHUB_DEV_BRANCH + "..." + discordSRV.gitRevision() + "?per_page=0")
.get().build();
try (Response response = discordSRV.httpClient().newCall(request).execute()) {
ResponseBody responseBody = checkResponse(request, response, response.body());
GitHubCompareResponse compare = discordSRV.json().readValue(responseBody.byteStream(), GitHubCompareResponse.class);
VersionCheck versionCheck = new VersionCheck();
versionCheck.amount = compare.behind_by;
versionCheck.amountType = (compare.behind_by == 1 ? "commit" : "commits");
versionCheck.amountSource = VersionCheck.AmountSource.GITHUB;
if ("behind".equals(compare.status)) {
versionCheck.status = VersionCheck.Status.OUTDATED;
} else if ("identical".equals(compare.status)) {
versionCheck.status = VersionCheck.Status.UP_TO_DATE;
} else {
versionCheck.status = VersionCheck.Status.UNKNOWN;
}
return versionCheck;
}
}
String version = discordSRV.version();
int versionsBehind = 0;
boolean found = false;
int perPage = 100;
int page = 0;
for (int i = 0; i < 3 /* max 3 loops */; i++) {
Request request = new Request.Builder()
.url(GITHUB_API_HOST + "/repos/" + GITHUB_REPOSITORY + "/releases?per_page=" + perPage + "&page=" + page)
.get().build();
try (Response response = discordSRV.httpClient().newCall(request).execute()) {
ResponseBody responseBody = checkResponse(request, response, response.body());
List<GithubRelease> releases = discordSRV.json().readValue(responseBody.byteStream(), new TypeReference<List<GithubRelease>>() {});
for (GithubRelease release : releases) {
if (version.equals(release.tag_name)) {
found = true;
break;
}
versionsBehind++;
}
if (found || releases.size() < perPage) {
break;
}
}
}
VersionCheck versionCheck = new VersionCheck();
versionCheck.amountSource = VersionCheck.AmountSource.GITHUB;
versionCheck.amountType = (versionsBehind == 1 ? "release" : "releases");
if (!found) {
versionCheck.status = VersionCheck.Status.UNKNOWN;
versionCheck.amount = -1;
} else {
versionCheck.status = versionsBehind == 0
? VersionCheck.Status.UP_TO_DATE
: VersionCheck.Status.OUTDATED;
versionCheck.amount = versionsBehind;
}
return versionCheck;
}
private void log(VersionCheck versionCheck, boolean logUpToDate) {
switch (versionCheck.status) {
case UP_TO_DATE: {
if (logUpToDate) {
logger.info("DiscordSRV is up-to-date.");
loggedCheck = versionCheck;
}
break;
}
case OUTDATED: {
// only log if there is new information
if (loggedCheck == null || loggedCheck.amount != versionCheck.amount) {
logger.warning(
"DiscordSRV is outdated by " + versionCheck.amount + " " + versionCheck.amountType
+ ". Get the latest version from https://discordsrv.com/dowload");
loggedCheck = versionCheck;
}
break;
}
default:
throw new IllegalStateException("Unexpected version check status: " + versionCheck.status.name());
}
}
@Subscribe
public void onPlayerConnected(PlayerConnectedEvent event) {
UpdateConfig config = discordSRV.connectionConfig().update;
if (!config.notificationEnabled || !config.notificationInGame) {
return;
}
if (latestCheck == null || latestCheck.status != VersionCheck.Status.OUTDATED) {
return;
}
IPlayer player = event.player();
if (!player.hasPermission("discordsrv.updatenotification")) {
return;
}
player.sendMessage(
Component.text("There is a new version of DiscordSRV available, ", NamedTextColor.AQUA)
.append(Component.text()
.content("click here to download it")
.clickEvent(ClickEvent.openUrl(USER_DOWNLOAD_URL)))
);
}
}

View File

@ -0,0 +1,43 @@
/*
* 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.update;
import java.util.List;
public class VersionCheck {
public Status status;
public int amount;
public AmountSource amountSource;
public String amountType;
public boolean insecure;
public List<String> securityIssues;
public enum Status {
UP_TO_DATE,
OUTDATED,
UNKNOWN
}
public enum AmountSource {
GITHUB
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.update.github;
public class GitHubCompareResponse {
public String status;
public int behind_by;
}

View File

@ -0,0 +1,24 @@
/*
* 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.update.github;
public class GithubRelease {
public String tag_name;
}

View File

@ -82,6 +82,11 @@ public final class DiscordSRVTranslation {
String fileIdentifier = config.getFileName(); String fileIdentifier = config.getFileName();
ConfigurationNode commentSection = node.node(fileIdentifier + "_comments"); ConfigurationNode commentSection = node.node(fileIdentifier + "_comments");
String header = configManager.defaultOptions().header();
if (header != null) {
commentSection.node("$header").set(header);
}
ObjectMapper.Factory mapperFactory = configManager.configObjectMapperBuilder() ObjectMapper.Factory mapperFactory = configManager.configObjectMapperBuilder()
.addProcessor(Untranslated.class, untranslatedProcessorFactory) .addProcessor(Untranslated.class, untranslatedProcessorFactory)
.build(); .build();