From 49abca2ccfee01eabcb425ada89ad91d539c0c92 Mon Sep 17 00:00:00 2001 From: alexd <42614139+alexcrea@users.noreply.github.com> Date: Fri, 27 Feb 2026 19:44:30 +0100 Subject: [PATCH] update checker --- CREDITS.MD | 1 + .../cuanvil/update/ModrinthUpdateChecker.java | 214 ++++++++++++++++++ src/main/kotlin/io/delilaheve/CustomAnvil.kt | 21 +- 3 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 src/main/java/xyz/alexcrea/cuanvil/update/ModrinthUpdateChecker.java diff --git a/CREDITS.MD b/CREDITS.MD index e4f2533..b4a3ce8 100644 --- a/CREDITS.MD +++ b/CREDITS.MD @@ -13,6 +13,7 @@ Here dependencies are used by custom anvil - [modrinth-publish](https://github.com/cloudnode-pro/modrinth-publish) by Zefir - [discord-webhook](https://github.com/tsickert/discord-webhook) by tsickert - Thanks [bstats](https://bstats.org/) for keeping me motivated +- [ModrinthUpdateChecker](https://github.com/Clickism/ModrinthUpdateChecker) by Clickism and thanks to the modrinth team ### Compatibility Here is to credits all the author of plugins diff --git a/src/main/java/xyz/alexcrea/cuanvil/update/ModrinthUpdateChecker.java b/src/main/java/xyz/alexcrea/cuanvil/update/ModrinthUpdateChecker.java new file mode 100644 index 0000000..489c636 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/update/ModrinthUpdateChecker.java @@ -0,0 +1,214 @@ +/* + * MIT License + * + * Copyright (c) 2025 Clickism + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR + * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package xyz.alexcrea.cuanvil.update; + +import com.google.gson.*; +import org.jetbrains.annotations.Nullable; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Utility class to check for newer versions of a project hosted on Modrinth. + */ +public class ModrinthUpdateChecker { + + private static final String API_URL = "https://api.modrinth.com/v2/project/{id}/version"; + + private final String projectId; + private final String loader; + @Nullable + private final String minecraftVersion; + + @Nullable + private Boolean featured = null; + + @Nullable + public Consumer onError = null; + @Nullable + public Function getRawVersion = ModrinthUpdateChecker::getRawVersion; + + /** + * Create a new update checker for the given project. + * This will check the latest version for the given loader and any minecraft version. + * + * @param projectId the project ID + * @param loader the loader + */ + public ModrinthUpdateChecker(String projectId, String loader) { + this(projectId, loader, null); + } + + /** + * Create a new update checker for the given project. + * This will check the latest version for the given loader and minecraft version. + * + * @param projectId the project ID + * @param loader the loader + * @param minecraftVersion the minecraft version, or null for any version + */ + public ModrinthUpdateChecker(String projectId, String loader, @Nullable String minecraftVersion) { + this.projectId = projectId; + this.loader = loader; + this.minecraftVersion = minecraftVersion; + } + + /** + * Check the latest version of the project for the given loader and minecraft version + * and call the consumer with it. + * + * @param consumer the consumer + */ + public void checkVersion(Consumer consumer) { + try { + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(prepareURI()) + .GET() + .build(); + + client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .thenAcceptAsync(response -> { + if (response.statusCode() != 200) { + if(onError != null) + onError.accept(new RuntimeException("wrong response status code: " + response.statusCode())); + return; + } + JsonArray versionsArray = JsonParser.parseString(response.body()).getAsJsonArray(); + String latestVersion = getLatestVersion(versionsArray); + if (latestVersion == null) { + if(onError != null) + onError.accept(new RuntimeException("latest version is null")); + return; + } + consumer.accept(latestVersion); + }); + } catch (Exception e) { + if(onError != null) onError.accept(e); + } + } + + /** + * Get the latest compatible version from the versions array. + * + * @param versions the versions array + * @return the latest compatible version + */ + @Nullable + protected String getLatestVersion(JsonArray versions) { + return versions.asList().stream().findFirst() + .map(JsonElement::getAsJsonObject) + .map(version -> version.get("version_number").getAsString()) + .map(getRawVersion != null ? getRawVersion : (v -> v)) + .orElse(null); + } + + /** + * Gets the raw version from a version string. + * i.E: "fabric-1.2+1.17.1" -> "1.2" + * + * @param version the version string + * @return the raw version string + */ + public static String getRawVersion(String version) { + if (version.isEmpty()) return version; + version = version.replaceAll("^\\D+", ""); + String[] split = version.split("\\+"); + return split[0]; + } + + /** + * Prepare this request uri based on current parameters. + * @return the request uri + */ + private URI prepareURI() { + var url = new StringBuilder(API_URL.replace("{id}", projectId)); + + var parameters = prepareParameters(); + String[] paramArray = new String[parameters.size()]; + int i = 0; + for (Map.Entry entry : parameters.entrySet()) { + paramArray[i++] = entry.getKey() + '=' + entry.getValue(); + } + url.append('?').append(String.join("&", paramArray)); + + return URI.create(url.toString()); + } + + /** + * Get the parameters for the version request. + * + * @return a map of key-value map of the request parameters + */ + private Map prepareParameters(){ + var parameters = new HashMap(); + + parameters.put("loaders", List.of(loader).toString()); + if(minecraftVersion != null) parameters.put("game_versions", List.of(minecraftVersion).toString()); + if(featured != null) parameters.put("featured", featured.toString()); + + parameters.put("include_changelog", "false"); + return parameters; + } + + /** + * Only get featured or non-featured versions. + * Null represent no filter. + * @param featured should be restricted to featured version ? default null if not called + * @return this + */ + public ModrinthUpdateChecker setFeatured(@Nullable Boolean featured) { + this.featured = featured; + return this; + } + + /** + * Function called on error calling the api. + * @param onError What should happen on error + * @return this + */ + public ModrinthUpdateChecker setOnError(@Nullable Consumer onError) { + this.onError = onError; + return this; + } + + /** + * Set the function to get raw version from the modrinth version. + * If null provided raw version will act as in the identity function. + * @param getRawVersion The function transforming modrinth version to raw version + * @return this + */ + public ModrinthUpdateChecker setGetRawVersion(@Nullable Function getRawVersion) { + this.getRawVersion = getRawVersion; + return this; + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/delilaheve/CustomAnvil.kt b/src/main/kotlin/io/delilaheve/CustomAnvil.kt index ca25147..b48f4a6 100644 --- a/src/main/kotlin/io/delilaheve/CustomAnvil.kt +++ b/src/main/kotlin/io/delilaheve/CustomAnvil.kt @@ -18,6 +18,7 @@ import xyz.alexcrea.cuanvil.listener.AnvilCloseListener import xyz.alexcrea.cuanvil.listener.AnvilResultListener import xyz.alexcrea.cuanvil.listener.ChatEventListener import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener +import xyz.alexcrea.cuanvil.update.ModrinthUpdateChecker import xyz.alexcrea.cuanvil.update.PluginSetDefault import xyz.alexcrea.cuanvil.update.UpdateHandler import xyz.alexcrea.cuanvil.util.Metrics @@ -31,8 +32,9 @@ import java.util.logging.Level open class CustomAnvil : JavaPlugin() { companion object { - // bstats plugin id + // pluginIDS private const val bstatsPluginId = 20923 + private const val modrinthPluginID = "S75Ueiq9" // Permission string required to use the plugin's features const val affectedByPluginPermission = "ca.affected" @@ -174,10 +176,25 @@ open class CustomAnvil : JavaPlugin() { logger.warning("Please note CustomAnvil is a more recent version of UnsafeEnchantsPlus") } - if(!PlatformUtil.isPaper) { + val isPaper = PlatformUtil.isPaper + if(!isPaper) { logger.warning("It seems you are using spigot") logger.warning("Please take notice that spigot is less supported than paper and derivatives") } + + val loader = if(isPaper) "paper" else "spigot" + + val version = description.version + val featured = if(version.contains("dev")) null else true + + ModrinthUpdateChecker(modrinthPluginID, loader, null) + .setFeatured(featured) + .setOnError { logger.log(Level.WARNING, "error trying to fetch latest update", it) } + .checkVersion { latestver: String? -> + if(latestver == null || version.contains(latestver)) return@checkVersion + + logger.warning("An update may be available: $latestver") + } } private fun registerListeners() {