mirror of
https://github.com/revanced/revanced-api.git
synced 2025-04-29 22:24:31 +02:00
refactor: Refactor into services and repositories
This commit is contained in:
parent
7a1957d013
commit
fa2f8b2f86
@ -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<DefaultTask>("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)
|
||||
}
|
||||
|
@ -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" }
|
||||
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
5
gradle/wrapper/gradle-wrapper.properties
vendored
5
gradle/wrapper/gradle-wrapper.properties
vendored
@ -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
|
||||
|
41
gradlew
vendored
41
gradlew
vendored
@ -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.
|
||||
|
35
gradlew.bat
vendored
35
gradlew.bat
vendored
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
@ -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())) }
|
@ -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)
|
||||
}
|
@ -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
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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<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
|
||||
}
|
||||
|
||||
internal class AnnouncementRepository(private val database: Database) {
|
||||
init {
|
||||
transaction {
|
||||
SchemaUtils.create(Announcements, Attachments)
|
||||
SchemaUtils.create(AnnouncementTable, AttachmentTable)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> 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 <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
|
||||
}
|
||||
}
|
@ -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,
|
@ -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<OkHttpConfig>.() -> 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<BackendOrganization.BackendRepository.BackendContributor>
|
||||
abstract suspend fun contributors(owner: String, repository: String): Set<BackendOrganization.BackendRepository.BackendContributor>
|
||||
|
||||
/**
|
||||
* 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<BackendOrganization.BackendMember>
|
||||
abstract suspend fun members(organization: String): Set<BackendOrganization.BackendMember>
|
||||
}
|
@ -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<BackendContributor> {
|
||||
@ -96,7 +96,7 @@ class GitHubBackend(token: String? = null) : Backend({
|
||||
}.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.
|
||||
val members: Set<GitHubMember> = client.get(Members(organization)).body()
|
||||
|
@ -1,4 +1,4 @@
|
||||
package app.revanced.api.backend.github.api
|
||||
package app.revanced.api.repository.backend.github.api
|
||||
|
||||
import io.ktor.resources.*
|
||||
|
@ -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
|
@ -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)
|
||||
}
|
||||
}
|
33
src/main/kotlin/app/revanced/api/services/ApiService.kt
Normal file
33
src/main/kotlin/app/revanced/api/services/ApiService.kt
Normal 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)
|
||||
}
|
||||
}
|
@ -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<AuthService>().configureSecurity
|
||||
configureSecurity()
|
||||
}
|
87
src/main/kotlin/app/revanced/api/services/PatchesService.kt
Normal file
87
src/main/kotlin/app/revanced/api/services/PatchesService.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -80,4 +80,4 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user