diff --git a/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt b/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt index 24ebb54..5256dd4 100644 --- a/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt +++ b/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt @@ -27,6 +27,7 @@ import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonNamingStrategy import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.transactions.TransactionManager import org.koin.core.module.dsl.singleOf import org.koin.core.parameter.parameterArrayOf import org.koin.dsl.module @@ -41,6 +42,7 @@ fun Application.configureDependencies( single { Dotenv.configure().load() } + factory { params -> val defaultRequestUri: String = params.get() val configBlock = params.getOrNull<(HttpClientConfig.() -> Unit)>() ?: {} @@ -54,17 +56,6 @@ fun Application.configureDependencies( } val repositoryModule = module { - single { - val dotenv = get() - - Database.connect( - url = dotenv["DB_URL"], - user = dotenv["DB_USER"], - password = dotenv["DB_PASSWORD"], - driver = "org.h2.Driver", - ) - } - single { GitHubBackendRepository( get { @@ -106,7 +97,17 @@ fun Application.configureDependencies( Toml.decodeFromStream(configFile.inputStream()) } - singleOf(::AnnouncementRepository) + single { + val dotenv = get() + + TransactionManager.defaultDatabase = Database.connect( + url = dotenv["DB_URL"], + user = dotenv["DB_USER"], + password = dotenv["DB_PASSWORD"], + ) + + AnnouncementRepository() + } } val serviceModule = module { diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt index 9835454..95c4daf 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt @@ -1,47 +1,41 @@ 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.APIResponseAnnouncement import app.revanced.api.configuration.schema.APIResponseAnnouncementId import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.runBlocking 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.dao.load import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime import org.jetbrains.exposed.sql.kotlin.datetime.datetime import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction +import org.jetbrains.exposed.sql.transactions.experimental.suspendedTransactionAsync -internal class AnnouncementRepository(private val database: Database) { +internal class AnnouncementRepository { init { runBlocking { transaction { - SchemaUtils.create(AnnouncementTable, AttachmentTable) + SchemaUtils.create(Announcements, Attachments) } } } suspend fun all() = transaction { - buildSet { - AnnouncementEntity.all().forEach { announcement -> - add(announcement.toApi()) - } - } + Announcement.all() } suspend fun all(channel: String) = transaction { - buildSet { - AnnouncementEntity.find { AnnouncementTable.channel eq channel }.forEach { announcement -> - add(announcement.toApi()) - } - } + Announcement.find { Announcements.channel eq channel } } suspend fun delete(id: Int) = transaction { - val announcement = AnnouncementEntity.findById(id) ?: return@transaction + val announcement = Announcement.findById(id) ?: return@transaction announcement.delete() } @@ -49,21 +43,21 @@ internal class AnnouncementRepository(private val database: Database) { // TODO: These are inefficient, but I'm not sure how to make them more efficient. suspend fun latest() = transaction { - AnnouncementEntity.all().maxByOrNull { it.createdAt }?.toApi() + Announcement.all().maxByOrNull { it.id }?.load(Announcement::attachments) } suspend fun latest(channel: String) = transaction { - AnnouncementEntity.find { AnnouncementTable.channel eq channel }.maxByOrNull { it.createdAt }?.toApi() + Announcement.find { Announcements.channel eq channel }.maxByOrNull { it.id }?.load(Announcement::attachments) } suspend fun latestId() = transaction { - AnnouncementEntity.all().maxByOrNull { it.createdAt }?.id?.value?.let { + Announcement.all().maxByOrNull { it.id }?.id?.value?.let { APIResponseAnnouncementId(it) } } suspend fun latestId(channel: String) = transaction { - AnnouncementEntity.find { AnnouncementTable.channel eq channel }.maxByOrNull { it.createdAt }?.id?.value?.let { + Announcement.find { Announcements.channel eq channel }.maxByOrNull { it.id }?.id?.value?.let { APIResponseAnnouncementId(it) } } @@ -72,106 +66,98 @@ internal class AnnouncementRepository(private val database: Database) { id: Int, archivedAt: LocalDateTime?, ) = transaction { - AnnouncementEntity.findById(id)?.apply { - this.archivedAt = archivedAt ?: java.time.LocalDateTime.now().toKotlinLocalDateTime() + Announcement.findByIdAndUpdate(id) { + it.archivedAt = archivedAt ?: java.time.LocalDateTime.now().toKotlinLocalDateTime() } } suspend fun unarchive(id: Int) = transaction { - AnnouncementEntity.findById(id)?.apply { - archivedAt = null + Announcement.findByIdAndUpdate(id) { + it.archivedAt = null } } suspend fun new(new: APIAnnouncement) = transaction { - AnnouncementEntity.new announcement@{ + Announcement.new { 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 + new.attachmentUrls.map { newUrl -> + suspendedTransactionAsync { + Attachment.new { + url = newUrl + announcement = newAnnouncement + } } - } + }.awaitAll() } } suspend 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 + Announcement.findByIdAndUpdate(id) { + it.author = new.author + it.title = new.title + it.content = new.content + it.channel = new.channel + it.archivedAt = new.archivedAt + it.level = new.level + }?.also { newAnnouncement -> + newAnnouncement.attachments.map { + suspendedTransactionAsync { + it.delete() } - } + }.awaitAll() + + new.attachmentUrls.map { newUrl -> + suspendedTransactionAsync { + Attachment.new { + url = newUrl + announcement = newAnnouncement + } + } + }.awaitAll() } } - private suspend fun transaction(statement: Transaction.() -> T) = - newSuspendedTransaction(Dispatchers.IO, database, statement = statement) + private suspend fun transaction(statement: suspend Transaction.() -> T) = + newSuspendedTransaction(Dispatchers.IO, statement = statement) - private object AnnouncementTable : IntIdTable() { + 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 createdAt = datetime("createdAt").defaultExpression(CurrentDateTime) val archivedAt = datetime("archivedAt").nullable() val level = integer("level") } - private object AttachmentTable : IntIdTable() { + private object Attachments : IntIdTable() { val url = varchar("url", 256) - val announcement = reference("announcement", AnnouncementTable, onDelete = ReferenceOption.CASCADE) + val announcement = reference("announcement", Announcements, onDelete = ReferenceOption.CASCADE) } - class AnnouncementEntity(id: EntityID) : IntEntity(id) { - companion object : IntEntityClass(AnnouncementTable) + class Announcement(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(Announcements) - 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, - ) + var author by Announcements.author + var title by Announcements.title + var content by Announcements.content + val attachments by Attachment referrersOn Attachments.announcement + var channel by Announcements.channel + var createdAt by Announcements.createdAt + var archivedAt by Announcements.archivedAt + var level by Announcements.level } - class AttachmentEntity(id: EntityID) : IntEntity(id) { - companion object : IntEntityClass(AttachmentTable) + class Attachment(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(Attachments) - var url by AttachmentTable.url - var announcement by AnnouncementEntity referencedOn AttachmentTable.announcement + var url by Attachments.url + var announcement by Announcement referencedOn Attachments.announcement } } diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/BackendRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/BackendRepository.kt index 1ed0469..69429ca 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/BackendRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/BackendRepository.kt @@ -30,7 +30,8 @@ abstract class BackendRepository internal constructor( * @property members The members of the organization. */ class BackendOrganization( - val members: Set, + // Using a list instead of a set because set semantics are unnecessary here. + val members: List, ) { /** * A member of an organization. @@ -55,7 +56,8 @@ abstract class BackendRepository internal constructor( * @property url The URL to the GPG master key. */ class GpgKeys( - val ids: Set, + // Using a list instead of a set because set semantics are unnecessary here. + val ids: List, val url: String, ) } @@ -66,7 +68,8 @@ abstract class BackendRepository internal constructor( * @property contributors The contributors of the repository. */ class BackendRepository( - val contributors: Set, + // Using a list instead of a set because set semantics are unnecessary here. + val contributors: List, ) { /** * A contributor of a repository. @@ -95,10 +98,11 @@ abstract class BackendRepository internal constructor( val tag: String, val releaseNote: String, val createdAt: LocalDateTime, - val assets: Set, + // Using a list instead of a set because set semantics are unnecessary here. + val assets: List, ) { companion object { - fun Set.first(assetRegex: Regex) = first { assetRegex.containsMatchIn(it.name) } + fun List.first(assetRegex: Regex) = first { assetRegex.containsMatchIn(it.name) } } /** @@ -149,7 +153,7 @@ abstract class BackendRepository internal constructor( * @param repository The name of the repository. * @return The contributors. */ - abstract suspend fun contributors(owner: String, repository: String): Set + abstract suspend fun contributors(owner: String, repository: String): List /** * Get the members of an organization. @@ -157,7 +161,7 @@ abstract class BackendRepository internal constructor( * @param organization The name of the organization. * @return The members. */ - abstract suspend fun members(organization: String): Set + abstract suspend fun members(organization: String): List /** * Get the rate limit of the backend. diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/GitHubBackendRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/GitHubBackendRepository.kt index 2664afe..a032f4d 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/GitHubBackendRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/GitHubBackendRepository.kt @@ -39,15 +39,15 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) { name = it.name, downloadUrl = it.browserDownloadUrl, ) - }.toSet(), + }, ) } override suspend fun contributors( owner: String, repository: String, - ): Set { - val contributors: Set = client.get( + ): List { + val contributors: List = client.get( Contributors( owner, repository, @@ -61,12 +61,12 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) { url = it.htmlUrl, contributions = it.contributions, ) - }.toSet() + } } - override suspend fun members(organization: String): Set { + override suspend fun members(organization: String): List { // Get the list of members of the organization. - val members: Set = client.get(Organization.Members(organization)).body() + val members: List = client.get(Organization.Members(organization)).body() return coroutineScope { members.map { member -> @@ -78,7 +78,7 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) { }, async { // Get the GPG key of the user. - client.get(User.GpgKeys(member.login)).body>() + client.get(User.GpgKeys(member.login)).body>() }, ) } @@ -87,7 +87,7 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) { val user = responses[0] as GitHubUser @Suppress("UNCHECKED_CAST") - val gpgKeys = responses[1] as Set + val gpgKeys = responses[1] as List BackendMember( name = user.login, @@ -96,11 +96,11 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) { bio = user.bio, gpgKeys = BackendMember.GpgKeys( - ids = gpgKeys.map { it.keyId }.toSet(), + ids = gpgKeys.map { it.keyId }, url = "https://api.github.com/users/${user.login}.gpg", ), ) - }.toSet() + } } override suspend fun rateLimit(): BackendRateLimit { @@ -153,7 +153,8 @@ class GitHubOrganization { @Serializable class GitHubRelease( val tagName: String, - val assets: Set, + // Using a list instead of a set because set semantics are unnecessary here. + val assets: List, val createdAt: Instant, val body: String, ) { diff --git a/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt b/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt index 8accc5a..888f811 100644 --- a/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt +++ b/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt @@ -8,7 +8,8 @@ class APIRelease( val version: String, val createdAt: LocalDateTime, val description: String, - val assets: Set, + // Using a list instead of a set because set semantics are unnecessary here. + val assets: List, ) interface APIUser { @@ -42,7 +43,8 @@ class APIContributor( @Serializable class APIContributable( val name: String, - val contributors: Set, + // Using a list instead of a set because set semantics are unnecessary here. + val contributors: List, ) @Serializable @@ -69,7 +71,8 @@ class APIAnnouncement( val author: String? = null, val title: String, val content: String? = null, - val attachmentUrls: Set = emptySet(), + // Using a list instead of a set because set semantics are unnecessary here. + val attachmentUrls: List = emptyList(), val channel: String? = null, val archivedAt: LocalDateTime? = null, val level: Int = 0, @@ -81,7 +84,8 @@ class APIResponseAnnouncement( val author: String? = null, val title: String, val content: String? = null, - val attachmentUrls: Set = emptySet(), + // Using a list instead of a set because set semantics are unnecessary here. + val attachmentUrls: List = emptyList(), val channel: String? = null, val createdAt: LocalDateTime, val archivedAt: LocalDateTime? = null, diff --git a/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt b/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt index 7befd57..b0142e5 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt @@ -2,6 +2,7 @@ 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.APIResponseAnnouncement import app.revanced.api.configuration.schema.APIResponseAnnouncementId import kotlinx.datetime.LocalDateTime @@ -11,11 +12,11 @@ internal class AnnouncementService( suspend fun latestId(channel: String): APIResponseAnnouncementId? = announcementRepository.latestId(channel) suspend fun latestId(): APIResponseAnnouncementId? = announcementRepository.latestId() - suspend fun latest(channel: String) = announcementRepository.latest(channel) - suspend fun latest() = announcementRepository.latest() + suspend fun latest(channel: String) = announcementRepository.latest(channel)?.toApi() + suspend fun latest() = announcementRepository.latest()?.toApi() - suspend fun all(channel: String) = announcementRepository.all(channel) - suspend fun all() = announcementRepository.all() + suspend fun all(channel: String) = announcementRepository.all(channel).map { it.toApi() } + suspend fun all() = announcementRepository.all().map { it.toApi() } suspend fun new(new: APIAnnouncement) { announcementRepository.new(new) @@ -32,4 +33,16 @@ internal class AnnouncementService( suspend fun delete(id: Int) { announcementRepository.delete(id) } + + private fun AnnouncementRepository.Announcement.toApi() = APIResponseAnnouncement( + id.value, + author, + title, + content, + attachments.map { it.url }, + channel, + createdAt, + archivedAt, + level, + ) } diff --git a/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt b/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt index 10b54cf..7834362 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt @@ -19,11 +19,11 @@ internal class ApiService( it, backendRepository.contributors(configurationRepository.organization, it).map { APIContributor(it.name, it.avatarUrl, it.url, it.contributions) - }.toSet(), + }, ) } } - }.awaitAll().toSet() + }.awaitAll() suspend fun team() = backendRepository.members(configurationRepository.organization).map { member -> APIMember( @@ -41,7 +41,7 @@ internal class ApiService( }, ) - }.toSet() + } suspend fun rateLimit() = backendRepository.rateLimit()?.let { APIRateLimit(it.limit, it.remaining, it.reset) diff --git a/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt b/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt index de23442..31a0670 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt @@ -51,7 +51,7 @@ internal class PatchesService( patchesRelease.tag, patchesRelease.createdAt, patchesRelease.releaseNote, - setOf(patchesAsset, integrationsAsset), + listOf(patchesAsset, integrationsAsset), ) }