refactor: Refactor into services and repositories

This commit is contained in:
oSumAtrIX
2024-06-05 03:07:28 +02:00
parent 7a1957d013
commit fa2f8b2f86
30 changed files with 623 additions and 542 deletions

View File

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

View File

@@ -0,0 +1,83 @@
package app.revanced.api.configuration
import app.revanced.api.repository.AnnouncementRepository
import app.revanced.api.repository.ConfigurationRepository
import app.revanced.api.repository.backend.BackendRepository
import app.revanced.api.repository.backend.github.GitHubBackendRepository
import app.revanced.api.services.AnnouncementService
import app.revanced.api.services.ApiService
import app.revanced.api.services.AuthService
import app.revanced.api.services.PatchesService
import com.akuleshov7.ktoml.Toml
import com.akuleshov7.ktoml.source.decodeFromStream
import io.github.cdimascio.dotenv.Dotenv
import io.ktor.server.application.*
import org.jetbrains.exposed.sql.Database
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.bind
import org.koin.dsl.module
import org.koin.ktor.plugin.Koin
import java.io.File
fun Application.configureDependencies() {
val globalModule = module {
single {
Dotenv.configure()
.systemProperties()
.load()
}
}
val repositoryModule = module {
single {
val dotenv = get<Dotenv>()
Database.connect(
url = dotenv["DB_URL"],
user = dotenv["DB_USER"],
password = dotenv["DB_PASSWORD"],
driver = "org.h2.Driver",
)
}
single {
val configFilePath = get<Dotenv>()["CONFIG_FILE_PATH"]
val configFile = File(configFilePath).inputStream()
Toml.decodeFromStream<ConfigurationRepository>(configFile)
}
singleOf(::AnnouncementRepository)
}
val serviceModule = module {
single {
val dotenv = get<Dotenv>()
val jwtSecret = dotenv["JWT_SECRET"]
val issuer = dotenv["JWT_ISSUER"]
val validityInMin = dotenv["JWT_VALIDITY_IN_MIN"].toInt()
val basicUsername = dotenv["BASIC_USERNAME"]
val basicPassword = dotenv["BASIC_PASSWORD"]
AuthService(issuer, validityInMin, jwtSecret, basicUsername, basicPassword)
}
single {
val token = get<Dotenv>()["GITHUB_TOKEN"]
GitHubBackendRepository(token)
} bind BackendRepository::class
singleOf(::AnnouncementService)
singleOf(::PatchesService)
singleOf(::ApiService)
}
install(Koin) {
modules(
globalModule,
repositoryModule,
serviceModule,
)
}
}

View File

@@ -1,4 +1,4 @@
package app.revanced.api.modules
package app.revanced.api.configuration
import io.ktor.http.*
import io.ktor.http.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())) }

View File

@@ -0,0 +1,9 @@
package app.revanced.api.configuration
import app.revanced.api.services.AuthService
import io.ktor.server.application.*
import org.koin.ktor.ext.get
fun Application.configureSecurity() {
get<AuthService>().configureSecurity(this)
}

View File

@@ -0,0 +1,19 @@
package app.revanced.api.configuration
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNamingStrategy
@OptIn(ExperimentalSerializationApi::class)
fun Application.configureSerialization() {
install(ContentNegotiation) {
json(
Json {
namingStrategy = JsonNamingStrategy.SnakeCase
},
)
}
}

View File

@@ -0,0 +1,19 @@
package app.revanced.api.configuration.routing
import app.revanced.api.configuration.routing.routes.configureAnnouncementsRoute
import app.revanced.api.configuration.routing.routes.configurePatchesRoute
import app.revanced.api.configuration.routing.routes.configureRootRoute
import app.revanced.api.repository.ConfigurationRepository
import io.ktor.server.application.*
import io.ktor.server.routing.*
import org.koin.ktor.ext.get
internal fun Application.configureRouting() = routing {
val configuration = get<ConfigurationRepository>()
route("/v${configuration.apiVersion}") {
configureRootRoute()
configurePatchesRoute()
configureAnnouncementsRoute()
}
}

View File

