diff --git a/Essentials/src/main/java/com/earth2me/essentials/Essentials.java b/Essentials/src/main/java/com/earth2me/essentials/Essentials.java index 796f694b2..55ae9de73 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/Essentials.java +++ b/Essentials/src/main/java/com/earth2me/essentials/Essentials.java @@ -36,6 +36,7 @@ import com.earth2me.essentials.signs.SignPlayerListener; import com.earth2me.essentials.textreader.IText; import com.earth2me.essentials.textreader.KeywordReplacer; import com.earth2me.essentials.textreader.SimpleTextInput; +import com.earth2me.essentials.updatecheck.UpdateChecker; import com.earth2me.essentials.utils.VersionUtil; import io.papermc.lib.PaperLib; import net.ess3.api.Economy; @@ -147,6 +148,7 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials { private transient MaterialTagProvider materialTagProvider; private transient Kits kits; private transient RandomTeleport randomTeleport; + private transient UpdateChecker updateChecker; static { // TODO: improve legacy code @@ -390,6 +392,16 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials { metrics = new MetricsWrapper(this, 858, true); + updateChecker = new UpdateChecker(this); + runTaskAsynchronously(() -> { + LOGGER.log(Level.INFO, tl("versionFetching")); + for (String str : updateChecker.getVersionMessages(false, true)) { + LOGGER.log(Level.WARNING, str); + } + }); + + execTimer.mark("Init(External)"); + final String timeroutput = execTimer.end(); if (getSettings().isDebug()) { LOGGER.log(Level.INFO, "Essentials load {0}", timeroutput); @@ -776,6 +788,11 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials { return randomTeleport; } + @Override + public UpdateChecker getUpdateChecker() { + return updateChecker; + } + @Deprecated @Override public User getUser(final Object base) { diff --git a/Essentials/src/main/java/com/earth2me/essentials/EssentialsPlayerListener.java b/Essentials/src/main/java/com/earth2me/essentials/EssentialsPlayerListener.java index e2ae8d1de..9a24f3fd2 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/EssentialsPlayerListener.java +++ b/Essentials/src/main/java/com/earth2me/essentials/EssentialsPlayerListener.java @@ -422,6 +422,14 @@ public class EssentialsPlayerListener implements Listener { final TextPager pager = new TextPager(output, true); pager.showPage("1", null, "motd", user.getSource()); } + + if (user.isAuthorized("essentials.updatecheck")) { + ess.runTaskAsynchronously(() -> { + for (String str : ess.getUpdateChecker().getVersionMessages(false, false)) { + user.sendMessage(str); + } + }); + } } } } diff --git a/Essentials/src/main/java/com/earth2me/essentials/IEssentials.java b/Essentials/src/main/java/com/earth2me/essentials/IEssentials.java index 93e00c4ac..3d0e0ed17 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/IEssentials.java +++ b/Essentials/src/main/java/com/earth2me/essentials/IEssentials.java @@ -4,6 +4,7 @@ import com.earth2me.essentials.api.IItemDb; import com.earth2me.essentials.api.IJails; import com.earth2me.essentials.api.IWarps; import com.earth2me.essentials.perm.PermissionsHandler; +import com.earth2me.essentials.updatecheck.UpdateChecker; import net.ess3.provider.MaterialTagProvider; import net.ess3.provider.ContainerProvider; import net.ess3.provider.FormattedCommandAliasProvider; @@ -73,6 +74,8 @@ public interface IEssentials extends Plugin { RandomTeleport getRandomTeleport(); + UpdateChecker getUpdateChecker(); + BukkitTask runTaskAsynchronously(Runnable run); BukkitTask runTaskLaterAsynchronously(Runnable run, long delay); diff --git a/Essentials/src/main/java/com/earth2me/essentials/ISettings.java b/Essentials/src/main/java/com/earth2me/essentials/ISettings.java index 9328050eb..97c61b5aa 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/ISettings.java +++ b/Essentials/src/main/java/com/earth2me/essentials/ISettings.java @@ -391,6 +391,8 @@ public interface ISettings extends IConf { boolean isRespawnAtBed(); + boolean isUpdateCheckEnabled(); + enum KeepInvPolicy { KEEP, DELETE, diff --git a/Essentials/src/main/java/com/earth2me/essentials/Settings.java b/Essentials/src/main/java/com/earth2me/essentials/Settings.java index e4b655844..3aff06af8 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/Settings.java +++ b/Essentials/src/main/java/com/earth2me/essentials/Settings.java @@ -1767,4 +1767,9 @@ public class Settings implements net.ess3.api.ISettings { public boolean isRespawnAtBed() { return config.getBoolean("respawn-at-home-bed", true); } + + @Override + public boolean isUpdateCheckEnabled() { + return config.getBoolean("update-check", true); + } } diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandessentials.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandessentials.java index 2005c7ba3..212e9f727 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandessentials.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandessentials.java @@ -388,10 +388,16 @@ public class Commandessentials extends EssentialsCommand { sender.sendMessage(tl("serverUnsupportedLimitedApi")); break; } - if (VersionUtil.getSupportStatusClass() != null) { sender.sendMessage(tl("serverUnsupportedClass", VersionUtil.getSupportStatusClass())); } + + sender.sendMessage(tl("versionFetching")); + ess.runTaskAsynchronously(() -> { + for (String str : ess.getUpdateChecker().getVersionMessages(true, true)) { + sender.sendMessage(str); + } + }); } @Override diff --git a/Essentials/src/main/java/com/earth2me/essentials/metrics/MetricsWrapper.java b/Essentials/src/main/java/com/earth2me/essentials/metrics/MetricsWrapper.java index 0292b6fe9..33403d364 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/metrics/MetricsWrapper.java +++ b/Essentials/src/main/java/com/earth2me/essentials/metrics/MetricsWrapper.java @@ -38,6 +38,7 @@ public class MetricsWrapper { checkForcedMetrics(); addPermsChart(); addEconomyChart(); + addReleaseBranchChart(); // bStats' backend currently doesn't support multi-line charts or advanced bar charts // These are included for when bStats is ready to accept this data @@ -87,6 +88,10 @@ public class MetricsWrapper { })); } + private void addReleaseBranchChart() { + metrics.addCustomChart(new Metrics.SimplePie("releaseBranch", ess.getUpdateChecker()::getVersionBranch)); + } + private void addCommandsChart() { for (final String command : plugin.getDescription().getCommands().keySet()) { markCommand(command, false); diff --git a/Essentials/src/main/java/com/earth2me/essentials/updatecheck/UpdateChecker.java b/Essentials/src/main/java/com/earth2me/essentials/updatecheck/UpdateChecker.java new file mode 100644 index 000000000..7bc76dc3a --- /dev/null +++ b/Essentials/src/main/java/com/earth2me/essentials/updatecheck/UpdateChecker.java @@ -0,0 +1,269 @@ +package com.earth2me.essentials.updatecheck; + +import com.earth2me.essentials.Essentials; +import com.google.common.base.Charsets; +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +import static com.earth2me.essentials.I18n.tl; + +public final class UpdateChecker { + private static final String REPO = "EssentialsX/Essentials"; + private static final String BRANCH = "2.x"; + + private final Essentials ess; + private final String versionIdentifier; + private final String versionBranch; + private final boolean devBuild; + + private long lastFetchTime = 0; + private CompletableFuture pendingDevFuture; + private CompletableFuture pendingReleaseFuture; + private String latestRelease = null; + private RemoteVersion cachedDev = null; + private RemoteVersion cachedRelease = null; + + public UpdateChecker(Essentials ess) { + String identifier = "INVALID"; + String branch = "INVALID"; + boolean dev = false; + + final InputStream inputStream = UpdateChecker.class.getClassLoader().getResourceAsStream("release"); + if (inputStream != null) { + final List versionStr = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines().collect(Collectors.toList()); + if (versionStr.size() == 2) { + if (versionStr.get(0).matches("\\d+\\.\\d+\\.\\d+-dev\\+\\d\\d-[0-9a-f]{7,40}")) { + identifier = versionStr.get(0).split("-")[2]; + dev = true; + } else { + identifier = versionStr.get(0); + } + branch = versionStr.get(1); + } + } + + this.ess = ess; + this.versionIdentifier = identifier; + this.versionBranch = branch; + this.devBuild = dev; + } + + public boolean isDevBuild() { + return devBuild; + } + + public CompletableFuture fetchLatestDev() { + if (cachedDev == null || ((System.currentTimeMillis() - lastFetchTime) > 1800000L)) { + if (pendingDevFuture != null) { + return pendingDevFuture; + } + pendingDevFuture = new CompletableFuture<>(); + new Thread(() -> { + pendingDevFuture.complete(cachedDev = fetchDistance(BRANCH, getVersionIdentifier())); + pendingDevFuture = null; + lastFetchTime = System.currentTimeMillis(); + }).start(); + return pendingDevFuture; + } + return CompletableFuture.completedFuture(cachedDev); + } + + public CompletableFuture fetchLatestRelease() { + if (cachedRelease == null || ((System.currentTimeMillis() - lastFetchTime) > 1800000L)) { + if (pendingReleaseFuture != null) { + return pendingReleaseFuture; + } + pendingReleaseFuture = new CompletableFuture<>(); + new Thread(() -> { + catchBlock: + try { + final HttpURLConnection connection = (HttpURLConnection) new URL("https://api.github.com/repos/" + REPO + "/releases/latest").openConnection(); + connection.connect(); + + if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) { + // Locally built? + pendingReleaseFuture.complete(cachedRelease = new RemoteVersion(BranchStatus.UNKNOWN)); + break catchBlock; + } + if (connection.getResponseCode() == HttpURLConnection.HTTP_INTERNAL_ERROR) { + // Github is down + pendingReleaseFuture.complete(new RemoteVersion(BranchStatus.ERROR)); + break catchBlock; + } + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), Charsets.UTF_8))) { + latestRelease = new Gson().fromJson(reader, JsonObject.class).get("tag_name").getAsString(); + pendingReleaseFuture.complete(cachedRelease = fetchDistance(latestRelease, getVersionIdentifier())); + } catch (JsonSyntaxException | NumberFormatException e) { + e.printStackTrace(); + pendingReleaseFuture.complete(new RemoteVersion(BranchStatus.ERROR)); + } + } catch (IOException e) { + e.printStackTrace(); + pendingReleaseFuture.complete(new RemoteVersion(BranchStatus.ERROR)); + } + pendingReleaseFuture = null; + lastFetchTime = System.currentTimeMillis(); + }).start(); + return pendingReleaseFuture; + } + return CompletableFuture.completedFuture(cachedRelease); + } + + public String getVersionIdentifier() { + return versionIdentifier; + } + + public String getVersionBranch() { + return versionBranch; + } + + public String getBuildInfo() { + return "id:'" + getVersionIdentifier() + "' branch:'" + getVersionBranch() + "' isDev:" + isDevBuild(); + } + + public String getLatestRelease() { + return latestRelease; + } + + private RemoteVersion fetchDistance(final String head, final String hash) { + try { + final HttpURLConnection connection = (HttpURLConnection) new URL("https://api.github.com/repos/" + REPO + "/compare/" + head + "..." + hash).openConnection(); + connection.connect(); + + if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) { + // Locally built? + return new RemoteVersion(BranchStatus.UNKNOWN); + } + if (connection.getResponseCode() == HttpURLConnection.HTTP_INTERNAL_ERROR) { + // Github is down + return new RemoteVersion(BranchStatus.ERROR); + } + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), Charsets.UTF_8))) { + final JsonObject obj = new Gson().fromJson(reader, JsonObject.class); + switch (obj.get("status").getAsString()) { + case "identical": { + return new RemoteVersion(BranchStatus.IDENTICAL, 0); + } + case "ahead": { + return new RemoteVersion(BranchStatus.AHEAD, 0); + } + case "behind": { + return new RemoteVersion(BranchStatus.BEHIND, obj.get("behind_by").getAsInt()); + } + case "diverged": { + return new RemoteVersion(BranchStatus.DIVERGED, obj.get("behind_by").getAsInt()); + } + default: { + return new RemoteVersion(BranchStatus.UNKNOWN); + } + } + } catch (JsonSyntaxException | NumberFormatException e) { + e.printStackTrace(); + return new RemoteVersion(BranchStatus.ERROR); + } + } catch (IOException e) { + e.printStackTrace(); + return new RemoteVersion(BranchStatus.ERROR); + } + } + + public String[] getVersionMessages(final boolean sendLatestMessage, final boolean verboseErrors) { + if (!ess.getSettings().isUpdateCheckEnabled()) { + return new String[] {tl("versionCheckDisabled")}; + } + + if (this.isDevBuild()) { + final RemoteVersion latestDev = this.fetchLatestDev().join(); + switch (latestDev.getBranchStatus()) { + case IDENTICAL: { + return sendLatestMessage ? new String[] {tl("versionDevLatest")} : new String[] {}; + } + case BEHIND: { + return new String[] {tl("versionDevBehind", latestDev.getDistance()), + tl("versionReleaseNewLink", "https://essentialsx.net/downloads.html")}; + } + case AHEAD: + case DIVERGED: { + return new String[] {tl(latestDev.getDistance() == 0 ? "versionDevDivergedLatest" : "versionDevDiverged", latestDev.getDistance()), + tl("versionDevDivergedBranch", this.getVersionBranch()) }; + } + case UNKNOWN: { + return verboseErrors ? new String[] {tl("versionCustom", this.getBuildInfo())} : new String[] {}; + } + case ERROR: { + return new String[] {tl(verboseErrors ? "versionError" : "versionErrorPlayer", this.getBuildInfo())}; + } + default: { + return new String[] {}; + } + } + } else { + final RemoteVersion latestRelease = this.fetchLatestRelease().join(); + switch (latestRelease.getBranchStatus()) { + case IDENTICAL: { + return sendLatestMessage ? new String[] {tl("versionReleaseLatest")} : new String[] {}; + } + case BEHIND: { + return new String[] {tl("versionReleaseNew", this.getLatestRelease()), + tl("versionReleaseNewLink", "https://essentialsx.net/downloads.html?branch=stable")}; + } + case DIVERGED: //WhatChamp + case AHEAD: //monkaW? + case UNKNOWN: { + return verboseErrors ? new String[] {tl("versionCustom", this.getBuildInfo())} : new String[] {}; + } + case ERROR: { + return new String[] {tl(verboseErrors ? "versionError" : "versionErrorPlayer", this.getBuildInfo())}; + } + default: { + return new String[] {}; + } + } + } + } + + private static class RemoteVersion { + private final BranchStatus branchStatus; + private final int distance; + + RemoteVersion(BranchStatus branchStatus) { + this(branchStatus, 0); + } + + RemoteVersion(BranchStatus branchStatus, int distance) { + this.branchStatus = branchStatus; + this.distance = distance; + } + + public BranchStatus getBranchStatus() { + return branchStatus; + } + + public int getDistance() { + return distance; + } + } + + private enum BranchStatus { + IDENTICAL, + AHEAD, + BEHIND, + DIVERGED, + ERROR, + UNKNOWN + } +} diff --git a/Essentials/src/main/resources/config.yml b/Essentials/src/main/resources/config.yml index fece3f482..95d75c2e7 100644 --- a/Essentials/src/main/resources/config.yml +++ b/Essentials/src/main/resources/config.yml @@ -674,6 +674,11 @@ log-command-block-commands: true # Set the maximum speed for projectiles spawned with /fireball. max-projectile-speed: 8 +# Should EssentialsX check for updates? +# If set to true, EssentialsX will show notifications when a new version is available. +# This uses the public GitHub API and no identifying information is sent or stored. +update-check: true + ############################################################ # +------------------------------------------------------+ # # | Homes | # diff --git a/Essentials/src/main/resources/messages.properties b/Essentials/src/main/resources/messages.properties index c853ed060..28b215ac1 100644 --- a/Essentials/src/main/resources/messages.properties +++ b/Essentials/src/main/resources/messages.properties @@ -936,6 +936,16 @@ vanish=\u00a76Vanish for {0}\u00a76\: {1} vanishCommandDescription=Hide yourself from other players. vanishCommandUsage=/ [player] [on|off] vanished=\u00a76You are now completely invisible to normal users, and hidden from in-game commands. +versionCheckDisabled=\u00a76Update checking disabled in config. +versionCustom=\u00a76Unable to check your version! Self-built? Build information: \u00a7c{0}\u00a76. +versionDevBehind=\u00a74You''re \u00a7c{0} \u00a74EssentialsX dev build(s) out of date! +versionDevDiverged=\u00a76You''re running an experimental build of EssentialsX that is \u00a7c{0} \u00a76builds behind the latest dev build! +versionDevDivergedBranch=\u00a76Feature Branch: \u00a7c{0}\u00a76. +versionDevDivergedLatest=\u00a76You''re running an up to date experimental EssentialsX build! +versionDevLatest=\u00a76You''re running the latest EssentialsX dev build! +versionError=\u00a74Error while fetching EssentialsX version information! Build information: \u00a7c{0}\u00a76. +versionErrorPlayer=\u00a76Error while checking EssentialsX version information! +versionFetching=\u00a76Fetching version information... versionOutputVaultMissing=\u00a74Vault is not installed. Chat and permissions may not work. versionOutputFine=\u00a76{0} version: \u00a7a{1} versionOutputWarn=\u00a76{0} version: \u00a7c{1} @@ -943,6 +953,9 @@ versionOutputUnsupported=\u00a7d{0} \u00a76version: \u00a7d{1} versionOutputUnsupportedPlugins=\u00a76You are running \u00a7dunsupported plugins\u00a76! versionMismatch=\u00a74Version mismatch\! Please update {0} to the same version. versionMismatchAll=\u00a74Version mismatch\! Please update all Essentials jars to the same version. +versionReleaseLatest=\u00a76You''re running the latest stable version of EssentialsX! +versionReleaseNew=\u00a74There is a new EssentialsX version available for download: \u00a7c{0}\u00a74. +versionReleaseNewLink=\u00a74Download it here:\u00a7c {0} voiceSilenced=\u00a76Your voice has been silenced\! voiceSilencedTime=\u00a76Your voice has been silenced for {0}\! voiceSilencedReason=\u00a76Your voice has been silenced\! Reason: \u00a7c{0} diff --git a/Essentials/src/main/resources/release b/Essentials/src/main/resources/release new file mode 100644 index 000000000..eef57ac62 --- /dev/null +++ b/Essentials/src/main/resources/release @@ -0,0 +1,2 @@ +${full.version} +${git.branch} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 52f5b2b4e..f27955587 100644 --- a/build.gradle +++ b/build.gradle @@ -36,9 +36,10 @@ def commitsSinceLastTag() { ext { GIT_COMMIT = grgit == null ? "unknown" : grgit.head().abbreviatedId + GIT_BRANCH = grgit == null ? "detached-head" : grgit.branch.current().name GIT_DEPTH = commitsSinceLastTag() - fullVersion = "${version}-${GIT_COMMIT}".replace("-SNAPSHOT", "-dev+${GIT_DEPTH}") + fullVersion = "${version}".replace("-SNAPSHOT", "-dev+${GIT_DEPTH}-${GIT_COMMIT}") checkstyleVersion = '8.36.2' spigotVersion = '1.16.5-R0.1-SNAPSHOT' @@ -92,9 +93,12 @@ subprojects { // Version Injection processResources { + // Always process resources if version string or git branch changes inputs.property('fullVersion', fullVersion) + inputs.property('gitBranch', GIT_BRANCH) filter(ReplaceTokens, beginToken: '${', - endToken: '}', tokens: ["full.version": fullVersion]) + endToken: '}', tokens: ["full.version": fullVersion, "git.branch": GIT_BRANCH]) + } indra {