Initial commit

This commit is contained in:
Minecon724 2025-06-01 14:47:33 +02:00
commit 52b235aa31
Signed by: Minecon724
GPG key ID: A02E6E67AB961189
12 changed files with 625 additions and 0 deletions

38
.gitignore vendored Normal file
View file

@ -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

5
.idea/.gitignore generated vendored Normal file
View file

@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Environment-dependent path to Maven home directory
/mavenHomeManager.xml

7
.idea/encodings.xml generated Normal file
View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>

14
.idea/misc.xml generated Normal file
View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="temurin-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

100
pom.xml Normal file
View file

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>eu.m724</groupId>
<artifactId>jarupdater-spigot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<maven.compiler.release>21</maven.compiler.release>
<jarsigner.keystore>${project.basedir}/testkeystore.jks</jarsigner.keystore>
<jarsigner.alias>testkey</jarsigner.alias>
<jarsigner.storepass>123456</jarsigner.storepass>
</properties>
<repositories>
<repository>
<id>spigot-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
</repository>
<repository>
<id>m724-repo</id>
<url>https://git.m724.eu/api/packages/Minecon724/maven</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.16.5-R0.1-SNAPSHOT</version>
<scope>provided</scope>
<!-- Fix warning about vulnerabilities of things we don't use -->
<exclusions>
<exclusion>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</exclusion>
<exclusion>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>eu.m724</groupId>
<artifactId>jarupdater</artifactId>
<version>0.2.0</version>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins> <!-- versions: https://maven.apache.org/plugins/ -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-release-plugin</artifactId>
<version>3.1.1</version>
<configuration>
<allowTimestampedSnapshots>true</allowTimestampedSnapshots>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jarsigner-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>sign</id>
<goals>
<goal>sign</goal>
</goals>
</execution>
<execution>
<id>verify</id>
<goals>
<goal>verify</goal>
</goals>
</execution>
</executions>
<configuration>
<keystore>${jarsigner.keystore}</keystore>
<alias>${jarsigner.alias}</alias>
<storepass>${jarsigner.storepass}</storepass>
<tsa>http://time.certum.pl</tsa>
</configuration>
</plugin>
</plugins>
</build>
</project>

View file

@ -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;
}
}

View file

@ -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<String, Throwable> errorConsumer;
public CheckForUpdateAction(SpigotPluginUpdater updater, BiConsumer<String, Throwable> 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"));
}
}

View file

@ -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<String, Throwable> errorConsumer;
public DownloadUpdateAction(SpigotPluginUpdater updater, BiConsumer<String, Throwable> 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();
}
}

View file

@ -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<String, Throwable> errorConsumer;
public InstallUpdateAction(SpigotPluginUpdater updater, BiConsumer<String, Throwable> 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();
}
}

View file

@ -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<String, Throwable> 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");
}
}
}

View file

@ -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();
}
}