Compare commits

...

5 Commits

Author SHA1 Message Date
semantic-release-bot
1a09b028b7 chore: Release v1.4.0-dev.5 [skip ci]
# [1.4.0-dev.5](https://github.com/ReVanced/revanced-api/compare/v1.4.0-dev.4...v1.4.0-dev.5) (2024-11-05)
2024-11-05 19:01:39 +00:00
oSumAtrIX
0ddbf5beda build(Needs bump): Bump dependencies 2024-11-05 19:58:20 +01:00
semantic-release-bot
bf41fa1596 chore: Release v1.4.0-dev.4 [skip ci]
# [1.4.0-dev.4](https://github.com/ReVanced/revanced-api/compare/v1.4.0-dev.3...v1.4.0-dev.4) (2024-11-01)

### Features

* Remove "archived" query parameter ([8ad614e](8ad614ef4f))
* Use tag name directly instead of ID ([fc40427](fc40427fba))
2024-11-01 23:59:33 +00:00
oSumAtrIX
8ad614ef4f feat: Remove "archived" query parameter
It doesn't seem to be necessary for the purpose of viewing announcements.
2024-11-02 00:57:42 +01:00
oSumAtrIX
fc40427fba feat: Use tag name directly instead of ID 2024-11-02 00:57:42 +01:00
8 changed files with 66 additions and 81 deletions

View File

