mirror of
https://github.com/ReVanced/revanced-api.git
synced 2026-01-18 00:43:57 +00:00
refactor: Refactor into services and repositories
This commit is contained in:
@@ -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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user