From c606701f7160c8994044fe2f91172e7d2d265685 Mon Sep 17 00:00:00 2001 From: Pasqual Koschmieder Date: Sat, 28 Jun 2025 09:25:05 +0200 Subject: [PATCH] chore: migrate to paper downloads api v3 --- .../eu/cloudnetservice/node/impl/Node.java | 12 +++ .../PaperApiVersionFetchStepExecutor.java | 96 ++++++++++--------- .../src/main/resources/files/versions.json | 30 +++--- 3 files changed, 80 insertions(+), 58 deletions(-) diff --git a/node/impl/src/main/java/eu/cloudnetservice/node/impl/Node.java b/node/impl/src/main/java/eu/cloudnetservice/node/impl/Node.java index f92b73a13f..942b10fa5a 100644 --- a/node/impl/src/main/java/eu/cloudnetservice/node/impl/Node.java +++ b/node/impl/src/main/java/eu/cloudnetservice/node/impl/Node.java @@ -17,6 +17,7 @@ package eu.cloudnetservice.node.impl; import dev.derklaro.aerogel.Order; +import eu.cloudnetservice.driver.CloudNetVersion; import eu.cloudnetservice.driver.channel.ChannelMessage; import eu.cloudnetservice.driver.database.Database; import eu.cloudnetservice.driver.database.DatabaseProvider; @@ -80,6 +81,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BooleanSupplier; +import kong.unirest.core.Unirest; import lombok.NonNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -134,6 +136,16 @@ private void greetUser(@NonNull Console console) { HeaderReader.readAndPrintHeader(console); } + @Inject + @Order(75) + private void setupDefaultUnirestUserAgent(@NonNull CloudNetVersion version) { + var formattedVersion = String.format( + "%d.%d.%d-%s[%s]", + version.major(), version.minor(), version.patch(), version.versionType(), version.revision()); + var userAgent = String.format("CloudNetService/CloudNet/%s (https://discord.cloudnetservice.eu)", formattedVersion); + Unirest.config().addDefaultHeader("User-Agent", userAgent); + } + @Inject @Order(100) private void registerDummyRPCHandlers(@NonNull RPCFactory rpcFactory, @NonNull RPCHandlerRegistry handlerRegistry) { diff --git a/node/impl/src/main/java/eu/cloudnetservice/node/impl/version/execute/defaults/PaperApiVersionFetchStepExecutor.java b/node/impl/src/main/java/eu/cloudnetservice/node/impl/version/execute/defaults/PaperApiVersionFetchStepExecutor.java index 7d7de6b888..4be563799b 100644 --- a/node/impl/src/main/java/eu/cloudnetservice/node/impl/version/execute/defaults/PaperApiVersionFetchStepExecutor.java +++ b/node/impl/src/main/java/eu/cloudnetservice/node/impl/version/execute/defaults/PaperApiVersionFetchStepExecutor.java @@ -18,23 +18,21 @@ import eu.cloudnetservice.driver.document.Document; import eu.cloudnetservice.driver.document.DocumentFactory; -import eu.cloudnetservice.node.impl.version.ServiceVersionType; import eu.cloudnetservice.node.impl.version.execute.InstallStepExecutor; import eu.cloudnetservice.node.impl.version.information.VersionInstaller; import eu.cloudnetservice.utils.base.StringUtil; -import io.leangen.geantyref.TypeFactory; -import java.lang.reflect.Type; import java.nio.file.Path; -import java.util.Collections; +import java.util.Objects; import java.util.Set; import kong.unirest.core.Unirest; import lombok.NonNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class PaperApiVersionFetchStepExecutor implements InstallStepExecutor { - private static final String VERSION_LIST_URL = "https://api.papermc.io/v2/projects/%s/versions/%s"; - private static final String DOWNLOAD_URL = "https://api.papermc.io/v2/projects/%s/versions/%s/builds/%d/downloads/%s-%s-%d.jar"; - private static final Type INT_SET_TYPE = TypeFactory.parameterizedClass(Set.class, Integer.class); + private static final Logger LOGGER = LoggerFactory.getLogger(PaperApiVersionFetchStepExecutor.class); + private static final String LATEST_BUILD_URL = "https://fill.papermc.io/v3/projects/%s/versions/%s/builds/latest"; @Override public @NonNull Set execute( @@ -46,47 +44,59 @@ public class PaperApiVersionFetchStepExecutor implements InstallStepExecutor { var enabled = installer.serviceVersion().properties().getBoolean("fetchOverPaperApi"); var versionGroup = installer.serviceVersion().properties().getString("versionGroup"); if (enabled && versionGroup != null) { - // resolve the project name we should use for the api request - var project = this.decideApiProjectName(installer.serviceVersionType()); - var versionInformation = this.makeRequest(String.format(VERSION_LIST_URL, project, versionGroup)); - // check if there are any builds for the version - if (versionInformation.contains("builds")) { - // extract the build numbers from the response - Set builds = versionInformation.readObject("builds", INT_SET_TYPE); - // find the highest build number (the newest build) - var newestBuild = builds.stream().reduce(Math::max); - // check if there is a build - if (newestBuild.isPresent()) { - // set the download url of the service version required in the download step - int build = newestBuild.get(); - installer.serviceVersion() - .url(String.format(DOWNLOAD_URL, project, versionGroup, build, project, versionGroup, build)); - } else { - throw new IllegalStateException( - "Unable to retrieve latest build for papermc project " + project + " version-group " + versionGroup); - } - } else { - throw new IllegalStateException( - "Unable to load build information for papermc project " + project + " version-group " + versionGroup); + // resolve the download url to the latest available version for the version group + var projectName = StringUtil.toLower(installer.serviceVersionType().name()); + var latestBuildUri = String.format(LATEST_BUILD_URL, projectName, versionGroup); + var latestBuildInfo = this.makeRequest(latestBuildUri); + if (latestBuildInfo.empty()) { + throw new IllegalStateException(String.format( + "Unable to load latest build info from papermc api [project=%s, versionGroup=%s]", + projectName, versionGroup)); + } + + LOGGER.debug( + "Latest build available in papermc api for [project={}, versionGroup={}] is {} ({})", + projectName, versionGroup, latestBuildInfo.getInt("id"), latestBuildInfo.getString("time")); + + // get the available downloads (spigot and mojang mapped server jars) + var downloads = latestBuildInfo.readDocument("downloads"); + var defaultMappedDownload = downloads.readDocument("server:default", null); + var mojangMappedDownload = downloads.readDocument("server:mojang", null); + if (defaultMappedDownload == null && mojangMappedDownload == null) { + throw new IllegalStateException(String.format( + "Unable to resolve download info from papermc api response for [project=%s, versionGroup=%s], got %s", + projectName, versionGroup, latestBuildInfo.serializeToString())); + } + + // resolve the url to use for downloading the service version + var download = Objects.requireNonNullElse(defaultMappedDownload, mojangMappedDownload); + var downloadUrl = download.getString("url"); + if (downloadUrl == null) { + throw new IllegalStateException(String.format( + "No download url provided by papermc api for [project=%s, versionGroup=%s, build=%s]", + projectName, versionGroup, latestBuildInfo.getInt("id"))); } - } - // we generated no paths - return Collections.emptySet(); - } - private @NonNull Document makeRequest(@NonNull String apiUrl) { - var response = Unirest.get(apiUrl) - .accept("application/json") - .asString(); - if (response.isSuccess()) { - return DocumentFactory.json().parse(response.getBody()); + // update the download url for the service version + LOGGER.debug( + "Resolved download url from papermc api for [project={}, versionGroup={}, build={}] to {}", + projectName, versionGroup, latestBuildInfo.getInt("id"), downloadUrl); + installer.serviceVersion().url(downloadUrl); } - return Document.newJsonDocument(); + return Set.of(); } - @NonNull - private String decideApiProjectName(@NonNull ServiceVersionType type) { - return StringUtil.toLower(type.name()); + /** + * Makes a single api request to the given uri, returning the parsed response body on success. An empty document is + * returned in case the request was not successful. + * + * @param uri the uri to send a get request to. + * @return the parsed response body in the form of a document on success, an empty document on failure. + * @throws NullPointerException if the given uri is null. + */ + private @NonNull Document makeRequest(@NonNull String uri) { + var response = Unirest.get(uri).accept("application/json").asString(); + return response.isSuccess() ? DocumentFactory.json().parse(response.getBody()) : Document.emptyDocument(); } } diff --git a/node/impl/src/main/resources/files/versions.json b/node/impl/src/main/resources/files/versions.json index 1c977991a4..ef43cb1024 100644 --- a/node/impl/src/main/resources/files/versions.json +++ b/node/impl/src/main/resources/files/versions.json @@ -106,7 +106,7 @@ }, { "name": "1.21.5", - "url": "https://api.papermc.io/v2/projects/paper/versions/1.21.5/builds/114/downloads/paper-1.21.5-114.jar", + "url": "https://fill-data.papermc.io/v1/objects/2ae6ae22adf417699746e0f89fc2ef6cb6ee050a5f6608cee58f0535d60b509e/paper-1.21.5-114.jar", "deprecated": true, "properties": { "copy": { @@ -121,7 +121,7 @@ }, { "name": "1.21.4", - "url": "https://api.papermc.io/v2/projects/paper/versions/1.21.4/builds/232/downloads/paper-1.21.4-232.jar", + "url": "https://fill-data.papermc.io/v1/objects/5ee4f542f628a14c644410b08c94ea42e772ef4d29fe92973636b6813d4eaffc/paper-1.21.4-232.jar", "deprecated": true, "properties": { "copy": { @@ -136,7 +136,7 @@ }, { "name": "1.20.6", - "url": "https://api.papermc.io/v2/projects/paper/versions/1.20.6/builds/151/downloads/paper-1.20.6-151.jar", + "url": "https://fill-data.papermc.io/v1/objects/4b011f5adb5f6c72007686a223174fce82f31aeb4b34faf4652abc840b47e640/paper-1.20.6-151.jar", "deprecated": true, "properties": { "copy": { @@ -151,7 +151,7 @@ }, { "name": "1.19.4", - "url": "https://api.papermc.io/v2/projects/paper/versions/1.19.4/builds/550/downloads/paper-1.19.4-550.jar", + "url": "https://fill-data.papermc.io/v1/objects/e587d78cba3e99ef8c4bc24cf20cc3bdbbe89e33b0b572070446af4eb6be5ccf/paper-1.19.4-550.jar", "deprecated": true, "properties": { "copy": { @@ -166,7 +166,7 @@ }, { "name": "1.18.2", - "url": "https://api.papermc.io/v2/projects/paper/versions/1.18.2/builds/387/downloads/paper-1.18.2-387.jar", + "url": "https://fill-data.papermc.io/v1/objects/0578f18f4d632b494b468ec56b3b414b5b56fea087ee7d39cf6dcdf4c9d01f05/paper-1.18.2-388.jar", "deprecated": true, "properties": { "copy": { @@ -181,7 +181,7 @@ }, { "name": "1.17.1", - "url": "https://api.papermc.io/v2/projects/paper/versions/1.17.1/builds/409/downloads/paper-1.17.1-409.jar", + "url": "https://fill-data.papermc.io/v1/objects/6cc1ee2f94253ce10b5374ed85fffc735a97d8f1b64db293683dfa24dd3cc05f/paper-1.17.1-411.jar", "deprecated": true, "properties": { "copy": { @@ -194,7 +194,7 @@ }, { "name": "1.16.5", - "url": "https://api.papermc.io/v2/projects/paper/versions/1.16.5/builds/794/downloads/paper-1.16.5-794.jar", + "url": "https://fill-data.papermc.io/v1/objects/e67da4851d08cde378ab2b89be58849238c303351ed2482181a99c2c2b489276/paper-1.16.5-794.jar", "deprecated": true, "properties": { "copy": { @@ -207,7 +207,7 @@ }, { "name": "1.15.2", - "url": "https://api.papermc.io/v2/projects/paper/versions/1.15.2/builds/393/downloads/paper-1.15.2-393.jar", + "url": "https://fill-data.papermc.io/v1/objects/bd2dd6f2cc489cf9e2bb800cb4fb6d63e9d293945d3ac10b09dd9c6098fa9f34/paper-1.15.2-393.jar", "deprecated": true, "properties": { "copy": { @@ -220,7 +220,7 @@ }, { "name": "1.14.4", - "url": "https://api.papermc.io/v2/projects/paper/versions/1.14.4/builds/245/downloads/paper-1.14.4-245.jar", + "url": "https://fill-data.papermc.io/v1/objects/bd8ec5cdb22370d37816a6de26798df3d2b0d6f9c7c96c88ca45a1303fea50e8/paper-1.14.4-245.jar", "deprecated": true, "properties": { "copy": { @@ -233,7 +233,7 @@ }, { "name": "1.13.2", - "url": "https://api.papermc.io/v2/projects/paper/versions/1.13.2/builds/657/downloads/paper-1.13.2-657.jar", + "url": "https://fill-data.papermc.io/v1/objects/11e828d0565ab76a0a0e180c056364a95de44958cfd6a6af3f9b1dc70b03e9cd/paper-1.13.2-657.jar", "deprecated": true, "properties": { "copy": { @@ -246,7 +246,7 @@ }, { "name": "1.12.2", - "url": "https://api.papermc.io/v2/projects/paper/versions/1.12.2/builds/1620/downloads/paper-1.12.2-1620.jar", + "url": "https://fill-data.papermc.io/v1/objects/3a2041807f492dcdc34ebb324a287414946e3e05ec3df6fd03f5b5f7d9afc210/paper-1.12.2-1620.jar", "deprecated": true, "properties": { "copy": { @@ -259,7 +259,7 @@ }, { "name": "1.11.2", - "url": "https://api.papermc.io/v2/projects/paper/versions/1.11.2/builds/1106/downloads/paper-1.11.2-1106.jar", + "url": "https://fill-data.papermc.io/v1/objects/3d0f40ec1f9630dfdbafa626cc20c266d7fb90fc22583dc1b995e7fbfb76830d/paper-1.11.2-1106.jar", "deprecated": true, "properties": { "copy": { @@ -272,7 +272,7 @@ }, { "name": "1.10.2", - "url": "https://api.papermc.io/v2/projects/paper/versions/1.10.2/builds/918/downloads/paper-1.10.2-918.jar", + "url": "https://fill-data.papermc.io/v1/objects/83354d24a22b6265e76c089b3d17a568abb446c0ccd12c2452f5e148412b16c2/paper-1.10.2-918.jar", "deprecated": true, "properties": { "copy": { @@ -285,7 +285,7 @@ }, { "name": "1.9.4", - "url": "https://api.papermc.io/v2/projects/paper/versions/1.9.4/builds/775/downloads/paper-1.9.4-775.jar", + "url": "https://fill-data.papermc.io/v1/objects/15a5821ddeacc596432c3fbf24262a2d264f556060ecd6f1838fb01ab5629a81/paper-1.9.4-775.jar", "deprecated": true, "properties": { "copy": { @@ -295,7 +295,7 @@ }, { "name": "1.8.8", - "url": "https://api.papermc.io/v2/projects/paper/versions/1.8.8/builds/445/downloads/paper-1.8.8-445.jar", + "url": "https://fill-data.papermc.io/v1/objects/7ff6d2cec671ef0d95b3723b5c92890118fb882d73b7f8fa0a2cd31d97c55f86/paper-1.8.8-445.jar", "deprecated": true, "properties": { "copy": {