From 0b4b5298abe23aa5dabcbc6ce613b3bc338efd72 Mon Sep 17 00:00:00 2001 From: Florian CUNY Date: Sun, 14 Jun 2020 11:31:13 +0200 Subject: [PATCH] First try at making a GitHub-based update checker --- pom.xml | 2 +- .../world/bentobox/bentobox/Settings.java | 17 ++- .../commands/BentoBoxVersionCommand.java | 18 +++ .../bentobox/listeners/JoinLeaveListener.java | 14 +++ .../bentobox/managers/WebManager.java | 62 +++++++++- .../bentobox/versions/UpdateChecker.java | 111 ++++++++++++++++++ src/main/resources/locales/en-US.yml | 6 + src/main/resources/plugin.yml | 3 + 8 files changed, 226 insertions(+), 7 deletions(-) create mode 100644 src/main/java/world/bentobox/bentobox/versions/UpdateChecker.java diff --git a/pom.xml b/pom.xml index 96faaa218..76e3165d2 100644 --- a/pom.xml +++ b/pom.xml @@ -74,7 +74,7 @@ 1.7 1.7 2.10.5 - d5f5e0bbd8 + e7597818f5 3.0-SNAPSHOT 7.0.0 diff --git a/src/main/java/world/bentobox/bentobox/Settings.java b/src/main/java/world/bentobox/bentobox/Settings.java index 02d45db15..143116dc8 100644 --- a/src/main/java/world/bentobox/bentobox/Settings.java +++ b/src/main/java/world/bentobox/bentobox/Settings.java @@ -273,12 +273,17 @@ public class Settings implements ConfigObject { @ConfigEntry(path = "web.github.connection-interval", since = "1.5.0") private int githubConnectionInterval = 120; - @ConfigEntry(path = "web.updater.check-updates.bentobox", since = "1.3.0", hidden = true) + @ConfigComment("Checks for BentoBox updates.") + @ConfigEntry(path = "web.updater.check-updates.bentobox", since = "1.14.0") private boolean checkBentoBoxUpdates = true; - @ConfigEntry(path = "web.updater.check-updates.addons", since = "1.3.0", hidden = true) + @ConfigComment("Checks for addons updates.") + @ConfigEntry(path = "web.updater.check-updates.addons", since = "1.14.0") private boolean checkAddonsUpdates = true; + @ConfigEntry(path = "web.updater.check-updates.include-prereleases", since = "1.14.0") + private boolean checkPreReleasesUpdates = false; + // --------------------------------------------- // Getters and setters @@ -713,4 +718,12 @@ public class Settings implements ConfigObject { public void setPanelFillerMaterial(Material panelFillerMaterial) { this.panelFillerMaterial = panelFillerMaterial; } + + public boolean isCheckPreReleasesUpdates() { + return checkPreReleasesUpdates; + } + + public void setCheckPreReleasesUpdates(boolean checkPreReleasesUpdates) { + this.checkPreReleasesUpdates = checkPreReleasesUpdates; + } } diff --git a/src/main/java/world/bentobox/bentobox/commands/BentoBoxVersionCommand.java b/src/main/java/world/bentobox/bentobox/commands/BentoBoxVersionCommand.java index db9befa94..38a07bf0f 100644 --- a/src/main/java/world/bentobox/bentobox/commands/BentoBoxVersionCommand.java +++ b/src/main/java/world/bentobox/bentobox/commands/BentoBoxVersionCommand.java @@ -6,6 +6,7 @@ import java.util.Locale; import java.util.Map; import java.util.Optional; +import javafx.scene.text.Text; import org.bukkit.Bukkit; import world.bentobox.bentobox.api.addons.GameModeAddon; @@ -14,6 +15,7 @@ import world.bentobox.bentobox.api.localization.TextVariables; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.versions.ServerCompatibility; import world.bentobox.bentobox.versions.ServerCompatibility.ServerSoftware; +import world.bentobox.bentobox.versions.UpdateChecker; /** * Displays information about Gamemodes, Addons and versioning. @@ -85,6 +87,22 @@ public class BentoBoxVersionCommand extends CompositeCommand { .forEach(a -> user.sendMessage("commands.bentobox.version.addon-syntax", TextVariables.NAME, a.getDescription().getName(), TextVariables.VERSION, a.getDescription().getVersion(), "[state]", a.getState().toString())); + long availableUpdates = getPlugin().getWebManager().getUpdateCheckers().stream() + .filter(updateChecker -> updateChecker.getResult() != null) + .count(); + + if (availableUpdates > 0) { + user.sendMessage("commands.bentobox.version.available-updates", TextVariables.NUMBER, String.valueOf(availableUpdates)); + for (UpdateChecker updateChecker : getPlugin().getWebManager().getUpdateCheckers()) { + UpdateChecker.Result result = updateChecker.getResult(); + if (result != null) { + user.sendMessage("commands.bentobox.version.update", "[repo]", updateChecker.getRepository(), + TextVariables.VERSION, result.getVersion(), + "[link]", "https://github.com/" + updateChecker.getRepository() + "/releases/tag/" + result.getVersion()); + } + } + } + return true; } } diff --git a/src/main/java/world/bentobox/bentobox/listeners/JoinLeaveListener.java b/src/main/java/world/bentobox/bentobox/listeners/JoinLeaveListener.java index 889fa7085..7e2112f81 100644 --- a/src/main/java/world/bentobox/bentobox/listeners/JoinLeaveListener.java +++ b/src/main/java/world/bentobox/bentobox/listeners/JoinLeaveListener.java @@ -89,8 +89,22 @@ public class JoinLeaveListener implements Listener { // Clear inventory if required clearPlayersInventory(Util.getWorld(event.getPlayer().getWorld()), user); + + // Notify player about updates + notifyUpdates(user); } + private void notifyUpdates(User user) { + if (user.hasPermission("bentobox.notify-updates")) { + long availableUpdates = plugin.getWebManager().getUpdateCheckers().stream() + .filter(updateChecker -> updateChecker.getResult() != null) + .count(); + + if (availableUpdates > 0) { + user.sendMessage("updates-available"); + } + } + } private void firstTime(User user) { // Make sure the player is loaded into the cache or create the player if they don't exist diff --git a/src/main/java/world/bentobox/bentobox/managers/WebManager.java b/src/main/java/world/bentobox/bentobox/managers/WebManager.java index e8450c2d3..8b0fbf020 100644 --- a/src/main/java/world/bentobox/bentobox/managers/WebManager.java +++ b/src/main/java/world/bentobox/bentobox/managers/WebManager.java @@ -10,6 +10,7 @@ import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; +import io.github.TheBusyBiscuit.GitHubWebAPI4Java.objects.users.GitHubOrganization; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; @@ -23,6 +24,7 @@ import io.github.TheBusyBiscuit.GitHubWebAPI4Java.objects.repositories.GitHubCon import io.github.TheBusyBiscuit.GitHubWebAPI4Java.objects.repositories.GitHubRepository; import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.Settings; +import world.bentobox.bentobox.versions.UpdateChecker; import world.bentobox.bentobox.web.catalog.CatalogEntry; import world.bentobox.bentobox.web.credits.Contributor; @@ -34,17 +36,21 @@ import world.bentobox.bentobox.web.credits.Contributor; */ public class WebManager { - private @NonNull BentoBox plugin; + private @NonNull final BentoBox plugin; private @Nullable GitHubWebAPI gitHub; - private @NonNull List addonsCatalog; - private @NonNull List gamemodesCatalog; - private @NonNull Map> contributors; + private @NonNull final List addonsCatalog; + private @NonNull final List gamemodesCatalog; + private @NonNull final Map> contributors; + + @NonNull + private final List updateCheckers; public WebManager(@NonNull BentoBox plugin) { this.plugin = plugin; this.addonsCatalog = new ArrayList<>(); this.gamemodesCatalog = new ArrayList<>(); this.contributors = new HashMap<>(); + this.updateCheckers = new ArrayList<>(); // Setup the GitHub connection if (plugin.getSettings().isGithubDownloadData()) { @@ -100,12 +106,14 @@ public class WebManager { .stream().map(addon -> addon.getDescription().getRepository()) .filter(repo -> !repo.isEmpty()) .collect(Collectors.toList())); + /* repositories.addAll(addonsCatalog.stream().map(CatalogEntry::getRepository) .filter(repo -> !repositories.contains(repo)) .collect(Collectors.toList())); repositories.addAll(gamemodesCatalog.stream().map(CatalogEntry::getRepository) .filter(repo -> !repositories.contains(repo)) .collect(Collectors.toList())); + */ /* Download the contributors */ if (plugin.getSettings().isLogGithubDownloadData()) { @@ -128,6 +136,13 @@ public class WebManager { } } + /* Check for updates */ + if (plugin.getSettings().isLogGithubDownloadData()) { + plugin.log("Checking for updates..."); + } + + checkUpdates(gh); + // People were concerned that the download took ages, so we need to tell them it's over now. if (plugin.getSettings().isLogGithubDownloadData()) { plugin.log("Successfully downloaded data from GitHub."); @@ -223,6 +238,36 @@ public class WebManager { } } + private void checkUpdates(@NonNull GitHubWebAPI gh) { + if (updateCheckers.isEmpty()) { + // Grab the repositories we will have to go through + Map repositories = new HashMap<>(); + + if (plugin.getSettings().isCheckBentoBoxUpdates()) { + repositories.put("BentoBoxWorld/BentoBox", plugin.getDescription().getVersion()); + } + if (plugin.getSettings().isCheckAddonsUpdates()) { + repositories.putAll(plugin.getAddonsManager().getEnabledAddons() + .stream() + .filter(addon -> !addon.getDescription().getRepository().isEmpty()) + .collect(Collectors.toMap(addon -> addon.getDescription().getRepository(), addon -> addon.getDescription().getVersion()))); + } + + for (Map.Entry repo : repositories.entrySet()) { + UpdateChecker updateChecker = new UpdateChecker(gh, repo.getKey(), repo.getValue()); + updateCheckers.add(updateChecker); + } + } + + for (UpdateChecker updateChecker : updateCheckers) { + try { + updateChecker.checkUpdates(); + } catch (IllegalAccessException e) { + // Fail silently + } + } + } + /** * Returns the contents of the addons catalog (may be an empty list). * @return the contents of the addons catalog. @@ -263,4 +308,13 @@ public class WebManager { public Optional getGitHub() { return Optional.ofNullable(gitHub); } + + /** + * Returns the list of update checkers. + * @return the list of update checkers. + * @since 1.14.0 + */ + public List getUpdateCheckers() { + return updateCheckers; + } } diff --git a/src/main/java/world/bentobox/bentobox/versions/UpdateChecker.java b/src/main/java/world/bentobox/bentobox/versions/UpdateChecker.java new file mode 100644 index 000000000..71265c5ea --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/versions/UpdateChecker.java @@ -0,0 +1,111 @@ +package world.bentobox.bentobox.versions; + +import io.github.TheBusyBiscuit.GitHubWebAPI4Java.GitHubWebAPI; +import io.github.TheBusyBiscuit.GitHubWebAPI4Java.objects.repositories.GitHubRelease; +import org.eclipse.jdt.annotation.Nullable; +import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.util.Util; + +import java.util.List; + +/** + * Checks for updates through the GitHub API. + * @author Poslovitch + * @since 1.14.0 + */ +public class UpdateChecker { + + private final BentoBox plugin; + private final GitHubWebAPI gitHub; + private final String repository; + private final String currentVersion; + + @Nullable + private Result result; + + public UpdateChecker(GitHubWebAPI gitHub, String repository, String currentVersion) { + this.plugin = BentoBox.getInstance(); + this.gitHub = gitHub; + this.repository = repository; + this.currentVersion = currentVersion; + } + + public void checkUpdates() throws IllegalAccessException { + String[] repo = repository.split("/"); + + List releases = gitHub.getRepository(repo[0], repo[1]).getReleases(); + if (!releases.isEmpty()) { + for (GitHubRelease release : releases) { + if (release.isDraft()) { + // Drafts should be ignored (they're not published yet) + continue; + } + + if (release.isPreRelease() && !plugin.getSettings().isCheckPreReleasesUpdates()) { + // We don't care about pre-releases + continue; + } + + String newVersion = release.getTagName(); + if (isMoreRecent(newVersion)) { + // We found the new version, and it should be the latest. + this.result = new Result(newVersion, release.isPreRelease()); + break; + } + } + } else { + this.result = null; + } + } + + private boolean isMoreRecent(String newVersion) { + String[] currentVer = currentVersion.split("\\D"); + String[] newVer = newVersion.split("\\D"); + + for (int i = 0; i < currentVer.length; i++) { + int newVersionNumber = 0; + if (i < newVer.length && Util.isInteger(newVer[i], false)) { + newVersionNumber = Integer.parseInt(newVer[i]); + } + int currentVersionNumber = Util.isInteger(currentVer[i], false) ? Integer.parseInt(currentVer[i]) : -1; + + if (newVersionNumber > currentVersionNumber) { + return false; // The current version is greater than the "new" version -> up to date. + } + if (newVersionNumber < currentVersionNumber) { + return true; // The current version is outdated + } + // If it is equal, go to the next number + } + + return false; // Everything is equal, so return true + } + + @Nullable + public Result getResult() { + return result; + } + + public String getRepository() { + return repository; + } + + public static class Result { + + private final String version; + private final boolean preRelease; + + public Result(String version, boolean preRelease) { + this.version = version; + this.preRelease = preRelease; + } + + public String getVersion() { + return version; + } + + public boolean isPreRelease() { + return preRelease; + } + } +} diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index 667ea2202..aa7b86ac0 100644 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -413,6 +413,8 @@ commands: game-world: "&2 [name] &7 (&3 [addon]&7 ): &3 [worlds]" server: "&2 Running &3 [name] [version]&2 ." database: "&2 Database: &3 [database]" + available-updates: "Available updates ([number]):" + update: "&2 [repo] &3 [version]&7 : &e [link]" manage: description: "displays the Management Panel" catalog: @@ -1405,6 +1407,10 @@ panel: &a Allow BentoBox to connect to GitHub in &a the configuration or try again later. +updates-available: | + [prefix_bentobox]&6 &l Update Checker + [prefix_bentobox]&a Updates are available. Check &b /bentobox version &a for details. + successfully-loaded: | &6 ____ _ ____ diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 0526bc03e..1f6f5bed3 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -39,3 +39,6 @@ permissions: bentobox.version: description: Allows to use /bentobox version default: op + bentobox.notify-updates: + description: Sends players a message about available updates when they join the server + default: op