feat: Add /list route

This commit is contained in:
oSumAtrIX 2024-02-07 01:16:35 +01:00
parent e798a4c070
commit 6c930fff9a
No known key found for this signature in database
GPG Key ID: A9B3094ACDB604B4
5 changed files with 90 additions and 27 deletions

View File

@ -21,7 +21,7 @@ tasks {
Because semantic-release is not designed to handle this case, we need to hack it. 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 RE: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435
*/ */
register<DefaultTask>("publish") { register<DefaultTask>("publish") {
group = "publishing" group = "publishing"
description = "Dummy task to hack gradle-semantic-release-plugin to release ReVanced API" description = "Dummy task to hack gradle-semantic-release-plugin to release ReVanced API"
@ -41,6 +41,9 @@ ktor {
repositories { repositories {
mavenCentral() mavenCentral()
google()
maven { url = uri("https://jitpack.io") }
mavenLocal()
} }
dependencies { dependencies {
@ -72,6 +75,9 @@ dependencies {
implementation(libs.ktoml.file) implementation(libs.ktoml.file)
implementation(libs.picocli) implementation(libs.picocli)
implementation(libs.kotlinx.datetime) implementation(libs.kotlinx.datetime)
implementation(libs.revanced.patcher)
implementation(libs.revanced.library)
implementation(libs.caffeine)
testImplementation(libs.mockk) testImplementation(libs.mockk)
testImplementation(libs.ktor.server.tests) testImplementation(libs.ktor.server.tests)

View File

@ -10,6 +10,9 @@ ktoml = "0.5.1"
picocli = "4.7.3" picocli = "4.7.3"
datetime = "0.5.0" datetime = "0.5.0"
mockk = "1.13.9" mockk = "1.13.9"
revanced-patcher = "19.2.0"
revanced-library = "1.5.0"
caffeine = "3.1.8"
[libraries] [libraries]
ktor-client-core = { module = "io.ktor:ktor-client-core" } 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" } picocli = { module = "info.picocli:picocli", version.ref = "picocli" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" }
mockk = { module = "io.mockk:mockk", version.ref = "mockk" } 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] [plugins]
serilization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } serilization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

View File

@ -19,8 +19,6 @@ fun Application.configureHTTP() {
anyHost() // @TODO: Don't do this in production if possible. Try to limit it. anyHost() // @TODO: Don't do this in production if possible. Try to limit it.
} }
install(CachingHeaders) { install(CachingHeaders) {
options { _, _ -> options { _, _ -> CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 5.minutes.inWholeSeconds.toInt())) }
CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 5.minutes.inWholeSeconds.toInt()))
}
} }
} }

View File

@ -2,6 +2,9 @@ package app.revanced.api.modules
import app.revanced.api.backend.Backend import app.revanced.api.backend.Backend
import app.revanced.api.schema.* 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.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.auth.* import io.ktor.server.auth.*
@ -12,6 +15,8 @@ import io.ktor.server.routing.*
import io.ktor.util.pipeline.* import io.ktor.util.pipeline.*
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import java.io.File
import java.net.URL
import org.koin.ktor.ext.get as koinGet import org.koin.ktor.ext.get as koinGet
fun Application.configureRouting() { fun Application.configureRouting() {
@ -116,34 +121,70 @@ fun Application.configureRouting() {
route("/patches") { route("/patches") {
route("latest") { route("latest") {
get { get {
val patches = backend.getRelease(configuration.organization, configuration.patchesRepository) val patchesRelease =
val integrations = backend.getRelease(configuration.organization, configuration.patchesRepository)
configuration.integrationsRepositoryNames.map { val integrationsReleases = configuration.integrationsRepositoryNames.map {
async { backend.getRelease(configuration.organization, it) } async { backend.getRelease(configuration.organization, it) }
}.awaitAll() }.awaitAll()
val assets = val assets = (patchesRelease.assets + integrationsReleases.flatMap { it.assets })
(patches.assets + integrations.flatMap { it.assets }).filter { .map { APIAsset(it.downloadUrl) }
it.downloadUrl.endsWith(".apk") || it.downloadUrl.endsWith(".jar") .filter { it.type != APIAsset.Type.UNKNOWN }
}.map { APIAsset(it.downloadUrl) }.toSet() .toSet()
val release = val apiRelease = APIRelease(
APIRelease( patchesRelease.tag,
patches.tag, patchesRelease.createdAt,
patches.createdAt, patchesRelease.releaseNote,
patches.releaseNote, assets,
assets, )
)
call.respond(release) call.respond(apiRelease)
} }
get("/version") { 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<String, File> { _, value, _ -> value?.delete() }
.maximumSize(1)
.build<String, File>()
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,
)
}
} }
} }
} }

View File

@ -1,6 +1,7 @@
package app.revanced.api.schema package app.revanced.api.schema
import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
@ -44,9 +45,20 @@ class APIAsset(
val downloadUrl: String, val downloadUrl: String,
) { ) {
val type = when { val type = when {
downloadUrl.endsWith(".jar") -> "patches" downloadUrl.endsWith(".jar") -> Type.PATCHES
downloadUrl.endsWith(".apk") -> "integrations" downloadUrl.endsWith(".apk") -> Type.INTEGRATIONS
else -> "unknown" else -> Type.UNKNOWN
}
enum class Type {
@SerialName("patches")
PATCHES,
@SerialName("integrations")
INTEGRATIONS,
@SerialName("unknown")
UNKNOWN,
} }
} }