diff --git a/build.gradle.kts b/build.gradle.kts index c9b481ba..a62319d8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,21 +11,11 @@ tasks { expand("projectVersion" to project.version) } - /* - Dummy task to hack gradle-semantic-release-plugin to release this project. - - Explanation: - SemVer is a standard for versioning libraries. - For that reason the semantic-release plugin uses the "publish" task to publish libraries. - However, this subproject is not a library, and the "publish" task is not available for this subproject. - 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 - */ + // Needed by gradle-semantic-release-plugin. + // Tracking: 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" - dependsOn(startShadowScripts) + dependsOn(shadowJar) } } @@ -42,8 +32,15 @@ ktor { repositories { mavenCentral() google() - maven { url = uri("https://jitpack.io") } mavenLocal() + maven { + // A repository must be specified for some reason. "registry" is a dummy. + url = uri("https://maven.pkg.github.com/revanced/registry") + credentials { + username = project.findProperty("gpr.user") as String? ?: System.getenv("GITHUB_ACTOR") + password = project.findProperty("gpr.key") as String? ?: System.getenv("GITHUB_TOKEN") + } + } } dependencies { @@ -78,8 +75,4 @@ dependencies { implementation(libs.revanced.patcher) implementation(libs.revanced.library) implementation(libs.caffeine) - - testImplementation(libs.mockk) - testImplementation(libs.ktor.server.tests) - testImplementation(libs.kotlin.test.junit) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f00cb0f7..66cbed37 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -kotlin = "1.9.22" +kotlin = "2.0.0" logback = "1.4.14" exposed = "0.41.1" h2 = "2.2.224" @@ -7,11 +7,10 @@ koin = "3.5.3" dotenv = "6.4.1" ktor = "2.3.7" ktoml = "0.5.1" -picocli = "4.7.3" +picocli = "4.7.5" datetime = "0.5.0" -mockk = "1.13.9" -revanced-patcher = "19.2.0" -revanced-library = "1.5.0" +revanced-patcher = "19.3.1" +revanced-library = "2.3.0" caffeine = "3.1.8" [libraries] @@ -39,13 +38,10 @@ exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "e exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" } exposed-kotlin-datetime = { module = "org.jetbrains.exposed:exposed-kotlin-datetime", version.ref = "exposed" } dotenv-kotlin = { module = "io.github.cdimascio:dotenv-kotlin", version.ref = "dotenv" } -ktor-server-tests = { module = "io.ktor:ktor-server-tests" } -kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } ktoml-core = { module = "com.akuleshov7:ktoml-core", version.ref = "ktoml" } 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" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7454180f..e6441136 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e411586a..8a1f6b97 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,8 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionSha256Sum=a4b4158601f8636cdeeab09bd76afb640030bb5b144aafe261a5e8af027dc612 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1b6c7873..b740cf13 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,13 +80,11 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,22 +131,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,11 +198,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ @@ -205,6 +214,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index 107acd32..25da30db 100755 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,13 +41,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -56,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt index b1898ef9..585730a7 100644 --- a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt +++ b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt @@ -1,6 +1,7 @@ package app.revanced.api.command -import app.revanced.api.modules.* +import app.revanced.api.configuration.* +import app.revanced.api.configuration.routing.configureRouting import io.ktor.server.engine.* import io.ktor.server.netty.* import picocli.CommandLine @@ -27,7 +28,7 @@ internal object StartAPICommand : Runnable { override fun run() { embeddedServer(Netty, port, host) { configureDependencies() - configureHTTP() + configureHTTP(allowedHost = host) configureSerialization() configureSecurity() configureRouting() diff --git a/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt b/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt new file mode 100644 index 00000000..dc8f6204 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt @@ -0,0 +1,83 @@ +package app.revanced.api.configuration + +import app.revanced.api.repository.AnnouncementRepository +import app.revanced.api.repository.ConfigurationRepository +import app.revanced.api.repository.backend.BackendRepository +import app.revanced.api.repository.backend.github.GitHubBackendRepository +import app.revanced.api.services.AnnouncementService +import app.revanced.api.services.ApiService +import app.revanced.api.services.AuthService +import app.revanced.api.services.PatchesService +import com.akuleshov7.ktoml.Toml +import com.akuleshov7.ktoml.source.decodeFromStream +import io.github.cdimascio.dotenv.Dotenv +import io.ktor.server.application.* +import org.jetbrains.exposed.sql.Database +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.bind +import org.koin.dsl.module +import org.koin.ktor.plugin.Koin +import java.io.File + +fun Application.configureDependencies() { + val globalModule = module { + single { + Dotenv.configure() + .systemProperties() + .load() + } + } + + val repositoryModule = module { + single { + val dotenv = get() + + Database.connect( + url = dotenv["DB_URL"], + user = dotenv["DB_USER"], + password = dotenv["DB_PASSWORD"], + driver = "org.h2.Driver", + ) + } + + single { + val configFilePath = get()["CONFIG_FILE_PATH"] + val configFile = File(configFilePath).inputStream() + + Toml.decodeFromStream(configFile) + } + + singleOf(::AnnouncementRepository) + } + + val serviceModule = module { + single { + val dotenv = get() + + val jwtSecret = dotenv["JWT_SECRET"] + val issuer = dotenv["JWT_ISSUER"] + val validityInMin = dotenv["JWT_VALIDITY_IN_MIN"].toInt() + + val basicUsername = dotenv["BASIC_USERNAME"] + val basicPassword = dotenv["BASIC_PASSWORD"] + + AuthService(issuer, validityInMin, jwtSecret, basicUsername, basicPassword) + } + single { + val token = get()["GITHUB_TOKEN"] + + GitHubBackendRepository(token) + } bind BackendRepository::class + singleOf(::AnnouncementService) + singleOf(::PatchesService) + singleOf(::ApiService) + } + + install(Koin) { + modules( + globalModule, + repositoryModule, + serviceModule, + ) + } +} diff --git a/src/main/kotlin/app/revanced/api/modules/HTTP.kt b/src/main/kotlin/app/revanced/api/configuration/HTTP.kt similarity index 81% rename from src/main/kotlin/app/revanced/api/modules/HTTP.kt rename to src/main/kotlin/app/revanced/api/configuration/HTTP.kt index 1f3248f9..336b100b 100644 --- a/src/main/kotlin/app/revanced/api/modules/HTTP.kt +++ b/src/main/kotlin/app/revanced/api/configuration/HTTP.kt @@ -1,4 +1,4 @@ -package app.revanced.api.modules +package app.revanced.api.configuration import io.ktor.http.* import io.ktor.http.content.* @@ -8,7 +8,9 @@ import io.ktor.server.plugins.conditionalheaders.* import io.ktor.server.plugins.cors.routing.* import kotlin.time.Duration.Companion.minutes -fun Application.configureHTTP() { +fun Application.configureHTTP( + allowedHost: String, +) { install(ConditionalHeaders) install(CORS) { allowMethod(HttpMethod.Options) @@ -16,7 +18,7 @@ fun Application.configureHTTP() { allowMethod(HttpMethod.Delete) allowMethod(HttpMethod.Patch) allowHeader(HttpHeaders.Authorization) - anyHost() // @TODO: Don't do this in production if possible. Try to limit it. + allowHost(allowedHost) } install(CachingHeaders) { options { _, _ -> CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 5.minutes.inWholeSeconds.toInt())) } diff --git a/src/main/kotlin/app/revanced/api/configuration/Security.kt b/src/main/kotlin/app/revanced/api/configuration/Security.kt new file mode 100644 index 00000000..2543fb14 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/configuration/Security.kt @@ -0,0 +1,9 @@ +package app.revanced.api.configuration + +import app.revanced.api.services.AuthService +import io.ktor.server.application.* +import org.koin.ktor.ext.get + +fun Application.configureSecurity() { + get().configureSecurity(this) +} diff --git a/src/main/kotlin/app/revanced/api/configuration/Serialization.kt b/src/main/kotlin/app/revanced/api/configuration/Serialization.kt new file mode 100644 index 00000000..4e9f7ed0 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/configuration/Serialization.kt @@ -0,0 +1,19 @@ +package app.revanced.api.configuration + +import io.ktor.serialization.kotlinx.json.* +import io.ktor.server.application.* +import io.ktor.server.plugins.contentnegotiation.* +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonNamingStrategy + +@OptIn(ExperimentalSerializationApi::class) +fun Application.configureSerialization() { + install(ContentNegotiation) { + json( + Json { + namingStrategy = JsonNamingStrategy.SnakeCase + }, + ) + } +} diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/Routing.kt b/src/main/kotlin/app/revanced/api/configuration/routing/Routing.kt new file mode 100644 index 00000000..18420b6c --- /dev/null +++ b/src/main/kotlin/app/revanced/api/configuration/routing/Routing.kt @@ -0,0 +1,19 @@ +package app.revanced.api.configuration.routing + +import app.revanced.api.configuration.routing.routes.configureAnnouncementsRoute +import app.revanced.api.configuration.routing.routes.configurePatchesRoute +import app.revanced.api.configuration.routing.routes.configureRootRoute +import app.revanced.api.repository.ConfigurationRepository +import io.ktor.server.application.* +import io.ktor.server.routing.* +import org.koin.ktor.ext.get + +internal fun Application.configureRouting() = routing { + val configuration = get() + + route("/v${configuration.apiVersion}") { + configureRootRoute() + configurePatchesRoute() + configureAnnouncementsRoute() + } +} diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt b/src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt new file mode 100644 index 00000000..4f9f038b --- /dev/null +++ b/src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt @@ -0,0 +1,86 @@ +package app.revanced.api.configuration.routing.routes + +import app.revanced.api.schema.APIAnnouncement +import app.revanced.api.schema.APIAnnouncementArchivedAt +import app.revanced.api.services.AnnouncementService +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import io.ktor.server.util.* +import org.koin.ktor.ext.get as koinGet + +internal fun Route.configureAnnouncementsRoute() = route("/announcements") { + val announcementService = koinGet() + + route("/{channel}/latest") { + get("/id") { + val channel: String by call.parameters + + call.respond( + announcementService.latestId(channel) ?: return@get call.respond(HttpStatusCode.NotFound), + ) + } + + get { + val channel: String by call.parameters + + call.respond( + announcementService.latest(channel) ?: return@get call.respond(HttpStatusCode.NotFound), + ) + } + } + + get("/{channel}") { + val channel: String by call.parameters + + call.respond(announcementService.all(channel)) + } + + route("/latest") { + get("/id") { + call.respond(announcementService.latestId() ?: return@get call.respond(HttpStatusCode.NotFound)) + } + + get { + call.respond(announcementService.latest() ?: return@get call.respond(HttpStatusCode.NotFound)) + } + } + + get { + call.respond(announcementService.all()) + } + + authenticate("jwt") { + post { + announcementService.new(call.receive()) + } + + post("/{id}/archive") { + val id: Int by call.parameters + val archivedAt = call.receiveNullable()?.archivedAt + + announcementService.archive(id, archivedAt) + } + + post("/{id}/unarchive") { + val id: Int by call.parameters + + announcementService.unarchive(id) + } + + patch("/{id}") { + val id: Int by call.parameters + + announcementService.update(id, call.receive()) + } + + delete("/{id}") { + val id: Int by call.parameters + + announcementService.delete(id) + } + } +} diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/routes/ApiRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routing/routes/ApiRoute.kt new file mode 100644 index 00000000..b502ac82 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/configuration/routing/routes/ApiRoute.kt @@ -0,0 +1,41 @@ +package app.revanced.api.configuration.routing.routes + +import app.revanced.api.services.ApiService +import app.revanced.api.services.AuthService +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.http.content.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import org.koin.ktor.ext.get + +internal fun Route.configureRootRoute() { + val apiService = get() + val authService = get() + + get("/contributors") { + call.respond(apiService.contributors()) + } + + get("/team") { + call.respond(apiService.team()) + } + + route("/ping") { + handle { + call.respond(HttpStatusCode.NoContent) + } + } + + authenticate("basic") { + get("/token") { + call.respond(authService.newToken()) + } + } + + staticResources("/", "/static/api") { + contentType { ContentType.Application.Json } + extensions("json") + } +} diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/routes/PatchesRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routing/routes/PatchesRoute.kt new file mode 100644 index 00000000..21e811ef --- /dev/null +++ b/src/main/kotlin/app/revanced/api/configuration/routing/routes/PatchesRoute.kt @@ -0,0 +1,26 @@ +package app.revanced.api.configuration.routing.routes + +import app.revanced.api.services.PatchesService +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import org.koin.ktor.ext.get as koinGet + +internal fun Route.configurePatchesRoute() = route("/patches") { + val patchesService = koinGet() + + route("latest") { + get { + call.respond(patchesService.latestRelease()) + } + + get("/version") { + call.respond(patchesService.latestVersion()) + } + + get("/list") { + call.respondBytes(ContentType.Application.Json) { patchesService.list() } + } + } +} diff --git a/src/main/kotlin/app/revanced/api/modules/Dependencies.kt b/src/main/kotlin/app/revanced/api/modules/Dependencies.kt deleted file mode 100644 index cf8afe07..00000000 --- a/src/main/kotlin/app/revanced/api/modules/Dependencies.kt +++ /dev/null @@ -1,75 +0,0 @@ -package app.revanced.api.modules - -import app.revanced.api.backend.Backend -import app.revanced.api.backend.github.GitHubBackend -import app.revanced.api.schema.APIConfiguration -import com.akuleshov7.ktoml.Toml -import com.akuleshov7.ktoml.source.decodeFromStream -import io.github.cdimascio.dotenv.Dotenv -import io.ktor.server.application.* -import org.jetbrains.exposed.sql.Database -import org.koin.dsl.bind -import org.koin.dsl.module -import org.koin.ktor.plugin.Koin -import java.io.File - -fun Application.configureDependencies() { - install(Koin) { - modules( - globalModule, - gitHubBackendModule, - databaseModule, - authModule, - ) - } -} - -val globalModule = module { - single { - Dotenv.configure() - .systemProperties() - .load() - } - single { - val configFilePath = get()["CONFIG_FILE_PATH"] - Toml.decodeFromStream(File(configFilePath).inputStream()) - } -} - -val gitHubBackendModule = module { - single { - val token = get()["GITHUB_TOKEN"] - GitHubBackend(token) - } bind Backend::class -} - -val databaseModule = module { - single { - val dotenv = get() - - Database.connect( - url = dotenv["DB_URL"], - user = dotenv["DB_USER"], - password = dotenv["DB_PASSWORD"], - driver = "org.h2.Driver", - ) - } - factory { - AnnouncementService(get()) - } -} - -val authModule = module { - single { - val dotenv = get() - - val jwtSecret = dotenv["JWT_SECRET"] - val issuer = dotenv["JWT_ISSUER"] - val validityInMin = dotenv["JWT_VALIDITY_IN_MIN"].toInt() - - val basicUsername = dotenv["BASIC_USERNAME"] - val basicPassword = dotenv["BASIC_PASSWORD"] - - AuthService(issuer, validityInMin, jwtSecret, basicUsername, basicPassword) - } -} diff --git a/src/main/kotlin/app/revanced/api/modules/Routing.kt b/src/main/kotlin/app/revanced/api/modules/Routing.kt deleted file mode 100644 index 616e2e5e..00000000 --- a/src/main/kotlin/app/revanced/api/modules/Routing.kt +++ /dev/null @@ -1,232 +0,0 @@ -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.* -import io.ktor.server.http.content.* -import io.ktor.server.request.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import io.ktor.util.pipeline.* -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import java.io.ByteArrayOutputStream -import java.net.URL -import org.koin.ktor.ext.get as koinGet - -fun Application.configureRouting() { - val backend: Backend = koinGet() - val configuration: APIConfiguration = koinGet() - val announcementService: AnnouncementService = koinGet() - val authService: AuthService = koinGet() - - routing { - route("/v${configuration.apiVersion}") { - route("/announcements") { - suspend fun PipelineContext<*, ApplicationCall>.announcement(block: AnnouncementService.() -> APIResponseAnnouncement?) = - announcementService.block()?.let { call.respond(it) } - ?: call.respond(HttpStatusCode.NotFound) - - suspend fun PipelineContext<*, ApplicationCall>.announcementId(block: AnnouncementService.() -> APILatestAnnouncement?) = - announcementService.block()?.let { call.respond(it) } - ?: call.respond(HttpStatusCode.NotFound) - - suspend fun PipelineContext<*, ApplicationCall>.channel(block: suspend (String) -> Unit) = - block(call.parameters["channel"]!!) - - route("/{channel}/latest") { - get("/id") { - channel { - announcementId { - latestId(it) - } - } - } - - get { - channel { - announcement { - latest(it) - } - } - } - } - - get("/{channel}") { - channel { - call.respond(announcementService.read(it)) - } - } - - route("/latest") { - get("/id") { - announcementId { - latestId() - } - } - - get { - announcement { - latest() - } - } - } - - get { - call.respond(announcementService.read()) - } - - authenticate("jwt") { - suspend fun PipelineContext<*, ApplicationCall>.id(block: suspend (Int) -> Unit) = - call.parameters["id"]!!.toIntOrNull()?.let { - block(it) - } ?: call.respond(HttpStatusCode.BadRequest) - - post { - announcementService.new(call.receive()) - } - - post("/{id}/archive") { - id { - val archivedAt = call.receiveNullable()?.archivedAt - announcementService.archive(it, archivedAt) - } - } - - post("/{id}/unarchive") { - id { - announcementService.unarchive(it) - } - } - - patch("/{id}") { - id { - announcementService.update(it, call.receive()) - } - } - - delete("/{id}") { - id { - announcementService.delete(it) - } - } - } - } - - route("/patches") { - route("latest") { - get { - val patchesRelease = - backend.getRelease(configuration.organization, configuration.patchesRepository) - val integrationsReleases = configuration.integrationsRepositoryNames.map { - async { backend.getRelease(configuration.organization, it) } - }.awaitAll() - - val assets = (patchesRelease.assets + integrationsReleases.flatMap { it.assets }) - .map { APIAsset(it.downloadUrl) } - .filter { it.type != APIAsset.Type.UNKNOWN } - .toSet() - - val apiRelease = APIRelease( - patchesRelease.tag, - patchesRelease.createdAt, - patchesRelease.releaseNote, - assets, - ) - - call.respond(apiRelease) - } - - get("/version") { - val patchesRelease = - backend.getRelease(configuration.organization, configuration.patchesRepository) - - val apiPatchesRelease = APIReleaseVersion(patchesRelease.tag) - - call.respond(apiPatchesRelease) - } - - val patchesListCache = Caffeine - .newBuilder() - .maximumSize(1) - .build() - - get("/list") { - val patchesRelease = - backend.getRelease(configuration.organization, configuration.patchesRepository) - - val patchesListByteArray = patchesListCache.getIfPresent(patchesRelease.tag) ?: run { - val downloadUrl = patchesRelease.assets - .map { APIAsset(it.downloadUrl) } - .find { it.type == APIAsset.Type.PATCHES } - ?.downloadUrl - - val patches = kotlin.io.path.createTempFile().toFile().apply { - outputStream().use { URL(downloadUrl).openStream().copyTo(it) } - }.let { file -> - PatchBundleLoader.Jar(file).also { file.delete() } - } - - ByteArrayOutputStream().use { stream -> - PatchUtils.Json.serialize(patches, outputStream = stream) - - stream.toByteArray() - }.also { - patchesListCache.put(patchesRelease.tag, it) - } - } - - call.respondBytes(ContentType.Application.Json) { patchesListByteArray } - } - } - } - - staticResources("/", "/static/api") { - contentType { ContentType.Application.Json } - extensions("json") - } - - get("/contributors") { - val contributors = - configuration.contributorsRepositoryNames.map { - async { - APIContributable( - it, - backend.getContributors(configuration.organization, it).map { - APIContributor(it.name, it.avatarUrl, it.url, it.contributions) - }.toSet(), - ) - } - }.awaitAll() - - call.respond(contributors) - } - - get("/team") { - val team = - backend.getMembers(configuration.organization).map { - APIMember(it.name, it.avatarUrl, it.url, it.gpgKeysUrl) - } - - call.respond(team) - } - - route("/ping") { - handle { - call.respond(HttpStatusCode.NoContent) - } - } - - authenticate("basic") { - get("/token") { - call.respond(authService.newToken()) - } - } - } - } -} diff --git a/src/main/kotlin/app/revanced/api/modules/Serialization.kt b/src/main/kotlin/app/revanced/api/modules/Serialization.kt deleted file mode 100644 index ef385585..00000000 --- a/src/main/kotlin/app/revanced/api/modules/Serialization.kt +++ /dev/null @@ -1,11 +0,0 @@ -package app.revanced.api.modules - -import io.ktor.serialization.kotlinx.json.* -import io.ktor.server.application.* -import io.ktor.server.plugins.contentnegotiation.* - -fun Application.configureSerialization() { - install(ContentNegotiation) { - json() - } -} diff --git a/src/main/kotlin/app/revanced/api/modules/Database.kt b/src/main/kotlin/app/revanced/api/repository/AnnouncementRepository.kt similarity index 52% rename from src/main/kotlin/app/revanced/api/modules/Database.kt rename to src/main/kotlin/app/revanced/api/repository/AnnouncementRepository.kt index 4727a0b4..a6c4b633 100644 --- a/src/main/kotlin/app/revanced/api/modules/Database.kt +++ b/src/main/kotlin/app/revanced/api/repository/AnnouncementRepository.kt @@ -1,6 +1,6 @@ -package app.revanced.api.modules +package app.revanced.api.repository -import app.revanced.api.modules.AnnouncementService.Attachments.announcement +import app.revanced.api.repository.AnnouncementRepository.AttachmentTable.announcement import app.revanced.api.schema.APIAnnouncement import app.revanced.api.schema.APILatestAnnouncement import app.revanced.api.schema.APIResponseAnnouncement @@ -10,96 +10,54 @@ import org.jetbrains.exposed.dao.IntEntityClass import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.sql.* -import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.kotlin.datetime.datetime import org.jetbrains.exposed.sql.transactions.transaction -class AnnouncementService(private val database: Database) { - private object Announcements : IntIdTable() { - val author = varchar("author", 32).nullable() - val title = varchar("title", 64) - val content = text("content").nullable() - val channel = varchar("channel", 16).nullable() - val createdAt = datetime("createdAt") - val archivedAt = datetime("archivedAt").nullable() - val level = integer("level") - } - - private object Attachments : IntIdTable() { - val url = varchar("url", 256) - val announcement = reference("announcement", Announcements, onDelete = ReferenceOption.CASCADE) - } - - class Announcement(id: EntityID) : IntEntity(id) { - companion object : IntEntityClass(Announcements) - - var author by Announcements.author - var title by Announcements.title - var content by Announcements.content - val attachments by Attachment referrersOn announcement - var channel by Announcements.channel - var createdAt by Announcements.createdAt - var archivedAt by Announcements.archivedAt - var level by Announcements.level - - fun api() = APIResponseAnnouncement( - id.value, - author, - title, - content, - attachments.map(Attachment::url).toSet(), - channel, - createdAt, - archivedAt, - level, - ) - } - - class Attachment(id: EntityID) : IntEntity(id) { - companion object : IntEntityClass(Attachments) - - var url by Attachments.url - var announcement by Announcement referencedOn Attachments.announcement - } - +internal class AnnouncementRepository(private val database: Database) { init { transaction { - SchemaUtils.create(Announcements, Attachments) + SchemaUtils.create(AnnouncementTable, AttachmentTable) } } - private fun transaction(block: Transaction.() -> T) = transaction(database, block) - - fun read() = transaction { - Announcement.all().map { it.api() }.toSet() + fun all() = transaction { + buildSet { + AnnouncementEntity.all().forEach { announcement -> + add(announcement.toApi()) + } + } } - fun read(channel: String) = transaction { - Announcement.find { Announcements.channel eq channel }.map { it.api() }.toSet() + fun all(channel: String) = transaction { + buildSet { + AnnouncementEntity.find { AnnouncementTable.channel eq channel }.forEach { announcement -> + add(announcement.toApi()) + } + } } fun delete(id: Int) = transaction { - val announcement = Announcement.findById(id) ?: return@transaction + val announcement = AnnouncementEntity.findById(id) ?: return@transaction announcement.delete() } fun latest() = transaction { - Announcement.all().maxByOrNull { it.createdAt }?.api() + AnnouncementEntity.all().maxByOrNull { it.createdAt }?.toApi() } fun latest(channel: String) = transaction { - Announcement.find { Announcements.channel eq channel }.maxByOrNull { it.createdAt }?.api() + AnnouncementEntity.find { AnnouncementTable.channel eq channel }.maxByOrNull { it.createdAt }?.toApi() } fun latestId() = transaction { - Announcement.all().maxByOrNull { it.createdAt }?.id?.value?.let { + AnnouncementEntity.all().maxByOrNull { it.createdAt }?.id?.value?.let { APILatestAnnouncement(it) } } fun latestId(channel: String) = transaction { - Announcement.find { Announcements.channel eq channel }.maxByOrNull { it.createdAt }?.id?.value?.let { + AnnouncementEntity.find { AnnouncementTable.channel eq channel }.maxByOrNull { it.createdAt }?.id?.value?.let { APILatestAnnouncement(it) } } @@ -108,19 +66,19 @@ class AnnouncementService(private val database: Database) { id: Int, archivedAt: LocalDateTime?, ) = transaction { - Announcement.findById(id)?.apply { + AnnouncementEntity.findById(id)?.apply { this.archivedAt = archivedAt ?: java.time.LocalDateTime.now().toKotlinLocalDateTime() } } fun unarchive(id: Int) = transaction { - Announcement.findById(id)?.apply { + AnnouncementEntity.findById(id)?.apply { archivedAt = null } } fun new(new: APIAnnouncement) = transaction { - Announcement.new announcement@{ + AnnouncementEntity.new announcement@{ author = new.author title = new.title content = new.content @@ -130,7 +88,7 @@ class AnnouncementService(private val database: Database) { level = new.level }.also { newAnnouncement -> new.attachmentUrls.map { - Attachment.new { + AttachmentEntity.new { url = it announcement = newAnnouncement } @@ -139,7 +97,7 @@ class AnnouncementService(private val database: Database) { } fun update(id: Int, new: APIAnnouncement) = transaction { - Announcement.findById(id)?.apply { + AnnouncementEntity.findById(id)?.apply { author = new.author title = new.title content = new.content @@ -147,13 +105,66 @@ class AnnouncementService(private val database: Database) { archivedAt = new.archivedAt level = new.level - attachments.forEach(Attachment::delete) + attachments.forEach(AttachmentEntity::delete) new.attachmentUrls.map { - Attachment.new { + AttachmentEntity.new { url = it announcement = this@apply } } } } + + private fun transaction(block: Transaction.() -> T) = transaction(database, block) + + private object AnnouncementTable : IntIdTable() { + val author = varchar("author", 32).nullable() + val title = varchar("title", 64) + val content = text("content").nullable() + val channel = varchar("channel", 16).nullable() + val createdAt = datetime("createdAt") + val archivedAt = datetime("archivedAt").nullable() + val level = integer("level") + } + + private object AttachmentTable : IntIdTable() { + val url = varchar("url", 256) + val announcement = reference("announcement", AnnouncementTable, onDelete = ReferenceOption.CASCADE) + } + + class AnnouncementEntity(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(AnnouncementTable) + + var author by AnnouncementTable.author + var title by AnnouncementTable.title + var content by AnnouncementTable.content + val attachments by AttachmentEntity referrersOn announcement + var channel by AnnouncementTable.channel + var createdAt by AnnouncementTable.createdAt + var archivedAt by AnnouncementTable.archivedAt + var level by AnnouncementTable.level + + fun toApi() = APIResponseAnnouncement( + id.value, + author, + title, + content, + attachmentUrls = buildSet { + attachments.forEach { + add(it.url) + } + }, + channel, + createdAt, + archivedAt, + level, + ) + } + + class AttachmentEntity(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(AttachmentTable) + + var url by AttachmentTable.url + var announcement by AnnouncementEntity referencedOn AttachmentTable.announcement + } } diff --git a/src/main/kotlin/app/revanced/api/schema/ConfigurationSchema.kt b/src/main/kotlin/app/revanced/api/repository/ConfigurationRepository.kt similarity index 85% rename from src/main/kotlin/app/revanced/api/schema/ConfigurationSchema.kt rename to src/main/kotlin/app/revanced/api/repository/ConfigurationRepository.kt index 5eff0b6c..531de56e 100644 --- a/src/main/kotlin/app/revanced/api/schema/ConfigurationSchema.kt +++ b/src/main/kotlin/app/revanced/api/repository/ConfigurationRepository.kt @@ -1,10 +1,10 @@ -package app.revanced.api.schema +package app.revanced.api.repository import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -class APIConfiguration( +internal class ConfigurationRepository( val organization: String, @SerialName("patches-repository") val patchesRepository: String, diff --git a/src/main/kotlin/app/revanced/api/backend/Backend.kt b/src/main/kotlin/app/revanced/api/repository/backend/BackendRepository.kt similarity index 91% rename from src/main/kotlin/app/revanced/api/backend/Backend.kt rename to src/main/kotlin/app/revanced/api/repository/backend/BackendRepository.kt index c3e53c1c..47ec9d8a 100644 --- a/src/main/kotlin/app/revanced/api/backend/Backend.kt +++ b/src/main/kotlin/app/revanced/api/repository/backend/BackendRepository.kt @@ -1,4 +1,4 @@ -package app.revanced.api.backend +package app.revanced.api.repository.backend import io.ktor.client.* import io.ktor.client.engine.okhttp.* @@ -10,7 +10,7 @@ import kotlinx.serialization.Serializable * * @param httpClientConfig The configuration of the HTTP client. */ -abstract class Backend( +abstract class BackendRepository internal constructor( httpClientConfig: HttpClientConfig.() -> Unit = {}, ) { protected val client: HttpClient = HttpClient(OkHttp, httpClientConfig) @@ -114,7 +114,7 @@ abstract class Backend( * @param tag The tag of the release. If null, the latest release is returned. * @return The release. */ - abstract suspend fun getRelease( + abstract suspend fun release( owner: String, repository: String, tag: String? = null, @@ -127,7 +127,7 @@ abstract class Backend( * @param repository The name of the repository. * @return The contributors. */ - abstract suspend fun getContributors(owner: String, repository: String): Set + abstract suspend fun contributors(owner: String, repository: String): Set /** * Get the members of an organization. @@ -135,5 +135,5 @@ abstract class Backend( * @param organization The name of the organization. * @return The members. */ - abstract suspend fun getMembers(organization: String): Set + abstract suspend fun members(organization: String): Set } diff --git a/src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt b/src/main/kotlin/app/revanced/api/repository/backend/github/GitHubBackendRepository.kt similarity index 67% rename from src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt rename to src/main/kotlin/app/revanced/api/repository/backend/github/GitHubBackendRepository.kt index be0db97b..5687f72d 100644 --- a/src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt +++ b/src/main/kotlin/app/revanced/api/repository/backend/github/GitHubBackendRepository.kt @@ -1,18 +1,18 @@ -package app.revanced.api.backend.github +package app.revanced.api.repository.backend.github -import app.revanced.api.backend.Backend -import app.revanced.api.backend.Backend.BackendOrganization.BackendMember -import app.revanced.api.backend.Backend.BackendOrganization.BackendRepository.BackendContributor -import app.revanced.api.backend.Backend.BackendOrganization.BackendRepository.BackendRelease -import app.revanced.api.backend.Backend.BackendOrganization.BackendRepository.BackendRelease.BackendAsset -import app.revanced.api.backend.github.api.Request -import app.revanced.api.backend.github.api.Request.Organization.Members -import app.revanced.api.backend.github.api.Request.Organization.Repository.Contributors -import app.revanced.api.backend.github.api.Request.Organization.Repository.Releases -import app.revanced.api.backend.github.api.Response -import app.revanced.api.backend.github.api.Response.GitHubOrganization.GitHubMember -import app.revanced.api.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubContributor -import app.revanced.api.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubRelease +import app.revanced.api.repository.backend.BackendRepository +import app.revanced.api.repository.backend.BackendRepository.BackendOrganization.BackendMember +import app.revanced.api.repository.backend.BackendRepository.BackendOrganization.BackendRepository.BackendContributor +import app.revanced.api.repository.backend.BackendRepository.BackendOrganization.BackendRepository.BackendRelease +import app.revanced.api.repository.backend.BackendRepository.BackendOrganization.BackendRepository.BackendRelease.BackendAsset +import app.revanced.api.repository.backend.github.api.Request +import app.revanced.api.repository.backend.github.api.Request.Organization.Members +import app.revanced.api.repository.backend.github.api.Request.Organization.Repository.Contributors +import app.revanced.api.repository.backend.github.api.Request.Organization.Repository.Releases +import app.revanced.api.repository.backend.github.api.Response +import app.revanced.api.repository.backend.github.api.Response.GitHubOrganization.GitHubMember +import app.revanced.api.repository.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubContributor +import app.revanced.api.repository.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubRelease import io.ktor.client.call.* import io.ktor.client.plugins.* import io.ktor.client.plugins.auth.* @@ -30,7 +30,7 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonNamingStrategy @OptIn(ExperimentalSerializationApi::class) -class GitHubBackend(token: String? = null) : Backend({ +class GitHubBackendRepository(token: String? = null) : BackendRepository({ install(HttpCache) install(Resources) install(ContentNegotiation) { @@ -59,7 +59,7 @@ class GitHubBackend(token: String? = null) : Backend({ } } }) { - override suspend fun getRelease( + override suspend fun release( owner: String, repository: String, tag: String?, @@ -80,7 +80,7 @@ class GitHubBackend(token: String? = null) : Backend({ ) } - override suspend fun getContributors( + override suspend fun contributors( owner: String, repository: String, ): Set { @@ -96,7 +96,7 @@ class GitHubBackend(token: String? = null) : Backend({ }.toSet() } - override suspend fun getMembers(organization: String): Set { + override suspend fun members(organization: String): Set { // Get the list of members of the organization. val members: Set = client.get(Members(organization)).body() diff --git a/src/main/kotlin/app/revanced/api/backend/github/api/RequestResource.kt b/src/main/kotlin/app/revanced/api/repository/backend/github/api/Request.kt similarity index 93% rename from src/main/kotlin/app/revanced/api/backend/github/api/RequestResource.kt rename to src/main/kotlin/app/revanced/api/repository/backend/github/api/Request.kt index dc480ba6..557f9e23 100644 --- a/src/main/kotlin/app/revanced/api/backend/github/api/RequestResource.kt +++ b/src/main/kotlin/app/revanced/api/repository/backend/github/api/Request.kt @@ -1,4 +1,4 @@ -package app.revanced.api.backend.github.api +package app.revanced.api.repository.backend.github.api import io.ktor.resources.* diff --git a/src/main/kotlin/app/revanced/api/backend/github/api/ResponseSchema.kt b/src/main/kotlin/app/revanced/api/repository/backend/github/api/Response.kt similarity index 96% rename from src/main/kotlin/app/revanced/api/backend/github/api/ResponseSchema.kt rename to src/main/kotlin/app/revanced/api/repository/backend/github/api/Response.kt index 693a8975..2ddc8f1f 100644 --- a/src/main/kotlin/app/revanced/api/backend/github/api/ResponseSchema.kt +++ b/src/main/kotlin/app/revanced/api/repository/backend/github/api/Response.kt @@ -1,4 +1,4 @@ -package app.revanced.api.backend.github.api +package app.revanced.api.repository.backend.github.api import kotlinx.datetime.Instant import kotlinx.serialization.Serializable diff --git a/src/main/kotlin/app/revanced/api/services/AnnouncementService.kt b/src/main/kotlin/app/revanced/api/services/AnnouncementService.kt new file mode 100644 index 00000000..63418da8 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/services/AnnouncementService.kt @@ -0,0 +1,35 @@ +package app.revanced.api.services + +import app.revanced.api.repository.AnnouncementRepository +import app.revanced.api.schema.APIAnnouncement +import app.revanced.api.schema.APILatestAnnouncement +import kotlinx.datetime.LocalDateTime + +internal class AnnouncementService( + private val announcementRepository: AnnouncementRepository, +) { + fun latestId(channel: String): APILatestAnnouncement? = announcementRepository.latestId(channel) + fun latestId(): APILatestAnnouncement? = announcementRepository.latestId() + + fun latest(channel: String) = announcementRepository.latest(channel) + fun latest() = announcementRepository.latest() + + fun all(channel: String) = announcementRepository.all(channel) + fun all() = announcementRepository.all() + + fun new(new: APIAnnouncement) { + announcementRepository.new(new) + } + fun archive(id: Int, archivedAt: LocalDateTime?) { + announcementRepository.archive(id, archivedAt) + } + fun unarchive(id: Int) { + announcementRepository.unarchive(id) + } + fun update(id: Int, new: APIAnnouncement) { + announcementRepository.update(id, new) + } + fun delete(id: Int) { + announcementRepository.delete(id) + } +} diff --git a/src/main/kotlin/app/revanced/api/services/ApiService.kt b/src/main/kotlin/app/revanced/api/services/ApiService.kt new file mode 100644 index 00000000..d36e64b0 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/services/ApiService.kt @@ -0,0 +1,33 @@ +package app.revanced.api.services + +import app.revanced.api.repository.ConfigurationRepository +import app.revanced.api.repository.backend.BackendRepository +import app.revanced.api.schema.APIContributable +import app.revanced.api.schema.APIContributor +import app.revanced.api.schema.APIMember +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.withContext + +internal class ApiService( + private val backendRepository: BackendRepository, + private val configurationRepository: ConfigurationRepository, +) { + suspend fun contributors() = withContext(Dispatchers.IO) { + configurationRepository.contributorsRepositoryNames.map { + async { + APIContributable( + it, + backendRepository.contributors(configurationRepository.organization, it).map { + APIContributor(it.name, it.avatarUrl, it.url, it.contributions) + }.toSet(), + ) + } + } + }.awaitAll() + + suspend fun team() = backendRepository.members(configurationRepository.organization).map { + APIMember(it.name, it.avatarUrl, it.url, it.gpgKeysUrl) + } +} diff --git a/src/main/kotlin/app/revanced/api/modules/Security.kt b/src/main/kotlin/app/revanced/api/services/AuthService.kt similarity index 87% rename from src/main/kotlin/app/revanced/api/modules/Security.kt rename to src/main/kotlin/app/revanced/api/services/AuthService.kt index 02cc5f3c..af824a0b 100644 --- a/src/main/kotlin/app/revanced/api/modules/Security.kt +++ b/src/main/kotlin/app/revanced/api/services/AuthService.kt @@ -1,15 +1,14 @@ -package app.revanced.api.modules +package app.revanced.api.services import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm import io.ktor.server.application.* import io.ktor.server.auth.* import io.ktor.server.auth.jwt.* -import org.koin.ktor.ext.get import java.util.* import kotlin.time.Duration.Companion.minutes -class AuthService( +internal class AuthService( private val issuer: String, private val validityInMin: Int, private val jwtSecret: String, @@ -46,8 +45,3 @@ class AuthService( .sign(Algorithm.HMAC256(jwtSecret)) } } - -fun Application.configureSecurity() { - val configureSecurity = get().configureSecurity - configureSecurity() -} diff --git a/src/main/kotlin/app/revanced/api/services/PatchesService.kt b/src/main/kotlin/app/revanced/api/services/PatchesService.kt new file mode 100644 index 00000000..22afd709 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/services/PatchesService.kt @@ -0,0 +1,87 @@ +package app.revanced.api.services + +import app.revanced.api.repository.ConfigurationRepository +import app.revanced.api.repository.backend.BackendRepository +import app.revanced.api.schema.APIAsset +import app.revanced.api.schema.APIRelease +import app.revanced.api.schema.APIReleaseVersion +import app.revanced.library.PatchUtils +import app.revanced.patcher.PatchBundleLoader +import com.github.benmanes.caffeine.cache.Caffeine +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.withContext +import java.io.ByteArrayOutputStream +import java.net.URL + +internal class PatchesService( + private val backendRepository: BackendRepository, + private val configurationRepository: ConfigurationRepository, +) { + private val patchesListCache = Caffeine + .newBuilder() + .maximumSize(1) + .build() + + suspend fun latestRelease(): APIRelease { + val patchesRelease = backendRepository.release( + configurationRepository.organization, + configurationRepository.patchesRepository, + ) + val integrationsReleases = withContext(Dispatchers.Default) { + configurationRepository.integrationsRepositoryNames.map { + async { backendRepository.release(configurationRepository.organization, it) } + } + }.awaitAll() + + val assets = (patchesRelease.assets + integrationsReleases.flatMap { it.assets }) + .map { APIAsset(it.downloadUrl) } + .filter { it.type != APIAsset.Type.UNKNOWN } + .toSet() + + return APIRelease( + patchesRelease.tag, + patchesRelease.createdAt, + patchesRelease.releaseNote, + assets, + ) + } + + suspend fun latestVersion(): APIReleaseVersion { + val patchesRelease = backendRepository.release( + configurationRepository.organization, + configurationRepository.patchesRepository, + ) + + return APIReleaseVersion(patchesRelease.tag) + } + + suspend fun list(): ByteArray { + val patchesRelease = backendRepository.release( + configurationRepository.organization, + configurationRepository.patchesRepository, + ) + + return patchesListCache.getIfPresent(patchesRelease.tag) ?: run { + val downloadUrl = patchesRelease.assets + .map { APIAsset(it.downloadUrl) } + .find { it.type == APIAsset.Type.PATCHES } + ?.downloadUrl + + val patches = kotlin.io.path.createTempFile().toFile().apply { + outputStream().use { URL(downloadUrl).openStream().copyTo(it) } + }.let { file -> + PatchBundleLoader.Jar(file).also { file.delete() } + } + + ByteArrayOutputStream().use { stream -> + PatchUtils.Json.serialize(patches, outputStream = stream) + + stream.toByteArray() + }.also { + patchesListCache.put(patchesRelease.tag, it) + } + } + } +} diff --git a/src/main/resources/static/api/about.json b/src/main/resources/static/api/about.json index 1a0d1cbf..a947c48a 100644 --- a/src/main/resources/static/api/about.json +++ b/src/main/resources/static/api/about.json @@ -80,4 +80,4 @@ } ] } -} \ No newline at end of file +} diff --git a/src/test/kotlin/app/revanced/ApplicationTest.kt b/src/test/kotlin/app/revanced/ApplicationTest.kt deleted file mode 100644 index 5f1ce2e8..00000000 --- a/src/test/kotlin/app/revanced/ApplicationTest.kt +++ /dev/null @@ -1,57 +0,0 @@ -package app.revanced - -import app.revanced.api.modules.* -import app.revanced.api.schema.APIConfiguration -import com.akuleshov7.ktoml.Toml -import io.github.cdimascio.dotenv.Dotenv -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.http.* -import io.ktor.server.testing.* -import io.ktor.util.* -import io.mockk.every -import io.mockk.mockk -import kotlinx.serialization.encodeToString -import kotlin.test.* - -class ApplicationTest { - @Test - fun `successfully create a token`() = testApplication { - val apiConfigurationFile = kotlin.io.path.createTempFile().toFile().apply { - Toml.encodeToString( - APIConfiguration( - organization = "ReVanced", - patchesRepository = "", - integrationsRepositoryNames = setOf(), - contributorsRepositoryNames = setOf(), - ), - ).let(::writeText) - - deleteOnExit() - } - - val dotenv = mockk() - every { dotenv[any()] } returns "ReVanced" - every { dotenv["JWT_VALIDITY_IN_MIN"] } returns "5" - every { dotenv["CONFIG_FILE_PATH"] } returns apiConfigurationFile.absolutePath - - application { - configureDependencies() - configureHTTP() - configureSerialization() - configureSecurity() - configureRouting() - } - - val token = client.get("/v1/token") { - headers { - append( - HttpHeaders.Authorization, - "Basic ${"${dotenv["BASIC_USERNAME"]}:${dotenv["BASIC_PASSWORD"]}".encodeBase64()}", - ) - } - }.bodyAsText() - - assert(token.isNotEmpty()) - } -}