chore: Move files to correct folders

This commit is contained in:
oSumAtrIX
2024-06-05 17:27:25 +02:00
parent 2430be75d8
commit 659cce3e03
19 changed files with 60 additions and 60 deletions

View File

@@ -1,14 +1,14 @@
package app.revanced.api.configuration
import app.revanced.api.repository.AnnouncementRepository
import app.revanced.api.repository.ConfigurationRepository
import app.revanced.api.repository.OldApiService
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 app.revanced.api.configuration.repository.AnnouncementRepository
import app.revanced.api.configuration.repository.ConfigurationRepository
import app.revanced.api.configuration.repository.backend.BackendRepository
import app.revanced.api.configuration.repository.backend.github.GitHubBackendRepository
import app.revanced.api.configuration.services.AnnouncementService
import app.revanced.api.configuration.services.ApiService
import app.revanced.api.configuration.services.AuthService
import app.revanced.api.configuration.services.OldApiService
import app.revanced.api.configuration.services.PatchesService
import com.akuleshov7.ktoml.Toml
import com.akuleshov7.ktoml.source.decodeFromStream
import io.github.cdimascio.dotenv.Dotenv

View File

@@ -1,6 +1,6 @@
package app.revanced.api.configuration
import app.revanced.api.services.AuthService
import app.revanced.api.configuration.services.AuthService
import io.ktor.server.application.*
import org.koin.ktor.ext.get

View File