@@ -0,0 +1,86 @@
package app.revanced.api.configuration.routing.routes
import app.revanced.api.schema.APIAnnouncement
import app.revanced.api.schema.APIAnnouncementArchivedAt
import app.revanced.api.services.AnnouncementService
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.util.*
import org.koin.ktor.ext.get as koinGet
internal fun Route.configureAnnouncementsRoute() = route("/announcements") {
val announcementService = koinGet<AnnouncementService>()
route("/{channel}/latest") {
get("/id") {
val channel: String by call.parameters
call.respond(
announcementService.latestId(channel) ?: return@get call.respond(HttpStatusCode.NotFound),
)
}
get {
val channel: String by call.parameters
call.respond(
announcementService.latest(channel) ?: return@get call.respond(HttpStatusCode.NotFound),
)
}
}
get("/{channel}") {
val channel: String by call.parameters
call.respond(announcementService.all(channel))
}
route("/latest") {
get("/id") {
call.respond(announcementService.latestId() ?: return@get call.respond(HttpStatusCode.NotFound))
}
get {
call.respond(announcementService.latest() ?: return@get call.respond(HttpStatusCode.NotFound))
}
}
get {
call.respond(announcementService.all())
}
authenticate("jwt") {
post {
announcementService.new(call.receive<APIAnnouncement>())
}
post("/{id}/archive") {
val id: Int by call.parameters
val archivedAt = call.receiveNullable<APIAnnouncementArchivedAt>()?.archivedAt
announcementService.archive(id, archivedAt)
}
post("/{id}/unarchive") {
val id: Int by call.parameters
announcementService.unarchive(id)
}
patch("/{id}") {
val id: Int by call.parameters
announcementService.update(id, call.receive<APIAnnouncement>())
}
delete("/{id}") {
val id: Int by call.parameters
announcementService.delete(id)
}
}
}

View File

@@ -0,0 +1,41 @@
package app.revanced.api.configuration.routing.routes
import app.revanced.api.services.ApiService
import app.revanced.api.services.AuthService
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.http.content.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import org.koin.ktor.ext.get
internal fun Route.configureRootRoute() {
val apiService = get<ApiService>()
val authService = get<AuthService>()
get("/contributors") {
call.respond(apiService.contributors())
}
get("/team") {
call.respond(apiService.team())
}
route("/ping") {
handle {
call.respond(HttpStatusCode.NoContent)
}
}
authenticate("basic") {
get("/token") {
call.respond(authService.newToken())
}
}
staticResources("/", "/static/api") {
contentType { ContentType.Application.Json }
extensions("json")
}
}

View File

@@ -0,0 +1,26 @@
package app.revanced.api.configuration.routing.routes
import app.revanced.api.services.PatchesService
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import org.koin.ktor.ext.get as koinGet
internal fun Route.configurePatchesRoute() = route("/patches") {
val patchesService = koinGet<PatchesService>()
route("latest") {
get {
call.respond(patchesService.latestRelease())
}
get("/version") {
call.respond(patchesService.latestVersion())
}
get("/list") {
call.respondBytes(ContentType.Application.Json) { patchesService.list() }
}
}
}

View File

@@ -1,75 +0,0 @@
package app.revanced.api.modules
import app.revanced.api.backend.Backend
import app.revanced.api.backend.github.GitHubBackend
import app.revanced.api.schema.APIConfiguration
import com.akuleshov7.ktoml.Toml
import com.akuleshov7.ktoml.source.decodeFromStream
import io.github.cdimascio.dotenv.Dotenv
import io.ktor.server.application.*
import org.jetbrains.exposed.sql.Database
import org.koin.dsl.bind
import org.koin.dsl.module
import org.koin.ktor.plugin.Koin
import java.io.File
fun Application.configureDependencies() {
install(Koin) {
modules(
globalModule,
gitHubBackendModule,
databaseModule,
authModule,
)
}
}
val globalModule = module {
single {
Dotenv.configure()
.systemProperties()
.load()
}
single {
val configFilePath = get<Dotenv>()["CONFIG_FILE_PATH"]
Toml.decodeFromStream<APIConfiguration>(File(configFilePath).inputStream())
}
}
val gitHubBackendModule = module {
single {
val token = get<Dotenv>()["GITHUB_TOKEN"]
GitHubBackend(token)
} bind Backend::class
}
val databaseModule = module {
single {
val dotenv = get<Dotenv>()
Database.connect(
url = dotenv["DB_URL"],
user = dotenv["DB_USER"],
password = dotenv["DB_PASSWORD"],
driver = "org.h2.Driver",
)
}
factory<AnnouncementService> {
AnnouncementService(get())
}
}
val authModule = module {
single {
val dotenv = get<Dotenv>()
val jwtSecret = dotenv["JWT_SECRET"]
val issuer = dotenv["JWT_ISSUER"]
val validityInMin = dotenv["JWT_VALIDITY_IN_MIN"].toInt()
val basicUsername = dotenv["BASIC_USERNAME"]
val basicPassword = dotenv["BASIC_PASSWORD"]
AuthService(issuer, validityInMin, jwtSecret, basicUsername, basicPassword)
}
}

