diff --git a/api/src/main/java/com/discordsrv/api/discord/entity/message/ReceivedDiscordMessage.java b/api/src/main/java/com/discordsrv/api/discord/entity/message/ReceivedDiscordMessage.java index cc449093..a87b15ae 100644 --- a/api/src/main/java/com/discordsrv/api/discord/entity/message/ReceivedDiscordMessage.java +++ b/api/src/main/java/com/discordsrv/api/discord/entity/message/ReceivedDiscordMessage.java @@ -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.DiscordGuildMember; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; import java.util.List; @@ -43,10 +44,11 @@ import java.util.concurrent.CompletableFuture; public interface ReceivedDiscordMessage extends Snowflake { /** - * Gets the content of this message. - * @return the message content + * Gets the content of this message. This will return {@code null} if the bot doesn't have the MESSAGE_CONTENT intent, + * 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(); /** diff --git a/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java b/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java index d4835764..516bdfca 100644 --- a/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java +++ b/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java @@ -33,6 +33,7 @@ import com.discordsrv.common.channel.GlobalChannelLookupModule; import com.discordsrv.common.command.game.GameCommandModule; import com.discordsrv.common.component.ComponentFactory; 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.MainConfig; 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.storage.Storage; 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 net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDAInfo; @@ -133,12 +136,15 @@ public abstract class AbstractDiscordSRV updateChecker.check(true)); + } + if (initial) { + scheduler().runAtFixedRate(() -> updateChecker.check(false), 6L, TimeUnit.HOURS); + } + if (flags.contains(ReloadFlag.LINKED_ACCOUNT_PROVIDER)) { LinkedAccountConfig linkedAccountConfig = config().linkedAccounts; if (linkedAccountConfig != null && linkedAccountConfig.enabled) { diff --git a/common/src/main/java/com/discordsrv/common/config/connection/BotConfig.java b/common/src/main/java/com/discordsrv/common/config/connection/BotConfig.java new file mode 100644 index 00000000..85e8e251 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/config/connection/BotConfig.java @@ -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 . + */ + +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"; + +} diff --git a/common/src/main/java/com/discordsrv/common/config/connection/ConnectionConfig.java b/common/src/main/java/com/discordsrv/common/config/connection/ConnectionConfig.java index 8b938959..5611de89 100644 --- a/common/src/main/java/com/discordsrv/common/config/connection/ConnectionConfig.java +++ b/common/src/main/java/com/discordsrv/common/config/connection/ConnectionConfig.java @@ -20,7 +20,6 @@ package com.discordsrv.common.config.connection; import com.discordsrv.common.config.Config; import org.spongepowered.configurate.objectmapping.ConfigSerializable; -import org.spongepowered.configurate.objectmapping.meta.Comment; @ConfigSerializable public class ConnectionConfig implements Config { @@ -32,15 +31,9 @@ public class ConnectionConfig implements Config { return FILE_NAME; } - public Bot bot = new Bot(); + public BotConfig bot = new BotConfig(); public StorageConfig storage = new StorageConfig(); - @ConfigSerializable - public static class Bot { - - @Comment("Don't know what this is? Neither do I") - public String token = "Token here"; - - } + public UpdateConfig update = new UpdateConfig(); } diff --git a/common/src/main/java/com/discordsrv/common/config/connection/UpdateConfig.java b/common/src/main/java/com/discordsrv/common/config/connection/UpdateConfig.java new file mode 100644 index 00000000..ed977182 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/config/connection/UpdateConfig.java @@ -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 . + */ + +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; + + } +} diff --git a/common/src/main/java/com/discordsrv/common/config/manager/ConnectionConfigManager.java b/common/src/main/java/com/discordsrv/common/config/manager/ConnectionConfigManager.java index 4265c58f..07d85ea7 100644 --- a/common/src/main/java/com/discordsrv/common/config/manager/ConnectionConfigManager.java +++ b/common/src/main/java/com/discordsrv/common/config/manager/ConnectionConfigManager.java @@ -22,6 +22,7 @@ import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.config.connection.ConnectionConfig; import com.discordsrv.common.config.manager.loader.YamlConfigLoaderProvider; import com.discordsrv.common.config.manager.manager.TranslatedConfigManager; +import org.spongepowered.configurate.ConfigurationOptions; import org.spongepowered.configurate.yaml.YamlConfigurationLoader; public abstract class ConnectionConfigManager @@ -32,6 +33,18 @@ public abstract class ConnectionConfigManager 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 protected String fileName() { return ConnectionConfig.FILE_NAME; diff --git a/common/src/main/java/com/discordsrv/common/config/manager/manager/TranslatedConfigManager.java b/common/src/main/java/com/discordsrv/common/config/manager/manager/TranslatedConfigManager.java index d22fa20f..f7f2f7b3 100644 --- a/common/src/main/java/com/discordsrv/common/config/manager/manager/TranslatedConfigManager.java +++ b/common/src/main/java/com/discordsrv/common/config/manager/manager/TranslatedConfigManager.java @@ -25,6 +25,7 @@ import org.jetbrains.annotations.Nullable; import org.spongepowered.configurate.CommentedConfigurationNode; import org.spongepowered.configurate.ConfigurateException; import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.ConfigurationOptions; import org.spongepowered.configurate.loader.AbstractConfigurationLoader; import org.spongepowered.configurate.serialize.SerializationException; import org.spongepowered.configurate.yaml.YamlConfigurationLoader; @@ -37,6 +38,8 @@ import java.util.List; public abstract class TranslatedConfigManager> extends ConfigurateConfigManager { + private String header; + public TranslatedConfigManager(DiscordSRV discordSRV) { super(discordSRV); } @@ -48,6 +51,15 @@ public abstract class TranslatedConfigManager) config.getClass(), node); translateNode(node, translation, comments); } catch (ConfigurateException e) { diff --git a/common/src/main/java/com/discordsrv/common/discord/connection/jda/JDAConnectionManager.java b/common/src/main/java/com/discordsrv/common/discord/connection/jda/JDAConnectionManager.java index 7b269bb3..b02d4ec8 100644 --- a/common/src/main/java/com/discordsrv/common/discord/connection/jda/JDAConnectionManager.java +++ b/common/src/main/java/com/discordsrv/common/discord/connection/jda/JDAConnectionManager.java @@ -27,6 +27,7 @@ import com.discordsrv.api.event.events.lifecycle.DiscordSRVShuttingDownEvent; import com.discordsrv.api.event.events.placeholder.PlaceholderLookupEvent; import com.discordsrv.api.placeholder.PlaceholderLookupResult; import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.config.connection.BotConfig; import com.discordsrv.common.config.connection.ConnectionConfig; import com.discordsrv.common.discord.api.DiscordAPIImpl; 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 MessageRequest.setDefaultMentions(Collections.emptyList()); + // Disable this warning (that doesn't even have a stacktrace) + Message.suppressContentIntentWarning(); + discordSRV.eventBus().subscribe(this); } @@ -247,7 +251,7 @@ public class JDAConnectionManager implements DiscordConnectionManager { TimeUnit.SECONDS ); - ConnectionConfig.Bot botConfig = discordSRV.connectionConfig().bot; + BotConfig botConfig = discordSRV.connectionConfig().bot; DiscordConnectionDetails connectionDetails = discordSRV.discordConnectionDetails(); Set intents = connectionDetails.getGatewayIntents(); boolean membersIntent = intents.contains(GatewayIntent.GUILD_MEMBERS); diff --git a/common/src/main/java/com/discordsrv/common/logging/impl/DiscordSRVLogger.java b/common/src/main/java/com/discordsrv/common/logging/impl/DiscordSRVLogger.java index 26569764..a9ea153b 100644 --- a/common/src/main/java/com/discordsrv/common/logging/impl/DiscordSRVLogger.java +++ b/common/src/main/java/com/discordsrv/common/logging/impl/DiscordSRVLogger.java @@ -100,6 +100,12 @@ public class DiscordSRVLogger implements Logger { @Override 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) { Permission permission = ((InsufficientPermissionException) throwable).getPermission(); String msg = "The bot is missing the \"" + permission.getName() + "\" permission"; diff --git a/common/src/main/java/com/discordsrv/common/scheduler/Scheduler.java b/common/src/main/java/com/discordsrv/common/scheduler/Scheduler.java index 84d13782..6c44abb1 100644 --- a/common/src/main/java/com/discordsrv/common/scheduler/Scheduler.java +++ b/common/src/main/java/com/discordsrv/common/scheduler/Scheduler.java @@ -83,7 +83,7 @@ public interface Scheduler { 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 rate the rate in the given unit @@ -91,7 +91,7 @@ public interface Scheduler { */ @ApiStatus.NonExtendable default ScheduledFuture runAtFixedRate(@NotNull Runnable task, long rate, @NotNull TimeUnit unit) { - return runAtFixedRate(task, 0, rate, unit); + return runAtFixedRate(task, rate, rate, unit); } /** diff --git a/common/src/main/java/com/discordsrv/common/update/UpdateChecker.java b/common/src/main/java/com/discordsrv/common/update/UpdateChecker.java new file mode 100644 index 00000000..61490ba6 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/update/UpdateChecker.java @@ -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 . + */ + +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 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 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 releases = discordSRV.json().readValue(responseBody.byteStream(), new TypeReference>() {}); + + 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))) + ); + } +} diff --git a/common/src/main/java/com/discordsrv/common/update/VersionCheck.java b/common/src/main/java/com/discordsrv/common/update/VersionCheck.java new file mode 100644 index 00000000..041dfa2f --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/update/VersionCheck.java @@ -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 . + */ + +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 securityIssues; + + public enum Status { + UP_TO_DATE, + OUTDATED, + UNKNOWN + } + + public enum AmountSource { + GITHUB + } +} diff --git a/common/src/main/java/com/discordsrv/common/update/github/GitHubCompareResponse.java b/common/src/main/java/com/discordsrv/common/update/github/GitHubCompareResponse.java new file mode 100644 index 00000000..b8015b06 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/update/github/GitHubCompareResponse.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.update.github; + +public class GitHubCompareResponse { + + public String status; + public int behind_by; + +} diff --git a/common/src/main/java/com/discordsrv/common/update/github/GithubRelease.java b/common/src/main/java/com/discordsrv/common/update/github/GithubRelease.java new file mode 100644 index 00000000..7440166d --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/update/github/GithubRelease.java @@ -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 . + */ + +package com.discordsrv.common.update.github; + +public class GithubRelease { + + public String tag_name; +} diff --git a/i18n/src/main/java/com/discordsrv/config/DiscordSRVTranslation.java b/i18n/src/main/java/com/discordsrv/config/DiscordSRVTranslation.java index 7eec2588..70f8ca43 100644 --- a/i18n/src/main/java/com/discordsrv/config/DiscordSRVTranslation.java +++ b/i18n/src/main/java/com/discordsrv/config/DiscordSRVTranslation.java @@ -81,6 +81,11 @@ public final class DiscordSRVTranslation { Config config = (Config) configManager.createConfiguration(); String fileIdentifier = config.getFileName(); ConfigurationNode commentSection = node.node(fileIdentifier + "_comments"); + + String header = configManager.defaultOptions().header(); + if (header != null) { + commentSection.node("$header").set(header); + } ObjectMapper.Factory mapperFactory = configManager.configObjectMapperBuilder() .addProcessor(Untranslated.class, untranslatedProcessorFactory)