refactor: Refactor into services and repositories

This commit is contained in:
oSumAtrIX 2024-06-05 03:07:28 +02:00
parent 7a1957d013
commit fa2f8b2f86
No known key found for this signature in database
GPG Key ID: A9B3094ACDB604B4
30 changed files with 623 additions and 542 deletions

View File

@ -11,21 +11,11 @@ tasks {
expand("projectVersion" to project.version) expand("projectVersion" to project.version)
} }
/* // Needed by gradle-semantic-release-plugin.
Dummy task to hack gradle-semantic-release-plugin to release this project. // Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435
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
*/
register<DefaultTask>("publish") { register<DefaultTask>("publish") {
group = "publishing" group = "publishing"
description = "Dummy task to hack gradle-semantic-release-plugin to release ReVanced API" dependsOn(shadowJar)
dependsOn(startShadowScripts)
} }
} }
@ -42,8 +32,15 @@ ktor {
repositories { repositories {
mavenCentral() mavenCentral()
google() google()
maven { url = uri("https://jitpack.io") }
mavenLocal() 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 { dependencies {
@ -78,8 +75,4 @@ dependencies {
implementation(libs.revanced.patcher) implementation(libs.revanced.patcher)
implementation(libs.revanced.library) implementation(libs.revanced.library)
implementation(libs.caffeine) implementation(libs.caffeine)
testImplementation(libs.mockk)
testImplementation(libs.ktor.server.tests)
testImplementation(libs.kotlin.test.junit)
} }

View File

@ -1,5 +1,5 @@
[versions] [versions]
kotlin = "1.9.22" kotlin = "2.0.0"
logback = "1.4.14" logback = "1.4.14"
exposed = "0.41.1" exposed = "0.41.1"
h2 = "2.2.224" h2 = "2.2.224"
@ -7,11 +7,10 @@ koin = "3.5.3"
dotenv = "6.4.1" dotenv = "6.4.1"
ktor = "2.3.7" ktor = "2.3.7"
ktoml = "0.5.1" ktoml = "0.5.1"
picocli = "4.7.3" picocli = "4.7.5"
datetime = "0.5.0" datetime = "0.5.0"
mockk = "1.13.9" revanced-patcher = "19.3.1"
revanced-patcher = "19.2.0" revanced-library = "2.3.0"
revanced-library = "1.5.0"
caffeine = "3.1.8" caffeine = "3.1.8"
[libraries] [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-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" }
exposed-kotlin-datetime = { module = "org.jetbrains.exposed:exposed-kotlin-datetime", 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" } 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-core = { module = "com.akuleshov7:ktoml-core", version.ref = "ktoml" }
ktoml-file = { module = "com.akuleshov7:ktoml-file", version.ref = "ktoml" } 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" }
revanced-patcher = { module = "app.revanced:revanced-patcher", version.ref = "revanced-patcher" } revanced-patcher = { module = "app.revanced:revanced-patcher", version.ref = "revanced-patcher" }
revanced-library = { module = "app.revanced:revanced-library", version.ref = "revanced-library" } revanced-library = { module = "app.revanced:revanced-library", version.ref = "revanced-library" }
caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "caffeine" } caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "caffeine" }

Binary file not shown.

View File

@ -1,5 +1,8 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

41
gradlew vendored
View File

@ -55,7 +55,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (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. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@ -80,13 +80,11 @@ do
esac esac
done done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # This is normally unused
# shellcheck disable=SC2034
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@ -133,22 +131,29 @@ location of your Java installation."
fi fi
else else
JAVACMD=java 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 Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi fi
fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #( case $MAX_FD in #(
max*) 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 ) || MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit" warn "Could not query maximum file descriptor limit"
esac esac
case $MAX_FD in #( case $MAX_FD in #(
'' | soft) :;; #( '' | 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" || ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD" warn "Could not set maximum file descriptor limit to $MAX_FD"
esac esac
@ -193,11 +198,15 @@ if "$cygwin" || "$msys" ; then
done done
fi fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
# shell script including quotes and variable substitutions, so put them in DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded. # 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 -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
@ -205,6 +214,12 @@ set -- \
org.gradle.wrapper.GradleWrapperMain \ 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. # Use "xargs" to parse quoted args.
# #
# With -n1 it outputs one arg per line, with the quotes and backslashes removed. # With -n1 it outputs one arg per line, with the quotes and backslashes removed.

31
gradlew.bat vendored
View File

@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@ -40,13 +41,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail
@ -56,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail
@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd if %ERRORLEVEL% equ 0 goto mainEnd
:fail :fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code! rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 set EXIT_CODE=%ERRORLEVEL%
exit /b 1 if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd :mainEnd
if "%OS%"=="Windows_NT" endlocal if "%OS%"=="Windows_NT" endlocal

View File

@ -1,6 +1,7 @@
package app.revanced.api.command 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.engine.*
import io.ktor.server.netty.* import io.ktor.server.netty.*
import picocli.CommandLine import picocli.CommandLine
@ -27,7 +28,7 @@ internal object StartAPICommand : Runnable {
override fun run() { override fun run() {
embeddedServer(Netty, port, host) { embeddedServer(Netty, port, host) {
configureDependencies() configureDependencies()
configureHTTP() configureHTTP(allowedHost = host)
configureSerialization() configureSerialization()
configureSecurity() configureSecurity()
configureRouting() configureRouting()

View File

@ -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<Dotenv>()
Database.connect(
url = dotenv["DB_URL"],
user = dotenv["DB_USER"],
password = dotenv["DB_PASSWORD"],
driver = "org.h2.Driver",
)
}
single {
val configFilePath = get<Dotenv>()["CONFIG_FILE_PATH"]
val configFile = File(configFilePath).inputStream()
Toml.decodeFromStream<ConfigurationRepository>(configFile)
}
singleOf(::AnnouncementRepository)
}
val serviceModule = module {
single {
val dotenv = get<Dotenv>()
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<Dotenv>()["GITHUB_TOKEN"]
GitHubBackendRepository(token)
} bind BackendRepository::class
singleOf(::AnnouncementService)
singleOf(::PatchesService)
singleOf(::ApiService)
}
install(Koin) {
modules(
globalModule,
repositoryModule,
serviceModule,
)
}
}

View File

@ -1,4 +1,4 @@
package app.revanced.api.modules package app.revanced.api.configuration
import io.ktor.http.* import io.ktor.http.*
import io.ktor.http.content.* import io.ktor.http.content.*
@ -8,7 +8,9 @@ import io.ktor.server.plugins.conditionalheaders.*
import io.ktor.server.plugins.cors.routing.* import io.ktor.server.plugins.cors.routing.*
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
fun Application.configureHTTP() { fun Application.configureHTTP(
allowedHost: String,
) {
install(ConditionalHeaders) install(ConditionalHeaders)
install(CORS) { install(CORS) {
allowMethod(HttpMethod.Options) allowMethod(HttpMethod.Options)
@ -16,7 +18,7 @@ fun Application.configureHTTP() {
allowMethod(HttpMethod.Delete) allowMethod(HttpMethod.Delete)
allowMethod(HttpMethod.Patch) allowMethod(HttpMethod.Patch)
allowHeader(HttpHeaders.Authorization) allowHeader(HttpHeaders.Authorization)
anyHost() // @TODO: Don't do this in production if possible. Try to limit it. allowHost(allowedHost)
} }
install(CachingHeaders) { install(CachingHeaders) {
options { _, _ -> CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 5.minutes.inWholeSeconds.toInt())) } options { _, _ -> CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 5.minutes.inWholeSeconds.toInt())) }

View File

@ -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<AuthService>().configureSecurity(this)
}

View File

@ -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
},
)
}
}

