Skip to content

Commit

Permalink
improve
Browse files Browse the repository at this point in the history
  • Loading branch information
oSumAtrIX committed Oct 17, 2024
1 parent 50b81fd commit aac0bfe
Show file tree
Hide file tree
Showing 14 changed files with 622 additions and 386 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ with updates and ReVanced Patches.

Some of the features ReVanced API include:

- 📢 **Announcements**: Post and get announcements grouped by channels
- 📢 **Announcements**: Post and get announcements
- ℹ️ **About**: Get more information such as a description, ways to donate to,
and links of the hoster of ReVanced API
- 🧩 **Patches**: Get the latest updates of ReVanced Patches, directly from ReVanced API
Expand Down
8 changes: 8 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ kotlin {
}
}

tasks {
test {
useJUnitPlatform()
}
}

repositories {
mavenCentral()
google()
Expand Down Expand Up @@ -98,6 +104,8 @@ dependencies {
implementation(libs.caffeine)
implementation(libs.bouncy.castle.provider)
implementation(libs.bouncy.castle.pgp)

testImplementation(kotlin("test"))
}

// The maven-publish plugin is necessary to make signing work.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,140 +1,189 @@
package app.revanced.api.configuration.repository

import app.revanced.api.configuration.schema.APIAnnouncement
import app.revanced.api.configuration.schema.APIResponseAnnouncement
import app.revanced.api.configuration.schema.APIResponseAnnouncementId
import app.revanced.api.configuration.schema.ApiAnnouncement
import app.revanced.api.configuration.schema.ApiAnnouncementTag
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.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
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 {
// This is better than doing a maxByOrNull { it.id }.
// This is better than doing a maxByOrNull { it.id } on every request.
private var latestAnnouncement: Announcement? = null
private val latestAnnouncementByChannel = mutableMapOf<String, Announcement>()

private fun updateLatestAnnouncement(new: Announcement) {
if (latestAnnouncement?.id?.value == new.id.value) {
latestAnnouncement = new
latestAnnouncementByChannel[new.channel ?: return] = new
}
}
private val latestAnnouncementByTag = mutableMapOf<Int, Announcement>()

init {
runBlocking {
transaction {
SchemaUtils.create(Announcements, Attachments)
SchemaUtils.create(
Announcements,
Attachments,
Tags,
AnnouncementTags,
)

// Initialize the latest announcement.
latestAnnouncement = Announcement.all().onEach {
latestAnnouncementByChannel[it.channel ?: return@onEach] = it
}.maxByOrNull { it.id } ?: return@transaction
initializeLatestAnnouncements()
}
}
}

suspend fun all() = transaction {
Announcement.all().map { it.toApi() }
}
private fun initializeLatestAnnouncements() {
latestAnnouncement = Announcement.all().orderBy(Announcements.id to SortOrder.DESC).firstOrNull()

suspend fun all(channel: String) = transaction {
Announcement.find { Announcements.channel eq channel }.map { it.toApi() }
Tag.all().map { it.id.value }.forEach(::updateLatestAnnouncementForTag)
}

suspend fun delete(id: Int) = transaction {
val announcement = Announcement.findById(id) ?: return@transaction
private fun updateLatestAnnouncement(new: Announcement) {
if (latestAnnouncement == null || latestAnnouncement!!.id.value <= new.id.value) {
latestAnnouncement = new
new.tags.forEach { tag -> latestAnnouncementByTag[tag.id.value] = new }
}
}

announcement.delete()
private fun updateLatestAnnouncementForTag(tag: Int) {
val latestAnnouncementForTag = AnnouncementTags.select(AnnouncementTags.announcement)
.where { AnnouncementTags.tag eq tag }
.map { it[AnnouncementTags.announcement] }
.mapNotNull { Announcement.findById(it) }
.maxByOrNull { it.id }

// In case the latest announcement was deleted, query the new latest announcement again.
if (latestAnnouncement?.id?.value == id) {
latestAnnouncement = Announcement.all().maxByOrNull { it.id }
latestAnnouncementForTag?.let { latestAnnouncementByTag[tag] = it }
}

// If no latest announcement was found, remove it from the channel map.
if (latestAnnouncement == null) {
latestAnnouncementByChannel.remove(announcement.channel)
} else {
latestAnnouncementByChannel[latestAnnouncement!!.channel ?: return@transaction] = latestAnnouncement!!
}
}
suspend fun latest() = transaction {
latestAnnouncement.toApiResponseAnnouncement()
}

fun latest() = latestAnnouncement?.toApi()
suspend fun latest(tags: Set<Int>) = transaction {
tags.mapNotNull { tag -> latestAnnouncementByTag[tag] }.toApiAnnouncement()
}

fun latest(channel: String) = latestAnnouncementByChannel[channel]?.toApi()
fun latestId() = latestAnnouncement?.id?.value.toApiResponseAnnouncementId()

fun latestId() = latest()?.id?.toApi()
fun latestId(tags: Set<Int>) =
tags.map { tag -> latestAnnouncementByTag[tag]?.id?.value }.toApiResponseAnnouncementId()

fun latestId(channel: String) = latest(channel)?.id?.toApi()
suspend fun paged(offset: Int, count: Int, tags: Set<Int>?) = transaction {
if (tags == null) {
Announcement.all()
} else {
@Suppress("NAME_SHADOWING")
val tags = tags.mapNotNull { Tag.findById(it)?.id }

suspend fun archive(
id: Int,
archivedAt: LocalDateTime?,
) = transaction {
Announcement.findByIdAndUpdate(id) {
it.archivedAt = archivedAt ?: java.time.LocalDateTime.now().toKotlinLocalDateTime()
}?.also(::updateLatestAnnouncement)
Announcement.find {
Announcements.id inSubQuery Announcements.innerJoin(AnnouncementTags)
.select(Announcements.id)
.where { AnnouncementTags.tag inList tags }
.withDistinct()
}
}.orderBy(Announcements.id to SortOrder.DESC).limit(count, offset.toLong()).map { it }.toApiAnnouncement()
}

suspend fun unarchive(id: Int) = transaction {
Announcement.findByIdAndUpdate(id) {
it.archivedAt = null
}?.also(::updateLatestAnnouncement)
suspend fun get(id: Int) = transaction {
Announcement.findById(id).toApiResponseAnnouncement()
}

suspend fun new(new: APIAnnouncement) = transaction {
suspend fun new(new: ApiAnnouncement) = transaction {
Announcement.new {
author = new.author
title = new.title
content = new.content
channel = new.channel
archivedAt = new.archivedAt
level = new.level
}.also { newAnnouncement ->
new.attachmentUrls.map { newUrl ->
suspendedTransactionAsync {
Attachment.new {
url = newUrl
announcement = newAnnouncement
}
tags = SizedCollection(
new.tags.map { tag -> Tag.find { Tags.name eq tag }.firstOrNull() ?: Tag.new { name = tag } },
)
}.apply {
new.attachments.map { attachmentUrl ->
Attachment.new {
url = attachmentUrl
announcement = this@apply
}
}.awaitAll()
}.also(::updateLatestAnnouncement)
}
}.let(::updateLatestAnnouncement)
}

suspend fun update(id: Int, new: APIAnnouncement) = transaction {
suspend fun update(id: Int, new: ApiAnnouncement) = transaction {
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
}

// Get the old tags, create new tags if they don't exist,
// and delete tags that are not in the new tags, after updating the announcement.
val oldTags = it.tags.toList()
val updatedTags = new.tags.map { name ->
Tag.find { Tags.name eq name }.firstOrNull() ?: Tag.new { this.name = name }
}
it.tags = SizedCollection(updatedTags)
oldTags.forEach { tag ->
if (tag in updatedTags || !tag.announcements.empty()) return@forEach
tag.delete()
}

// Delete old attachments and create new attachments.
it.attachments.forEach { attachment -> attachment.delete() }
new.attachments.map { attachment ->
Attachment.new {
url = attachment
announcement = it
}
}.awaitAll()
}?.also(::updateLatestAnnouncement)
}
}?.let(::updateLatestAnnouncement) ?: Unit
}

suspend fun delete(id: Int) = transaction {
val announcement = Announcement.findById(id) ?: return@transaction

// Delete the tag if no other announcements are referencing it.
// One count means that the announcement is the only one referencing the tag.
announcement.tags.filter { tag -> tag.announcements.count() == 1L }.forEach { tag ->
latestAnnouncementByTag -= tag.id.value
tag.delete()
}

announcement.delete()

// If the deleted announcement is the latest announcement, set the new latest announcement.
if (latestAnnouncement?.id?.value == id) {
latestAnnouncement = Announcement.all().orderBy(Announcements.id to SortOrder.DESC).firstOrNull()
}

// The new announcement may be the latest for a specific tag. Set the new latest announcement for that tag.
latestAnnouncementByTag.keys.forEach { tag ->
updateLatestAnnouncementForTag(tag)
}
}

suspend fun archive(
id: Int,
archivedAt: LocalDateTime?,
) = transaction {
Announcement.findByIdAndUpdate(id) {
it.archivedAt = archivedAt ?: java.time.LocalDateTime.now().toKotlinLocalDateTime()
}?.let(::updateLatestAnnouncement)
}

suspend fun unarchive(id: Int) = transaction {
Announcement.findByIdAndUpdate(id) {
it.archivedAt = null
}?.let(::updateLatestAnnouncement) ?: Unit
}

suspend fun tags() = transaction {
Tag.all().toList().toApiTag()
}

private suspend fun <T> transaction(statement: suspend Transaction.() -> T) =
Expand All @@ -144,7 +193,6 @@ internal class AnnouncementRepository {
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").defaultExpression(CurrentDateTime)
val archivedAt = datetime("archivedAt").nullable()
val level = integer("level")
Expand All @@ -155,14 +203,27 @@ internal class AnnouncementRepository {
val announcement = reference("announcement", Announcements, onDelete = ReferenceOption.CASCADE)
}

private object Tags : IntIdTable() {
val name = varchar("name", 16).uniqueIndex()
}

private object AnnouncementTags : Table() {
val tag = reference("tag", Tags, onDelete = ReferenceOption.CASCADE)
val announcement = reference("announcement", Announcements, onDelete = ReferenceOption.CASCADE)

init {
uniqueIndex(tag, announcement)
}
}

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 Attachments.announcement
var channel by Announcements.channel
var tags by Tag via AnnouncementTags
var createdAt by Announcements.createdAt
var archivedAt by Announcements.archivedAt
var level by Announcements.level
Expand All @@ -175,17 +236,32 @@ internal class AnnouncementRepository {
var announcement by Announcement referencedOn Attachments.announcement
}

private fun Announcement.toApi() = APIResponseAnnouncement(
id.value,
author,
title,
content,
attachments.map { it.url },
channel,
createdAt,
archivedAt,
level,
)
class Tag(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<Tag>(Tags)

var name by Tags.name
var announcements by Announcement via AnnouncementTags
}

private fun Announcement?.toApiResponseAnnouncement() = this?.let {
ApiResponseAnnouncement(
id.value,
author,
title,
content,
attachments.map { it.url },
tags.map { it.id.value },
createdAt,
archivedAt,
level,
)
}

private fun List<Announcement>.toApiAnnouncement() = map { it.toApiResponseAnnouncement()!! }

private fun List<Tag>.toApiTag() = map { ApiAnnouncementTag(it.id.value, it.name) }

private fun Int?.toApiResponseAnnouncementId() = this?.let { ApiResponseAnnouncementId(this) }

private fun Int.toApi() = APIResponseAnnouncementId(this)
private fun List<Int?>.toApiResponseAnnouncementId() = map { it.toApiResponseAnnouncementId() }
}
Loading

0 comments on commit aac0bfe

Please sign in to comment.