View File

@@ -1,232 +0,0 @@
package app.revanced.api.modules
import app.revanced.api.backend.Backend
import app.revanced.api.schema.*
import app.revanced.library.PatchUtils
import app.revanced.patcher.PatchBundleLoader
import com.github.benmanes.caffeine.cache.Caffeine
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.http.content.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.util.pipeline.*
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import java.io.ByteArrayOutputStream
import java.net.URL
import org.koin.ktor.ext.get as koinGet
fun Application.configureRouting() {
val backend: Backend = koinGet()
val configuration: APIConfiguration = koinGet()
val announcementService: AnnouncementService = koinGet()
val authService: AuthService = koinGet()
routing {
route("/v${configuration.apiVersion}") {
route("/announcements") {
suspend fun PipelineContext<*, ApplicationCall>.announcement(block: AnnouncementService.() -> APIResponseAnnouncement?) =
announcementService.block()?.let { call.respond(it) }
?: call.respond(HttpStatusCode.NotFound)
suspend fun PipelineContext<*, ApplicationCall>.announcementId(block: AnnouncementService.() -> APILatestAnnouncement?) =
announcementService.block()?.let { call.respond(it) }
?: call.respond(HttpStatusCode.NotFound)
suspend fun PipelineContext<*, ApplicationCall>.channel(block: suspend (String) -> Unit) =
block(call.parameters["channel"]!!)
route("/{channel}/latest") {
get("/id") {
channel {
announcementId {
latestId(it)
}
}
}
get {
channel {
announcement {
latest(it)
}
}
}
}
get("/{channel}") {
channel {
call.respond(announcementService.read(it))
}
}
route("/latest") {
get("/id") {
announcementId {
latestId()
}
}
get {
announcement {
latest()
}
}
}
get {
call.respond(announcementService.read())
}
authenticate("jwt") {
suspend fun PipelineContext<*, ApplicationCall>.id(block: suspend (Int) -> Unit) =
call.parameters["id"]!!.toIntOrNull()?.let {
block(it)
} ?: call.respond(HttpStatusCode.BadRequest)
post {
announcementService.new(call.receive<APIAnnouncement>())
}
post("/{id}/archive") {
id {
val archivedAt = call.receiveNullable<APIAnnouncementArchivedAt>()?.archivedAt
announcementService.archive(it, archivedAt)
}
}
post("/{id}/unarchive") {
id {
announcementService.unarchive(it)
}
}
patch("/{id}") {
id {
announcementService.update(it, call.receive<APIAnnouncement>())
}
}
delete("/{id}") {
id {
announcementService.delete(it)
}
}
}
}
route("/patches") {
route("latest") {
get {
val patchesRelease =
backend.getRelease(configuration.organization, configuration.patchesRepository)
val integrationsReleases = configuration.integrationsRepositoryNames.map {
async { backend.getRelease(configuration.organization, it) }
}.awaitAll()
val assets = (patchesRelease.assets + integrationsReleases.flatMap { it.assets })
.map { APIAsset(it.downloadUrl) }
.filter { it.type != APIAsset.Type.UNKNOWN }
.toSet()
val apiRelease = APIRelease(
patchesRelease.tag,
patchesRelease.createdAt,
patchesRelease.releaseNote,
assets,
)
call.respond(apiRelease)
}
get("/version") {
val patchesRelease =
backend.getRelease(configuration.organization, configuration.patchesRepository)
val apiPatchesRelease = APIReleaseVersion(patchesRelease.tag)
call.respond(apiPatchesRelease)
}
val patchesListCache = Caffeine
.newBuilder()
.maximumSize(1)
.build<String, ByteArray>()
get("/list") {
val patchesRelease =
backend.getRelease(configuration.organization, configuration.patchesRepository)
val patchesListByteArray = patchesListCache.getIfPresent(patchesRelease.tag) ?: run {
val downloadUrl = patchesRelease.assets
.map { APIAsset(it.downloadUrl) }
.find { it.type == APIAsset.Type.PATCHES }
?.downloadUrl
val patches = kotlin.io.path.createTempFile().toFile().apply {
outputStream().use { URL(downloadUrl).openStream().copyTo(it) }
}.let { file ->
PatchBundleLoader.Jar(file).also { file.delete() }
}
ByteArrayOutputStream().use { stream ->
PatchUtils.Json.serialize(patches, outputStream = stream)
stream.toByteArray()
}.also {
patchesListCache.put(patchesRelease.tag, it)
}
}
call.respondBytes(ContentType.Application.Json) { patchesListByteArray }
}
}
}
staticResources("/", "/static/api") {
contentType { ContentType.Application.Json }
extensions("json")
}
get("/contributors") {
val contributors =
configuration.contributorsRepositoryNames.map {
async {
APIContributable(
it,
backend.getContributors(configuration.organization, it).map {
APIContributor(it.name, it.avatarUrl, it.url, it.contributions)
}.toSet(),
)
}
}.awaitAll()
call.respond(contributors)
}
get("/team") {
val team =
backend.getMembers(configuration.organization).map {
APIMember(it.name, it.avatarUrl, it.url, it.gpgKeysUrl)
}
call.respond(team)
}
route("/ping") {
handle {
call.respond(HttpStatusCode.NoContent)
}
}
authenticate("basic") {
get("/token") {
call.respond(authService.newToken())
}
}
}
}
}