View File

@ -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<ConfigurationRepository>()
route("/v${configuration.apiVersion}") {
configureRootRoute()
configurePatchesRoute()
configureAnnouncementsRoute()
}
}

View File

@ -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<AnnouncementService>()
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<APIAnnouncement>())
}
post("/{id}/archive") {
val id: Int by call.parameters
val archivedAt = call.receiveNullable<APIAnnouncementArchivedAt>()?.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<APIAnnouncement>())
}
delete("/{id}") {
val id: Int by call.parameters
announcementService.delete(id)
}
}
}

View File

@ -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<ApiService>()
val authService = get<AuthService>()
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")
}
}

View File

@ -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<PatchesService>()
route("latest") {
get {
call.respond(patchesService.latestRelease())
}
get("/version") {
call.respond(patchesService.latestVersion())
}
get("/list") {
call.respondBytes(ContentType.Application.Json) { patchesService.list() }
}
}
}

View File

@ -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<Dotenv>()["CONFIG_FILE_PATH"]
Toml.decodeFromStream<APIConfiguration>(File(configFilePath).inputStream())
}
}
val gitHubBackendModule = module {
single {
val token = get<Dotenv>()["GITHUB_TOKEN"]
GitHubBackend(token)
} bind Backend::class
}
val databaseModule = module {
single {
val dotenv = get<Dotenv>()
Database.connect(
url = dotenv["DB_URL"],
user = dotenv["DB_USER"],
password = dotenv["DB_PASSWORD"],
driver = "org.h2.Driver",
)
}
factory<AnnouncementService> {
AnnouncementService(get())
}
}
val authModule = module {
single {
val dotenv = get<Dotenv>()
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)
}
}

