commit 52b235aa31aaaaded343c67527c863d97fe2d0fb Author: Minecon724 Date: Sun Jun 1 14:47:33 2025 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ff6309 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..a0ccf77 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Environment-dependent path to Maven home directory +/mavenHomeManager.xml diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..463551f --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..9754043 --- /dev/null +++ b/pom.xml @@ -0,0 +1,100 @@ + + + 4.0.0 + + eu.m724 + jarupdater-spigot + 0.0.1-SNAPSHOT + + + 21 + + ${project.basedir}/testkeystore.jks + testkey + 123456 + + + + + spigot-repo + https://hub.spigotmc.org/nexus/content/repositories/snapshots/ + + + m724-repo + https://git.m724.eu/api/packages/Minecon724/maven + + + + + + + org.spigotmc + spigot-api + 1.16.5-R0.1-SNAPSHOT + provided + + + + com.google.guava + guava + + + org.yaml + snakeyaml + + + + + eu.m724 + jarupdater + 0.2.0 + + + + + + + src/main/resources + true + + + + + org.apache.maven.plugins + maven-release-plugin + 3.1.1 + + true + + + + org.apache.maven.plugins + maven-jarsigner-plugin + 3.1.0 + + + sign + + sign + + + + verify + + verify + + + + + ${jarsigner.keystore} + ${jarsigner.alias} + ${jarsigner.storepass} + http://time.certum.pl + + + + + + \ No newline at end of file diff --git a/src/main/java/eu/m724/jarupdater/spigot/SpigotPluginUpdater.java b/src/main/java/eu/m724/jarupdater/spigot/SpigotPluginUpdater.java new file mode 100644 index 0000000..346c13a --- /dev/null +++ b/src/main/java/eu/m724/jarupdater/spigot/SpigotPluginUpdater.java @@ -0,0 +1,95 @@ +package eu.m724.jarupdater.spigot; + +import eu.m724.jarupdater.download.Downloader; +import eu.m724.jarupdater.download.SimpleDownloader; +import eu.m724.jarupdater.environment.ConstantEnvironment; +import eu.m724.jarupdater.environment.Environment; +import eu.m724.jarupdater.live.MetadataDAO; +import eu.m724.jarupdater.live.MetadataFacade; +import eu.m724.jarupdater.updater.Updater; +import eu.m724.jarupdater.verify.SignatureVerifier; +import eu.m724.jarupdater.verify.Verifier; +import org.bukkit.plugin.java.FileGetter; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; + +public class SpigotPluginUpdater extends Updater { + private final String pluginName; + + private boolean restartRequired = false; + + private SpigotPluginUpdater(Environment environment, MetadataFacade metadataProvider, Downloader downloader, Verifier verifier, String pluginName) { + super(environment, metadataProvider, downloader, verifier); + + this.pluginName = pluginName; + } + + /** + * Builds a PluginUpdater instance with the following arguments. + * + * @param plugin The plugin + * @param metadataDAO From where to update the plugin + * @param channel The update channel + * @return The PluginUpdater instance + * @throws IOException If loading signature failed + */ + public static SpigotPluginUpdater build(JavaPlugin plugin, MetadataDAO metadataDAO, String channel) throws IOException { + String pluginPath = FileGetter.getFile(plugin).getPath() + .replace(".paper-remapped/", ""); // Paper returns the wrong path + + Environment environment = new ConstantEnvironment( + plugin.getDescription().getVersion(), + channel, + Path.of(pluginPath) + ); + + MetadataFacade metadataFacade = new MetadataFacade(environment, metadataDAO); + Downloader downloader = new SimpleDownloader(plugin.getDescription().getName()); + SignatureVerifier signatureVerifier = getSignatureVerifier(plugin); + + return new SpigotPluginUpdater(environment, metadataFacade, downloader, signatureVerifier, plugin.getDescription().getName()); + } + + private static SignatureVerifier getSignatureVerifier(JavaPlugin plugin) throws IOException { + SignatureVerifier verifier = null; + + try (InputStream inputStream = plugin.getResource("verifies_downloaded_jars.pem")) { // TODO make changeable + if (inputStream != null) { + verifier = new SignatureVerifier(); + verifier.loadPublicKey(inputStream); + } + } + + return verifier; + } + + /** + * Gets the plugin name. + * + * @return The plugin name + */ + public String getPluginName() { + return pluginName; + } + + /** + * Gets whether a restart is required. + * + * @return Whether a restart is required. + */ + public boolean isRestartRequired() { + return restartRequired; + } + + /** + * Sets whether a restart is required. + * + * @param restartRequired Whether a restart is required. + */ + public void setRestartRequired(boolean restartRequired) { + this.restartRequired = restartRequired; + } +} diff --git a/src/main/java/eu/m724/jarupdater/spigot/command/CheckForUpdateAction.java b/src/main/java/eu/m724/jarupdater/spigot/command/CheckForUpdateAction.java new file mode 100644 index 0000000..02a47e4 --- /dev/null +++ b/src/main/java/eu/m724/jarupdater/spigot/command/CheckForUpdateAction.java @@ -0,0 +1,160 @@ +package eu.m724.jarupdater.spigot.command; + +import eu.m724.jarupdater.object.Version; +import eu.m724.jarupdater.spigot.SpigotPluginUpdater; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.ClickEvent; +import net.md_5.bungee.api.chat.ComponentBuilder; +import net.md_5.bungee.api.chat.HoverEvent; +import net.md_5.bungee.api.chat.hover.content.Text; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.function.BiConsumer; + +public class CheckForUpdateAction { + private final SpigotPluginUpdater updater; + private final BiConsumer errorConsumer; + + public CheckForUpdateAction(SpigotPluginUpdater updater, BiConsumer errorConsumer) { + this.updater = updater; + this.errorConsumer = errorConsumer; + } + + /** + * Calls the update check action. + * + * @param sender The command sender, be it console or player + * @param commandLabel The command label + */ + void checkForUpdate(CommandSender sender, String commandLabel) { + sender.sendMessage(ChatColor.GRAY + "Please wait..."); + + // TODO show current version? + + updater.getLatestVersion() + .thenAccept(latestVersion -> sendUpdateAvailableMessage(sender, commandLabel, latestVersion)) + .exceptionally(e -> sendErrorMessage(sender, "Error checking for update", e)); + } + + /** + * Sends the update available notice to the sender + * + * @param sender The command sender + * @param commandLabel The command executed + * @param latestVersion The latest version + */ + private void sendUpdateAvailableMessage(CommandSender sender, String commandLabel, Version latestVersion) { + if (sender instanceof Player) { + sender.spigot().sendMessage(buildInteractiveUpdateAvailableMessage(commandLabel, latestVersion)); + } else { + sender.sendMessage("New update: %s %s released %s".formatted(updater.getPluginName(), latestVersion.getLabel(), formattedDateFromTimestamp(latestVersion.getTimestamp()))); + sender.sendMessage("To download: /%s download".formatted(commandLabel)); + + if (latestVersion.getChangelogUrl() != null) { + sender.sendMessage("Changelog URL: " + latestVersion.getChangelogUrl()); + } else { + sender.sendMessage("No changelog is available for this version"); + } + } + } + + /** + * Sends an error message to the sender and callback + * + * @param sender The command sender + * @param message The error description + * @param throwable The throwable + * @return null + */ + private Void sendErrorMessage(CommandSender sender, String message, Throwable throwable) { + errorConsumer.accept(message, throwable); + sender.sendMessage(ChatColor.RED + "An error has occurred, see console for details."); + return null; + } + + /** + * Build the entire "update is available" notice + * + * @param commandLabel The command to execute + * @param latestVersion The latest version + * @return The entire notice + */ + private BaseComponent[] buildInteractiveUpdateAvailableMessage(String commandLabel, Version latestVersion) { + return new ComponentBuilder() + .append("An update is available!").color(ChatColor.YELLOW).bold(true) + .append("\n") + .append(buildVersionInfo(latestVersion)) + .append("\n") + .append("Actions: ").color(ChatColor.GOLD) + .append(buildChangelogButton(latestVersion.getChangelogUrl())) + .append(" | ").color(ChatColor.GOLD) + .append(buildDownloadButton(commandLabel)) + .create(); + } + + /** + * Builds version info component + * + * @param version The version + * @return The version info component + */ + private BaseComponent[] buildVersionInfo(Version version) { + return new ComponentBuilder() + .append(updater.getPluginName() + " " + version.getLabel()).color(ChatColor.AQUA) + .append(" released " + formattedDateFromTimestamp(version.getTimestamp())).color(ChatColor.GOLD) + .create(); + } + + /** + * Builds a clickable changelog text component + * + * @param changelogUrl The URL to the changelog + * @return The clickable changelog text component + */ + private BaseComponent[] buildChangelogButton(String changelogUrl) { + if (changelogUrl == null) { + return new ComponentBuilder() + .append("Changelog").color(ChatColor.GRAY).strikethrough(true) + .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("No changelog is available for this version"))) + .create(); + } + + return new ComponentBuilder() + .append("Changelog").color(ChatColor.AQUA).underlined(true) + .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("Click here to open the changelog."))) + .event(new ClickEvent(ClickEvent.Action.OPEN_URL, changelogUrl)) + .create(); + } + + /** + * Builds a clickable download text component + * + * @param commandLabel The command to execute + * @return The clickable download text component + */ + private BaseComponent[] buildDownloadButton(String commandLabel) { + return new ComponentBuilder() + .append("Download").color(ChatColor.AQUA).underlined(true) + .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("Click here to download the latest update."))) + .event(new ClickEvent(ClickEvent.Action.RUN_COMMAND, commandLabel + " download")) + .create(); + } + + /** + * Builds a dd.MM.yyyy date from epoch seconds + * + * @param timestamp epoch seconds + * @return The formatted date + */ + private String formattedDateFromTimestamp(long timestamp) { + if (timestamp < 0) { + return "Unknown date"; + } + + return LocalDate.ofEpochDay(timestamp / 86400).format(DateTimeFormatter.ofPattern("dd.MM.yyyy")); + } +} diff --git a/src/main/java/eu/m724/jarupdater/spigot/command/DownloadUpdateAction.java b/src/main/java/eu/m724/jarupdater/spigot/command/DownloadUpdateAction.java new file mode 100644 index 0000000..561f49b --- /dev/null +++ b/src/main/java/eu/m724/jarupdater/spigot/command/DownloadUpdateAction.java @@ -0,0 +1,63 @@ +package eu.m724.jarupdater.spigot.command; + +import eu.m724.jarupdater.spigot.SpigotPluginUpdater; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.ClickEvent; +import net.md_5.bungee.api.chat.ComponentBuilder; +import net.md_5.bungee.api.chat.HoverEvent; +import net.md_5.bungee.api.chat.hover.content.Text; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.function.BiConsumer; + +public class DownloadUpdateAction { + private final SpigotPluginUpdater updater; + private final BiConsumer errorConsumer; + + public DownloadUpdateAction(SpigotPluginUpdater updater, BiConsumer errorConsumer) { + this.updater = updater; + this.errorConsumer = errorConsumer; + } + + void downloadUpdate(CommandSender sender, String commandLabel) { + sender.sendMessage(ChatColor.GRAY + "Download started"); + + updater.downloadLatestVersion() + .thenAccept(file -> sendDownloadFinishedMessage(sender, commandLabel)) + .exceptionally(e -> sendErrorMessage(sender, "Error downloading update", e)); + } + + void sendDownloadFinishedMessage(CommandSender sender, String commandLabel) { + if (sender instanceof Player) { + sender.spigot().sendMessage(buildInteractiveDownloadFinishedMessage(commandLabel)); + } else { + sender.sendMessage("Download finished. Install it with: /%s install".formatted(commandLabel)); + } + } + + /** + * Sends an error message to the sender and callback + * + * @param sender The command sender + * @param message The error description + * @param throwable The throwable + * @return null + */ + private Void sendErrorMessage(CommandSender sender, String message, Throwable throwable) { + errorConsumer.accept(message, throwable); + sender.sendMessage(ChatColor.RED + "An error has occurred, see console for details."); + return null; + } + + private BaseComponent[] buildInteractiveDownloadFinishedMessage(String commandLabel) { + ComponentBuilder builder = new ComponentBuilder() + .append("Update downloaded. ").color(ChatColor.GREEN) + .append("Click here to install.").color(ChatColor.AQUA) + .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("Click here to install."))) + .event(new ClickEvent(ClickEvent.Action.RUN_COMMAND, commandLabel + " install")); + + return builder.create(); + } +} diff --git a/src/main/java/eu/m724/jarupdater/spigot/command/InstallUpdateAction.java b/src/main/java/eu/m724/jarupdater/spigot/command/InstallUpdateAction.java new file mode 100644 index 0000000..f57723f --- /dev/null +++ b/src/main/java/eu/m724/jarupdater/spigot/command/InstallUpdateAction.java @@ -0,0 +1,71 @@ +package eu.m724.jarupdater.spigot.command; + +import eu.m724.jarupdater.spigot.SpigotPluginUpdater; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.ClickEvent; +import net.md_5.bungee.api.chat.ComponentBuilder; +import net.md_5.bungee.api.chat.HoverEvent; +import net.md_5.bungee.api.chat.hover.content.Text; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.nio.file.NoSuchFileException; +import java.util.function.BiConsumer; + +public class InstallUpdateAction { + private final SpigotPluginUpdater updater; + private final BiConsumer errorConsumer; + + public InstallUpdateAction(SpigotPluginUpdater updater, BiConsumer errorConsumer) { + this.updater = updater; + this.errorConsumer = errorConsumer; + } + + void installUpdate(CommandSender sender, String commandLabel) { + try { + updater.installLatestVersion() + .thenAccept(v -> sendInstallationCompletedMessage(sender)) + .exceptionally(e -> sendErrorMessage(sender, "Error installing update", e)); + } catch (NoSuchFileException e) { + sendDownloadRequiredMessage(sender, commandLabel); + } + } + + private void sendInstallationCompletedMessage(CommandSender sender) { + updater.setRestartRequired(true); + sender.sendMessage(ChatColor.GREEN + "Installation completed, restart the server to apply."); + } + + private void sendDownloadRequiredMessage(CommandSender sender, String commandLabel) { + if (sender instanceof Player) { + sender.spigot().sendMessage(buildInteractiveDownloadRequiredMessage(commandLabel)); + } else { + sender.sendMessage("Update not downloaded. Download it with: /%s download".formatted(commandLabel)); + } + } + + /** + * Sends an error message to the sender and callback + * + * @param sender The command sender + * @param message The error description + * @param throwable The throwable + * @return null + */ + private Void sendErrorMessage(CommandSender sender, String message, Throwable throwable) { + errorConsumer.accept(message, throwable); + sender.sendMessage(ChatColor.RED + "An error has occurred, see console for details."); + return null; + } + + private BaseComponent[] buildInteractiveDownloadRequiredMessage(String commandLabel) { + ComponentBuilder builder = new ComponentBuilder() + .append("Update not downloaded. ").color(ChatColor.YELLOW) + .append("Click here to download.").color(ChatColor.AQUA).underlined(true) + .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("Click here to download."))) + .event(new ClickEvent(ClickEvent.Action.RUN_COMMAND, commandLabel + " download")); + + return builder.create(); + } +} diff --git a/src/main/java/eu/m724/jarupdater/spigot/command/UpdateCommand.java b/src/main/java/eu/m724/jarupdater/spigot/command/UpdateCommand.java new file mode 100644 index 0000000..4b24ac9 --- /dev/null +++ b/src/main/java/eu/m724/jarupdater/spigot/command/UpdateCommand.java @@ -0,0 +1,57 @@ +package eu.m724.jarupdater.spigot.command; + +import eu.m724.jarupdater.spigot.SpigotPluginUpdater; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.chat.ComponentBuilder; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.function.BiConsumer; + +public class UpdateCommand implements CommandExecutor { + private final SpigotPluginUpdater updater; + + private final CheckForUpdateAction checkForUpdateAction; + private final InstallUpdateAction installUpdateAction; + private final DownloadUpdateAction downloadUpdateAction; + + public UpdateCommand(SpigotPluginUpdater updater, BiConsumer errorConsumer) { + this.updater = updater; + + this.checkForUpdateAction = new CheckForUpdateAction(updater, errorConsumer); + this.installUpdateAction = new InstallUpdateAction(updater, errorConsumer); + this.downloadUpdateAction = new DownloadUpdateAction(updater, errorConsumer); + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (updater.isRestartRequired()) { + sendRestartRequiredMessage(sender); + } + + String action = args.length > 0 ? args[0].toLowerCase() : ""; + + switch (action) { + case "download" -> downloadUpdateAction.downloadUpdate(sender, label); + case "install" -> installUpdateAction.installUpdate(sender, label); + default -> checkForUpdateAction.checkForUpdate(sender, label); + } + + return true; + } + + private void sendRestartRequiredMessage(CommandSender sender) { + if (sender instanceof Player) { + sender.spigot().sendMessage( + new ComponentBuilder() + .append("(!) ").color(ChatColor.RED).bold(true) + .append("Server restart required").color(ChatColor.YELLOW) + .create() + ); + } else { + sender.sendMessage("(!) Server restart required"); + } + } +} diff --git a/src/main/java/org/bukkit/plugin/java/FileGetter.java b/src/main/java/org/bukkit/plugin/java/FileGetter.java new file mode 100644 index 0000000..4f46d38 --- /dev/null +++ b/src/main/java/org/bukkit/plugin/java/FileGetter.java @@ -0,0 +1,9 @@ +package org.bukkit.plugin.java; + +import java.io.File; + +public class FileGetter { + public static File getFile(JavaPlugin plugin) { + return plugin.getFile(); + } +}