View File

@@ -1,11 +0,0 @@
package app.revanced.api.modules
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
fun Application.configureSerialization() {
install(ContentNegotiation) {
json()
}
}

View File

@@ -1,6 +1,6 @@
package app.revanced.api.modules
package app.revanced.api.repository
import app.revanced.api.modules.AnnouncementService.Attachments.announcement
import app.revanced.api.repository.AnnouncementRepository.AttachmentTable.announcement
import app.revanced.api.schema.APIAnnouncement
import app.revanced.api.schema.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
}
}

View File

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

View File

@@ -1,4 +1,4 @@
package app.revanced.api.backend
package app.revanced.api.repository.backend
import io.ktor.client.*
import io.ktor.client.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>
}

View File

@@ -1,18 +1,18 @@
package app.revanced.api.backend.github
package app.revanced.api.repository.backend.github
import app.revanced.api.backend.Backend
import app.revanced.api.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()

View File

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

View File

@@ -1,4 +1,4 @@
package app.revanced.api.backend.github.api
package app.revanced.api.repository.backend.github.api
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable

View File

@@ -0,0 +1,35 @@
package app.revanced.api.services
import app.revanced.api.repository.AnnouncementRepository
import app.revanced.api.schema.APIAnnouncement
import app.revanced.api.schema.APILatestAnnouncement
import kotlinx.datetime.LocalDateTime
internal class AnnouncementService(
private val announcementRepository: AnnouncementRepository,
) {
fun latestId(channel: String): APILatestAnnouncement? = announcementRepository.latestId(channel)
fun latestId(): APILatestAnnouncement? = announcementRepository.latestId()
fun latest(channel: String) = announcementRepository.latest(channel)
fun latest() = announcementRepository.latest()
fun all(channel: String) = announcementRepository.all(channel)
fun all() = announcementRepository.all()
fun new(new: APIAnnouncement) {
announcementRepository.new(new)
}
fun archive(id: Int, archivedAt: LocalDateTime?) {
announcementRepository.archive(id, archivedAt)
}
fun unarchive(id: Int) {
announcementRepository.unarchive(id)
}
fun update(id: Int, new: APIAnnouncement) {
announcementRepository.update(id, new)
}
fun delete(id: Int) {
announcementRepository.delete(id)
}
}