@@ -0,0 +1,170 @@
package app.revanced.api.configuration.repository
import app.revanced.api.configuration.repository.AnnouncementRepository.AttachmentTable.announcement
import app.revanced.api.configuration.schema.APIAnnouncement
import app.revanced.api.configuration.schema.APILatestAnnouncement
import app.revanced.api.configuration.schema.APIResponseAnnouncement
import kotlinx.datetime.*
import org.jetbrains.exposed.dao.IntEntity
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.kotlin.datetime.datetime
import org.jetbrains.exposed.sql.transactions.transaction
internal class AnnouncementRepository(private val database: Database) {
init {
transaction {
SchemaUtils.create(AnnouncementTable, AttachmentTable)
}
}
fun all() = transaction {
buildSet {
AnnouncementEntity.all().forEach { announcement ->
add(announcement.toApi())
}
}
}
fun all(channel: String) = transaction {
buildSet {
AnnouncementEntity.find { AnnouncementTable.channel eq channel }.forEach { announcement ->
add(announcement.toApi())
}
}
}
fun delete(id: Int) = transaction {
val announcement = AnnouncementEntity.findById(id) ?: return@transaction
announcement.delete()
}
fun latest() = transaction {
AnnouncementEntity.all().maxByOrNull { it.createdAt }?.toApi()
}
fun latest(channel: String) = transaction {
AnnouncementEntity.find { AnnouncementTable.channel eq channel }.maxByOrNull { it.createdAt }?.toApi()
}
fun latestId() = transaction {
AnnouncementEntity.all().maxByOrNull { it.createdAt }?.id?.value?.let {
APILatestAnnouncement(it)
}
}
fun latestId(channel: String) = transaction {
AnnouncementEntity.find { AnnouncementTable.channel eq channel }.maxByOrNull { it.createdAt }?.id?.value?.let {
APILatestAnnouncement(it)
}
}
fun archive(
id: Int,
archivedAt: LocalDateTime?,
) = transaction {
AnnouncementEntity.findById(id)?.apply {
this.archivedAt = archivedAt ?: java.time.LocalDateTime.now().toKotlinLocalDateTime()
}
}
fun unarchive(id: Int) = transaction {
AnnouncementEntity.findById(id)?.apply {
archivedAt = null
}
}
fun new(new: APIAnnouncement) = transaction {
AnnouncementEntity.new announcement@{
author = new.author
title = new.title
content = new.content
channel = new.channel
createdAt = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
archivedAt = new.archivedAt
level = new.level
}.also { newAnnouncement ->
new.attachmentUrls.map {
AttachmentEntity.new {
url = it
announcement = newAnnouncement
}
}
}
}
fun update(id: Int, new: APIAnnouncement) = transaction {
AnnouncementEntity.findById(id)?.apply {
author = new.author
title = new.title
content = new.content
channel = new.channel
archivedAt = new.archivedAt
level = new.level
attachments.forEach(AttachmentEntity::delete)
new.attachmentUrls.map {
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

@@ -0,0 +1,17 @@
package app.revanced.api.configuration.repository
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
internal class ConfigurationRepository(
val organization: String,
@SerialName("patches-repository")
val patchesRepository: String,
@SerialName("integrations-repositories")
val integrationsRepositoryNames: Set<String>,
@SerialName("contributors-repositories")
val contributorsRepositoryNames: Set<String>,
@SerialName("api-version")
val apiVersion: Int = 1,
)

View File

@@ -0,0 +1,136 @@
package app.revanced.api.configuration.repository.backend
import io.ktor.client.*
import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.Serializable
/**
* The backend of the application used to get data for the API.
*
* @param client The HTTP client to use for requests.
*/
abstract class BackendRepository internal constructor(
protected val client: HttpClient,
) {
/**
* A user.
*
* @property name The name of the user.
* @property avatarUrl The URL to the avatar of the user.
* @property url The URL to the profile of the user.
*/
interface BackendUser {
val name: String
val avatarUrl: String
val url: String
}
/**
* An organization.
*
* @property members The members of the organization.
*/
class BackendOrganization(
val members: Set<BackendMember>,
) {
/**
* A member of an organization.
*
* @property name The name of the member.
* @property avatarUrl The URL to the avatar of the member.
* @property url The URL to the profile of the member.
* @property bio The bio of the member.
* @property gpgKeysUrl The URL to the GPG keys of the member.
*/
@Serializable
class BackendMember(
override val name: String,
override val avatarUrl: String,
override val url: String,
val bio: String?,
val gpgKeysUrl: String,
) : BackendUser
/**
* A repository of an organization.
*
* @property contributors The contributors of the repository.
*/
class BackendRepository(
val contributors: Set<BackendContributor>,
) {
/**
* A contributor of a repository.
*
* @property name The name of the contributor.
* @property avatarUrl The URL to the avatar of the contributor.
* @property url The URL to the profile of the contributor.
* @property contributions The number of contributions of the contributor.
*/
@Serializable
class BackendContributor(
override val name: String,
override val avatarUrl: String,
override val url: String,
val contributions: Int,
) : BackendUser
/**
* A release of a repository.
*
* @property tag The tag of the release.
* @property assets The assets of the release.
* @property createdAt The date and time the release was created.
* @property releaseNote The release note of the release.
*/
@Serializable
class BackendRelease(
val tag: String,
val releaseNote: String,
val createdAt: LocalDateTime,
val assets: Set<BackendAsset>,
) {
/**
* An asset of a release.
*
* @property downloadUrl The URL to download the asset.
*/
@Serializable
class BackendAsset(
val downloadUrl: String,
)
}
}
}
/**
* Get a release of a repository.
*
* @param owner The owner of the repository.
* @param repository The name of the repository.
* @param tag The tag of the release. If null, the latest release is returned.
* @return The release.
*/
abstract suspend fun release(
owner: String,
repository: String,
tag: String? = null,
): BackendOrganization.BackendRepository.BackendRelease
/**
* Get the contributors of a repository.
*
* @param owner The owner of the repository.
* @param repository The name of the repository.
* @return The contributors.
*/
abstract suspend fun contributors(owner: String, repository: String): Set<BackendOrganization.BackendRepository.BackendContributor>
/**
* Get the members of an organization.
*
* @param organization The name of the organization.
* @return The members.
*/
abstract suspend fun members(organization: String): Set<BackendOrganization.BackendMember>
}

View File

@@ -0,0 +1,83 @@
package app.revanced.api.configuration.repository.backend.github
import app.revanced.api.configuration.repository.backend.BackendRepository
import app.revanced.api.configuration.repository.backend.BackendRepository.BackendOrganization.BackendMember
import app.revanced.api.configuration.repository.backend.BackendRepository.BackendOrganization.BackendRepository.BackendContributor
import app.revanced.api.configuration.repository.backend.BackendRepository.BackendOrganization.BackendRepository.BackendRelease
import app.revanced.api.configuration.repository.backend.BackendRepository.BackendOrganization.BackendRepository.BackendRelease.BackendAsset
import app.revanced.api.configuration.repository.backend.github.api.Request
import app.revanced.api.configuration.repository.backend.github.api.Request.Organization.Members
import app.revanced.api.configuration.repository.backend.github.api.Request.Organization.Repository.Contributors
import app.revanced.api.configuration.repository.backend.github.api.Request.Organization.Repository.Releases
import app.revanced.api.configuration.repository.backend.github.api.Response
import app.revanced.api.configuration.repository.backend.github.api.Response.GitHubOrganization.GitHubMember
import app.revanced.api.configuration.repository.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubContributor
import app.revanced.api.configuration.repository.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubRelease
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.resources.*
import kotlinx.coroutines.*
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) {
override suspend fun release(
owner: String,
repository: String,
tag: String?,
): BackendRelease {
val release: GitHubRelease = if (tag != null) {
client.get(Releases.Tag(owner, repository, tag)).body()
} else {
client.get(Releases.Latest(owner, repository)).body()
}
return BackendRelease(
tag = release.tagName,
releaseNote = release.body,
createdAt = release.createdAt.toLocalDateTime(TimeZone.UTC),
assets = release.assets.map {
BackendAsset(downloadUrl = it.browserDownloadUrl)
}.toSet(),
)
}
override suspend fun contributors(
owner: String,
repository: String,
): Set<BackendContributor> {
val contributors: Set<GitHubContributor> = client.get(Contributors(owner, repository)).body()
return contributors.map {
BackendContributor(
name = it.login,
avatarUrl = it.avatarUrl,
url = it.url,
contributions = it.contributions,
)
}.toSet()
}
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()
return runBlocking(Dispatchers.Default) {
members.map { member ->
// Map the member to a user in order to get the bio.
async {
client.get(Request.User(member.login)).body<Response.GitHubUser>()
}
}
}.awaitAll().map { user ->
// Map the user back to a member.
BackendMember(
name = user.login,
avatarUrl = user.avatarUrl,
url = user.url,
bio = user.bio,
gpgKeysUrl = "https://github.com/${user.login}.gpg",
)
}.toSet()
}
}

View File

@@ -0,0 +1,27 @@
package app.revanced.api.configuration.repository.backend.github.api
import io.ktor.resources.*
class Request {
@Resource("/users/{username}")
class User(val username: String)
class Organization {
@Resource("/orgs/{org}/members")
class Members(val org: String)
class Repository {
@Resource("/repos/{owner}/{repo}/contributors")
class Contributors(val owner: String, val repo: String)
@Resource("/repos/{owner}/{repo}/releases")
class Releases(val owner: String, val repo: String) {
@Resource("/repos/{owner}/{repo}/releases/tags/{tag}")
class Tag(val owner: String, val repo: String, val tag: String)
@Resource("/repos/{owner}/{repo}/releases/latest")
class Latest(val owner: String, val repo: String)
}
}
}
}

View File

@@ -0,0 +1,52 @@
package app.revanced.api.configuration.repository.backend.github.api
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
class Response {
interface IGitHubUser {
val login: String
val avatarUrl: String
val url: String
}
@Serializable
class GitHubUser(
override val login: String,
override val avatarUrl: String,
override val url: String,
val bio: String?,
) : IGitHubUser
class GitHubOrganization {
@Serializable
class GitHubMember(
override val login: String,
override val avatarUrl: String,
override val url: String,
) : IGitHubUser
class GitHubRepository {
@Serializable
class GitHubContributor(
override val login: String,
override val avatarUrl: String,
override val url: String,
val contributions: Int,
) : IGitHubUser
@Serializable
class GitHubRelease(
val tagName: String,
val assets: Set<GitHubAsset>,
val createdAt: Instant,
val body: String,
) {
@Serializable
class GitHubAsset(
val browserDownloadUrl: String,
)
}
}
}
}

View File

@@ -1,10 +1,10 @@
package app.revanced.api.configuration.routing
import app.revanced.api.configuration.repository.ConfigurationRepository
import app.revanced.api.configuration.routing.routes.announcementsRoute
import app.revanced.api.configuration.routing.routes.oldApiRoute
import app.revanced.api.configuration.routing.routes.patchesRoute
import app.revanced.api.configuration.routing.routes.rootRoute
import app.revanced.api.repository.ConfigurationRepository
import io.ktor.server.application.*
import io.ktor.server.routing.*
import org.koin.ktor.ext.get

View File

@@ -1,8 +1,8 @@
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 app.revanced.api.configuration.schema.APIAnnouncement
import app.revanced.api.configuration.schema.APIAnnouncementArchivedAt
import app.revanced.api.configuration.services.AnnouncementService
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*

View File

@@ -1,7 +1,7 @@
package app.revanced.api.configuration.routing.routes
import app.revanced.api.services.ApiService
import app.revanced.api.services.AuthService
import app.revanced.api.configuration.services.ApiService
import app.revanced.api.configuration.services.AuthService
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*

View File

@@ -1,6 +1,6 @@
package app.revanced.api.configuration.routing.routes
import app.revanced.api.repository.OldApiService
import app.revanced.api.configuration.services.OldApiService
import io.ktor.server.application.*
import io.ktor.server.routing.*
import org.koin.ktor.ext.get

View File

@@ -1,6 +1,6 @@
package app.revanced.api.configuration.routing.routes
import app.revanced.api.services.PatchesService
import app.revanced.api.configuration.services.PatchesService
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.response.*

View File

@@ -0,0 +1,102 @@
package app.revanced.api.configuration.schema
import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
class APIRelease(
val version: String,
val createdAt: LocalDateTime,
val changelog: String,
val assets: Set<APIAsset>,
)
interface APIUser {
val name: String
val avatarUrl: String
val url: String
}
@Serializable
class APIMember(
override val name: String,
override val avatarUrl: String,
override val url: String,
val gpgKeysUrl: String,
) : APIUser
@Serializable
class APIContributor(
override val name: String,
override val avatarUrl: String,
override val url: String,
val contributions: Int,
) : APIUser
@Serializable
class APIContributable(
val name: String,
val contributors: Set<APIContributor>,
)
@Serializable
class APIAsset(
val downloadUrl: String,
) {
val type = when {
downloadUrl.endsWith(".jar") -> Type.PATCHES
downloadUrl.endsWith(".apk") -> Type.INTEGRATIONS
else -> Type.UNKNOWN
}
enum class Type {
@SerialName("patches")
PATCHES,
@SerialName("integrations")
INTEGRATIONS,
@SerialName("unknown")
UNKNOWN,
}
}
@Serializable
class APIReleaseVersion(
val version: String,
)
@Serializable
class APIAnnouncement(
val author: String? = null,
val title: String,
val content: String? = null,
val attachmentUrls: Set<String> = emptySet(),
val channel: String? = null,
val archivedAt: LocalDateTime? = null,
val level: Int = 0,
)
@Serializable
class APIResponseAnnouncement(
val id: Int,
val author: String? = null,
val title: String,
val content: String? = null,
val attachmentUrls: Set<String> = emptySet(),
val channel: String? = null,
val createdAt: LocalDateTime,
val archivedAt: LocalDateTime? = null,
val level: Int = 0,
)
@Serializable
class APILatestAnnouncement(
val id: Int,
)
@Serializable
class APIAnnouncementArchivedAt(
val archivedAt: LocalDateTime,
)

View File

@@ -0,0 +1,35 @@
package app.revanced.api.configuration.services
import app.revanced.api.configuration.repository.AnnouncementRepository
import app.revanced.api.configuration.schema.APIAnnouncement
import app.revanced.api.configuration.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.configuration.services
import app.revanced.api.configuration.repository.ConfigurationRepository
import app.revanced.api.configuration.repository.backend.BackendRepository
import app.revanced.api.configuration.schema.APIContributable
import app.revanced.api.configuration.schema.APIContributor
import app.revanced.api.configuration.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

@@ -0,0 +1,45 @@
package app.revanced.api.configuration.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 java.util.*
import kotlin.time.Duration.Companion.minutes
internal class AuthService(
private val issuer: String,
private val validityInMin: Int,
private val jwtSecret: String,
private val basicUsername: String,
private val basicPassword: String,
) {
val configureSecurity: Application.() -> Unit = {
install(Authentication) {
jwt("jwt") {
verifier(
JWT.require(Algorithm.HMAC256(jwtSecret))
.withIssuer(issuer)
.build(),
)
validate { credential -> JWTPrincipal(credential.payload) }
}
basic("basic") {
validate { credentials ->
if (credentials.name == basicUsername && credentials.password == basicPassword) {
UserIdPrincipal(credentials.name)
} else {
null
}
}
}
}
}
fun newToken(): String = JWT.create()
.withIssuer(issuer)
.withExpiresAt(Date(System.currentTimeMillis() + validityInMin.minutes.inWholeMilliseconds))
.sign(Algorithm.HMAC256(jwtSecret))
}

View File

@@ -0,0 +1,68 @@
package app.revanced.api.configuration.services
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.util.*
import io.ktor.utils.io.*
internal class OldApiService(private val client: HttpClient) {
@OptIn(InternalAPI::class)
suspend fun proxy(call: ApplicationCall) {
val channel = call.request.receiveChannel()
val size = channel.availableForRead
val byteArray = ByteArray(size)
channel.readFully(byteArray)
val response: HttpResponse = client.request(call.request.uri) {
method = call.request.httpMethod
headers {
appendAll(
call.request.headers.filter { key, _ ->
!key.equals(
HttpHeaders.ContentType,
ignoreCase = true,
) &&
!key.equals(
HttpHeaders.ContentLength,
ignoreCase = true,
) &&
!key.equals(HttpHeaders.Host, ignoreCase = true)
},
)
}
if (call.request.httpMethod == HttpMethod.Post) {
body = ByteArrayContent(byteArray, call.request.contentType())
}
}
val headers = response.headers
call.respond(object : OutgoingContent.WriteChannelContent() {
override val contentLength: Long? = headers[HttpHeaders.ContentLength]?.toLong()
override val contentType = headers[HttpHeaders.ContentType]?.let { ContentType.parse(it) }
override val headers: Headers = Headers.build {
appendAll(
headers.filter { key, _ ->
!key.equals(
HttpHeaders.ContentType,
ignoreCase = true,
) &&
!key.equals(HttpHeaders.ContentLength, ignoreCase = true)
},
)
}
override val status = response.status
override suspend fun writeTo(channel: ByteWriteChannel) {
response.content.copyAndClose(channel)
}
})
}
}

View File

@@ -0,0 +1,87 @@
package app.revanced.api.configuration.services
import app.revanced.api.configuration.repository.ConfigurationRepository
import app.revanced.api.configuration.repository.backend.BackendRepository
import app.revanced.api.configuration.schema.APIAsset
import app.revanced.api.configuration.schema.APIRelease
import app.revanced.api.configuration.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)
}
}
}
}