diff --git a/Bukkit/build.gradle b/Bukkit/build.gradle index 3bc057a45..dfb4b7055 100644 --- a/Bukkit/build.gradle +++ b/Bukkit/build.gradle @@ -37,6 +37,12 @@ shadowJar { dependencies { include(dependency(':Core')) include(dependency('org.bstats:bstats-bukkit:1.4')) + // update notification stuff + include(dependency('com.github.Sauilitired:Jenkins4J:2.0-SNAPSHOT')) + include(dependency('com.squareup.retrofit2:retrofit:2.4.0')) + include(dependency('com.squareup.okhttp3:okhttp:3.14.0')) + include(dependency('com.squareup.okio:okio:2.2.2')) + include(dependency('org.jetbrains.kotlin:kotlin-stdlib:1.3.21')) } // relocate('org.mcstats', 'com.plotsquared.stats') archiveName = "${parent.name}-${project.name}-${parent.version}.jar" diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/bukkit/BukkitMain.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/bukkit/BukkitMain.java index 5036afae0..352326a43 100644 --- a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/bukkit/BukkitMain.java +++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/bukkit/BukkitMain.java @@ -137,6 +137,33 @@ public final class BukkitMain extends JavaPlugin implements Listener, IPlotMain } new PlotSquared(this, "Bukkit"); + + // Check for updates + if (PlotSquared.get().getUpdateUtility() != null) { + final UpdateUtility updateUtility = PlotSquared.get().getUpdateUtility(); + updateUtility.checkForUpdate(this.getPluginVersionString(), ((updateDescription, throwable) -> { + Bukkit.getScheduler().runTask(BukkitMain.this, () -> { + getLogger().info("-------- PlotSquared Update Check --------"); + if (throwable != null) { + getLogger().severe(String.format("Could not check for update. Reason: %s", + throwable.getMessage())); + } else { + if (updateDescription == null) { + getLogger().info("You appear to be running the latest version of PlotSquared. Congratulations!"); + } else { + getLogger().info("There appears to be a PlotSquared update available!"); + getLogger().info(String.format("You are running version %s," + + " the newest available version is %s", getPluginVersionString(), updateDescription.getVersion())); + getLogger().info(String.format("Update URL: %s", updateDescription.getUrl())); + } + } + getLogger().info("-------- PlotSquared Update Check --------"); + }); + })); + } else { + getLogger().warning("Update checking disabled. Skipping."); + } + if (Settings.Enabled_Components.METRICS) { this.startMetrics(); } else { diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/bukkit/listeners/PlayerEvents.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/bukkit/listeners/PlayerEvents.java index 09c9363b8..86315262a 100644 --- a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/bukkit/listeners/PlayerEvents.java +++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/bukkit/listeners/PlayerEvents.java @@ -631,6 +631,29 @@ import java.util.regex.Pattern; } EventUtil.manager.doJoinTask(pp); }, 20); + + if (pp.hasPermission(Captions.PERMISSION_ADMIN_UPDATE_NOTIFICATION.s()) && + PlotSquared.get().getUpdateUtility() != null) { + final UpdateUtility updateUtility = PlotSquared.get().getUpdateUtility(); + final BukkitMain bukkitMain = BukkitMain.getPlugin(BukkitMain.class); + updateUtility.checkForUpdate(bukkitMain.getPluginVersionString(), ((updateDescription, throwable) -> { + if (throwable != null) { + bukkitMain.getLogger().severe(String.format("Could not check for update. Reason: %s", + throwable.getMessage())); + } else { + if (updateDescription != null) { + new PlotMessage("-------- ").color("$2").text("PlotSquared Update Notification").color("$1").text(" --------").color("$2") + .send(pp); + new PlotMessage("There appears to be a PlotSquared update available!").color("$1").send(pp); + new PlotMessage(String.format("You are running version %s," + + " the newest available version is %s", bukkitMain.getPluginVersionString(), updateDescription.getVersion())).color("$1").send(pp); + new PlotMessage("Update URL").color("$1").text(": ").color("$2").text(updateDescription.getUrl()).tooltip("Download update").send(pp); + new PlotMessage("-------- ").color("$2").text("PlotSquared Update Notification").color("$1").text(" --------").color("$2") + .send(pp); + } + } + })); + } } @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) diff --git a/Core/build.gradle b/Core/build.gradle index 3a8864cfa..eadb2e153 100644 --- a/Core/build.gradle +++ b/Core/build.gradle @@ -1,9 +1,19 @@ +repositories { + maven { url 'https://jitpack.io' } +} + dependencies { testCompile 'junit:junit:4.12' compile 'org.yaml:snakeyaml:1.23' compile 'com.google.code.gson:gson:2.8.5' compileOnly 'org.projectlombok:lombok:1.18.4' + + compile 'com.github.Sauilitired:Jenkins4J:2.0-SNAPSHOT' + compile 'com.squareup.okhttp3:okhttp:3.14.0' + compile 'com.squareup.okio:okio:2.2.2' + compile 'org.jetbrains.kotlin:kotlin-stdlib:1.3.21' } + sourceCompatibility = 1.8 targetCompatibility = 1.8 diff --git a/Core/src/main/java/com/github/intellectualsites/plotsquared/plot/PlotSquared.java b/Core/src/main/java/com/github/intellectualsites/plotsquared/plot/PlotSquared.java index 2e76f6516..e53b36042 100644 --- a/Core/src/main/java/com/github/intellectualsites/plotsquared/plot/PlotSquared.java +++ b/Core/src/main/java/com/github/intellectualsites/plotsquared/plot/PlotSquared.java @@ -76,6 +76,7 @@ import java.util.zip.ZipInputStream; @Setter @Getter private ILogger logger; // Platform / Version / Update URL private PlotVersion version; + @Nullable @Getter private UpdateUtility updateUtility; // Files and configuration @Getter private File jarFile = null; // This file private File storageFile; @@ -1610,6 +1611,24 @@ import java.util.zip.ZipInputStream; } } Settings.load(configFile); + try { + copyFile("updater.properties", "config"); + try (BufferedReader bufferedReader = + new BufferedReader(new InputStreamReader(new FileInputStream(new File(new File(this.IMP.getDirectory(), + "config"), "updater.properties"))))) { + final Properties properties = new Properties(); + properties.load(bufferedReader); + final boolean enabled = Boolean.valueOf(properties.getOrDefault("enabled", true).toString()); + if (enabled) { + this.updateUtility = new UpdateUtility(properties.getProperty("path"), + properties.getProperty("job"), properties.getProperty("artifact")); + } + } catch (final IOException throwable) { + throwable.printStackTrace(); + } + } catch (final Throwable throwable) { + throwable.printStackTrace(); + } try (InputStream stream = getClass().getResourceAsStream("/plugin.properties"); BufferedReader br = new BufferedReader(new InputStreamReader(stream))) { //java.util.Scanner scanner = new java.util.Scanner(stream).useDelimiter("\\A"); diff --git a/Core/src/main/java/com/github/intellectualsites/plotsquared/plot/config/Captions.java b/Core/src/main/java/com/github/intellectualsites/plotsquared/plot/config/Captions.java index c735ccd08..9302ed743 100644 --- a/Core/src/main/java/com/github/intellectualsites/plotsquared/plot/config/Captions.java +++ b/Core/src/main/java/com/github/intellectualsites/plotsquared/plot/config/Captions.java @@ -41,7 +41,8 @@ public enum Captions { "plots.admin.interact.blockedcommands", "static.permissions"), PERMISSION_WORLDEDIT_BYPASS( "plots.worldedit.bypass", "static.permissions"), PERMISSION_PLOT_TOGGLE_TITLES( "plots.toggle.titles", "static.permissions"), PERMISSION_PLOT_TOGGLE_CHAT( - "plots.toggle.chat", "static.permissions"), PERMISSION_ADMIN_EXIT_DENIED( + "plots.toggle.chat", "static.permissions"), PERMISSION_ADMIN_UPDATE_NOTIFICATION( + "plots.admin.update.notify", "static.permissions"), PERMISSION_ADMIN_EXIT_DENIED( "plots.admin.exit.denied", "static.permissions"), PERMISSION_ADMIN_ENTRY_DENIED( "plots.admin.entry.denied", "static.permissions"), PERMISSION_ADMIN_ENTRY_FORCEFIELD( "plots.admin.entry.forcefield", "static.permissions"), PERMISSION_COMMANDS_CHAT( @@ -1066,5 +1067,4 @@ public enum Captions { } else { caller.sendMessage(msg); } - } -} + }} diff --git a/Core/src/main/java/com/github/intellectualsites/plotsquared/plot/util/UpdateUtility.java b/Core/src/main/java/com/github/intellectualsites/plotsquared/plot/util/UpdateUtility.java new file mode 100644 index 000000000..29d2126f6 --- /dev/null +++ b/Core/src/main/java/com/github/intellectualsites/plotsquared/plot/util/UpdateUtility.java @@ -0,0 +1,148 @@ +package com.github.intellectualsites.plotsquared.plot.util; + +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.incendo.jenkins.Jenkins; +import org.incendo.jenkins.objects.ArtifactDescription; +import org.incendo.jenkins.objects.BuildInfo; + +import java.util.Collection; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class UpdateUtility { + + private final String jobName; + private final Pattern artifactPattern; + + private final Jenkins jenkins; + + public UpdateUtility(@NonNull final String jenkinsPath, @NonNull final String jobName, + @NonNull final String artifactPattern) { + this.jobName = jobName; + this.artifactPattern = Pattern.compile(artifactPattern); + this.jenkins = Jenkins.newBuilder().withPath(jenkinsPath).build(); + } + + private void fetchLatestBuildInfo(final BiConsumer whenDone) { + this.jenkins.getJobInfo(jobName).whenCompleteAsync((jobInfo, exception) -> { + if (jobInfo == null && exception != null) { + whenDone.accept(null, exception); + } else if (jobInfo != null) { + jobInfo.getLastSuccessfulBuild().getBuildInfo() + .whenComplete(whenDone); + } else { + whenDone.accept(null, new IllegalStateException( + String.format("Could not fetch job info for job %s", this.jobName))); + } + }); + } + + private void getMatchingArtifact(final BiConsumer whenDone) { + this.fetchLatestBuildInfo((buildInfo, throwable) -> { + if (throwable != null) { + whenDone.accept(null, throwable); + } else { + final Collection artifacts = buildInfo.getArtifacts(); + final Optional artifact = artifacts.stream().filter(artifactDescription -> { + final String name = artifactDescription.getFileName(); + final Matcher matcher = artifactPattern.matcher(name); + return matcher.matches(); + }).findAny(); + if (artifact.isPresent()) { + final ArtifactDescription artifactDescription = artifact.get(); + whenDone.accept(artifactDescription, null); + } else { + whenDone.accept(null, + new NullPointerException(String.format("Could not find any matching artifacts in build %d", buildInfo.getId()))); + } + } + }); + } + + public void checkForUpdate(final String currentVersion, + final BiConsumer whenDone) { + this.getMatchingArtifact(((artifactDescription, throwable) -> { + if (throwable != null) { + whenDone.accept(null, new RuntimeException( + String.format("Failed to read artifact description: %s", throwable.getMessage()), throwable)); + } else { + try { + final String version = this.isNewer(currentVersion, artifactDescription); + if (version != null) { + whenDone.accept(new UpdateDescription(version, artifactDescription.getUrl()), + null); + } else { + whenDone.accept(null, null); + } + } catch (final Throwable exception) { + whenDone.accept(null, + new RuntimeException(String.format("Failed to compare versions: %s", + exception.getMessage()), exception)); + } + } + })); + } + + private String isNewer(@NonNull final String currentVersion, @NonNull final ArtifactDescription artifact) { + final Matcher matcher = artifactPattern.matcher(artifact.getFileName()); + if (!matcher.matches()) { + throw new IllegalArgumentException("Artifact file name does not match artifact pattern"); + } + final String version = matcher.group("version"); + if (version == null) { + throw new IllegalArgumentException("Given artifact does not contain version"); + } + return compareVersions(currentVersion, version) < 0 ? version : null; + } + + /** + * Compare two given versions in the format $major.$minor + * @param oldVersion current version + * @param newVersion other version + * @return -1 if the current version is older, 1 is the versions are the same, + * and 1 if the current version is newer + */ + private int compareVersions(@NonNull final String oldVersion, @NonNull final String newVersion) { + // Versions look this this: major.minor :P + final int[] oldNums = splitVersion(oldVersion); + final int[] newNums = splitVersion(newVersion); + + if (oldNums == null || newNums == null) { + throw new IllegalArgumentException("Could not extract version data"); + } + + // Compare major version + if (oldNums[0] != -1 && newNums[0] != -1) { + if (oldNums[0] < newNums[0]) { + return -1; + } else if (oldNums[0] > newNums[0]) { + return 1; + } + } + + // Compare minor versions + return Integer.compare(oldNums[1], newNums[1]); + } + + private int[] splitVersion(@NonNull final String versionString) { + final String[] parts = versionString.split("\\."); + switch (parts.length) { + case 0: return new int[] {-1, -1}; + case 1: return new int[] {-1, Integer.parseInt(parts[0])}; + case 2: return new int[] {Integer.parseInt(parts[0]), Integer.parseInt(parts[1])}; + default: return null; + } + } + + @Getter + @RequiredArgsConstructor + public static class UpdateDescription { + private final String version; + private final String url; + } + +} diff --git a/Core/src/main/resources/updater.properties b/Core/src/main/resources/updater.properties new file mode 100644 index 000000000..19ef0aed6 --- /dev/null +++ b/Core/src/main/resources/updater.properties @@ -0,0 +1,8 @@ +# Whether update notifications are enabled +enabled=true +# CI path +path=https://ci.athion.net/ +# Job name +job=PlotSquared-Releases +# Artifact pattern +artifact=^PlotSquared-Bukkit-(?[0-9.]+).jar$ diff --git a/build.gradle b/build.gradle index 39fb59087..1df3aaa8e 100644 --- a/build.gradle +++ b/build.gradle @@ -76,5 +76,6 @@ subprojects { mavenCentral() maven { url "http://maven.sk89q.com/repo/" } maven { url "http://repo.maven.apache.org/maven2" } + maven { url 'https://jitpack.io' } } }