View File

@@ -0,0 +1,33 @@
package app.revanced.api.services
import app.revanced.api.repository.ConfigurationRepository
import app.revanced.api.repository.backend.BackendRepository
import app.revanced.api.schema.APIContributable
import app.revanced.api.schema.APIContributor
import app.revanced.api.schema.APIMember
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.withContext
internal class ApiService(
private val backendRepository: BackendRepository,
private val configurationRepository: ConfigurationRepository,
) {
suspend fun contributors() = withContext(Dispatchers.IO) {
configurationRepository.contributorsRepositoryNames.map {
async {
APIContributable(
it,
backendRepository.contributors(configurationRepository.organization, it).map {
APIContributor(it.name, it.avatarUrl, it.url, it.contributions)
}.toSet(),
)
}
}
}.awaitAll()
suspend fun team() = backendRepository.members(configurationRepository.organization).map {
APIMember(it.name, it.avatarUrl, it.url, it.gpgKeysUrl)
}
}

View File

@@ -1,15 +1,14 @@
package app.revanced.api.modules
package app.revanced.api.services
import com.auth0.jwt.JWT
import com.auth0.jwt.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()
}

View File

@@ -0,0 +1,87 @@
package app.revanced.api.services
import app.revanced.api.repository.ConfigurationRepository
import app.revanced.api.repository.backend.BackendRepository
import app.revanced.api.schema.APIAsset
import app.revanced.api.schema.APIRelease
import app.revanced.api.schema.APIReleaseVersion
import app.revanced.library.PatchUtils
import app.revanced.patcher.PatchBundleLoader
import com.github.benmanes.caffeine.cache.Caffeine
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.withContext
import java.io.ByteArrayOutputStream
import java.net.URL
internal class PatchesService(
private val backendRepository: BackendRepository,
private val configurationRepository: ConfigurationRepository,
) {
private val patchesListCache = Caffeine
.newBuilder()
.maximumSize(1)
.build<String, ByteArray>()
suspend fun latestRelease(): APIRelease {
val patchesRelease = backendRepository.release(
configurationRepository.organization,
configurationRepository.patchesRepository,
)
val integrationsReleases = withContext(Dispatchers.Default) {
configurationRepository.integrationsRepositoryNames.map {
async { backendRepository.release(configurationRepository.organization, it) }
}
}.awaitAll()
val assets = (patchesRelease.assets + integrationsReleases.flatMap { it.assets })
.map { APIAsset(it.downloadUrl) }
.filter { it.type != APIAsset.Type.UNKNOWN }
.toSet()
return APIRelease(
patchesRelease.tag,
patchesRelease.createdAt,
patchesRelease.releaseNote,
assets,
)
}
suspend fun latestVersion(): APIReleaseVersion {
val patchesRelease = backendRepository.release(
configurationRepository.organization,
configurationRepository.patchesRepository,
)
return APIReleaseVersion(patchesRelease.tag)
}
suspend fun list(): ByteArray {
val patchesRelease = backendRepository.release(
configurationRepository.organization,
configurationRepository.patchesRepository,
)
return patchesListCache.getIfPresent(patchesRelease.tag) ?: run {
val downloadUrl = patchesRelease.assets
.map { APIAsset(it.downloadUrl) }
.find { it.type == APIAsset.Type.PATCHES }
?.downloadUrl
val patches = kotlin.io.path.createTempFile().toFile().apply {
outputStream().use { URL(downloadUrl).openStream().copyTo(it) }
}.let { file ->
PatchBundleLoader.Jar(file).also { file.delete() }
}
ByteArrayOutputStream().use { stream ->
PatchUtils.Json.serialize(patches, outputStream = stream)
stream.toByteArray()
}.also {
patchesListCache.put(patchesRelease.tag, it)
}
}
}
}

View File

@@ -80,4 +80,4 @@
}
]
}
}
}