View File

@ -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<APIAnnouncement>())
}
post("/{id}/archive") {
id {
val archivedAt = call.receiveNullable<APIAnnouncementArchivedAt>()?.archivedAt
announcementService.archive(it, archivedAt)
}
}
post("/{id}/unarchive") {
id {
announcementService.unarchive(it)
}
}
patch("/{id}") {
id {
announcementService.update(it, call.receive<APIAnnouncement>())
}
}
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<String, ByteArray>()
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())
}
}
}
}
}

View File

@ -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()
}
}

View File

@ -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.APIAnnouncement
import app.revanced.api.schema.APILatestAnnouncement import app.revanced.api.schema.APILatestAnnouncement
import app.revanced.api.schema.APIResponseAnnouncement 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.EntityID
import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.* 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.kotlin.datetime.datetime
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
class AnnouncementService(private val database: Database) { internal class AnnouncementRepository(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<Int>) : IntEntity(id) {
companion object : IntEntityClass<Announcement>(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<Int>) : IntEntity(id) {
companion object : IntEntityClass<Attachment>(Attachments)
var url by Attachments.url
var announcement by Announcement referencedOn Attachments.announcement
}
init { init {
transaction { transaction {
SchemaUtils.create(Announcements, Attachments) SchemaUtils.create(AnnouncementTable, AttachmentTable)
} }
} }
private fun <T> transaction(block: Transaction.() -> T) = transaction(database, block) fun all() = transaction {
buildSet {
fun read() = transaction { AnnouncementEntity.all().forEach { announcement ->
Announcement.all().map { it.api() }.toSet() add(announcement.toApi())
}
}
} }
fun read(channel: String) = transaction { fun all(channel: String) = transaction {
Announcement.find { Announcements.channel eq channel }.map { it.api() }.toSet() buildSet {
AnnouncementEntity.find { AnnouncementTable.channel eq channel }.forEach { announcement ->
add(announcement.toApi())
}
}
} }
fun delete(id: Int) = transaction { fun delete(id: Int) = transaction {
val announcement = Announcement.findById(id) ?: return@transaction val announcement = AnnouncementEntity.findById(id) ?: return@transaction
announcement.delete() announcement.delete()
} }
fun latest() = transaction { fun latest() = transaction {
Announcement.all().maxByOrNull { it.createdAt }?.api() AnnouncementEntity.all().maxByOrNull { it.createdAt }?.toApi()
} }
fun latest(channel: String) = transaction { 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 { fun latestId() = transaction {
Announcement.all().maxByOrNull { it.createdAt }?.id?.value?.let { AnnouncementEntity.all().maxByOrNull { it.createdAt }?.id?.value?.let {
APILatestAnnouncement(it) APILatestAnnouncement(it)
} }
} }
fun latestId(channel: String) = transaction { 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) APILatestAnnouncement(it)
} }
} }
@ -108,19 +66,19 @@ class AnnouncementService(private val database: Database) {
id: Int, id: Int,
archivedAt: LocalDateTime?, archivedAt: LocalDateTime?,
) = transaction { ) = transaction {
Announcement.findById(id)?.apply { AnnouncementEntity.findById(id)?.apply {
this.archivedAt = archivedAt ?: java.time.LocalDateTime.now().toKotlinLocalDateTime() this.archivedAt = archivedAt ?: java.time.LocalDateTime.now().toKotlinLocalDateTime()
} }
} }
fun unarchive(id: Int) = transaction { fun unarchive(id: Int) = transaction {
Announcement.findById(id)?.apply { AnnouncementEntity.findById(id)?.apply {
archivedAt = null archivedAt = null
} }
} }
fun new(new: APIAnnouncement) = transaction { fun new(new: APIAnnouncement) = transaction {
Announcement.new announcement@{ AnnouncementEntity.new announcement@{
author = new.author author = new.author
title = new.title title = new.title
content = new.content content = new.content
@ -130,7 +88,7 @@ class AnnouncementService(private val database: Database) {
level = new.level level = new.level
}.also { newAnnouncement -> }.also { newAnnouncement ->
new.attachmentUrls.map { new.attachmentUrls.map {
Attachment.new { AttachmentEntity.new {
url = it url = it
announcement = newAnnouncement announcement = newAnnouncement
} }
@ -139,7 +97,7 @@ class AnnouncementService(private val database: Database) {
} }
fun update(id: Int, new: APIAnnouncement) = transaction { fun update(id: Int, new: APIAnnouncement) = transaction {
Announcement.findById(id)?.apply { AnnouncementEntity.findById(id)?.apply {
author = new.author author = new.author
title = new.title title = new.title
content = new.content content = new.content
@ -147,13 +105,66 @@ class AnnouncementService(private val database: Database) {
archivedAt = new.archivedAt archivedAt = new.archivedAt
level = new.level level = new.level
attachments.forEach(Attachment::delete) attachments.forEach(AttachmentEntity::delete)
new.attachmentUrls.map { new.attachmentUrls.map {
Attachment.new { AttachmentEntity.new {
url = it url = it
announcement = this@apply announcement = this@apply
} }
} }
} }
} }
private fun <T> 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<Int>) : IntEntity(id) {
companion object : IntEntityClass<AnnouncementEntity>(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<Int>) : IntEntity(id) {
companion object : IntEntityClass<AttachmentEntity>(AttachmentTable)
var url by AttachmentTable.url
var announcement by AnnouncementEntity referencedOn AttachmentTable.announcement
}
} }

View File

@ -1,10 +1,10 @@
package app.revanced.api.schema package app.revanced.api.repository
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
class APIConfiguration( internal class ConfigurationRepository(
val organization: String, val organization: String,
@SerialName("patches-repository") @SerialName("patches-repository")
val patchesRepository: String, val patchesRepository: String,

View File

@ -1,4 +1,4 @@
package app.revanced.api.backend package app.revanced.api.repository.backend
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.engine.okhttp.* import io.ktor.client.engine.okhttp.*
@ -10,7 +10,7 @@ import kotlinx.serialization.Serializable
* *
* @param httpClientConfig The configuration of the HTTP client. * @param httpClientConfig The configuration of the HTTP client.
*/ */
abstract class Backend( abstract class BackendRepository internal constructor(
httpClientConfig: HttpClientConfig<OkHttpConfig>.() -> Unit = {}, httpClientConfig: HttpClientConfig<OkHttpConfig>.() -> Unit = {},
) { ) {
protected val client: HttpClient = HttpClient(OkHttp, httpClientConfig) 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. * @param tag The tag of the release. If null, the latest release is returned.
* @return The release. * @return The release.
*/ */
abstract suspend fun getRelease( abstract suspend fun release(
owner: String, owner: String,
repository: String, repository: String,
tag: String? = null, tag: String? = null,
@ -127,7 +127,7 @@ abstract class Backend(
* @param repository The name of the repository. * @param repository The name of the repository.
* @return The contributors. * @return The contributors.
*/ */
abstract suspend fun getContributors(owner: String, repository: String): Set<BackendOrganization.BackendRepository.BackendContributor> abstract suspend fun contributors(owner: String, repository: String): Set<BackendOrganization.BackendRepository.BackendContributor>
/** /**
* Get the members of an organization. * Get the members of an organization.
@ -135,5 +135,5 @@ abstract class Backend(
* @param organization The name of the organization. * @param organization The name of the organization.
* @return The members. * @return The members.
*/ */
abstract suspend fun getMembers(organization: String): Set<BackendOrganization.BackendMember> abstract suspend fun members(organization: String): Set<BackendOrganization.BackendMember>
} }

View File

@ -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.repository.backend.BackendRepository
import app.revanced.api.backend.Backend.BackendOrganization.BackendMember import app.revanced.api.repository.backend.BackendRepository.BackendOrganization.BackendMember
import app.revanced.api.backend.Backend.BackendOrganization.BackendRepository.BackendContributor import app.revanced.api.repository.backend.BackendRepository.BackendOrganization.BackendRepository.BackendContributor
import app.revanced.api.backend.Backend.BackendOrganization.BackendRepository.BackendRelease import app.revanced.api.repository.backend.BackendRepository.BackendOrganization.BackendRepository.BackendRelease
import app.revanced.api.backend.Backend.BackendOrganization.BackendRepository.BackendRelease.BackendAsset import app.revanced.api.repository.backend.BackendRepository.BackendOrganization.BackendRepository.BackendRelease.BackendAsset
import app.revanced.api.backend.github.api.Request import app.revanced.api.repository.backend.github.api.Request
import app.revanced.api.backend.github.api.Request.Organization.Members import app.revanced.api.repository.backend.github.api.Request.Organization.Members
import app.revanced.api.backend.github.api.Request.Organization.Repository.Contributors import app.revanced.api.repository.backend.github.api.Request.Organization.Repository.Contributors
import app.revanced.api.backend.github.api.Request.Organization.Repository.Releases import app.revanced.api.repository.backend.github.api.Request.Organization.Repository.Releases
import app.revanced.api.backend.github.api.Response import app.revanced.api.repository.backend.github.api.Response
import app.revanced.api.backend.github.api.Response.GitHubOrganization.GitHubMember import app.revanced.api.repository.backend.github.api.Response.GitHubOrganization.GitHubMember
import app.revanced.api.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubContributor import app.revanced.api.repository.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubContributor
import app.revanced.api.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubRelease import app.revanced.api.repository.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubRelease
import io.ktor.client.call.* import io.ktor.client.call.*
import io.ktor.client.plugins.* import io.ktor.client.plugins.*
import io.ktor.client.plugins.auth.* import io.ktor.client.plugins.auth.*
@ -30,7 +30,7 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNamingStrategy import kotlinx.serialization.json.JsonNamingStrategy
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
class GitHubBackend(token: String? = null) : Backend({ class GitHubBackendRepository(token: String? = null) : BackendRepository({
install(HttpCache) install(HttpCache)
install(Resources) install(Resources)
install(ContentNegotiation) { install(ContentNegotiation) {
@ -59,7 +59,7 @@ class GitHubBackend(token: String? = null) : Backend({
} }
} }
}) { }) {
override suspend fun getRelease( override suspend fun release(
owner: String, owner: String,
repository: String, repository: String,
tag: String?, tag: String?,
@ -80,7 +80,7 @@ class GitHubBackend(token: String? = null) : Backend({
) )
} }
override suspend fun getContributors( override suspend fun contributors(
owner: String, owner: String,
repository: String, repository: String,
): Set<BackendContributor> { ): Set<BackendContributor> {
@ -96,7 +96,7 @@ class GitHubBackend(token: String? = null) : Backend({
}.toSet() }.toSet()
} }
override suspend fun getMembers(organization: String): Set<BackendMember> { override suspend fun members(organization: String): Set<BackendMember> {
// Get the list of members of the organization. // Get the list of members of the organization.
val members: Set<GitHubMember> = client.get(Members(organization)).body() val members: Set<GitHubMember> = client.get(Members(organization)).body()

View File

@ -1,4 +1,4 @@
package app.revanced.api.backend.github.api package app.revanced.api.repository.backend.github.api
import io.ktor.resources.* import io.ktor.resources.*

View File

@ -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.datetime.Instant
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -1,15 +1,14 @@
package app.revanced.api.modules package app.revanced.api.services
import com.auth0.jwt.JWT import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm import com.auth0.jwt.algorithms.Algorithm
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.auth.* import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.* import io.ktor.server.auth.jwt.*
import org.koin.ktor.ext.get
import java.util.* import java.util.*
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
class AuthService( internal class AuthService(
private val issuer: String, private val issuer: String,
private val validityInMin: Int, private val validityInMin: Int,
private val jwtSecret: String, private val jwtSecret: String,
@ -46,8 +45,3 @@ class AuthService(
.sign(Algorithm.HMAC256(jwtSecret)) .sign(Algorithm.HMAC256(jwtSecret))
} }
} }
fun Application.configureSecurity() {
val configureSecurity = get<AuthService>().configureSecurity
configureSecurity()
}

View File

@ -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<String, ByteArray>()
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)
}
}
}
}

View File

@ -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<Dotenv>()
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())
}
}