From 56a00ddb85f302d441f0b222a9902ea2c1c18897 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Fri, 1 Nov 2024 02:49:36 +0100 Subject: [PATCH 01/19] feat: Improve announcements API (#192) Announcements can have tags now instead of being grouped into a single channel. You can get an announcement using its ID. You can page announcements and filter them by tags and whether they are archived. You can see a list of all available tags. Some route API overhaul. --- README.md | 2 +- build.gradle.kts | 8 + .../repository/AnnouncementRepository.kt | 300 ++++++++----- .../api/configuration/routes/Announcements.kt | 399 +++++++----------- .../api/configuration/routes/ApiRoute.kt | 6 +- .../api/configuration/routes/ManagerRoute.kt | 10 +- .../api/configuration/routes/PatchesRoute.kt | 14 +- .../api/configuration/schema/APISchema.kt | 58 +-- .../services/AnnouncementService.kt | 42 +- .../api/configuration/services/ApiService.kt | 8 +- .../services/AuthenticationService.kt | 4 +- .../configuration/services/ManagerService.kt | 10 +- .../configuration/services/PatchesService.kt | 20 +- .../services/AnnouncementServiceTest.kt | 194 +++++++++ 14 files changed, 632 insertions(+), 443 deletions(-) create mode 100644 src/test/kotlin/app/revanced/api/configuration/services/AnnouncementServiceTest.kt diff --git a/README.md b/README.md index 1ed9dac..fc2e854 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/build.gradle.kts b/build.gradle.kts index 0ee566e..0df4c07 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -48,6 +48,12 @@ kotlin { } } +tasks { + test { + useJUnitPlatform() + } +} + repositories { mavenCentral() google() @@ -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. 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 992b06f..3f6ff6f 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt @@ -1,10 +1,10 @@ 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 @@ -15,126 +15,175 @@ 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 +import java.time.LocalDateTime 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() - - private fun updateLatestAnnouncement(new: Announcement) { - if (latestAnnouncement?.id?.value == new.id.value) { - latestAnnouncement = new - latestAnnouncementByChannel[new.channel ?: return] = new - } - } + private val latestAnnouncementByTag = mutableMapOf() 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() + + Tag.all().map { it.id.value }.forEach(::updateLatestAnnouncementForTag) } - suspend fun all(channel: String) = transaction { - Announcement.find { Announcements.channel eq channel }.map { it.toApi() } + 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 } + } + } + + 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 } + + latestAnnouncementForTag?.let { latestAnnouncementByTag[tag] = it } + } + + suspend fun latest() = transaction { + latestAnnouncement.toApiResponseAnnouncement() + } + + suspend fun latest(tags: Set) = transaction { + tags.mapNotNull { tag -> latestAnnouncementByTag[tag] }.toApiAnnouncement() + } + + fun latestId() = latestAnnouncement?.id?.value.toApiResponseAnnouncementId() + + fun latestId(tags: Set) = + tags.map { tag -> latestAnnouncementByTag[tag]?.id?.value }.toApiResponseAnnouncementId() + + suspend fun paged(cursor: Int, count: Int, tags: Set?, archived: Boolean) = transaction { + Announcement.find { + fun idLessEq() = Announcements.id lessEq cursor + fun archivedAtIsNull() = Announcements.archivedAt.isNull() + fun archivedAtGreaterNow() = Announcements.archivedAt greater LocalDateTime.now().toKotlinLocalDateTime() + + if (tags == null) { + if (archived) { + idLessEq() + } else { + idLessEq() and (archivedAtIsNull() or archivedAtGreaterNow()) + } + } 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) + .where { AnnouncementTags.tag inList tags } + .withDistinct() + } + + idLessEq() and archivedAtGreaterOrNullOrTrue() and hasTags() + } + }.orderBy(Announcements.id to SortOrder.DESC).limit(count).toApiAnnouncement() + } + + suspend fun get(id: Int) = transaction { + Announcement.findById(id).toApiResponseAnnouncement() + } + + suspend fun new(new: ApiAnnouncement) = transaction { + Announcement.new { + author = new.author + title = new.title + content = new.content + archivedAt = new.archivedAt + level = new.level + 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 + } + } + }.let(::updateLatestAnnouncement) + } + + suspend fun update(id: Int, new: ApiAnnouncement) = transaction { + Announcement.findByIdAndUpdate(id) { + it.author = new.author + it.title = new.title + it.content = new.content + it.archivedAt = new.archivedAt + it.level = new.level + + // 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 + } + } + }?.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() - // In case the latest announcement was deleted, query the new latest announcement again. + // If the deleted announcement is the latest announcement, set the new latest announcement. if (latestAnnouncement?.id?.value == id) { - latestAnnouncement = Announcement.all().maxByOrNull { it.id } + latestAnnouncement = Announcement.all().orderBy(Announcements.id to SortOrder.DESC).firstOrNull() + } - // 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!! - } + // 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) } } - fun latest() = latestAnnouncement?.toApi() - - fun latest(channel: String) = latestAnnouncementByChannel[channel]?.toApi() - - fun latestId() = latest()?.id?.toApi() - - fun latestId(channel: String) = latest(channel)?.id?.toApi() - - suspend fun archive( - id: Int, - archivedAt: LocalDateTime?, - ) = transaction { - Announcement.findByIdAndUpdate(id) { - it.archivedAt = archivedAt ?: java.time.LocalDateTime.now().toKotlinLocalDateTime() - }?.also(::updateLatestAnnouncement) - } - - suspend fun unarchive(id: Int) = transaction { - Announcement.findByIdAndUpdate(id) { - it.archivedAt = null - }?.also(::updateLatestAnnouncement) - } - - 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 - } - } - }.awaitAll() - }.also(::updateLatestAnnouncement) - } - - 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 - } - } - }.awaitAll() - }?.also(::updateLatestAnnouncement) + suspend fun tags() = transaction { + Tag.all().toList().toApiTag() } private suspend fun transaction(statement: suspend Transaction.() -> T) = @@ -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") @@ -155,6 +203,19 @@ 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) : IntEntity(id) { companion object : IntEntityClass(Announcements) @@ -162,7 +223,7 @@ internal class AnnouncementRepository { 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 @@ -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) : IntEntity(id) { + companion object : IntEntityClass(Tags) - private fun Int.toApi() = APIResponseAnnouncementId(this) + 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 Iterable.toApiAnnouncement() = map { it.toApiResponseAnnouncement()!! } + + private fun Iterable.toApiTag() = map { ApiAnnouncementTag(it.id.value, it.name) } + + private fun Int?.toApiResponseAnnouncementId() = this?.let { ApiResponseAnnouncementId(this) } + + private fun Iterable.toApiResponseAnnouncementId() = map { it.toApiResponseAnnouncementId() } } diff --git a/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt b/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt index 2f2ffd8..aeb1c0f 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt @@ -4,10 +4,9 @@ import app.revanced.api.configuration.canRespondUnauthorized import app.revanced.api.configuration.installCache import app.revanced.api.configuration.installNotarizedRoute import app.revanced.api.configuration.respondOrNotFound -import app.revanced.api.configuration.schema.APIAnnouncement -import app.revanced.api.configuration.schema.APIAnnouncementArchivedAt -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.ApiResponseAnnouncement +import app.revanced.api.configuration.schema.ApiResponseAnnouncementId import app.revanced.api.configuration.services.AnnouncementService import io.bkbn.kompendium.core.metadata.* import io.bkbn.kompendium.json.schema.definition.TypeDefinition @@ -16,7 +15,6 @@ import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.auth.* import io.ktor.server.plugins.ratelimit.* -import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* import io.ktor.server.util.* @@ -32,76 +30,63 @@ internal fun Route.announcementsRoute() = route("announcements") { rateLimit(RateLimitName("strong")) { get { - call.respond(announcementService.all()) + val cursor = call.parameters["cursor"]?.toInt() ?: Int.MAX_VALUE + val count = call.parameters["count"]?.toInt() ?: 16 + 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)) } } - rateLimit(RateLimitName("strong")) { - route("{channel}/latest") { - installLatestChannelAnnouncementRouteDocumentation() - - get { - val channel: String by call.parameters - - call.respondOrNotFound(announcementService.latest(channel)) - } - - route("id") { - installLatestChannelAnnouncementIdRouteDocumentation() - - get { - val channel: String by call.parameters - - call.respondOrNotFound(announcementService.latestId(channel)) - } - } - } - } - - rateLimit(RateLimitName("strong")) { - route("{channel}") { - installChannelAnnouncementsRouteDocumentation() - - get { - val channel: String by call.parameters - - call.respond(announcementService.all(channel)) - } - } - } - - rateLimit(RateLimitName("strong")) { - route("latest") { - installLatestAnnouncementRouteDocumentation() - - get { - call.respondOrNotFound(announcementService.latest()) - } - - route("id") { - installLatestAnnouncementIdRouteDocumentation() - - get { - call.respondOrNotFound(announcementService.latestId()) - } - } - } - } - - rateLimit(RateLimitName("strong")) { + rateLimit(RateLimitName("weak")) { authenticate("jwt") { - installAnnouncementRouteDocumentation() - - post { announcement -> + post { announcement -> announcementService.new(announcement) call.respond(HttpStatusCode.OK) } + } - route("{id}") { - installAnnouncementIdRouteDocumentation() + route("latest") { + installAnnouncementsLatestRouteDocumentation() - patch { announcement -> + get { + val tags = call.parameters.getAll("tag") + + if (tags?.isNotEmpty() == true) { + call.respond(announcementService.latest(tags.map { it.toInt() }.toSet())) + } else { + call.respondOrNotFound(announcementService.latest()) + } + } + + route("id") { + installAnnouncementsLatestIdRouteDocumentation() + + get { + val tags = call.parameters.getAll("tag") + + if (tags?.isNotEmpty() == true) { + call.respond(announcementService.latestId(tags.map { it.toInt() }.toSet())) + } else { + call.respondOrNotFound(announcementService.latestId()) + } + } + } + } + + route("{id}") { + installAnnouncementsIdRouteDocumentation() + + get { + val id: Int by call.parameters + + call.respondOrNotFound(announcementService.get(id)) + } + + authenticate("jwt") { + patch { announcement -> val id: Int by call.parameters announcementService.update(id, announcement) @@ -116,31 +101,14 @@ internal fun Route.announcementsRoute() = route("announcements") { call.respond(HttpStatusCode.OK) } + } + } - route("archive") { - installAnnouncementArchiveRouteDocumentation() + route("tags") { + installAnnouncementsTagsRouteDocumentation() - post { - val id: Int by call.parameters - val archivedAt = call.receiveNullable()?.archivedAt - - announcementService.archive(id, archivedAt) - - call.respond(HttpStatusCode.OK) - } - } - - route("unarchive") { - installAnnouncementUnarchiveRouteDocumentation() - - post { - val id: Int by call.parameters - - announcementService.unarchive(id) - - call.respond(HttpStatusCode.OK) - } - } + get { + call.respond(announcementService.tags()) } } } @@ -154,16 +122,56 @@ private val authHeaderParameter = Parameter( examples = mapOf("Bearer authentication" to Parameter.Example("Bearer abc123")), ) -private fun Route.installAnnouncementRouteDocumentation() = installNotarizedRoute { +private fun Route.installAnnouncementsRouteDocumentation() = installNotarizedRoute { tags = setOf("Announcements") - parameters = listOf(authHeaderParameter) + get = GetInfo.builder { + description("Get a page of announcements") + summary("Get announcements") + parameters( + Parameter( + name = "cursor", + `in` = Parameter.Location.query, + schema = TypeDefinition.INT, + description = "The offset of the announcements. Default is Int.MAX_VALUE (Newest first)", + required = false, + ), + Parameter( + name = "count", + `in` = Parameter.Location.query, + schema = TypeDefinition.INT, + description = "The count of the announcements. Default is 16", + required = false, + ), + Parameter( + name = "tag", + `in` = Parameter.Location.query, + schema = TypeDefinition.INT, + description = "The tag IDs 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, + ), + ) + response { + responseCode(HttpStatusCode.OK) + mediaTypes("application/json") + description("The announcements") + responseType>() + } + } post = PostInfo.builder { description("Create a new announcement") summary("Create announcement") + parameters(authHeaderParameter) request { - requestType() + requestType() description("The new announcement") } response { @@ -175,17 +183,32 @@ private fun Route.installAnnouncementRouteDocumentation() = installNotarizedRout } } -private fun Route.installLatestAnnouncementRouteDocumentation() = installNotarizedRoute { +private fun Route.installAnnouncementsLatestRouteDocumentation() = installNotarizedRoute { tags = setOf("Announcements") get = GetInfo.builder { description("Get the latest announcement") summary("Get latest announcement") + parameters( + Parameter( + name = "tag", + `in` = Parameter.Location.query, + schema = TypeDefinition.INT, + description = "The tag IDs to filter the latest announcements by", + required = false, + ), + ) response { responseCode(HttpStatusCode.OK) mediaTypes("application/json") description("The latest announcement") - responseType() + responseType() + } + canRespond { + responseCode(HttpStatusCode.OK) + mediaTypes("application/json") + description("The latest announcements") + responseType>() } canRespond { responseCode(HttpStatusCode.NotFound) @@ -195,17 +218,32 @@ private fun Route.installLatestAnnouncementRouteDocumentation() = installNotariz } } -private fun Route.installLatestAnnouncementIdRouteDocumentation() = installNotarizedRoute { +private fun Route.installAnnouncementsLatestIdRouteDocumentation() = installNotarizedRoute { tags = setOf("Announcements") get = GetInfo.builder { - description("Get the id of the latest announcement") - summary("Get id of latest announcement") + description("Get the ID of the latest announcement") + summary("Get ID of latest announcement") + parameters( + Parameter( + name = "tag", + `in` = Parameter.Location.query, + schema = TypeDefinition.INT, + description = "The tag IDs to filter the latest announcements by", + required = false, + ), + ) response { responseCode(HttpStatusCode.OK) mediaTypes("application/json") - description("The id of the latest announcement") - responseType() + description("The ID of the latest announcement") + responseType() + } + canRespond { + responseCode(HttpStatusCode.OK) + mediaTypes("application/json") + description("The IDs of the latest announcements") + responseType>() } canRespond { responseCode(HttpStatusCode.NotFound) @@ -215,109 +253,40 @@ private fun Route.installLatestAnnouncementIdRouteDocumentation() = installNotar } } -private fun Route.installChannelAnnouncementsRouteDocumentation() = installNotarizedRoute { +private fun Route.installAnnouncementsIdRouteDocumentation() = installNotarizedRoute { tags = setOf("Announcements") parameters = listOf( Parameter( - name = "channel", + name = "id", `in` = Parameter.Location.path, - schema = TypeDefinition.STRING, - description = "The channel to get the announcements from", + schema = TypeDefinition.INT, + description = "The ID of the announcement to update", required = true, ), + authHeaderParameter, ) get = GetInfo.builder { - description("Get the announcements from a channel") - summary("Get announcements from channel") + description("Get an announcement") + summary("Get announcement") response { + description("The announcement") responseCode(HttpStatusCode.OK) - mediaTypes("application/json") - description("The announcements in the channel") - responseType>() + responseType() } - } -} - -private fun Route.installAnnouncementArchiveRouteDocumentation() = installNotarizedRoute { - tags = setOf("Announcements") - - parameters = listOf( - Parameter( - name = "id", - `in` = Parameter.Location.path, - schema = TypeDefinition.INT, - description = "The id of the announcement to archive", - required = true, - ), - Parameter( - name = "archivedAt", - `in` = Parameter.Location.query, - schema = TypeDefinition.STRING, - description = "The date and time the announcement to be archived", - required = false, - ), - authHeaderParameter, - ) - - post = PostInfo.builder { - description("Archive an announcement") - summary("Archive announcement") - response { - description("The announcement is archived") - responseCode(HttpStatusCode.OK) + canRespond { + responseCode(HttpStatusCode.NotFound) + description("The announcement does not exist") responseType() } - canRespondUnauthorized() } -} - -private fun Route.installAnnouncementUnarchiveRouteDocumentation() = installNotarizedRoute { - tags = setOf("Announcements") - - parameters = listOf( - Parameter( - name = "id", - `in` = Parameter.Location.path, - schema = TypeDefinition.INT, - description = "The id of the announcement to unarchive", - required = true, - ), - authHeaderParameter, - ) - - post = PostInfo.builder { - description("Unarchive an announcement") - summary("Unarchive announcement") - response { - description("The announcement is unarchived") - responseCode(HttpStatusCode.OK) - responseType() - } - canRespondUnauthorized() - } -} - -private fun Route.installAnnouncementIdRouteDocumentation() = installNotarizedRoute { - tags = setOf("Announcements") - - parameters = listOf( - Parameter( - name = "id", - `in` = Parameter.Location.path, - schema = TypeDefinition.INT, - description = "The id of the announcement to update", - required = true, - ), - authHeaderParameter, - ) patch = PatchInfo.builder { description("Update an announcement") summary("Update announcement") request { - requestType() + requestType() description("The new announcement") } response { @@ -340,77 +309,17 @@ private fun Route.installAnnouncementIdRouteDocumentation() = installNotarizedRo } } -private fun Route.installAnnouncementsRouteDocumentation() = installNotarizedRoute { +private fun Route.installAnnouncementsTagsRouteDocumentation() = installNotarizedRoute { tags = setOf("Announcements") get = GetInfo.builder { - description("Get the announcements") - summary("Get announcements") + description("Get all announcement tags") + summary("Get announcement tags") response { responseCode(HttpStatusCode.OK) mediaTypes("application/json") - description("The announcements") - responseType>() - } - } -} - -private fun Route.installLatestChannelAnnouncementRouteDocumentation() = installNotarizedRoute { - tags = setOf("Announcements") - - parameters = listOf( - Parameter( - name = "channel", - `in` = Parameter.Location.path, - schema = TypeDefinition.STRING, - description = "The channel to get the latest announcement from", - required = true, - ), - ) - - get = GetInfo.builder { - description("Get the latest announcement from a channel") - summary("Get latest channel announcement") - response { - responseCode(HttpStatusCode.OK) - mediaTypes("application/json") - description("The latest announcement in the channel") - responseType() - } - canRespond { - responseCode(HttpStatusCode.NotFound) - description("The channel does not exist") - responseType() - } - } -} - -private fun Route.installLatestChannelAnnouncementIdRouteDocumentation() = installNotarizedRoute { - tags = setOf("Announcements") - - parameters = listOf( - Parameter( - name = "channel", - `in` = Parameter.Location.path, - schema = TypeDefinition.STRING, - description = "The channel to get the latest announcement id from", - required = true, - ), - ) - - get = GetInfo.builder { - description("Get the id of the latest announcement from a channel") - summary("Get id of latest announcement from channel") - response { - responseCode(HttpStatusCode.OK) - mediaTypes("application/json") - description("The id of the latest announcement from the channel") - responseType() - } - canRespond { - responseCode(HttpStatusCode.NotFound) - description("The channel does not exist") - responseType() + description("The announcement tags") + responseType>() } } } diff --git a/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt index 2296fee..862a4d9 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt @@ -115,7 +115,7 @@ private fun Route.installRateLimitRouteDocumentation() = installNotarizedRoute { description("The rate limit of the backend") mediaTypes("application/json") responseCode(HttpStatusCode.OK) - responseType() + responseType() } } } @@ -144,7 +144,7 @@ private fun Route.installTeamRouteDocumentation() = installNotarizedRoute { description("The list of team members") mediaTypes("application/json") responseCode(HttpStatusCode.OK) - responseType>() + responseType>() } } } @@ -195,7 +195,7 @@ private fun Route.installTokenRouteDocumentation() = installNotarizedRoute { description("The authorization token") mediaTypes("application/json") responseCode(HttpStatusCode.OK) - responseType() + responseType() } canRespondUnauthorized() } diff --git a/src/main/kotlin/app/revanced/api/configuration/routes/ManagerRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routes/ManagerRoute.kt index 811f3a9..9485137 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routes/ManagerRoute.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/ManagerRoute.kt @@ -1,9 +1,9 @@ package app.revanced.api.configuration.routes import app.revanced.api.configuration.installNotarizedRoute -import app.revanced.api.configuration.schema.APIManagerAsset -import app.revanced.api.configuration.schema.APIRelease -import app.revanced.api.configuration.schema.APIReleaseVersion +import app.revanced.api.configuration.schema.ApiManagerAsset +import app.revanced.api.configuration.schema.ApiRelease +import app.revanced.api.configuration.schema.ApiReleaseVersion import app.revanced.api.configuration.services.ManagerService import io.bkbn.kompendium.core.metadata.GetInfo import io.ktor.http.* @@ -53,7 +53,7 @@ private fun Route.installManagerRouteDocumentation(deprecated: Boolean) = instal description("The latest manager release") mediaTypes("application/json") responseCode(HttpStatusCode.OK) - responseType>() + responseType>() } } } @@ -69,7 +69,7 @@ private fun Route.installManagerVersionRouteDocumentation(deprecated: Boolean) = description("The current manager release version") mediaTypes("application/json") responseCode(HttpStatusCode.OK) - responseType() + responseType() } } } diff --git a/src/main/kotlin/app/revanced/api/configuration/routes/PatchesRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routes/PatchesRoute.kt index efe7e10..7932f0b 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routes/PatchesRoute.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/PatchesRoute.kt @@ -2,10 +2,10 @@ package app.revanced.api.configuration.routes import app.revanced.api.configuration.installCache import app.revanced.api.configuration.installNotarizedRoute -import app.revanced.api.configuration.schema.APIAssetPublicKeys -import app.revanced.api.configuration.schema.APIPatchesAsset -import app.revanced.api.configuration.schema.APIRelease -import app.revanced.api.configuration.schema.APIReleaseVersion +import app.revanced.api.configuration.schema.ApiAssetPublicKeys +import app.revanced.api.configuration.schema.ApiPatchesAsset +import app.revanced.api.configuration.schema.ApiRelease +import app.revanced.api.configuration.schema.ApiReleaseVersion import app.revanced.api.configuration.services.PatchesService import io.bkbn.kompendium.core.metadata.GetInfo import io.ktor.http.* @@ -78,7 +78,7 @@ private fun Route.installPatchesRouteDocumentation(deprecated: Boolean) = instal description("The current patches release") mediaTypes("application/json") responseCode(HttpStatusCode.OK) - responseType>() + responseType>() } } } @@ -94,7 +94,7 @@ private fun Route.installPatchesVersionRouteDocumentation(deprecated: Boolean) = description("The current patches release version") mediaTypes("application/json") responseCode(HttpStatusCode.OK) - responseType() + responseType() } } } @@ -126,7 +126,7 @@ private fun Route.installPatchesPublicKeyRouteDocumentation(deprecated: Boolean) description("The public keys") mediaTypes("application/json") responseCode(HttpStatusCode.OK) - responseType() + responseType() } } } 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 6e749c1..1ff7979 100644 --- a/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt +++ b/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt @@ -3,44 +3,44 @@ package app.revanced.api.configuration.schema import kotlinx.datetime.LocalDateTime import kotlinx.serialization.Serializable -interface APIUser { +interface ApiUser { val name: String val avatarUrl: String val url: String } @Serializable -class APIMember( +class ApiMember( override val name: String, override val avatarUrl: String, override val url: String, val bio: String?, - val gpgKey: APIGpgKey?, -) : APIUser + val gpgKey: ApiGpgKey?, +) : ApiUser @Serializable -class APIGpgKey( +class ApiGpgKey( val id: String, val url: String, ) @Serializable -class APIContributor( +class ApiContributor( override val name: String, override val avatarUrl: String, override val url: String, val contributions: Int, -) : APIUser +) : ApiUser @Serializable class APIContributable( val name: String, // Using a list instead of a set because set semantics are unnecessary here. - val contributors: List, + val contributors: List, ) @Serializable -class APIRelease( +class ApiRelease( val version: String, val createdAt: LocalDateTime, val description: String, @@ -49,74 +49,82 @@ class APIRelease( ) @Serializable -class APIManagerAsset( +class ApiManagerAsset( val downloadUrl: String, ) @Serializable -class APIPatchesAsset( +class ApiPatchesAsset( val downloadUrl: String, val signatureDownloadUrl: String, // TODO: Remove this eventually when integrations are merged into patches. - val name: APIAssetName, + val name: ApiAssetName, ) @Serializable -enum class APIAssetName { +enum class ApiAssetName { PATCHES, INTEGRATION, } @Serializable -class APIReleaseVersion( +class ApiReleaseVersion( val version: String, ) @Serializable -class APIAnnouncement( +class ApiAnnouncement( val author: String? = null, val title: String, val content: String? = null, // Using a list instead of a set because set semantics are unnecessary here. - val attachmentUrls: List = emptyList(), - val channel: String? = null, + val attachments: List = emptyList(), + // Using a list instead of a set because set semantics are unnecessary here. + val tags: List = emptyList(), val archivedAt: LocalDateTime? = null, val level: Int = 0, ) @Serializable -class APIResponseAnnouncement( +class ApiResponseAnnouncement( val id: Int, val author: String? = null, val title: String, val content: String? = null, // Using a list instead of a set because set semantics are unnecessary here. - val attachmentUrls: List = emptyList(), - val channel: String? = null, + val attachments: List = emptyList(), + // Using a list instead of a set because set semantics are unnecessary here. + val tags: List = emptyList(), val createdAt: LocalDateTime, val archivedAt: LocalDateTime? = null, val level: Int = 0, ) @Serializable -class APIResponseAnnouncementId( +class ApiResponseAnnouncementId( val id: Int, ) @Serializable -class APIAnnouncementArchivedAt( +class ApiAnnouncementArchivedAt( val archivedAt: LocalDateTime, ) @Serializable -class APIRateLimit( +class ApiAnnouncementTag( + val id: Int, + val name: String, +) + +@Serializable +class ApiRateLimit( val limit: Int, val remaining: Int, val reset: LocalDateTime, ) @Serializable -class APIAssetPublicKeys( +class ApiAssetPublicKeys( val patchesPublicKey: String, val integrationsPublicKey: String, ) @@ -174,4 +182,4 @@ class APIAbout( } @Serializable -class APIToken(val token: String) +class ApiToken(val token: String) 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 909cff8..434f0a5 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt @@ -1,35 +1,29 @@ 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.APIResponseAnnouncementId -import kotlinx.datetime.LocalDateTime +import app.revanced.api.configuration.schema.ApiAnnouncement internal class AnnouncementService( private val announcementRepository: AnnouncementRepository, ) { - fun latestId(channel: String): APIResponseAnnouncementId? = announcementRepository.latestId(channel) - fun latestId(): APIResponseAnnouncementId? = announcementRepository.latestId() + suspend fun latest(tags: Set) = announcementRepository.latest(tags) - fun latest(channel: String) = announcementRepository.latest(channel) - fun latest() = announcementRepository.latest() + suspend fun latest() = announcementRepository.latest() - suspend fun all(channel: String) = announcementRepository.all(channel) - suspend fun all() = announcementRepository.all() + fun latestId(tags: Set) = announcementRepository.latestId(tags) - suspend fun new(new: APIAnnouncement) { - announcementRepository.new(new) - } - suspend fun archive(id: Int, archivedAt: LocalDateTime?) { - announcementRepository.archive(id, archivedAt) - } - suspend fun unarchive(id: Int) { - announcementRepository.unarchive(id) - } - suspend fun update(id: Int, new: APIAnnouncement) { - announcementRepository.update(id, new) - } - suspend fun delete(id: Int) { - announcementRepository.delete(id) - } + fun latestId() = announcementRepository.latestId() + + suspend fun paged(cursor: Int, limit: Int, tags: Set?, archived: Boolean) = + announcementRepository.paged(cursor, limit, tags, archived) + + suspend fun get(id: Int) = announcementRepository.get(id) + + suspend fun update(id: Int, new: ApiAnnouncement) = announcementRepository.update(id, new) + + suspend fun delete(id: Int) = announcementRepository.delete(id) + + suspend fun new(new: ApiAnnouncement) = announcementRepository.new(new) + + suspend fun tags() = announcementRepository.tags() } 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 5dc9f43..368c3b6 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt @@ -21,7 +21,7 @@ internal class ApiService( APIContributable( it, backendRepository.contributors(configurationRepository.organization, it).map { - APIContributor(it.name, it.avatarUrl, it.url, it.contributions) + ApiContributor(it.name, it.avatarUrl, it.url, it.contributions) }, ) } @@ -29,13 +29,13 @@ internal class ApiService( }.awaitAll() suspend fun team() = backendRepository.members(configurationRepository.organization).map { member -> - APIMember( + ApiMember( member.name, member.avatarUrl, member.url, member.bio, if (member.gpgKeys.ids.isNotEmpty()) { - APIGpgKey( + ApiGpgKey( // Must choose one of the GPG keys, because it does not make sense to have multiple GPG keys for the API. member.gpgKeys.ids.first(), member.gpgKeys.url, @@ -47,6 +47,6 @@ internal class ApiService( } suspend fun rateLimit() = backendRepository.rateLimit()?.let { - APIRateLimit(it.limit, it.remaining, it.reset) + ApiRateLimit(it.limit, it.remaining, it.reset) } } diff --git a/src/main/kotlin/app/revanced/api/configuration/services/AuthenticationService.kt b/src/main/kotlin/app/revanced/api/configuration/services/AuthenticationService.kt index f5bbb2b..a3150b1 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/AuthenticationService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/AuthenticationService.kt @@ -1,6 +1,6 @@ package app.revanced.api.configuration.services -import app.revanced.api.configuration.schema.APIToken +import app.revanced.api.configuration.schema.ApiToken import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm import io.ktor.server.auth.* @@ -43,7 +43,7 @@ internal class AuthenticationService private constructor( } } - fun newToken() = APIToken( + fun newToken() = ApiToken( JWT.create() .withIssuer(issuer) .withExpiresAt(Instant.now().plus(validityInMin, ChronoUnit.MINUTES)) diff --git a/src/main/kotlin/app/revanced/api/configuration/services/ManagerService.kt b/src/main/kotlin/app/revanced/api/configuration/services/ManagerService.kt index 17b0665..d844684 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/ManagerService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/ManagerService.kt @@ -9,17 +9,17 @@ internal class ManagerService( private val backendRepository: BackendRepository, private val configurationRepository: ConfigurationRepository, ) { - suspend fun latestRelease(): APIRelease { + suspend fun latestRelease(): ApiRelease { val managerRelease = backendRepository.release( configurationRepository.organization, configurationRepository.manager.repository, ) - val managerAsset = APIManagerAsset( + val managerAsset = ApiManagerAsset( managerRelease.assets.first(configurationRepository.manager.assetRegex).downloadUrl, ) - return APIRelease( + return ApiRelease( managerRelease.tag, managerRelease.createdAt, managerRelease.releaseNote, @@ -27,12 +27,12 @@ internal class ManagerService( ) } - suspend fun latestVersion(): APIReleaseVersion { + suspend fun latestVersion(): ApiReleaseVersion { val managerRelease = backendRepository.release( configurationRepository.organization, configurationRepository.manager.repository, ) - return APIReleaseVersion(managerRelease.tag) + return ApiReleaseVersion(managerRelease.tag) } } 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 6a70041..235536f 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt @@ -17,7 +17,7 @@ internal class PatchesService( private val backendRepository: BackendRepository, private val configurationRepository: ConfigurationRepository, ) { - suspend fun latestRelease(): APIRelease { + suspend fun latestRelease(): ApiRelease { val patchesRelease = backendRepository.release( configurationRepository.organization, configurationRepository.patches.repository, @@ -30,8 +30,8 @@ internal class PatchesService( fun ConfigurationRepository.SignedAssetConfiguration.asset( release: BackendRepository.BackendOrganization.BackendRepository.BackendRelease, - assetName: APIAssetName, - ) = APIPatchesAsset( + assetName: ApiAssetName, + ) = ApiPatchesAsset( release.assets.first(assetRegex).downloadUrl, release.assets.first(signatureAssetRegex).downloadUrl, assetName, @@ -39,14 +39,14 @@ internal class PatchesService( val patchesAsset = configurationRepository.patches.asset( patchesRelease, - APIAssetName.PATCHES, + ApiAssetName.PATCHES, ) val integrationsAsset = configurationRepository.integrations.asset( integrationsRelease, - APIAssetName.INTEGRATION, + ApiAssetName.INTEGRATION, ) - return APIRelease( + return ApiRelease( patchesRelease.tag, patchesRelease.createdAt, patchesRelease.releaseNote, @@ -54,13 +54,13 @@ internal class PatchesService( ) } - suspend fun latestVersion(): APIReleaseVersion { + suspend fun latestVersion(): ApiReleaseVersion { val patchesRelease = backendRepository.release( configurationRepository.organization, configurationRepository.patches.repository, ) - return APIReleaseVersion(patchesRelease.tag) + return ApiReleaseVersion(patchesRelease.tag) } private val patchesListCache = Caffeine @@ -111,12 +111,12 @@ internal class PatchesService( } } - fun publicKeys(): APIAssetPublicKeys { + fun publicKeys(): ApiAssetPublicKeys { fun readPublicKey( getSignedAssetConfiguration: ConfigurationRepository.() -> ConfigurationRepository.SignedAssetConfiguration, ) = configurationRepository.getSignedAssetConfiguration().publicKeyFile.readText() - return APIAssetPublicKeys( + return ApiAssetPublicKeys( readPublicKey { patches }, readPublicKey { integrations }, ) diff --git a/src/test/kotlin/app/revanced/api/configuration/services/AnnouncementServiceTest.kt b/src/test/kotlin/app/revanced/api/configuration/services/AnnouncementServiceTest.kt new file mode 100644 index 0000000..4b6e058 --- /dev/null +++ b/src/test/kotlin/app/revanced/api/configuration/services/AnnouncementServiceTest.kt @@ -0,0 +1,194 @@ +package app.revanced.api.configuration.services + +import app.revanced.api.configuration.repository.AnnouncementRepository +import app.revanced.api.configuration.schema.ApiAnnouncement +import kotlinx.coroutines.runBlocking +import kotlinx.datetime.toKotlinLocalDateTime +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.transactions.TransactionManager +import org.junit.jupiter.api.* +import org.junit.jupiter.api.Assertions.assertNull +import java.time.LocalDateTime +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +private object AnnouncementServiceTest { + private lateinit var announcementService: AnnouncementService + + @JvmStatic + @BeforeAll + fun setUp() { + TransactionManager.defaultDatabase = + Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false") + + announcementService = AnnouncementService(AnnouncementRepository()) + } + + @BeforeEach + fun clear() { + runBlocking { + while (true) { + val latestId = announcementService.latestId() ?: break + announcementService.delete(latestId.id) + } + } + } + + @Test + fun `can do basic crud`(): Unit = runBlocking { + announcementService.new(ApiAnnouncement(title = "title")) + + val latestId = announcementService.latestId()!!.id + + announcementService.update(latestId, ApiAnnouncement(title = "new title")) + assert(announcementService.get(latestId)?.title == "new title") + + announcementService.delete(latestId) + assertNull(announcementService.get(latestId)) + assertNull(announcementService.latestId()) + } + + @Test + fun `archiving works properly`() = runBlocking { + announcementService.new(ApiAnnouncement(title = "title")) + + val latest = announcementService.latest()!! + assertNull(announcementService.get(latest.id)?.archivedAt) + + val updated = ApiAnnouncement( + title = latest.title, + archivedAt = LocalDateTime.now().toKotlinLocalDateTime(), + ) + + announcementService.update(latest.id, updated) + assertNotNull(announcementService.get(latest.id)?.archivedAt) + + return@runBlocking + } + + @Test + fun `latest works properly`() = runBlocking { + announcementService.new(ApiAnnouncement(title = "title")) + announcementService.new(ApiAnnouncement(title = "title2")) + + var latest = announcementService.latest() + assert(latest?.title == "title2") + + announcementService.delete(latest!!.id) + + latest = announcementService.latest() + assert(latest?.title == "title") + + announcementService.delete(latest!!.id) + assertNull(announcementService.latest()) + + announcementService.new(ApiAnnouncement(title = "1", tags = listOf("tag1", "tag2"))) + announcementService.new(ApiAnnouncement(title = "2", tags = listOf("tag1", "tag3"))) + 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") + + val tag3 = announcementService.tags().find { it.name == "tag3" }!!.id + 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.any { it.title == "2" }) + assert(announcement2and3.any { it.title == "3" }) + + announcementService.delete(announcementService.latestId()!!.id) + assert(announcementService.latest(tag1and3).first().title == "2") + + announcementService.delete(announcementService.latestId()!!.id) + assert(announcementService.latest(tag1and3).first().title == "1") + + announcementService.delete(announcementService.latestId()!!.id) + assert(announcementService.latest(tag1and3).isEmpty()) + assert(announcementService.tags().isEmpty()) + } + + @Test + fun `tags work properly`() = runBlocking { + announcementService.new(ApiAnnouncement(title = "title", tags = listOf("tag1", "tag2"))) + announcementService.new(ApiAnnouncement(title = "title2", tags = listOf("tag1", "tag3"))) + + val tags = announcementService.tags() + assertEquals(3, tags.size) + assert(tags.any { it.name == "tag1" }) + assert(tags.any { it.name == "tag2" }) + assert(tags.any { it.name == "tag3" }) + + announcementService.delete(announcementService.latestId()!!.id) + assertEquals(2, announcementService.tags().size) + + announcementService.update( + announcementService.latestId()!!.id, + ApiAnnouncement(title = "title", tags = listOf("tag1", "tag3")), + ) + + assertEquals(2, announcementService.tags().size) + assert(announcementService.tags().any { it.name == "tag3" }) + } + + @Test + fun `attachments work properly`() = runBlocking { + announcementService.new(ApiAnnouncement(title = "title", attachments = listOf("attachment1", "attachment2"))) + + val latestAnnouncement = announcementService.latest()!! + val latestId = latestAnnouncement.id + + val attachments = latestAnnouncement.attachments + assertEquals(2, attachments.size) + assert(attachments.any { it == "attachment1" }) + assert(attachments.any { it == "attachment2" }) + + announcementService.update( + latestId, + ApiAnnouncement(title = "title", attachments = listOf("attachment1", "attachment3")), + ) + assert(announcementService.get(latestId)!!.attachments.any { it == "attachment3" }) + } + + @Test + fun `paging works correctly`() = runBlocking { + repeat(10) { + announcementService.new(ApiAnnouncement(title = "title$it")) + } + + val announcements = announcementService.paged(Int.MAX_VALUE, 5, null, true) + assertEquals(5, announcements.size, "Returns correct number of announcements") + assertEquals("title9", announcements.first().title, "Starts from the latest announcement") + + val announcements2 = announcementService.paged(5, 5, null, true) + assertEquals(5, announcements2.size, "Returns correct number of announcements when starting from the cursor") + assertEquals("title4", announcements2.first().title, "Starts from the cursor") + + (0..4).forEach { id -> + announcementService.update( + id, + ApiAnnouncement( + title = "title$id", + tags = (0..id).map { "tag$it" }, + archivedAt = if (id % 2 == 0) { + // Only two announcements will be archived. + LocalDateTime.now().plusDays(2).minusDays(id.toLong()).toKotlinLocalDateTime() + } else { + null + }, + ), + ) + } + + val tags = announcementService.tags() + assertEquals(5, tags.size, "Returns correct number of newly created tags") + + val announcements3 = announcementService.paged(5, 5, setOf(tags[1].id), true) + 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") + } +} From 65ee2700e7a800916158ab5ea2b086f826194a19 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 1 Nov 2024 01:51:53 +0000 Subject: [PATCH 02/19] chore: Release v1.4.0-dev.1 [skip ci] # [1.4.0-dev.1](https://github.com/ReVanced/revanced-api/compare/v1.3.0...v1.4.0-dev.1) (2024-11-01) ### Features * Improve announcements API ([#192](https://github.com/ReVanced/revanced-api/issues/192)) ([56a00dd](https://github.com/ReVanced/revanced-api/commit/56a00ddb85f302d441f0b222a9902ea2c1c18897)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65033cc..7bdd723 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [1.4.0-dev.1](https://github.com/ReVanced/revanced-api/compare/v1.3.0...v1.4.0-dev.1) (2024-11-01) + + +### Features + +* Improve announcements API ([#192](https://github.com/ReVanced/revanced-api/issues/192)) ([56a00dd](https://github.com/ReVanced/revanced-api/commit/56a00ddb85f302d441f0b222a9902ea2c1c18897)) + # [1.3.0](https://github.com/ReVanced/revanced-api/compare/v1.2.0...v1.3.0) (2024-10-07) diff --git a/gradle.properties b/gradle.properties index f621b40..99db55e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.parallel = true org.gradle.caching = true kotlin.code.style = official -version = 1.3.0 +version = 1.4.0-dev.1 From f91f3a65c5e07b5b58ccbff1d4b0a5ba9b15fc50 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Fri, 1 Nov 2024 03:09:11 +0100 Subject: [PATCH 03/19] feat: Make backend configurable --- configuration.example.toml | 1 + .../api/configuration/Dependencies.kt | 101 +++--------------- .../repository/AnnouncementRepository.kt | 4 +- .../repository/BackendRepository.kt | 52 ++++++++- .../repository/ConfigurationRepository.kt | 3 + .../repository/GitHubBackendRepository.kt | 14 ++- .../configuration/services/OldApiService.kt | 9 +- .../services/AnnouncementServiceTest.kt | 6 +- 8 files changed, 90 insertions(+), 100 deletions(-) diff --git a/configuration.example.toml b/configuration.example.toml index fec352c..3bd92cd 100644 --- a/configuration.example.toml +++ b/configuration.example.toml @@ -20,3 +20,4 @@ old-api-endpoint = "https://old-api.revanced.app" static-files-path = "static/root" versioned-static-files-path = "static/versioned" about-json-file-path = "about.json" +backend-service-name = "GitHub" diff --git a/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt b/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt index 451ae73..71a0d21 100644 --- a/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt +++ b/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt @@ -5,101 +5,39 @@ import app.revanced.api.configuration.repository.BackendRepository import app.revanced.api.configuration.repository.ConfigurationRepository import app.revanced.api.configuration.repository.GitHubBackendRepository import app.revanced.api.configuration.services.* -import app.revanced.api.configuration.services.AnnouncementService -import app.revanced.api.configuration.services.ApiService -import app.revanced.api.configuration.services.AuthenticationService -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.ktor.client.* -import io.ktor.client.engine.okhttp.* -import io.ktor.client.plugins.* -import io.ktor.client.plugins.auth.* -import io.ktor.client.plugins.auth.providers.* -import io.ktor.client.plugins.cache.* -import io.ktor.client.plugins.contentnegotiation.* -import io.ktor.client.plugins.resources.* -import io.ktor.serialization.kotlinx.json.* import io.ktor.server.application.* -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 import org.koin.ktor.plugin.Koin import java.io.File -@OptIn(ExperimentalSerializationApi::class) fun Application.configureDependencies( configFile: File, ) { - val miscModule = module { - factory { params -> - val defaultRequestUri: String = params.get() - val configBlock = params.getOrNull<(HttpClientConfig.() -> Unit)>() ?: {} - - HttpClient(OkHttp) { - defaultRequest { url(defaultRequestUri) } - - configBlock() - } - } - } - val repositoryModule = module { - single { - GitHubBackendRepository( - get { - val defaultRequestUri = "https://api.github.com" - val configBlock: HttpClientConfig.() -> Unit = { - install(HttpCache) - install(Resources) - install(ContentNegotiation) { - json( - Json { - ignoreUnknownKeys = true - namingStrategy = JsonNamingStrategy.SnakeCase - }, - ) - } - - System.getProperty("BACKEND_API_TOKEN")?.let { - install(Auth) { - bearer { - loadTokens { - BearerTokens( - accessToken = it, - refreshToken = "", // Required dummy value - ) - } - - sendWithoutRequest { true } - } - } - } - } - - parameterArrayOf(defaultRequestUri, configBlock) - }, - ) - } - - single { - Toml.decodeFromStream(configFile.inputStream()) - } - + single { Toml.decodeFromStream(configFile.inputStream()) } single { - TransactionManager.defaultDatabase = Database.connect( + Database.connect( url = System.getProperty("DB_URL"), user = System.getProperty("DB_USER"), password = System.getProperty("DB_PASSWORD"), ) + } + singleOf(::AnnouncementRepository) + singleOf(::GitHubBackendRepository) + single { + val backendServices = mapOf( + GitHubBackendRepository.SERVICE_NAME to { get() }, + // Implement more backend services here. + ) - AnnouncementRepository() + val configuration = get() + val backendFactory = backendServices[configuration.backendServiceName]!! + + backendFactory() } } @@ -113,15 +51,7 @@ fun Application.configureDependencies( AuthenticationService(issuer, validityInMin, jwtSecret, authSHA256DigestString) } - single { - val configuration = get() - - OldApiService( - get { - parameterArrayOf(configuration.oldApiEndpoint) - }, - ) - } + singleOf(::OldApiService) singleOf(::AnnouncementService) singleOf(::SignatureService) singleOf(::PatchesService) @@ -131,7 +61,6 @@ fun Application.configureDependencies( install(Koin) { modules( - miscModule, repositoryModule, serviceModule, ) 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 3f6ff6f..d7ce97b 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt @@ -17,7 +17,7 @@ import org.jetbrains.exposed.sql.kotlin.datetime.datetime import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction import java.time.LocalDateTime -internal class AnnouncementRepository { +internal class AnnouncementRepository(private val database: Database) { // This is better than doing a maxByOrNull { it.id } on every request. private var latestAnnouncement: Announcement? = null private val latestAnnouncementByTag = mutableMapOf() @@ -187,7 +187,7 @@ internal class AnnouncementRepository { } private suspend fun transaction(statement: suspend Transaction.() -> T) = - newSuspendedTransaction(Dispatchers.IO, statement = statement) + newSuspendedTransaction(Dispatchers.IO, database, statement = statement) private object Announcements : IntIdTable() { val author = varchar("author", 32).nullable() 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 69429ca..85d10bb 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/BackendRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/BackendRepository.kt @@ -1,16 +1,59 @@ package app.revanced.api.configuration.repository import io.ktor.client.* +import io.ktor.client.engine.okhttp.* +import io.ktor.client.plugins.* +import io.ktor.client.plugins.auth.* +import io.ktor.client.plugins.auth.providers.* +import io.ktor.client.plugins.cache.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.plugins.resources.* +import io.ktor.serialization.kotlinx.json.* import kotlinx.datetime.LocalDateTime +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonNamingStrategy /** * The backend of the API used to get data. * - * @param client The HTTP client to use for requests. + * @param defaultRequestUri The URI to use for requests. + * @param website The site of the backend users can visit. */ abstract class BackendRepository internal constructor( - protected val client: HttpClient, + defaultRequestUri: String, + internal val website: String, ) { + protected val client: HttpClient = HttpClient(OkHttp) { + defaultRequest { url(defaultRequestUri) } + + install(HttpCache) + install(Resources) + install(ContentNegotiation) { + json( + Json { + ignoreUnknownKeys = true + @Suppress("OPT_IN_USAGE") + namingStrategy = JsonNamingStrategy.SnakeCase + }, + ) + } + + System.getProperty("BACKEND_API_TOKEN")?.let { + install(Auth) { + bearer { + loadTokens { + BearerTokens( + accessToken = it, + refreshToken = "", // Required dummy value + ) + } + + sendWithoutRequest { true } + } + } + } + } + /** * A user. * @@ -153,7 +196,10 @@ abstract class BackendRepository internal constructor( * @param repository The name of the repository. * @return The contributors. */ - abstract suspend fun contributors(owner: String, repository: String): List + abstract suspend fun contributors( + owner: String, + repository: String, + ): List /** * Get the members of an organization. diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt index 65b82f6..9aebfa5 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt @@ -27,6 +27,7 @@ import kotlin.io.path.createDirectories * @property integrations The source of the integrations. * @property manager The source of the manager. * @property contributorsRepositoryNames The names of the repositories to get contributors from. + * @property backendServiceName The name of the backend service to use for the repositories, contributors, etc. * @property apiVersion The version to use for the API. * @property corsAllowedHosts The hosts allowed to make requests to the API. * @property endpoint The endpoint of the API. @@ -44,6 +45,8 @@ internal class ConfigurationRepository( val manager: AssetConfiguration, @SerialName("contributors-repositories") val contributorsRepositoryNames: Set, + @SerialName("backend-service-name") + val backendServiceName: String, @SerialName("api-version") val apiVersion: Int = 1, @SerialName("cors-allowed-hosts") 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 39d0236..420ccaf 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/GitHubBackendRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/GitHubBackendRepository.kt @@ -8,18 +8,19 @@ import app.revanced.api.configuration.repository.GitHubOrganization.GitHubReposi import app.revanced.api.configuration.repository.GitHubOrganization.GitHubRepository.GitHubRelease import app.revanced.api.configuration.repository.Organization.Repository.Contributors import app.revanced.api.configuration.repository.Organization.Repository.Releases -import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.plugins.resources.* import io.ktor.resources.* -import kotlinx.coroutines.* +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import kotlinx.datetime.Instant import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) { +class GitHubBackendRepository : BackendRepository("https://api.github.com", "https://github.com") { override suspend fun release( owner: String, repository: String, @@ -67,7 +68,8 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) { override suspend fun members(organization: String): List { // Get the list of members of the organization. - val publicMembers: List = client.get(Organization.PublicMembers(organization)).body() + val publicMembers: List = + client.get(Organization.PublicMembers(organization)).body() return coroutineScope { publicMembers.map { member -> @@ -113,6 +115,10 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) { reset = Instant.fromEpochSeconds(rateLimit.rate.reset).toLocalDateTime(TimeZone.UTC), ) } + + companion object { + const val SERVICE_NAME = "GitHub" + } } interface IGitHubUser { diff --git a/src/main/kotlin/app/revanced/api/configuration/services/OldApiService.kt b/src/main/kotlin/app/revanced/api/configuration/services/OldApiService.kt index 2c464aa..271f6f5 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/OldApiService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/OldApiService.kt @@ -1,6 +1,9 @@ package app.revanced.api.configuration.services +import app.revanced.api.configuration.repository.ConfigurationRepository import io.ktor.client.* +import io.ktor.client.engine.okhttp.* +import io.ktor.client.plugins.* import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* @@ -11,7 +14,11 @@ import io.ktor.server.response.* import io.ktor.util.* import io.ktor.utils.io.* -internal class OldApiService(private val client: HttpClient) { +internal class OldApiService(configurationRepository: ConfigurationRepository) { + private val client = HttpClient(OkHttp) { + defaultRequest { url(configurationRepository.oldApiEndpoint) } + } + @OptIn(InternalAPI::class) suspend fun proxy(call: ApplicationCall) { val channel = call.request.receiveChannel() diff --git a/src/test/kotlin/app/revanced/api/configuration/services/AnnouncementServiceTest.kt b/src/test/kotlin/app/revanced/api/configuration/services/AnnouncementServiceTest.kt index 4b6e058..3fdf4d6 100644 --- a/src/test/kotlin/app/revanced/api/configuration/services/AnnouncementServiceTest.kt +++ b/src/test/kotlin/app/revanced/api/configuration/services/AnnouncementServiceTest.kt @@ -5,7 +5,6 @@ import app.revanced.api.configuration.schema.ApiAnnouncement import kotlinx.coroutines.runBlocking import kotlinx.datetime.toKotlinLocalDateTime import org.jetbrains.exposed.sql.Database -import org.jetbrains.exposed.sql.transactions.TransactionManager import org.junit.jupiter.api.* import org.junit.jupiter.api.Assertions.assertNull import java.time.LocalDateTime @@ -18,10 +17,9 @@ private object AnnouncementServiceTest { @JvmStatic @BeforeAll fun setUp() { - TransactionManager.defaultDatabase = - Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false") + val database = Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false") - announcementService = AnnouncementService(AnnouncementRepository()) + announcementService = AnnouncementService(AnnouncementRepository(database)) } @BeforeEach From a5498aba2b99db89c28a65738cc58cc4c852c327 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Fri, 1 Nov 2024 18:43:39 +0100 Subject: [PATCH 04/19] feat: Add URL and use friendly name for `APIContributable` --- configuration.example.toml | 14 ++++++-------- .../repository/ConfigurationRepository.kt | 4 ++-- .../revanced/api/configuration/schema/APISchema.kt | 1 + .../api/configuration/services/ApiService.kt | 11 ++++++++--- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/configuration.example.toml b/configuration.example.toml index 3bd92cd..60369f8 100644 --- a/configuration.example.toml +++ b/configuration.example.toml @@ -2,14 +2,6 @@ organization = "revanced" patches = { repository = "revanced-patches", asset-regex = "jar$", signature-asset-regex = "asc$", public-key-file = "patches-public-key.asc", public-key-id = 0 } integrations = { repository = "revanced-integrations", asset-regex = "apk$", signature-asset-regex = "asc$", public-key-file = "integrations-public-key.asc", public-key-id = 0 } manager = { repository = "revanced-manager", asset-regex = "apk$" } -contributors-repositories = [ - "revanced-patcher", - "revanced-patches", - "revanced-integrations", - "revanced-website", - "revanced-cli", - "revanced-manager", -] api-version = 1 cors-allowed-hosts = [ "revanced.app", @@ -21,3 +13,9 @@ static-files-path = "static/root" versioned-static-files-path = "static/versioned" about-json-file-path = "about.json" backend-service-name = "GitHub" +[contributors-repositories] +revanced-patcher = "ReVanced Patcher" +revanced-patches = "ReVanced Patches" +revanced-website = "ReVanced Website" +revanced-cli = "ReVanced CLI" +revanced-manager = "ReVanced Manager" \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt index 9aebfa5..fea5663 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt @@ -26,7 +26,7 @@ import kotlin.io.path.createDirectories * @property patches The source of the patches. * @property integrations The source of the integrations. * @property manager The source of the manager. - * @property contributorsRepositoryNames The names of the repositories to get contributors from. + * @property contributorsRepositoryNames The friendly name of repos mapped to the repository names to get contributors from. * @property backendServiceName The name of the backend service to use for the repositories, contributors, etc. * @property apiVersion The version to use for the API. * @property corsAllowedHosts The hosts allowed to make requests to the API. @@ -44,7 +44,7 @@ internal class ConfigurationRepository( val integrations: SignedAssetConfiguration, val manager: AssetConfiguration, @SerialName("contributors-repositories") - val contributorsRepositoryNames: Set, + val contributorsRepositoryNames: Map, @SerialName("backend-service-name") val backendServiceName: String, @SerialName("api-version") 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 1ff7979..0294a9b 100644 --- a/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt +++ b/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt @@ -35,6 +35,7 @@ class ApiContributor( @Serializable class APIContributable( val name: String, + val url: String, // Using a list instead of a set because set semantics are unnecessary here. val contributors: List, ) 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 368c3b6..0e12f12 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt @@ -3,6 +3,7 @@ package app.revanced.api.configuration.services import app.revanced.api.configuration.repository.BackendRepository import app.revanced.api.configuration.repository.ConfigurationRepository import app.revanced.api.configuration.schema.* +import io.ktor.http.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -16,11 +17,15 @@ internal class ApiService( val about = configurationRepository.about suspend fun contributors() = withContext(Dispatchers.IO) { - configurationRepository.contributorsRepositoryNames.map { + configurationRepository.contributorsRepositoryNames.map { (repository, name) -> async { APIContributable( - it, - backendRepository.contributors(configurationRepository.organization, it).map { + name, + URLBuilder().apply { + takeFrom(backendRepository.website) + path(configurationRepository.organization, repository) + }.buildString(), + backendRepository.contributors(configurationRepository.organization, repository).map { ApiContributor(it.name, it.avatarUrl, it.url, it.contributions) }, ) From f1c10928ae3be1c6b1d675819755b3046fad70d8 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Fri, 1 Nov 2024 19:04:22 +0100 Subject: [PATCH 05/19] feat: Remove ReVanced Integrations There is no need for them anymore in Patcher v20+ --- .gitignore | 1 - README.md | 17 ++++---- configuration.example.toml | 1 - docker-compose.example.yml | 1 - .../repository/ConfigurationRepository.kt | 4 +- .../api/configuration/routes/ManagerRoute.kt | 3 +- .../api/configuration/routes/PatchesRoute.kt | 13 +++--- .../api/configuration/schema/APISchema.kt | 26 ++--------- .../configuration/services/ManagerService.kt | 11 ++--- .../configuration/services/PatchesService.kt | 43 +++---------------- .../services/SignatureService.kt | 2 +- 11 files changed, 32 insertions(+), 90 deletions(-) diff --git a/.gitignore b/.gitignore index d376259..c777613 100644 --- a/.gitignore +++ b/.gitignore @@ -41,7 +41,6 @@ persistence/ configuration.toml docker-compose.yml patches-public-key.asc -integrations-public-key.asc node_modules/ static/ about.json \ No newline at end of file diff --git a/README.md b/README.md index fc2e854..f3c5fac 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,8 @@ API server for ReVanced. ## ❓ About ReVanced API is a server that is used as the backend for ReVanced. -ReVanced API acts as the data source for [ReVanced Website](https://github.com/ReVanced/revanced-website) and powers [ReVanced Manager](https://github.com/ReVanced/revanced-manager) +ReVanced API acts as the data source for [ReVanced Website](https://github.com/ReVanced/revanced-website) and +powers [ReVanced Manager](https://github.com/ReVanced/revanced-manager) with updates and ReVanced Patches. ## đŸ’Ē Features @@ -76,8 +77,8 @@ with updates and ReVanced Patches. Some of the features ReVanced API include: - đŸ“ĸ **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 +- â„šī¸ **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 - đŸ‘Ĩ **Contributors**: List all contributors involved in the project - 🔄 **Backwards compatibility**: Proxy an old API for migration purposes and backwards compatibility @@ -90,7 +91,8 @@ ReVanced API can be deployed as a Docker container or used standalone. To deploy ReVanced API as a Docker container, you can use Docker Compose or Docker CLI. The Docker image is published on GitHub Container registry, -so before you can pull the image, you need to [authenticate to the Container registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-to-the-container-registry). +so before you can pull the image, you need +to [authenticate to the Container registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-to-the-container-registry). ### đŸ—„ī¸ Docker Compose @@ -114,8 +116,6 @@ so before you can pull the image, you need to [authenticate to the Container reg -v $(pwd)/configuration.toml:/app/configuration.toml \ # Mount the patches public key -v $(pwd)/patches-public-key.asc:/app/patches-public-key.asc \ - # Mount the integrations public key - -v $(pwd)/integrations-public-key.asc:/app/integrations-public-key.asc \ # Mount the static folder -v $(pwd)/static:/app/static \ # Mount the about.json file @@ -141,7 +141,7 @@ A Java Runtime Environment (JRE) must be installed. 1. [Download](https://github.com/ReVanced/revanced-api/releases/latest) ReVanced API to a folder 2. In the same folder, create an `.env` file using [.env.example](.env.example) as a template 3. In the same folder, create a `configuration.toml` file -using [configuration.example.toml](configuration.example.toml) as a template + using [configuration.example.toml](configuration.example.toml) as a template 4. In the same folder, create an `about.json` file using [about.example.json](about.example.json) as a template 5. Run `java -jar revanced-api.jar start` to start the server @@ -159,7 +159,8 @@ A Java Development Kit (JDK) and Git must be installed. ### 📙 Contributing -Thank you for considering contributing to ReVanced API. You can find the contribution guidelines [here](CONTRIBUTING.md). +Thank you for considering contributing to ReVanced API. You can find the contribution +guidelines [here](CONTRIBUTING.md). ### đŸ› ī¸ Building diff --git a/configuration.example.toml b/configuration.example.toml index 60369f8..5314b96 100644 --- a/configuration.example.toml +++ b/configuration.example.toml @@ -1,6 +1,5 @@ organization = "revanced" patches = { repository = "revanced-patches", asset-regex = "jar$", signature-asset-regex = "asc$", public-key-file = "patches-public-key.asc", public-key-id = 0 } -integrations = { repository = "revanced-integrations", asset-regex = "apk$", signature-asset-regex = "asc$", public-key-file = "integrations-public-key.asc", public-key-id = 0 } manager = { repository = "revanced-manager", asset-regex = "apk$" } api-version = 1 cors-allowed-hosts = [ diff --git a/docker-compose.example.yml b/docker-compose.example.yml index db40175..5609eae 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -7,7 +7,6 @@ services: - /data/revanced-api/.env:/app/.env - /data/revanced-api/configuration.toml:/app/configuration.toml - /data/revanced-api/patches-public-key.asc:/app/patches-public-key.asc - - /data/revanced-api/integrations-public-key.asc:/app/integrations-public-key.asc - /data/revanced-api/static:/app/static - /data/revanced-api/about.json:/app/about.json environment: diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt index fea5663..4e78c71 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt @@ -22,9 +22,8 @@ import kotlin.io.path.createDirectories /** * The repository storing the configuration for the API. * - * @property organization The API backends organization name where the repositories for the patches and integrations are. + * @property organization The API backends organization name where the repositories are. * @property patches The source of the patches. - * @property integrations The source of the integrations. * @property manager The source of the manager. * @property contributorsRepositoryNames The friendly name of repos mapped to the repository names to get contributors from. * @property backendServiceName The name of the backend service to use for the repositories, contributors, etc. @@ -41,7 +40,6 @@ import kotlin.io.path.createDirectories internal class ConfigurationRepository( val organization: String, val patches: SignedAssetConfiguration, - val integrations: SignedAssetConfiguration, val manager: AssetConfiguration, @SerialName("contributors-repositories") val contributorsRepositoryNames: Map, diff --git a/src/main/kotlin/app/revanced/api/configuration/routes/ManagerRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routes/ManagerRoute.kt index 9485137..04f217f 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routes/ManagerRoute.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/ManagerRoute.kt @@ -1,7 +1,6 @@ package app.revanced.api.configuration.routes import app.revanced.api.configuration.installNotarizedRoute -import app.revanced.api.configuration.schema.ApiManagerAsset import app.revanced.api.configuration.schema.ApiRelease import app.revanced.api.configuration.schema.ApiReleaseVersion import app.revanced.api.configuration.services.ManagerService @@ -53,7 +52,7 @@ private fun Route.installManagerRouteDocumentation(deprecated: Boolean) = instal description("The latest manager release") mediaTypes("application/json") responseCode(HttpStatusCode.OK) - responseType>() + responseType() } } } diff --git a/src/main/kotlin/app/revanced/api/configuration/routes/PatchesRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routes/PatchesRoute.kt index 7932f0b..ccf3529 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routes/PatchesRoute.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/PatchesRoute.kt @@ -2,8 +2,7 @@ package app.revanced.api.configuration.routes import app.revanced.api.configuration.installCache import app.revanced.api.configuration.installNotarizedRoute -import app.revanced.api.configuration.schema.ApiAssetPublicKeys -import app.revanced.api.configuration.schema.ApiPatchesAsset +import app.revanced.api.configuration.schema.ApiAssetPublicKey import app.revanced.api.configuration.schema.ApiRelease import app.revanced.api.configuration.schema.ApiReleaseVersion import app.revanced.api.configuration.services.PatchesService @@ -61,7 +60,7 @@ private fun Route.configure(deprecated: Boolean = false) { installPatchesPublicKeyRouteDocumentation(deprecated) get { - call.respond(patchesService.publicKeys()) + call.respond(patchesService.publicKey()) } } } @@ -78,7 +77,7 @@ private fun Route.installPatchesRouteDocumentation(deprecated: Boolean) = instal description("The current patches release") mediaTypes("application/json") responseCode(HttpStatusCode.OK) - responseType>() + responseType() } } } @@ -120,13 +119,13 @@ private fun Route.installPatchesPublicKeyRouteDocumentation(deprecated: Boolean) get = GetInfo.builder { if (deprecated) isDeprecated() - description("Get the public keys for verifying patches and integrations assets") - summary("Get patches and integrations public keys") + description("Get the public keys for verifying patches assets") + summary("Get patches public keys") response { description("The public keys") mediaTypes("application/json") responseCode(HttpStatusCode.OK) - responseType() + responseType() } } } 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 0294a9b..369dc16 100644 --- a/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt +++ b/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt @@ -41,33 +41,14 @@ class APIContributable( ) @Serializable -class ApiRelease( +class ApiRelease( val version: String, val createdAt: LocalDateTime, val description: String, - // Using a list instead of a set because set semantics are unnecessary here. - val assets: List, -) - -@Serializable -class ApiManagerAsset( val downloadUrl: String, + val signatureDownloadUrl: String? = null, ) -@Serializable -class ApiPatchesAsset( - val downloadUrl: String, - val signatureDownloadUrl: String, - // TODO: Remove this eventually when integrations are merged into patches. - val name: ApiAssetName, -) - -@Serializable -enum class ApiAssetName { - PATCHES, - INTEGRATION, -} - @Serializable class ApiReleaseVersion( val version: String, @@ -125,9 +106,8 @@ class ApiRateLimit( ) @Serializable -class ApiAssetPublicKeys( +class ApiAssetPublicKey( val patchesPublicKey: String, - val integrationsPublicKey: String, ) @Serializable diff --git a/src/main/kotlin/app/revanced/api/configuration/services/ManagerService.kt b/src/main/kotlin/app/revanced/api/configuration/services/ManagerService.kt index d844684..c180043 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/ManagerService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/ManagerService.kt @@ -3,27 +3,24 @@ package app.revanced.api.configuration.services import app.revanced.api.configuration.repository.BackendRepository import app.revanced.api.configuration.repository.BackendRepository.BackendOrganization.BackendRepository.BackendRelease.Companion.first import app.revanced.api.configuration.repository.ConfigurationRepository -import app.revanced.api.configuration.schema.* +import app.revanced.api.configuration.schema.ApiRelease +import app.revanced.api.configuration.schema.ApiReleaseVersion internal class ManagerService( private val backendRepository: BackendRepository, private val configurationRepository: ConfigurationRepository, ) { - suspend fun latestRelease(): ApiRelease { + suspend fun latestRelease(): ApiRelease { val managerRelease = backendRepository.release( configurationRepository.organization, configurationRepository.manager.repository, ) - val managerAsset = ApiManagerAsset( - managerRelease.assets.first(configurationRepository.manager.assetRegex).downloadUrl, - ) - return ApiRelease( managerRelease.tag, managerRelease.createdAt, managerRelease.releaseNote, - listOf(managerAsset), + managerRelease.assets.first(configurationRepository.manager.assetRegex).downloadUrl, ) } 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 235536f..08b1fb9 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt @@ -3,7 +3,9 @@ package app.revanced.api.configuration.services import app.revanced.api.configuration.repository.BackendRepository import app.revanced.api.configuration.repository.BackendRepository.BackendOrganization.BackendRepository.BackendRelease.Companion.first import app.revanced.api.configuration.repository.ConfigurationRepository -import app.revanced.api.configuration.schema.* +import app.revanced.api.configuration.schema.ApiAssetPublicKey +import app.revanced.api.configuration.schema.ApiRelease +import app.revanced.api.configuration.schema.ApiReleaseVersion import app.revanced.library.serializeTo import app.revanced.patcher.patch.loadPatchesFromJar import com.github.benmanes.caffeine.cache.Caffeine @@ -17,40 +19,18 @@ internal class PatchesService( private val backendRepository: BackendRepository, private val configurationRepository: ConfigurationRepository, ) { - suspend fun latestRelease(): ApiRelease { + suspend fun latestRelease(): ApiRelease { val patchesRelease = backendRepository.release( configurationRepository.organization, configurationRepository.patches.repository, ) - val integrationsRelease = backendRepository.release( - configurationRepository.organization, - configurationRepository.integrations.repository, - ) - - fun ConfigurationRepository.SignedAssetConfiguration.asset( - release: BackendRepository.BackendOrganization.BackendRepository.BackendRelease, - assetName: ApiAssetName, - ) = ApiPatchesAsset( - release.assets.first(assetRegex).downloadUrl, - release.assets.first(signatureAssetRegex).downloadUrl, - assetName, - ) - - val patchesAsset = configurationRepository.patches.asset( - patchesRelease, - ApiAssetName.PATCHES, - ) - val integrationsAsset = configurationRepository.integrations.asset( - integrationsRelease, - ApiAssetName.INTEGRATION, - ) - return ApiRelease( patchesRelease.tag, patchesRelease.createdAt, patchesRelease.releaseNote, - listOf(patchesAsset, integrationsAsset), + patchesRelease.assets.first(configurationRepository.patches.assetRegex).downloadUrl, + patchesRelease.assets.first(configurationRepository.patches.signatureAssetRegex).downloadUrl, ) } @@ -111,14 +91,5 @@ internal class PatchesService( } } - fun publicKeys(): ApiAssetPublicKeys { - fun readPublicKey( - getSignedAssetConfiguration: ConfigurationRepository.() -> ConfigurationRepository.SignedAssetConfiguration, - ) = configurationRepository.getSignedAssetConfiguration().publicKeyFile.readText() - - return ApiAssetPublicKeys( - readPublicKey { patches }, - readPublicKey { integrations }, - ) - } + fun publicKey() = ApiAssetPublicKey(configurationRepository.patches.publicKeyFile.readText()) } diff --git a/src/main/kotlin/app/revanced/api/configuration/services/SignatureService.kt b/src/main/kotlin/app/revanced/api/configuration/services/SignatureService.kt index 80eb2d1..fc69005 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/SignatureService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/SignatureService.kt @@ -12,7 +12,7 @@ import java.security.MessageDigest internal class SignatureService { private val signatureCache = Caffeine .newBuilder() - .maximumSize(2) // Assuming this is enough for patches and integrations. + .maximumSize(1) // 1 because currently only the latest patches is needed. .build() // Hash -> Verified. fun verify( From 3d3b7a7af83792e2add54527c0ff38f1332e4fc4 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Fri, 1 Nov 2024 19:09:01 +0100 Subject: [PATCH 06/19] chore: Use tables in configuration for readability --- configuration.example.toml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/configuration.example.toml b/configuration.example.toml index 5314b96..3b849db 100644 --- a/configuration.example.toml +++ b/configuration.example.toml @@ -1,6 +1,3 @@ -organization = "revanced" -patches = { repository = "revanced-patches", asset-regex = "jar$", signature-asset-regex = "asc$", public-key-file = "patches-public-key.asc", public-key-id = 0 } -manager = { repository = "revanced-manager", asset-regex = "apk$" } api-version = 1 cors-allowed-hosts = [ "revanced.app", @@ -10,8 +7,21 @@ endpoint = "https://api.revanced.app" old-api-endpoint = "https://old-api.revanced.app" static-files-path = "static/root" versioned-static-files-path = "static/versioned" -about-json-file-path = "about.json" backend-service-name = "GitHub" +about-json-file-path = "about.json" +organization = "revanced" + +[patches] +repository = "revanced-patches" +asset-regex = "jar$" +signature-asset-regex = "asc$" +public-key-file = "static/root/keys.asc" +public-key-id = 3897925568445097277 + +[manager] +repository = "revanced-manager" +asset-regex = "apk$" + [contributors-repositories] revanced-patcher = "ReVanced Patcher" revanced-patches = "ReVanced Patches" From 3b6212065a5cfb95c303b6d0551747ba1eb317f6 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Fri, 1 Nov 2024 19:11:33 +0100 Subject: [PATCH 07/19] fix: Add missing logging level environment variable to .env.example --- .env.example | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.env.example b/.env.example index 1b1b56f..b3921b8 100644 --- a/.env.example +++ b/.env.example @@ -13,3 +13,6 @@ AUTH_SHA256_DIGEST= JWT_SECRET= JWT_ISSUER= JWT_VALIDITY_IN_MIN= + +# Logging level for the application +LOG_LEVEL=INFO \ No newline at end of file From bb7aa5b0b4244d9ca98c6b98f930aef06e12c0c7 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 1 Nov 2024 18:13:29 +0000 Subject: [PATCH 08/19] chore: Release v1.4.0-dev.2 [skip ci] # [1.4.0-dev.2](https://github.com/ReVanced/revanced-api/compare/v1.4.0-dev.1...v1.4.0-dev.2) (2024-11-01) ### Bug Fixes * Add missing logging level environment variable to .env.example ([3b62120](https://github.com/ReVanced/revanced-api/commit/3b6212065a5cfb95c303b6d0551747ba1eb317f6)) ### Features * Add URL and use friendly name for `APIContributable` ([a5498ab](https://github.com/ReVanced/revanced-api/commit/a5498aba2b99db89c28a65738cc58cc4c852c327)) * Make backend configurable ([f91f3a6](https://github.com/ReVanced/revanced-api/commit/f91f3a65c5e07b5b58ccbff1d4b0a5ba9b15fc50)) * Remove ReVanced Integrations ([f1c1092](https://github.com/ReVanced/revanced-api/commit/f1c10928ae3be1c6b1d675819755b3046fad70d8)) --- CHANGELOG.md | 14 ++++++++++++++ gradle.properties | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bdd723..d6ddc8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +# [1.4.0-dev.2](https://github.com/ReVanced/revanced-api/compare/v1.4.0-dev.1...v1.4.0-dev.2) (2024-11-01) + + +### Bug Fixes + +* Add missing logging level environment variable to .env.example ([3b62120](https://github.com/ReVanced/revanced-api/commit/3b6212065a5cfb95c303b6d0551747ba1eb317f6)) + + +### Features + +* Add URL and use friendly name for `APIContributable` ([a5498ab](https://github.com/ReVanced/revanced-api/commit/a5498aba2b99db89c28a65738cc58cc4c852c327)) +* Make backend configurable ([f91f3a6](https://github.com/ReVanced/revanced-api/commit/f91f3a65c5e07b5b58ccbff1d4b0a5ba9b15fc50)) +* Remove ReVanced Integrations ([f1c1092](https://github.com/ReVanced/revanced-api/commit/f1c10928ae3be1c6b1d675819755b3046fad70d8)) + # [1.4.0-dev.1](https://github.com/ReVanced/revanced-api/compare/v1.3.0...v1.4.0-dev.1) (2024-11-01) diff --git a/gradle.properties b/gradle.properties index 99db55e..fd63a01 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.parallel = true org.gradle.caching = true kotlin.code.style = official -version = 1.4.0-dev.1 +version = 1.4.0-dev.2 From d42a3a393396a0f4e9085cda46e0af2c12b63cb1 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Fri, 1 Nov 2024 23:11:28 +0100 Subject: [PATCH 09/19] fix: Use new patches file extension --- configuration.example.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration.example.toml b/configuration.example.toml index 3b849db..2e2f28f 100644 --- a/configuration.example.toml +++ b/configuration.example.toml @@ -13,7 +13,7 @@ organization = "revanced" [patches] repository = "revanced-patches" -asset-regex = "jar$" +asset-regex = "rvp$" signature-asset-regex = "asc$" public-key-file = "static/root/keys.asc" public-key-id = 3897925568445097277 From e8dfefe6ae186832f1be02330dd179560ff08b16 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 1 Nov 2024 22:13:35 +0000 Subject: [PATCH 10/19] chore: Release v1.4.0-dev.3 [skip ci] # [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) ### Bug Fixes * Use new patches file extension ([d42a3a3](https://github.com/ReVanced/revanced-api/commit/d42a3a393396a0f4e9085cda46e0af2c12b63cb1)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6ddc8d..a6bc0f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [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) + + +### Bug Fixes + +* Use new patches file extension ([d42a3a3](https://github.com/ReVanced/revanced-api/commit/d42a3a393396a0f4e9085cda46e0af2c12b63cb1)) + # [1.4.0-dev.2](https://github.com/ReVanced/revanced-api/compare/v1.4.0-dev.1...v1.4.0-dev.2) (2024-11-01) diff --git a/gradle.properties b/gradle.properties index fd63a01..2ef9919 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.parallel = true org.gradle.caching = true kotlin.code.style = official -version = 1.4.0-dev.2 +version = 1.4.0-dev.3 From fc40427fbaafb523045eb6f5285d90949b206b8b Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sat, 2 Nov 2024 00:24:31 +0100 Subject: [PATCH 11/19] feat: Use tag name directly instead of ID --- .../repository/AnnouncementRepository.kt | 41 ++++++++++--------- .../api/configuration/routes/Announcements.kt | 23 ++++++----- .../api/configuration/schema/APISchema.kt | 3 +- .../services/AnnouncementService.kt | 6 +-- .../services/AnnouncementServiceTest.kt | 23 +++++------ 5 files changed, 48 insertions(+), 48 deletions(-) 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 d7ce97b..7ff88fe 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt @@ -6,7 +6,7 @@ import app.revanced.api.configuration.schema.ApiResponseAnnouncement import app.revanced.api.configuration.schema.ApiResponseAnnouncementId import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking -import kotlinx.datetime.* +import kotlinx.datetime.toKotlinLocalDateTime import org.jetbrains.exposed.dao.IntEntity import org.jetbrains.exposed.dao.IntEntityClass import org.jetbrains.exposed.dao.id.EntityID @@ -20,7 +20,7 @@ import java.time.LocalDateTime internal class AnnouncementRepository(private val database: Database) { // This is better than doing a maxByOrNull { it.id } on every request. private var latestAnnouncement: Announcement? = null - private val latestAnnouncementByTag = mutableMapOf() + private val latestAnnouncementByTag = mutableMapOf() init { runBlocking { @@ -40,22 +40,23 @@ internal class AnnouncementRepository(private val database: Database) { private fun initializeLatestAnnouncements() { 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) { if (latestAnnouncement == null || latestAnnouncement!!.id.value <= new.id.value) { latestAnnouncement = new - new.tags.forEach { tag -> latestAnnouncementByTag[tag.id.value] = new } + new.tags.forEach { tag -> latestAnnouncementByTag[tag.name] = new } } } - 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 } + private fun updateLatestAnnouncementForTag(tag: String) { + val latestAnnouncementForTag = Tags.innerJoin(AnnouncementTags) + .select(AnnouncementTags.announcement) + .where { Tags.name eq tag } + .orderBy(AnnouncementTags.announcement to SortOrder.DESC) + .limit(1) + .firstNotNullOfOrNull { Announcement.findById(it[AnnouncementTags.announcement]) } latestAnnouncementForTag?.let { latestAnnouncementByTag[tag] = it } } @@ -64,16 +65,16 @@ internal class AnnouncementRepository(private val database: Database) { latestAnnouncement.toApiResponseAnnouncement() } - suspend fun latest(tags: Set) = transaction { + suspend fun latest(tags: Set) = transaction { tags.mapNotNull { tag -> latestAnnouncementByTag[tag] }.toApiAnnouncement() } fun latestId() = latestAnnouncement?.id?.value.toApiResponseAnnouncementId() - fun latestId(tags: Set) = + fun latestId(tags: Set) = tags.map { tag -> latestAnnouncementByTag[tag]?.id?.value }.toApiResponseAnnouncementId() - suspend fun paged(cursor: Int, count: Int, tags: Set?, archived: Boolean) = transaction { + suspend fun paged(cursor: Int, count: Int, tags: Set?, archived: Boolean) = transaction { Announcement.find { fun idLessEq() = Announcements.id lessEq cursor fun archivedAtIsNull() = Announcements.archivedAt.isNull() @@ -92,12 +93,12 @@ internal class AnnouncementRepository(private val database: Database) { archivedAtIsNull() or archivedAtGreaterNow() } - fun hasTags() = tags.mapNotNull { Tag.findById(it)?.id }.let { tags -> - Announcements.id inSubQuery Announcements.leftJoin(AnnouncementTags) + fun hasTags() = Announcements.id inSubQuery ( + Tags.innerJoin(AnnouncementTags) .select(AnnouncementTags.announcement) - .where { AnnouncementTags.tag inList tags } + .where { Tags.name inList tags } .withDistinct() - } + ) idLessEq() and archivedAtGreaterOrNullOrTrue() and hasTags() } @@ -165,7 +166,7 @@ internal class AnnouncementRepository(private val database: Database) { // 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 + latestAnnouncementByTag -= tag.name tag.delete() } @@ -250,7 +251,7 @@ internal class AnnouncementRepository(private val database: Database) { title, content, attachments.map { it.url }, - tags.map { it.id.value }, + tags.map { it.name }, createdAt, archivedAt, level, @@ -259,7 +260,7 @@ internal class AnnouncementRepository(private val database: Database) { private fun Iterable.toApiAnnouncement() = map { it.toApiResponseAnnouncement()!! } - private fun Iterable.toApiTag() = map { ApiAnnouncementTag(it.id.value, it.name) } + private fun Iterable.toApiTag() = map { ApiAnnouncementTag(it.name) } private fun Int?.toApiResponseAnnouncementId() = this?.let { ApiResponseAnnouncementId(this) } diff --git a/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt b/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt index aeb1c0f..dd67e0f 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt @@ -8,7 +8,10 @@ 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.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.oas.payload.Parameter import io.ktor.http.* @@ -35,7 +38,7 @@ internal fun Route.announcementsRoute() = route("announcements") { 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(), archived)) } } @@ -55,7 +58,7 @@ internal fun Route.announcementsRoute() = route("announcements") { val tags = call.parameters.getAll("tag") if (tags?.isNotEmpty() == true) { - call.respond(announcementService.latest(tags.map { it.toInt() }.toSet())) + call.respond(announcementService.latest(tags.toSet())) } else { call.respondOrNotFound(announcementService.latest()) } @@ -68,7 +71,7 @@ internal fun Route.announcementsRoute() = route("announcements") { val tags = call.parameters.getAll("tag") if (tags?.isNotEmpty() == true) { - call.respond(announcementService.latestId(tags.map { it.toInt() }.toSet())) + call.respond(announcementService.latestId(tags.toSet())) } else { call.respondOrNotFound(announcementService.latestId()) } @@ -146,8 +149,8 @@ private fun Route.installAnnouncementsRouteDocumentation() = installNotarizedRou Parameter( name = "tag", `in` = Parameter.Location.query, - schema = TypeDefinition.INT, - description = "The tag IDs to filter the announcements by. Default is all tags", + schema = TypeDefinition.STRING, + description = "The tags to filter the announcements by. Default is all tags", required = false, ), Parameter( @@ -193,8 +196,8 @@ private fun Route.installAnnouncementsLatestRouteDocumentation() = installNotari Parameter( name = "tag", `in` = Parameter.Location.query, - schema = TypeDefinition.INT, - description = "The tag IDs to filter the latest announcements by", + schema = TypeDefinition.STRING, + description = "The tags to filter the latest announcements by", required = false, ), ) @@ -228,8 +231,8 @@ private fun Route.installAnnouncementsLatestIdRouteDocumentation() = installNota Parameter( name = "tag", `in` = Parameter.Location.query, - schema = TypeDefinition.INT, - description = "The tag IDs to filter the latest announcements by", + schema = TypeDefinition.STRING, + description = "The tags to filter the latest announcements by", required = false, ), ) 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 369dc16..2d3200d 100644 --- a/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt +++ b/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt @@ -76,7 +76,7 @@ class ApiResponseAnnouncement( // Using a list instead of a set because set semantics are unnecessary here. val attachments: List = emptyList(), // Using a list instead of a set because set semantics are unnecessary here. - val tags: List = emptyList(), + val tags: List = emptyList(), val createdAt: LocalDateTime, val archivedAt: LocalDateTime? = null, val level: Int = 0, @@ -94,7 +94,6 @@ class ApiAnnouncementArchivedAt( @Serializable class ApiAnnouncementTag( - val id: Int, val name: String, ) 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 434f0a5..7ba5970 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt @@ -6,15 +6,15 @@ import app.revanced.api.configuration.schema.ApiAnnouncement internal class AnnouncementService( private val announcementRepository: AnnouncementRepository, ) { - suspend fun latest(tags: Set) = announcementRepository.latest(tags) + suspend fun latest(tags: Set) = announcementRepository.latest(tags) suspend fun latest() = announcementRepository.latest() - fun latestId(tags: Set) = announcementRepository.latestId(tags) + fun latestId(tags: Set) = announcementRepository.latestId(tags) fun latestId() = announcementRepository.latestId() - suspend fun paged(cursor: Int, limit: Int, tags: Set?, archived: Boolean) = + suspend fun paged(cursor: Int, limit: Int, tags: Set?, archived: Boolean) = announcementRepository.paged(cursor, limit, tags, archived) suspend fun get(id: Int) = announcementRepository.get(id) diff --git a/src/test/kotlin/app/revanced/api/configuration/services/AnnouncementServiceTest.kt b/src/test/kotlin/app/revanced/api/configuration/services/AnnouncementServiceTest.kt index 3fdf4d6..d986b67 100644 --- a/src/test/kotlin/app/revanced/api/configuration/services/AnnouncementServiceTest.kt +++ b/src/test/kotlin/app/revanced/api/configuration/services/AnnouncementServiceTest.kt @@ -5,8 +5,10 @@ import app.revanced.api.configuration.schema.ApiAnnouncement import kotlinx.coroutines.runBlocking import kotlinx.datetime.toKotlinLocalDateTime import org.jetbrains.exposed.sql.Database -import org.junit.jupiter.api.* 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 kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -84,27 +86,22 @@ private object AnnouncementServiceTest { announcementService.new(ApiAnnouncement(title = "2", tags = listOf("tag1", "tag3"))) 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 - 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) + val announcement2and3 = announcementService.latest(setOf("tag1", "tag3")) assert(announcement2and3.size == 2) assert(announcement2and3.any { it.title == "2" }) assert(announcement2and3.any { it.title == "3" }) 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) - assert(announcementService.latest(tag1and3).first().title == "1") + assert(announcementService.latest(setOf("tag1", "tag3")).first().title == "1") announcementService.delete(announcementService.latestId()!!.id) - assert(announcementService.latest(tag1and3).isEmpty()) + assert(announcementService.latest(setOf("tag1", "tag3")).isEmpty()) assert(announcementService.tags().isEmpty()) } @@ -183,7 +180,7 @@ private object AnnouncementServiceTest { val tags = announcementService.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), true) assertEquals(4, announcements3.size, "Filters announcements by tag") val announcements4 = announcementService.paged(Int.MAX_VALUE, 10, null, false) From 8ad614ef4fdaf45af87a3316ef4db7e7236fd64a Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sat, 2 Nov 2024 00:57:31 +0100 Subject: [PATCH 12/19] feat: Remove "archived" query parameter It doesn't seem to be necessary for the purpose of viewing announcements. --- .../repository/AnnouncementRepository.kt | 24 ++++--------------- .../api/configuration/routes/Announcements.kt | 10 +------- .../services/AnnouncementService.kt | 4 ++-- .../services/AnnouncementServiceTest.kt | 9 +++---- 4 files changed, 11 insertions(+), 36 deletions(-) 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 7ff88fe..b425a86 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt @@ -6,7 +6,6 @@ import app.revanced.api.configuration.schema.ApiResponseAnnouncement import app.revanced.api.configuration.schema.ApiResponseAnnouncementId import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking -import kotlinx.datetime.toKotlinLocalDateTime import org.jetbrains.exposed.dao.IntEntity import org.jetbrains.exposed.dao.IntEntityClass import org.jetbrains.exposed.dao.id.EntityID @@ -15,7 +14,6 @@ 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 java.time.LocalDateTime internal class AnnouncementRepository(private val database: Database) { // This is better than doing a maxByOrNull { it.id } on every request. @@ -74,33 +72,21 @@ internal class AnnouncementRepository(private val database: Database) { fun latestId(tags: Set) = tags.map { tag -> latestAnnouncementByTag[tag]?.id?.value }.toApiResponseAnnouncementId() - suspend fun paged(cursor: Int, count: Int, tags: Set?, archived: Boolean) = transaction { + suspend fun paged(cursor: Int, count: Int, tags: Set?) = transaction { Announcement.find { fun idLessEq() = Announcements.id lessEq cursor - fun archivedAtIsNull() = Announcements.archivedAt.isNull() - fun archivedAtGreaterNow() = Announcements.archivedAt greater LocalDateTime.now().toKotlinLocalDateTime() if (tags == null) { - if (archived) { - idLessEq() - } else { - idLessEq() and (archivedAtIsNull() or archivedAtGreaterNow()) - } + idLessEq() } else { - fun archivedAtGreaterOrNullOrTrue() = if (archived) { - Op.TRUE - } else { - archivedAtIsNull() or archivedAtGreaterNow() - } - fun hasTags() = Announcements.id inSubQuery ( - Tags.innerJoin(AnnouncementTags) + AnnouncementTags.innerJoin(Tags) .select(AnnouncementTags.announcement) - .where { Tags.name inList tags } .withDistinct() + .where { Tags.name inList tags } ) - idLessEq() and archivedAtGreaterOrNullOrTrue() and hasTags() + idLessEq() and hasTags() } }.orderBy(Announcements.id to SortOrder.DESC).limit(count).toApiAnnouncement() } diff --git a/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt b/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt index dd67e0f..126b17c 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt @@ -36,9 +36,8 @@ internal fun Route.announcementsRoute() = route("announcements") { val cursor = call.parameters["cursor"]?.toInt() ?: Int.MAX_VALUE val count = call.parameters["count"]?.toInt() ?: 16 val tags = call.parameters.getAll("tag") - val archived = call.parameters["archived"]?.toBoolean() ?: true - call.respond(announcementService.paged(cursor, count, tags?.toSet(), archived)) + call.respond(announcementService.paged(cursor, count, tags?.toSet())) } } @@ -153,13 +152,6 @@ private fun Route.installAnnouncementsRouteDocumentation() = installNotarizedRou 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, - ), ) response { responseCode(HttpStatusCode.OK) 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 7ba5970..ab0b804 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt @@ -14,8 +14,8 @@ internal class AnnouncementService( fun latestId() = announcementRepository.latestId() - suspend fun paged(cursor: Int, limit: Int, tags: Set?, archived: Boolean) = - announcementRepository.paged(cursor, limit, tags, archived) + suspend fun paged(cursor: Int, limit: Int, tags: Set?) = + announcementRepository.paged(cursor, limit, tags) suspend fun get(id: Int) = announcementRepository.get(id) diff --git a/src/test/kotlin/app/revanced/api/configuration/services/AnnouncementServiceTest.kt b/src/test/kotlin/app/revanced/api/configuration/services/AnnouncementServiceTest.kt index d986b67..5d18646 100644 --- a/src/test/kotlin/app/revanced/api/configuration/services/AnnouncementServiceTest.kt +++ b/src/test/kotlin/app/revanced/api/configuration/services/AnnouncementServiceTest.kt @@ -153,11 +153,11 @@ private object AnnouncementServiceTest { 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("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("title4", announcements2.first().title, "Starts from the cursor") @@ -180,10 +180,7 @@ private object AnnouncementServiceTest { val tags = announcementService.tags() assertEquals(5, tags.size, "Returns correct number of newly created tags") - val announcements3 = announcementService.paged(5, 5, setOf(tags[1].name), true) + val announcements3 = announcementService.paged(5, 5, setOf(tags[1].name)) 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") } } From bf41fa15967a0f2c162c8fdb6cf108974bc0d297 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 1 Nov 2024 23:59:33 +0000 Subject: [PATCH 13/19] 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](https://github.com/ReVanced/revanced-api/commit/8ad614ef4fdaf45af87a3316ef4db7e7236fd64a)) * Use tag name directly instead of ID ([fc40427](https://github.com/ReVanced/revanced-api/commit/fc40427fbaafb523045eb6f5285d90949b206b8b)) --- CHANGELOG.md | 8 ++++++++ gradle.properties | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6bc0f3..ed7fc35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# [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) diff --git a/gradle.properties b/gradle.properties index 2ef9919..a6861dc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.parallel = true org.gradle.caching = true kotlin.code.style = official -version = 1.4.0-dev.3 +version = 1.4.0-dev.4 From 0ddbf5beda20483750123943b08112cfb25afb3b Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Tue, 5 Nov 2024 19:58:20 +0100 Subject: [PATCH 14/19] build(Needs bump): Bump dependencies --- gradle/libs.versions.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2816c9e..8cb1894 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] kompendium-core = "3.14.4" -kotlin = "2.0.0" +kotlin = "2.0.20" logback = "1.5.6" exposed = "0.52.0" h2 = "2.2.224" @@ -10,8 +10,8 @@ ktor = "2.3.7" ktoml = "0.5.2" picocli = "4.7.6" datetime = "0.6.0" -revanced-patcher = "20.0.0" -revanced-library = "3.0.1-dev.1" +revanced-patcher = "21.0.0" +revanced-library = "3.0.2" caffeine = "3.1.8" bouncy-castle = "1.78.1" From 1a09b028b749d5b2ce9c29792ca948c73e53701b Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 5 Nov 2024 19:01:39 +0000 Subject: [PATCH 15/19] 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) --- CHANGELOG.md | 2 ++ gradle.properties | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed7fc35..f5357e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +# [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) diff --git a/gradle.properties b/gradle.properties index a6861dc..764c872 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.parallel = true org.gradle.caching = true kotlin.code.style = official -version = 1.4.0-dev.4 +version = 1.4.0-dev.5 From 0b66fc2bcadabb780823816874228259378af5c5 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 6 Nov 2024 01:38:02 +0100 Subject: [PATCH 16/19] refactor: Move file to upper package --- .../revanced/api/configuration/{schema => }/APISchema.kt | 2 +- .../configuration/repository/AnnouncementRepository.kt | 8 ++++---- .../configuration/repository/ConfigurationRepository.kt | 2 +- .../revanced/api/configuration/routes/Announcements.kt | 6 +++--- .../app/revanced/api/configuration/routes/ApiRoute.kt | 1 - .../app/revanced/api/configuration/routes/ManagerRoute.kt | 4 ++-- .../app/revanced/api/configuration/routes/PatchesRoute.kt | 6 +++--- .../api/configuration/services/AnnouncementService.kt | 2 +- .../app/revanced/api/configuration/services/ApiService.kt | 2 +- .../api/configuration/services/AuthenticationService.kt | 2 +- .../revanced/api/configuration/services/ManagerService.kt | 4 ++-- .../revanced/api/configuration/services/PatchesService.kt | 6 +++--- .../api/configuration/services/AnnouncementServiceTest.kt | 2 +- 13 files changed, 23 insertions(+), 24 deletions(-) rename src/main/kotlin/app/revanced/api/configuration/{schema => }/APISchema.kt (98%) diff --git a/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt b/src/main/kotlin/app/revanced/api/configuration/APISchema.kt similarity index 98% rename from src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt rename to src/main/kotlin/app/revanced/api/configuration/APISchema.kt index 2d3200d..c4432ae 100644 --- a/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt +++ b/src/main/kotlin/app/revanced/api/configuration/APISchema.kt @@ -1,4 +1,4 @@ -package app.revanced.api.configuration.schema +package app.revanced.api.configuration import kotlinx.datetime.LocalDateTime import kotlinx.serialization.Serializable 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 b425a86..01d74e2 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt @@ -1,9 +1,9 @@ package app.revanced.api.configuration.repository -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 app.revanced.api.configuration.ApiAnnouncement +import app.revanced.api.configuration.ApiAnnouncementTag +import app.revanced.api.configuration.ApiResponseAnnouncement +import app.revanced.api.configuration.ApiResponseAnnouncementId import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import org.jetbrains.exposed.dao.IntEntity diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt index 4e78c71..e898857 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt @@ -1,6 +1,6 @@ package app.revanced.api.configuration.repository -import app.revanced.api.configuration.schema.APIAbout +import app.revanced.api.configuration.APIAbout import app.revanced.api.configuration.services.ManagerService import app.revanced.api.configuration.services.PatchesService import kotlinx.serialization.ExperimentalSerializationApi diff --git a/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt b/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt index 126b17c..bf759fe 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt @@ -1,12 +1,12 @@ package app.revanced.api.configuration.routes +import app.revanced.api.configuration.ApiAnnouncement +import app.revanced.api.configuration.ApiResponseAnnouncement +import app.revanced.api.configuration.ApiResponseAnnouncementId import app.revanced.api.configuration.canRespondUnauthorized import app.revanced.api.configuration.installCache import app.revanced.api.configuration.installNotarizedRoute import app.revanced.api.configuration.respondOrNotFound -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.services.AnnouncementService import io.bkbn.kompendium.core.metadata.DeleteInfo import io.bkbn.kompendium.core.metadata.GetInfo diff --git a/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt index 862a4d9..423c3e6 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt @@ -6,7 +6,6 @@ import app.revanced.api.configuration.installNoCache import app.revanced.api.configuration.installNotarizedRoute import app.revanced.api.configuration.repository.ConfigurationRepository import app.revanced.api.configuration.respondOrNotFound -import app.revanced.api.configuration.schema.* import app.revanced.api.configuration.services.ApiService import app.revanced.api.configuration.services.AuthenticationService import io.bkbn.kompendium.core.metadata.* diff --git a/src/main/kotlin/app/revanced/api/configuration/routes/ManagerRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routes/ManagerRoute.kt index 04f217f..0217c4b 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routes/ManagerRoute.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/ManagerRoute.kt @@ -1,8 +1,8 @@ package app.revanced.api.configuration.routes +import app.revanced.api.configuration.ApiRelease +import app.revanced.api.configuration.ApiReleaseVersion import app.revanced.api.configuration.installNotarizedRoute -import app.revanced.api.configuration.schema.ApiRelease -import app.revanced.api.configuration.schema.ApiReleaseVersion import app.revanced.api.configuration.services.ManagerService import io.bkbn.kompendium.core.metadata.GetInfo import io.ktor.http.* diff --git a/src/main/kotlin/app/revanced/api/configuration/routes/PatchesRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routes/PatchesRoute.kt index ccf3529..59086d0 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routes/PatchesRoute.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/PatchesRoute.kt @@ -1,10 +1,10 @@ package app.revanced.api.configuration.routes +import app.revanced.api.configuration.ApiAssetPublicKey +import app.revanced.api.configuration.ApiRelease +import app.revanced.api.configuration.ApiReleaseVersion import app.revanced.api.configuration.installCache import app.revanced.api.configuration.installNotarizedRoute -import app.revanced.api.configuration.schema.ApiAssetPublicKey -import app.revanced.api.configuration.schema.ApiRelease -import app.revanced.api.configuration.schema.ApiReleaseVersion import app.revanced.api.configuration.services.PatchesService import io.bkbn.kompendium.core.metadata.GetInfo import io.ktor.http.* 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 ab0b804..b32d9f9 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt @@ -1,7 +1,7 @@ package app.revanced.api.configuration.services +import app.revanced.api.configuration.ApiAnnouncement import app.revanced.api.configuration.repository.AnnouncementRepository -import app.revanced.api.configuration.schema.ApiAnnouncement internal class AnnouncementService( private val announcementRepository: AnnouncementRepository, 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 0e12f12..8825292 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt @@ -1,8 +1,8 @@ package app.revanced.api.configuration.services +import app.revanced.api.configuration.* import app.revanced.api.configuration.repository.BackendRepository import app.revanced.api.configuration.repository.ConfigurationRepository -import app.revanced.api.configuration.schema.* import io.ktor.http.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async diff --git a/src/main/kotlin/app/revanced/api/configuration/services/AuthenticationService.kt b/src/main/kotlin/app/revanced/api/configuration/services/AuthenticationService.kt index a3150b1..b691ada 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/AuthenticationService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/AuthenticationService.kt @@ -1,6 +1,6 @@ package app.revanced.api.configuration.services -import app.revanced.api.configuration.schema.ApiToken +import app.revanced.api.configuration.ApiToken import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm import io.ktor.server.auth.* diff --git a/src/main/kotlin/app/revanced/api/configuration/services/ManagerService.kt b/src/main/kotlin/app/revanced/api/configuration/services/ManagerService.kt index c180043..af53621 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/ManagerService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/ManagerService.kt @@ -1,10 +1,10 @@ package app.revanced.api.configuration.services +import app.revanced.api.configuration.ApiRelease +import app.revanced.api.configuration.ApiReleaseVersion import app.revanced.api.configuration.repository.BackendRepository import app.revanced.api.configuration.repository.BackendRepository.BackendOrganization.BackendRepository.BackendRelease.Companion.first import app.revanced.api.configuration.repository.ConfigurationRepository -import app.revanced.api.configuration.schema.ApiRelease -import app.revanced.api.configuration.schema.ApiReleaseVersion internal class ManagerService( private val backendRepository: BackendRepository, 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 08b1fb9..b9d3608 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt @@ -1,11 +1,11 @@ package app.revanced.api.configuration.services +import app.revanced.api.configuration.ApiAssetPublicKey +import app.revanced.api.configuration.ApiRelease +import app.revanced.api.configuration.ApiReleaseVersion import app.revanced.api.configuration.repository.BackendRepository import app.revanced.api.configuration.repository.BackendRepository.BackendOrganization.BackendRepository.BackendRelease.Companion.first import app.revanced.api.configuration.repository.ConfigurationRepository -import app.revanced.api.configuration.schema.ApiAssetPublicKey -import app.revanced.api.configuration.schema.ApiRelease -import app.revanced.api.configuration.schema.ApiReleaseVersion import app.revanced.library.serializeTo import app.revanced.patcher.patch.loadPatchesFromJar import com.github.benmanes.caffeine.cache.Caffeine diff --git a/src/test/kotlin/app/revanced/api/configuration/services/AnnouncementServiceTest.kt b/src/test/kotlin/app/revanced/api/configuration/services/AnnouncementServiceTest.kt index 5d18646..b2007a7 100644 --- a/src/test/kotlin/app/revanced/api/configuration/services/AnnouncementServiceTest.kt +++ b/src/test/kotlin/app/revanced/api/configuration/services/AnnouncementServiceTest.kt @@ -1,7 +1,7 @@ package app.revanced.api.configuration.services +import app.revanced.api.configuration.ApiAnnouncement import app.revanced.api.configuration.repository.AnnouncementRepository -import app.revanced.api.configuration.schema.ApiAnnouncement import kotlinx.coroutines.runBlocking import kotlinx.datetime.toKotlinLocalDateTime import org.jetbrains.exposed.sql.Database From eca40a69799240f7803aa8851eb3ee961937e4d6 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 6 Nov 2024 04:26:51 +0100 Subject: [PATCH 17/19] feat: Remove deprecated routes and old API Backwards compatibility should be and is now handled by the reverse proxy. --- README.md | 1 - configuration.example.toml | 1 - .../api/configuration/Dependencies.kt | 1 - .../app/revanced/api/configuration/Routing.kt | 4 - .../repository/ConfigurationRepository.kt | 3 - .../api/configuration/routes/ManagerRoute.kt | 19 +---- .../api/configuration/routes/OldApi.kt | 19 ----- .../api/configuration/routes/PatchesRoute.kt | 29 ++----- .../configuration/services/OldApiService.kt | 76 ------------------- 9 files changed, 12 insertions(+), 141 deletions(-) delete mode 100644 src/main/kotlin/app/revanced/api/configuration/routes/OldApi.kt delete mode 100644 src/main/kotlin/app/revanced/api/configuration/services/OldApiService.kt diff --git a/README.md b/README.md index f3c5fac..ed9513c 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,6 @@ Some of the features ReVanced API include: and links of the hoster of ReVanced API - 🧩 **Patches**: Get the latest updates of ReVanced Patches, directly from ReVanced API - đŸ‘Ĩ **Contributors**: List all contributors involved in the project -- 🔄 **Backwards compatibility**: Proxy an old API for migration purposes and backwards compatibility ## 🚀 How to get started diff --git a/configuration.example.toml b/configuration.example.toml index 2e2f28f..9e4ac10 100644 --- a/configuration.example.toml +++ b/configuration.example.toml @@ -4,7 +4,6 @@ cors-allowed-hosts = [ "*.revanced.app" ] endpoint = "https://api.revanced.app" -old-api-endpoint = "https://old-api.revanced.app" static-files-path = "static/root" versioned-static-files-path = "static/versioned" backend-service-name = "GitHub" diff --git a/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt b/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt index 71a0d21..630cf74 100644 --- a/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt +++ b/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt @@ -51,7 +51,6 @@ fun Application.configureDependencies( AuthenticationService(issuer, validityInMin, jwtSecret, authSHA256DigestString) } - singleOf(::OldApiService) singleOf(::AnnouncementService) singleOf(::SignatureService) singleOf(::PatchesService) diff --git a/src/main/kotlin/app/revanced/api/configuration/Routing.kt b/src/main/kotlin/app/revanced/api/configuration/Routing.kt index 1e0f467..2ed8106 100644 --- a/src/main/kotlin/app/revanced/api/configuration/Routing.kt +++ b/src/main/kotlin/app/revanced/api/configuration/Routing.kt @@ -4,7 +4,6 @@ import app.revanced.api.configuration.repository.ConfigurationRepository import app.revanced.api.configuration.routes.* import app.revanced.api.configuration.routes.announcementsRoute import app.revanced.api.configuration.routes.apiRoute -import app.revanced.api.configuration.routes.oldApiRoute import app.revanced.api.configuration.routes.patchesRoute import io.bkbn.kompendium.core.routes.redoc import io.bkbn.kompendium.core.routes.swagger @@ -55,7 +54,4 @@ internal fun Application.configureRouting() = routing { swagger(pageTitle = "ReVanced API", path = "/") redoc(pageTitle = "ReVanced API", path = "/redoc") - - // TODO: Remove, once migration period from v2 API is over (In 1-2 years). - oldApiRoute() } diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt index e898857..81f8a76 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt @@ -30,7 +30,6 @@ import kotlin.io.path.createDirectories * @property apiVersion The version to use for the API. * @property corsAllowedHosts The hosts allowed to make requests to the API. * @property endpoint The endpoint of the API. - * @property oldApiEndpoint The endpoint of the old API to proxy requests to. * @property staticFilesPath The path to the static files to be served under the root path. * @property versionedStaticFilesPath The path to the static files to be served under a versioned path. * @property about The path to the json file deserialized to [APIAbout] @@ -50,8 +49,6 @@ internal class ConfigurationRepository( @SerialName("cors-allowed-hosts") val corsAllowedHosts: Set, val endpoint: String, - @SerialName("old-api-endpoint") - val oldApiEndpoint: String, @Serializable(with = PathSerializer::class) @SerialName("static-files-path") val staticFilesPath: Path, diff --git a/src/main/kotlin/app/revanced/api/configuration/routes/ManagerRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routes/ManagerRoute.kt index 0217c4b..c82c0fb 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routes/ManagerRoute.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/ManagerRoute.kt @@ -13,18 +13,9 @@ import io.ktor.server.routing.* import org.koin.ktor.ext.get as koinGet internal fun Route.managerRoute() = route("manager") { - configure() - - // TODO: Remove this deprecated route eventually. - route("latest") { - configure(deprecated = true) - } -} - -private fun Route.configure(deprecated: Boolean = false) { val managerService = koinGet() - installManagerRouteDocumentation(deprecated) + installManagerRouteDocumentation() rateLimit(RateLimitName("weak")) { get { @@ -32,7 +23,7 @@ private fun Route.configure(deprecated: Boolean = false) { } route("version") { - installManagerVersionRouteDocumentation(deprecated) + installManagerVersionRouteDocumentation() get { call.respond(managerService.latestVersion()) @@ -41,11 +32,10 @@ private fun Route.configure(deprecated: Boolean = false) { } } -private fun Route.installManagerRouteDocumentation(deprecated: Boolean) = installNotarizedRoute { +private fun Route.installManagerRouteDocumentation() = installNotarizedRoute { tags = setOf("Manager") get = GetInfo.builder { - if (deprecated) isDeprecated() description("Get the current manager release") summary("Get current manager release") response { @@ -57,11 +47,10 @@ private fun Route.installManagerRouteDocumentation(deprecated: Boolean) = instal } } -private fun Route.installManagerVersionRouteDocumentation(deprecated: Boolean) = installNotarizedRoute { +private fun Route.installManagerVersionRouteDocumentation() = installNotarizedRoute { tags = setOf("Manager") get = GetInfo.builder { - if (deprecated) isDeprecated() description("Get the current manager release version") summary("Get current manager release version") response { diff --git a/src/main/kotlin/app/revanced/api/configuration/routes/OldApi.kt b/src/main/kotlin/app/revanced/api/configuration/routes/OldApi.kt deleted file mode 100644 index e72f470..0000000 --- a/src/main/kotlin/app/revanced/api/configuration/routes/OldApi.kt +++ /dev/null @@ -1,19 +0,0 @@ -package app.revanced.api.configuration.routes - -import app.revanced.api.configuration.services.OldApiService -import io.ktor.server.application.* -import io.ktor.server.plugins.ratelimit.* -import io.ktor.server.routing.* -import org.koin.ktor.ext.get - -internal fun Route.oldApiRoute() { - val oldApiService = get() - - rateLimit(RateLimitName("weak")) { - route(Regex("/(v2|tools|contributors).*")) { - handle { - oldApiService.proxy(call) - } - } - } -} diff --git a/src/main/kotlin/app/revanced/api/configuration/routes/PatchesRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routes/PatchesRoute.kt index 59086d0..9672d9e 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routes/PatchesRoute.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/PatchesRoute.kt @@ -16,18 +16,9 @@ import kotlin.time.Duration.Companion.days import org.koin.ktor.ext.get as koinGet internal fun Route.patchesRoute() = route("patches") { - configure() - - // TODO: Remove this deprecated route eventually. - route("latest") { - configure(deprecated = true) - } -} - -private fun Route.configure(deprecated: Boolean = false) { val patchesService = koinGet() - installPatchesRouteDocumentation(deprecated) + installPatchesRouteDocumentation() rateLimit(RateLimitName("weak")) { get { @@ -35,7 +26,7 @@ private fun Route.configure(deprecated: Boolean = false) { } route("version") { - installPatchesVersionRouteDocumentation(deprecated) + installPatchesVersionRouteDocumentation() get { call.respond(patchesService.latestVersion()) @@ -45,7 +36,7 @@ private fun Route.configure(deprecated: Boolean = false) { rateLimit(RateLimitName("strong")) { route("list") { - installPatchesListRouteDocumentation(deprecated) + installPatchesListRouteDocumentation() get { call.respondBytes(ContentType.Application.Json) { patchesService.list() } @@ -57,7 +48,7 @@ private fun Route.configure(deprecated: Boolean = false) { route("keys") { installCache(356.days) - installPatchesPublicKeyRouteDocumentation(deprecated) + installPatchesPublicKeyRouteDocumentation() get { call.respond(patchesService.publicKey()) @@ -66,11 +57,10 @@ private fun Route.configure(deprecated: Boolean = false) { } } -private fun Route.installPatchesRouteDocumentation(deprecated: Boolean) = installNotarizedRoute { +private fun Route.installPatchesRouteDocumentation() = installNotarizedRoute { tags = setOf("Patches") get = GetInfo.builder { - if (deprecated) isDeprecated() description("Get the current patches release") summary("Get current patches release") response { @@ -82,11 +72,10 @@ private fun Route.installPatchesRouteDocumentation(deprecated: Boolean) = instal } } -private fun Route.installPatchesVersionRouteDocumentation(deprecated: Boolean) = installNotarizedRoute { +private fun Route.installPatchesVersionRouteDocumentation() = installNotarizedRoute { tags = setOf("Patches") get = GetInfo.builder { - if (deprecated) isDeprecated() description("Get the current patches release version") summary("Get current patches release version") response { @@ -98,11 +87,10 @@ private fun Route.installPatchesVersionRouteDocumentation(deprecated: Boolean) = } } -private fun Route.installPatchesListRouteDocumentation(deprecated: Boolean) = installNotarizedRoute { +private fun Route.installPatchesListRouteDocumentation() = installNotarizedRoute { tags = setOf("Patches") get = GetInfo.builder { - if (deprecated) isDeprecated() description("Get the list of patches from the current patches release") summary("Get list of patches from current patches release") response { @@ -114,11 +102,10 @@ private fun Route.installPatchesListRouteDocumentation(deprecated: Boolean) = in } } -private fun Route.installPatchesPublicKeyRouteDocumentation(deprecated: Boolean) = installNotarizedRoute { +private fun Route.installPatchesPublicKeyRouteDocumentation() = installNotarizedRoute { tags = setOf("Patches") get = GetInfo.builder { - if (deprecated) isDeprecated() description("Get the public keys for verifying patches assets") summary("Get patches public keys") response { diff --git a/src/main/kotlin/app/revanced/api/configuration/services/OldApiService.kt b/src/main/kotlin/app/revanced/api/configuration/services/OldApiService.kt deleted file mode 100644 index 271f6f5..0000000 --- a/src/main/kotlin/app/revanced/api/configuration/services/OldApiService.kt +++ /dev/null @@ -1,76 +0,0 @@ -package app.revanced.api.configuration.services - -import app.revanced.api.configuration.repository.ConfigurationRepository -import io.ktor.client.* -import io.ktor.client.engine.okhttp.* -import io.ktor.client.plugins.* -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(configurationRepository: ConfigurationRepository) { - private val client = HttpClient(OkHttp) { - defaultRequest { url(configurationRepository.oldApiEndpoint) } - } - - @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) - ) - }, - ) - } - - when (call.request.httpMethod) { - HttpMethod.Post, - HttpMethod.Put, - HttpMethod.Patch, - HttpMethod.Delete, - -> 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) - } - }) - } -} From 814d3c946e31068e12e3886aa8beb3238ef126ae Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 6 Nov 2024 04:28:26 +0100 Subject: [PATCH 18/19] feat: Allow versioning by arbitrary path string --- configuration.example.toml | 2 +- src/main/kotlin/app/revanced/api/configuration/Routing.kt | 2 +- .../api/configuration/repository/ConfigurationRepository.kt | 2 +- .../kotlin/app/revanced/api/configuration/routes/ApiRoute.kt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/configuration.example.toml b/configuration.example.toml index 9e4ac10..71f983b 100644 --- a/configuration.example.toml +++ b/configuration.example.toml @@ -1,4 +1,4 @@ -api-version = 1 +api-version = "v1" cors-allowed-hosts = [ "revanced.app", "*.revanced.app" diff --git a/src/main/kotlin/app/revanced/api/configuration/Routing.kt b/src/main/kotlin/app/revanced/api/configuration/Routing.kt index 2ed8106..da1af7c 100644 --- a/src/main/kotlin/app/revanced/api/configuration/Routing.kt +++ b/src/main/kotlin/app/revanced/api/configuration/Routing.kt @@ -18,7 +18,7 @@ internal fun Application.configureRouting() = routing { installCache(5.minutes) - route("/v${configuration.apiVersion}") { + route("/${configuration.apiVersion}") { announcementsRoute() patchesRoute() managerRoute() diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt index 81f8a76..4c46887 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt @@ -45,7 +45,7 @@ internal class ConfigurationRepository( @SerialName("backend-service-name") val backendServiceName: String, @SerialName("api-version") - val apiVersion: Int = 1, + val apiVersion: String = "v1", @SerialName("cors-allowed-hosts") val corsAllowedHosts: Set, val endpoint: String, diff --git a/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt index 423c3e6..f5311b1 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt @@ -183,7 +183,7 @@ private fun Route.installTokenRouteDocumentation() = installNotarizedRoute { "username=\"ReVanced\", " + "realm=\"ReVanced\", " + "nonce=\"abc123\", " + - "uri=\"/v${configuration.apiVersion}/token\", " + + "uri=\"/${configuration.apiVersion}/token\", " + "algorithm=SHA-256, " + "response=\"yxz456\"", ), From f74012993e2ee3ff00e53bde34fd3bef2ff1c502 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 6 Nov 2024 03:39:26 +0000 Subject: [PATCH 19/19] chore: Release v1.4.0-dev.6 [skip ci] # [1.4.0-dev.6](https://github.com/ReVanced/revanced-api/compare/v1.4.0-dev.5...v1.4.0-dev.6) (2024-11-06) ### Features * Allow versioning by arbitrary path string ([814d3c9](https://github.com/ReVanced/revanced-api/commit/814d3c946e31068e12e3886aa8beb3238ef126ae)) * Remove deprecated routes and old API ([eca40a6](https://github.com/ReVanced/revanced-api/commit/eca40a69799240f7803aa8851eb3ee961937e4d6)) --- CHANGELOG.md | 8 ++++++++ gradle.properties | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5357e0..6a05a0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# [1.4.0-dev.6](https://github.com/ReVanced/revanced-api/compare/v1.4.0-dev.5...v1.4.0-dev.6) (2024-11-06) + + +### Features + +* Allow versioning by arbitrary path string ([814d3c9](https://github.com/ReVanced/revanced-api/commit/814d3c946e31068e12e3886aa8beb3238ef126ae)) +* Remove deprecated routes and old API ([eca40a6](https://github.com/ReVanced/revanced-api/commit/eca40a69799240f7803aa8851eb3ee961937e4d6)) + # [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) diff --git a/gradle.properties b/gradle.properties index 764c872..01a9c8d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.parallel = true org.gradle.caching = true kotlin.code.style = official -version = 1.4.0-dev.5 +version = 1.4.0-dev.6