@@ -1,3 +1,13 @@
# [1.4.0-dev.5](https://github.com/ReVanced/revanced-api/compare/v1.4.0-dev.4...v1.4.0-dev.5) (2024-11-05)
# [1.4.0-dev.4](https://github.com/ReVanced/revanced-api/compare/v1.4.0-dev.3...v1.4.0-dev.4) (2024-11-01)
### Features
* Remove "archived" query parameter ([8ad614e](https://github.com/ReVanced/revanced-api/commit/8ad614ef4fdaf45af87a3316ef4db7e7236fd64a))
* Use tag name directly instead of ID ([fc40427](https://github.com/ReVanced/revanced-api/commit/fc40427fbaafb523045eb6f5285d90949b206b8b))
# [1.4.0-dev.3](https://github.com/ReVanced/revanced-api/compare/v1.4.0-dev.2...v1.4.0-dev.3) (2024-11-01) # [1.4.0-dev.3](https://github.com/ReVanced/revanced-api/compare/v1.4.0-dev.2...v1.4.0-dev.3) (2024-11-01)

View File

@@ -1,4 +1,4 @@
org.gradle.parallel = true org.gradle.parallel = true
org.gradle.caching = true org.gradle.caching = true
kotlin.code.style = official kotlin.code.style = official
version = 1.4.0-dev.3 version = 1.4.0-dev.5

View File

@@ -1,6 +1,6 @@
[versions] [versions]
kompendium-core = "3.14.4" kompendium-core = "3.14.4"
kotlin = "2.0.0" kotlin = "2.0.20"
logback = "1.5.6" logback = "1.5.6"
exposed = "0.52.0" exposed = "0.52.0"
h2 = "2.2.224" h2 = "2.2.224"
@@ -10,8 +10,8 @@ ktor = "2.3.7"
ktoml = "0.5.2" ktoml = "0.5.2"
picocli = "4.7.6" picocli = "4.7.6"
datetime = "0.6.0" datetime = "0.6.0"
revanced-patcher = "20.0.0" revanced-patcher = "21.0.0"
revanced-library = "3.0.1-dev.1" revanced-library = "3.0.2"
caffeine = "3.1.8" caffeine = "3.1.8"
bouncy-castle = "1.78.1" bouncy-castle = "1.78.1"

View File

@@ -6,7 +6,6 @@ import app.revanced.api.configuration.schema.ApiResponseAnnouncement
import app.revanced.api.configuration.schema.ApiResponseAnnouncementId import app.revanced.api.configuration.schema.ApiResponseAnnouncementId
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.datetime.*
import org.jetbrains.exposed.dao.IntEntity import org.jetbrains.exposed.dao.IntEntity
import org.jetbrains.exposed.dao.IntEntityClass import org.jetbrains.exposed.dao.IntEntityClass
import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.EntityID
@@ -15,12 +14,11 @@ import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
import org.jetbrains.exposed.sql.kotlin.datetime.datetime import org.jetbrains.exposed.sql.kotlin.datetime.datetime
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import java.time.LocalDateTime
internal class AnnouncementRepository(private val database: Database) { internal class AnnouncementRepository(private val database: Database) {
// This is better than doing a maxByOrNull { it.id } on every request. // This is better than doing a maxByOrNull { it.id } on every request.
private var latestAnnouncement: Announcement? = null private var latestAnnouncement: Announcement? = null
private val latestAnnouncementByTag = mutableMapOf<Int, Announcement>() private val latestAnnouncementByTag = mutableMapOf<String, Announcement>()
init { init {
runBlocking { runBlocking {
@@ -40,22 +38,23 @@ internal class AnnouncementRepository(private val database: Database) {
private fun initializeLatestAnnouncements() { private fun initializeLatestAnnouncements() {
latestAnnouncement = Announcement.all().orderBy(Announcements.id to SortOrder.DESC).firstOrNull() latestAnnouncement = Announcement.all().orderBy(Announcements.id to SortOrder.DESC).firstOrNull()
Tag.all().map { it.id.value }.forEach(::updateLatestAnnouncementForTag) Tag.all().map { it.name }.forEach(::updateLatestAnnouncementForTag)
} }
private fun updateLatestAnnouncement(new: Announcement) { private fun updateLatestAnnouncement(new: Announcement) {
if (latestAnnouncement == null || latestAnnouncement!!.id.value <= new.id.value) { if (latestAnnouncement == null || latestAnnouncement!!.id.value <= new.id.value) {
latestAnnouncement = new latestAnnouncement = new
new.tags.forEach { tag -> latestAnnouncementByTag[tag.id.value] = new } new.tags.forEach { tag -> latestAnnouncementByTag[tag.name] = new }
} }
} }
private fun updateLatestAnnouncementForTag(tag: Int) { private fun updateLatestAnnouncementForTag(tag: String) {
val latestAnnouncementForTag = AnnouncementTags.select(AnnouncementTags.announcement) val latestAnnouncementForTag = Tags.innerJoin(AnnouncementTags)
.where { AnnouncementTags.tag eq tag } .select(AnnouncementTags.announcement)
.map { it[AnnouncementTags.announcement] } .where { Tags.name eq tag }
.mapNotNull { Announcement.findById(it) } .orderBy(AnnouncementTags.announcement to SortOrder.DESC)
.maxByOrNull { it.id } .limit(1)
.firstNotNullOfOrNull { Announcement.findById(it[AnnouncementTags.announcement]) }
latestAnnouncementForTag?.let { latestAnnouncementByTag[tag] = it } latestAnnouncementForTag?.let { latestAnnouncementByTag[tag] = it }
} }
@@ -64,42 +63,30 @@ internal class AnnouncementRepository(private val database: Database) {
latestAnnouncement.toApiResponseAnnouncement() latestAnnouncement.toApiResponseAnnouncement()
} }
suspend fun latest(tags: Set<Int>) = transaction { suspend fun latest(tags: Set<String>) = transaction {
tags.mapNotNull { tag -> latestAnnouncementByTag[tag] }.toApiAnnouncement() tags.mapNotNull { tag -> latestAnnouncementByTag[tag] }.toApiAnnouncement()
} }
fun latestId() = latestAnnouncement?.id?.value.toApiResponseAnnouncementId() fun latestId() = latestAnnouncement?.id?.value.toApiResponseAnnouncementId()
fun latestId(tags: Set<Int>) = fun latestId(tags: Set<String>) =
tags.map { tag -> latestAnnouncementByTag[tag]?.id?.value }.toApiResponseAnnouncementId() tags.map { tag -> latestAnnouncementByTag[tag]?.id?.value }.toApiResponseAnnouncementId()
suspend fun paged(cursor: Int, count: Int, tags: Set<Int>?, archived: Boolean) = transaction { suspend fun paged(cursor: Int, count: Int, tags: Set<String>?) = transaction {
Announcement.find { Announcement.find {
fun idLessEq() = Announcements.id lessEq cursor fun idLessEq() = Announcements.id lessEq cursor
fun archivedAtIsNull() = Announcements.archivedAt.isNull()
fun archivedAtGreaterNow() = Announcements.archivedAt greater LocalDateTime.now().toKotlinLocalDateTime()
if (tags == null) { if (tags == null) {
if (archived) {
idLessEq() idLessEq()
} else { } else {
idLessEq() and (archivedAtIsNull() or archivedAtGreaterNow()) fun hasTags() = Announcements.id inSubQuery (
} AnnouncementTags.innerJoin(Tags)
} else {
fun archivedAtGreaterOrNullOrTrue() = if (archived) {
Op.TRUE
} else {
archivedAtIsNull() or archivedAtGreaterNow()
}
fun hasTags() = tags.mapNotNull { Tag.findById(it)?.id }.let { tags ->
Announcements.id inSubQuery Announcements.leftJoin(AnnouncementTags)
.select(AnnouncementTags.announcement) .select(AnnouncementTags.announcement)
.where { AnnouncementTags.tag inList tags }
.withDistinct() .withDistinct()
} .where { Tags.name inList tags }
)
idLessEq() and archivedAtGreaterOrNullOrTrue() and hasTags() idLessEq() and hasTags()
} }
}.orderBy(Announcements.id to SortOrder.DESC).limit(count).toApiAnnouncement() }.orderBy(Announcements.id to SortOrder.DESC).limit(count).toApiAnnouncement()
} }
@@ -165,7 +152,7 @@ internal class AnnouncementRepository(private val database: Database) {
// Delete the tag if no other announcements are referencing it. // Delete the tag if no other announcements are referencing it.
// One count means that the announcement is the only one referencing the tag. // One count means that the announcement is the only one referencing the tag.
announcement.tags.filter { tag -> tag.announcements.count() == 1L }.forEach { tag -> announcement.tags.filter { tag -> tag.announcements.count() == 1L }.forEach { tag ->
latestAnnouncementByTag -= tag.id.value latestAnnouncementByTag -= tag.name
tag.delete() tag.delete()
} }
@@ -250,7 +237,7 @@ internal class AnnouncementRepository(private val database: Database) {
title, title,
content, content,
attachments.map { it.url }, attachments.map { it.url },
tags.map { it.id.value }, tags.map { it.name },
createdAt, createdAt,
archivedAt, archivedAt,
level, level,
@@ -259,7 +246,7 @@ internal class AnnouncementRepository(private val database: Database) {
private fun Iterable<Announcement>.toApiAnnouncement() = map { it.toApiResponseAnnouncement()!! } private fun Iterable<Announcement>.toApiAnnouncement() = map { it.toApiResponseAnnouncement()!! }
private fun Iterable<Tag>.toApiTag() = map { ApiAnnouncementTag(it.id.value, it.name) } private fun Iterable<Tag>.toApiTag() = map { ApiAnnouncementTag(it.name) }
private fun Int?.toApiResponseAnnouncementId() = this?.let { ApiResponseAnnouncementId(this) } private fun Int?.toApiResponseAnnouncementId() = this?.let { ApiResponseAnnouncementId(this) }

View File

@@ -8,7 +8,10 @@ import app.revanced.api.configuration.schema.ApiAnnouncement
import app.revanced.api.configuration.schema.ApiResponseAnnouncement import app.revanced.api.configuration.schema.ApiResponseAnnouncement
import app.revanced.api.configuration.schema.ApiResponseAnnouncementId import app.revanced.api.configuration.schema.ApiResponseAnnouncementId
import app.revanced.api.configuration.services.AnnouncementService import app.revanced.api.configuration.services.AnnouncementService
import io.bkbn.kompendium.core.metadata.* import io.bkbn.kompendium.core.metadata.DeleteInfo
import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.metadata.PatchInfo
import io.bkbn.kompendium.core.metadata.PostInfo
import io.bkbn.kompendium.json.schema.definition.TypeDefinition import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.oas.payload.Parameter import io.bkbn.kompendium.oas.payload.Parameter
import io.ktor.http.* import io.ktor.http.*
@@ -33,9 +36,8 @@ internal fun Route.announcementsRoute() = route("announcements") {
val cursor = call.parameters["cursor"]?.toInt() ?: Int.MAX_VALUE val cursor = call.parameters["cursor"]?.toInt() ?: Int.MAX_VALUE
val count = call.parameters["count"]?.toInt() ?: 16 val count = call.parameters["count"]?.toInt() ?: 16
val tags = call.parameters.getAll("tag") val tags = call.parameters.getAll("tag")
val archived = call.parameters["archived"]?.toBoolean() ?: true
call.respond(announcementService.paged(cursor, count, tags?.map { it.toInt() }?.toSet(), archived)) call.respond(announcementService.paged(cursor, count, tags?.toSet()))
} }
} }
@@ -55,7 +57,7 @@ internal fun Route.announcementsRoute() = route("announcements") {
val tags = call.parameters.getAll("tag") val tags = call.parameters.getAll("tag")
if (tags?.isNotEmpty() == true) { if (tags?.isNotEmpty() == true) {
call.respond(announcementService.latest(tags.map { it.toInt() }.toSet())) call.respond(announcementService.latest(tags.toSet()))
} else { } else {
call.respondOrNotFound(announcementService.latest()) call.respondOrNotFound(announcementService.latest())
} }
@@ -68,7 +70,7 @@ internal fun Route.announcementsRoute() = route("announcements") {
val tags = call.parameters.getAll("tag") val tags = call.parameters.getAll("tag")
if (tags?.isNotEmpty() == true) { if (tags?.isNotEmpty() == true) {
call.respond(announcementService.latestId(tags.map { it.toInt() }.toSet())) call.respond(announcementService.latestId(tags.toSet()))
} else { } else {
call.respondOrNotFound(announcementService.latestId()) call.respondOrNotFound(announcementService.latestId())
} }
@@ -146,15 +148,8 @@ private fun Route.installAnnouncementsRouteDocumentation() = installNotarizedRou
Parameter( Parameter(
name = "tag", name = "tag",
`in` = Parameter.Location.query, `in` = Parameter.Location.query,
schema = TypeDefinition.INT, schema = TypeDefinition.STRING,
description = "The tag IDs to filter the announcements by. Default is all tags", description = "The tags to filter the announcements by. Default is all tags",
required = false,
),
Parameter(
name = "archived",
`in` = Parameter.Location.query,
schema = TypeDefinition.BOOLEAN,
description = "Whether to include archived announcements. Default is true",
required = false, required = false,
), ),
) )
@@ -193,8 +188,8 @@ private fun Route.installAnnouncementsLatestRouteDocumentation() = installNotari
Parameter( Parameter(
name = "tag", name = "tag",
`in` = Parameter.Location.query, `in` = Parameter.Location.query,
schema = TypeDefinition.INT, schema = TypeDefinition.STRING,
description = "The tag IDs to filter the latest announcements by", description = "The tags to filter the latest announcements by",
required = false, required = false,
), ),
) )
@@ -228,8 +223,8 @@ private fun Route.installAnnouncementsLatestIdRouteDocumentation() = installNota
Parameter( Parameter(
name = "tag", name = "tag",
`in` = Parameter.Location.query, `in` = Parameter.Location.query,
schema = TypeDefinition.INT, schema = TypeDefinition.STRING,
description = "The tag IDs to filter the latest announcements by", description = "The tags to filter the latest announcements by",
required = false, required = false,
), ),
) )

View File

@@ -76,7 +76,7 @@ class ApiResponseAnnouncement(
// Using a list instead of a set because set semantics are unnecessary here. // Using a list instead of a set because set semantics are unnecessary here.
val attachments: List<String> = emptyList(), val attachments: List<String> = emptyList(),
// Using a list instead of a set because set semantics are unnecessary here. // Using a list instead of a set because set semantics are unnecessary here.
val tags: List<Int> = emptyList(), val tags: List<String> = emptyList(),
val createdAt: LocalDateTime, val createdAt: LocalDateTime,
val archivedAt: LocalDateTime? = null, val archivedAt: LocalDateTime? = null,
val level: Int = 0, val level: Int = 0,
@@ -94,7 +94,6 @@ class ApiAnnouncementArchivedAt(
@Serializable @Serializable
class ApiAnnouncementTag( class ApiAnnouncementTag(
val id: Int,
val name: String, val name: String,
) )

View File

@@ -6,16 +6,16 @@ import app.revanced.api.configuration.schema.ApiAnnouncement
internal class AnnouncementService( internal class AnnouncementService(
private val announcementRepository: AnnouncementRepository, private val announcementRepository: AnnouncementRepository,
) { ) {
suspend fun latest(tags: Set<Int>) = announcementRepository.latest(tags) suspend fun latest(tags: Set<String>) = announcementRepository.latest(tags)
suspend fun latest() = announcementRepository.latest() suspend fun latest() = announcementRepository.latest()
fun latestId(tags: Set<Int>) = announcementRepository.latestId(tags) fun latestId(tags: Set<String>) = announcementRepository.latestId(tags)
fun latestId() = announcementRepository.latestId() fun latestId() = announcementRepository.latestId()
suspend fun paged(cursor: Int, limit: Int, tags: Set<Int>?, archived: Boolean) = suspend fun paged(cursor: Int, limit: Int, tags: Set<String>?) =
announcementRepository.paged(cursor, limit, tags, archived) announcementRepository.paged(cursor, limit, tags)
suspend fun get(id: Int) = announcementRepository.get(id) suspend fun get(id: Int) = announcementRepository.get(id)

View File

@@ -5,8 +5,10 @@ import app.revanced.api.configuration.schema.ApiAnnouncement
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.datetime.toKotlinLocalDateTime import kotlinx.datetime.toKotlinLocalDateTime
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
import org.junit.jupiter.api.*
import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.time.LocalDateTime import java.time.LocalDateTime
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertNotNull import kotlin.test.assertNotNull
@@ -84,27 +86,22 @@ private object AnnouncementServiceTest {
announcementService.new(ApiAnnouncement(title = "2", tags = listOf("tag1", "tag3"))) announcementService.new(ApiAnnouncement(title = "2", tags = listOf("tag1", "tag3")))
announcementService.new(ApiAnnouncement(title = "3", tags = listOf("tag1", "tag4"))) announcementService.new(ApiAnnouncement(title = "3", tags = listOf("tag1", "tag4")))
val tag2 = announcementService.tags().find { it.name == "tag2" }!!.id assert(announcementService.latest(setOf("tag2")).first().title == "1")
assert(announcementService.latest(setOf(tag2)).first().title == "1") assert(announcementService.latest(setOf("tag3")).last().title == "2")
val tag3 = announcementService.tags().find { it.name == "tag3" }!!.id val announcement2and3 = announcementService.latest(setOf("tag1", "tag3"))
assert(announcementService.latest(setOf(tag3)).last().title == "2")
val tag1and3 =
announcementService.tags().filter { it.name == "tag1" || it.name == "tag3" }.map { it.id }.toSet()
val announcement2and3 = announcementService.latest(tag1and3)
assert(announcement2and3.size == 2) assert(announcement2and3.size == 2)
assert(announcement2and3.any { it.title == "2" }) assert(announcement2and3.any { it.title == "2" })
assert(announcement2and3.any { it.title == "3" }) assert(announcement2and3.any { it.title == "3" })
announcementService.delete(announcementService.latestId()!!.id) announcementService.delete(announcementService.latestId()!!.id)
assert(announcementService.latest(tag1and3).first().title == "2") assert(announcementService.latest(setOf("tag1", "tag3")).first().title == "2")
announcementService.delete(announcementService.latestId()!!.id) announcementService.delete(announcementService.latestId()!!.id)
assert(announcementService.latest(tag1and3).first().title == "1") assert(announcementService.latest(setOf("tag1", "tag3")).first().title == "1")
announcementService.delete(announcementService.latestId()!!.id) announcementService.delete(announcementService.latestId()!!.id)
assert(announcementService.latest(tag1and3).isEmpty()) assert(announcementService.latest(setOf("tag1", "tag3")).isEmpty())
assert(announcementService.tags().isEmpty()) assert(announcementService.tags().isEmpty())
} }
@@ -156,11 +153,11 @@ private object AnnouncementServiceTest {
announcementService.new(ApiAnnouncement(title = "title$it")) announcementService.new(ApiAnnouncement(title = "title$it"))
} }
val announcements = announcementService.paged(Int.MAX_VALUE, 5, null, true) val announcements = announcementService.paged(Int.MAX_VALUE, 5, null)
assertEquals(5, announcements.size, "Returns correct number of announcements") assertEquals(5, announcements.size, "Returns correct number of announcements")
assertEquals("title9", announcements.first().title, "Starts from the latest announcement") assertEquals("title9", announcements.first().title, "Starts from the latest announcement")
val announcements2 = announcementService.paged(5, 5, null, true) val announcements2 = announcementService.paged(5, 5, null)
assertEquals(5, announcements2.size, "Returns correct number of announcements when starting from the cursor") assertEquals(5, announcements2.size, "Returns correct number of announcements when starting from the cursor")
assertEquals("title4", announcements2.first().title, "Starts from the cursor") assertEquals("title4", announcements2.first().title, "Starts from the cursor")
@@ -183,10 +180,7 @@ private object AnnouncementServiceTest {
val tags = announcementService.tags() val tags = announcementService.tags()
assertEquals(5, tags.size, "Returns correct number of newly created tags") assertEquals(5, tags.size, "Returns correct number of newly created tags")
val announcements3 = announcementService.paged(5, 5, setOf(tags[1].id), true) val announcements3 = announcementService.paged(5, 5, setOf(tags[1].name))
assertEquals(4, announcements3.size, "Filters announcements by tag") assertEquals(4, announcements3.size, "Filters announcements by tag")
val announcements4 = announcementService.paged(Int.MAX_VALUE, 10, null, false)
assertEquals(8, announcements4.size, "Filters out archived announcements")
} }
} }