diff --git a/build.gradle.kts b/build.gradle.kts index 82436b3..c9b481b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -21,7 +21,7 @@ tasks { Because semantic-release is not designed to handle this case, we need to hack it. RE: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435 - */ + */ register("publish") { group = "publishing" description = "Dummy task to hack gradle-semantic-release-plugin to release ReVanced API" @@ -41,6 +41,9 @@ ktor { repositories { mavenCentral() + google() + maven { url = uri("https://jitpack.io") } + mavenLocal() } dependencies { @@ -72,6 +75,9 @@ dependencies { implementation(libs.ktoml.file) implementation(libs.picocli) implementation(libs.kotlinx.datetime) + implementation(libs.revanced.patcher) + implementation(libs.revanced.library) + implementation(libs.caffeine) testImplementation(libs.mockk) testImplementation(libs.ktor.server.tests) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6bce316..f00cb0f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,6 +10,9 @@ ktoml = "0.5.1" picocli = "4.7.3" datetime = "0.5.0" mockk = "1.13.9" +revanced-patcher = "19.2.0" +revanced-library = "1.5.0" +caffeine = "3.1.8" [libraries] ktor-client-core = { module = "io.ktor:ktor-client-core" } @@ -43,6 +46,9 @@ ktoml-file = { module = "com.akuleshov7:ktoml-file", version.ref = "ktoml" } picocli = { module = "info.picocli:picocli", version.ref = "picocli" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" } mockk = { module = "io.mockk:mockk", version.ref = "mockk" } +revanced-patcher = { module = "app.revanced:revanced-patcher", version.ref = "revanced-patcher" } +revanced-library = { module = "app.revanced:revanced-library", version.ref = "revanced-library" } +caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "caffeine" } [plugins] serilization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } diff --git a/src/main/kotlin/app/revanced/api/modules/HTTP.kt b/src/main/kotlin/app/revanced/api/modules/HTTP.kt index 590629b..1f3248f 100644 --- a/src/main/kotlin/app/revanced/api/modules/HTTP.kt +++ b/src/main/kotlin/app/revanced/api/modules/HTTP.kt @@ -19,8 +19,6 @@ fun Application.configureHTTP() { anyHost() // @TODO: Don't do this in production if possible. Try to limit it. } install(CachingHeaders) { - options { _, _ -> - CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 5.minutes.inWholeSeconds.toInt())) - } + options { _, _ -> CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 5.minutes.inWholeSeconds.toInt())) } } } diff --git a/src/main/kotlin/app/revanced/api/modules/Routing.kt b/src/main/kotlin/app/revanced/api/modules/Routing.kt index 846bfc2..61ad106 100644 --- a/src/main/kotlin/app/revanced/api/modules/Routing.kt +++ b/src/main/kotlin/app/revanced/api/modules/Routing.kt @@ -2,6 +2,9 @@ package app.revanced.api.modules import app.revanced.api.backend.Backend import app.revanced.api.schema.* +import app.revanced.library.PatchUtils +import app.revanced.patcher.PatchBundleLoader +import com.github.benmanes.caffeine.cache.Caffeine import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.auth.* @@ -12,6 +15,8 @@ import io.ktor.server.routing.* import io.ktor.util.pipeline.* import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import java.io.File +import java.net.URL import org.koin.ktor.ext.get as koinGet fun Application.configureRouting() { @@ -116,34 +121,70 @@ fun Application.configureRouting() { route("/patches") { route("latest") { get { - val patches = backend.getRelease(configuration.organization, configuration.patchesRepository) - val integrations = - configuration.integrationsRepositoryNames.map { - async { backend.getRelease(configuration.organization, it) } - }.awaitAll() + val patchesRelease = + backend.getRelease(configuration.organization, configuration.patchesRepository) + val integrationsReleases = configuration.integrationsRepositoryNames.map { + async { backend.getRelease(configuration.organization, it) } + }.awaitAll() - val assets = - (patches.assets + integrations.flatMap { it.assets }).filter { - it.downloadUrl.endsWith(".apk") || it.downloadUrl.endsWith(".jar") - }.map { APIAsset(it.downloadUrl) }.toSet() + val assets = (patchesRelease.assets + integrationsReleases.flatMap { it.assets }) + .map { APIAsset(it.downloadUrl) } + .filter { it.type != APIAsset.Type.UNKNOWN } + .toSet() - val release = - APIRelease( - patches.tag, - patches.createdAt, - patches.releaseNote, - assets, - ) + val apiRelease = APIRelease( + patchesRelease.tag, + patchesRelease.createdAt, + patchesRelease.releaseNote, + assets, + ) - call.respond(release) + call.respond(apiRelease) } get("/version") { - val patches = backend.getRelease(configuration.organization, configuration.patchesRepository) + val patchesRelease = + backend.getRelease(configuration.organization, configuration.patchesRepository) - val release = APIReleaseVersion(patches.tag) + val apiPatchesRelease = APIReleaseVersion(patchesRelease.tag) - call.respond(release) + call.respond(apiPatchesRelease) + } + + val fileCache = Caffeine + .newBuilder() + .evictionListener { _, value, _ -> value?.delete() } + .maximumSize(1) + .build() + + get("/list") { + val patchesRelease = + backend.getRelease(configuration.organization, configuration.patchesRepository) + + // Get the cached patches file or download and cache a new one. + // The old file is deleted on eviction. + val patchesFile = fileCache.getIfPresent(patchesRelease.tag) ?: run { + val downloadUrl = patchesRelease.assets + .map { APIAsset(it.downloadUrl) } + .find { it.type == APIAsset.Type.PATCHES } + ?.downloadUrl + + kotlin.io.path.createTempFile().toFile().apply { + outputStream().use { URL(downloadUrl).openStream().copyTo(it) } + }.also { + fileCache.put(patchesRelease.tag, it) + it.deleteOnExit() + } + } + + call.respondOutputStream( + contentType = ContentType.Application.Json, + ) { + PatchUtils.Json.serialize( + PatchBundleLoader.Jar(patchesFile), + outputStream = this, + ) + } } } } diff --git a/src/main/kotlin/app/revanced/api/schema/APISchema.kt b/src/main/kotlin/app/revanced/api/schema/APISchema.kt index fe3e2c9..ec6355b 100644 --- a/src/main/kotlin/app/revanced/api/schema/APISchema.kt +++ b/src/main/kotlin/app/revanced/api/schema/APISchema.kt @@ -1,6 +1,7 @@ package app.revanced.api.schema import kotlinx.datetime.LocalDateTime +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable @@ -44,9 +45,20 @@ class APIAsset( val downloadUrl: String, ) { val type = when { - downloadUrl.endsWith(".jar") -> "patches" - downloadUrl.endsWith(".apk") -> "integrations" - else -> "unknown" + downloadUrl.endsWith(".jar") -> Type.PATCHES + downloadUrl.endsWith(".apk") -> Type.INTEGRATIONS + else -> Type.UNKNOWN + } + + enum class Type { + @SerialName("patches") + PATCHES, + + @SerialName("integrations") + INTEGRATIONS, + + @SerialName("unknown") + UNKNOWN, } }