diff --git a/LavalinkServer/build.gradle.kts b/LavalinkServer/build.gradle.kts index ddb62b35a..2e7b623b8 100644 --- a/LavalinkServer/build.gradle.kts +++ b/LavalinkServer/build.gradle.kts @@ -8,6 +8,7 @@ plugins { application kotlin("jvm") id("org.jetbrains.dokka") + kotlin("plugin.serialization") alias(libs.plugins.maven.publish.base) } @@ -30,6 +31,15 @@ java { sourceCompatibility = JavaVersion.VERSION_17 } +kotlin { + sourceSets { + all { + languageSettings.optIn("kotlinx.serialization.ExperimentalSerializationApi") + languageSettings.optIn("nl.adaptivity.xmlutil.ExperimentalXmlUtilApi") + } + } +} + configurations { compileOnly { extendsFrom(annotationProcessor.get()) @@ -42,6 +52,9 @@ dependencies { exclude(group = "org.springframework.boot", module = "spring-boot-starter-tomcat") } + implementation(libs.xmlutil.jdk) + implementation(libs.xmlutil.serialization) + implementation(libs.bundles.metrics) implementation(libs.bundles.spring) { exclude(group = "org.springframework.boot", module = "spring-boot-starter-tomcat") diff --git a/LavalinkServer/src/main/java/lavalink/server/bootstrap/PluginManager.kt b/LavalinkServer/src/main/java/lavalink/server/bootstrap/PluginManager.kt index c3ed4cb73..1ca92028a 100644 --- a/LavalinkServer/src/main/java/lavalink/server/bootstrap/PluginManager.kt +++ b/LavalinkServer/src/main/java/lavalink/server/bootstrap/PluginManager.kt @@ -1,5 +1,8 @@ package lavalink.server.bootstrap +import dev.arbjerg.lavalink.protocol.v4.Version +import nl.adaptivity.xmlutil.core.KtXmlReader +import nl.adaptivity.xmlutil.serialization.XML import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.boot.autoconfigure.SpringBootApplication @@ -7,6 +10,7 @@ import org.springframework.core.io.support.PathMatchingResourcePatternResolver import java.io.File import java.io.FileOutputStream import java.io.InputStream +import java.net.HttpURLConnection import java.net.URL import java.net.URLClassLoader import java.nio.channels.Channels @@ -44,7 +48,7 @@ class PluginManager(val config: PluginsConfig) { loadPluginManifests(jar).map { manifest -> PluginJar(manifest, file) } } } - ?.onEach { log.info("Found plugin '${it.manifest.name}' version ${it.manifest.version}") } + ?.onEach { log.info("Found plugin '${it.manifest.name}' version '${it.manifest.version}'") } ?: return val declarations = config.plugins.map { declaration -> @@ -83,6 +87,44 @@ class PluginManager(val config: PluginsConfig) { val file = File(directory, declaration.canonicalJarName) downloadJar(file, url) } + + checkPluginForUpdates(declaration) + } + } + + private fun checkPluginForUpdates(declaration: Declaration) { + val baseSplitPath = declaration.url.split('/').dropLast(2) + val metadataUrl = baseSplitPath.joinToString("/") + "/maven-metadata.xml" + + val url = URL(metadataUrl) + val conn = url.openConnection() as HttpURLConnection + + if (conn.responseCode != HttpURLConnection.HTTP_OK) { + log.warn("Failed to check for updates for ${declaration.name}: ${conn.responseMessage}") + return + } + + val metadata: Metadata + conn.inputStream.use { + metadata = XML.decodeFromReader(Metadata.serializer(), KtXmlReader(it)) + } + + val current = Version.fromSemver(declaration.version) + var latest = metadata.versioning.latest + if (latest.isEmpty()) { + latest = metadata.versioning.release + } + if (latest.isEmpty()) { + latest = (metadata.versioning.versions.lastOrNull() ?: "").toString() + } + + if (latest.isEmpty()) { + return + } + + val latestVersion = Version.fromSemver(latest) + if (latestVersion > current) { + log.warn("A newer version of '${declaration.name}' was found: '$latestVersion', The current version is '$current' please update the version in your configuration.") } } @@ -98,7 +140,7 @@ class PluginManager(val config: PluginsConfig) { return PathMatchingResourcePatternResolver() .getResources("classpath*:lavalink-plugins/*.properties") .map { parsePluginManifest(it.inputStream) } - .onEach { log.info("Found plugin '${it.name}' version ${it.version}") } + .onEach { log.info("Found plugin '${it.name}' version '${it.version}'") } } private fun loadJars(): List { @@ -132,14 +174,15 @@ class PluginManager(val config: PluginsConfig) { for (entry in it.entries()) { if (entry.isDirectory || !entry.name.endsWith(".class") || - allowedPaths.none(entry.name::startsWith)) continue + allowedPaths.none(entry.name::startsWith) + ) continue cl.loadClass(entry.name.dropLast(6).replace("/", ".")) classCount++ } } - log.info("Loaded ${file.name} ($classCount classes)") + log.info("Loaded '${file.name}' ($classCount classes)") return manifests } diff --git a/LavalinkServer/src/main/java/lavalink/server/bootstrap/maven.kt b/LavalinkServer/src/main/java/lavalink/server/bootstrap/maven.kt new file mode 100644 index 000000000..ec3d798d5 --- /dev/null +++ b/LavalinkServer/src/main/java/lavalink/server/bootstrap/maven.kt @@ -0,0 +1,31 @@ +package lavalink.server.bootstrap + +import kotlinx.serialization.Serializable +import nl.adaptivity.xmlutil.serialization.XmlChildrenName +import nl.adaptivity.xmlutil.serialization.XmlElement +import nl.adaptivity.xmlutil.serialization.XmlSerialName + +@Serializable +@XmlSerialName("metadata") +data class Metadata( + @XmlElement(true) + val groupId: String, + @XmlElement(true) + val artifactId: String, + @XmlElement(true) + val versioning: Versioning +) + +@Serializable +@XmlSerialName("versioning") +data class Versioning( + @XmlElement(true) + val latest: String, + @XmlElement(true) + val release: String, + @XmlElement(true) + @XmlChildrenName("version") + val versions: List, + @XmlElement(true) + val lastUpdated: String +) diff --git a/protocol/src/commonMain/kotlin/dev/arbjerg/lavalink/protocol/v4/info.kt b/protocol/src/commonMain/kotlin/dev/arbjerg/lavalink/protocol/v4/info.kt index d739c53e3..2040c25cc 100644 --- a/protocol/src/commonMain/kotlin/dev/arbjerg/lavalink/protocol/v4/info.kt +++ b/protocol/src/commonMain/kotlin/dev/arbjerg/lavalink/protocol/v4/info.kt @@ -62,6 +62,70 @@ data class Version( return Version(semver, major.toInt(), minor.toInt(), patch.toInt(), preRelease) } } + + override fun toString(): String { + var baseSemver = "${major}.${minor}.${patch}" + + if(!preRelease.isNullOrEmpty()) + baseSemver += "-${preRelease}" + + return baseSemver + } + + operator fun compareTo(other: Version): Int { + // Compare major, minor, and patch + val majorDiff = major - other.major + if (majorDiff != 0) return majorDiff + + val minorDiff = minor - other.minor + if (minorDiff != 0) return minorDiff + + val patchDiff = patch - other.patch + if (patchDiff != 0) return patchDiff + + // Compare prerelease (null means no prerelease and is greater) + return when { + preRelease.isNullOrEmpty() && other.preRelease.isNullOrEmpty() -> 0 + preRelease.isNullOrEmpty() && !other.preRelease.isNullOrEmpty() -> 1 + !preRelease.isNullOrEmpty() && other.preRelease.isNullOrEmpty() -> -1 + !preRelease.isNullOrEmpty() && !other.preRelease.isNullOrEmpty() -> comparePreRelease(preRelease, other.preRelease) + else -> 0 + } + } + + private fun comparePreRelease(part1: String, part2: String): Int { + val components1 = part1.split(".") + val components2 = part2.split(".") + val maxLength = maxOf(components1.size, components2.size) + + for (i in 0 until maxLength) { + val comp1 = components1.getOrNull(i) + val comp2 = components2.getOrNull(i) + + if (comp1 == null) return -1 // `part1` is shorter and considered smaller + if (comp2 == null) return 1 // `part2` is shorter and considered smaller + + val isNumeric1 = comp1.all { it.isDigit() } + val isNumeric2 = comp2.all { it.isDigit() } + + when { + isNumeric1 && isNumeric2 -> { + // Compare numerically + val diff = comp1.toInt() - comp2.toInt() + if (diff != 0) return diff + } + isNumeric1 -> return -1 // Numeric parts come before string parts + isNumeric2 -> return 1 // String parts come after numeric parts + else -> { + // Compare lexicographically + val diff = comp1.compareTo(comp2) + if (diff != 0) return diff + } + } + } + + return 0 // Parts are equal + } } @Serializable diff --git a/settings.gradle.kts b/settings.gradle.kts index 14cbf4aa8..0a09abe54 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -73,6 +73,9 @@ fun VersionCatalogBuilder.common() { library("kotlinx-serialization-json", "org.jetbrains.kotlinx", "kotlinx-serialization-json").version("1.7.0") library("kotlinx-datetime", "org.jetbrains.kotlinx", "kotlinx-datetime").version("0.6.0") + library("xmlutil-jdk", "io.github.pdvrieze.xmlutil", "core-jdk").version("0.90.3") + library("xmlutil-serialization", "io.github.pdvrieze.xmlutil", "serialization-jvm").version("0.90.3") + library("logback", "ch.qos.logback", "logback-classic").version("1.5.6") library("sentry-logback", "io.sentry", "sentry-logback").version("7.10.0") library("oshi", "com.github.oshi", "oshi-core").version("6.4.11")