Compare commits

..

No commits in common. "main" and "v1.4.0-dev.3" have entirely different histories.

29 changed files with 307 additions and 319 deletions

View File

@ -1,105 +1,3 @@
# [1.6.0](https://github.com/ReVanced/revanced-api/compare/v1.5.0...v1.6.0) (2025-02-04)
### Features
* Add status page link to about ([8a957cd](https://github.com/ReVanced/revanced-api/commit/8a957cd797e7e42f43670baaed60ac0d3543342f))
* Add support for prereleases ([c25bc8b](https://github.com/ReVanced/revanced-api/commit/c25bc8b4ba2bd4bf1708f19dc8bc228a7f54d548))
* Allow setting `Announcement.createdAt` when creating an announcement ([7f6e29d](https://github.com/ReVanced/revanced-api/commit/7f6e29de5205f63ac4aaea490c844b58e14000c8))
* Make some announcements schema fields nullable ([db22874](https://github.com/ReVanced/revanced-api/commit/db22874f063bae0c9e7f0c99a20cdf1b16addd89))
# [1.6.0-dev.3](https://github.com/ReVanced/revanced-api/compare/v1.6.0-dev.2...v1.6.0-dev.3) (2024-12-25)
### Features
* Add status page link to about ([8a957cd](https://github.com/ReVanced/revanced-api/commit/8a957cd797e7e42f43670baaed60ac0d3543342f))
* Add support for prereleases ([c25bc8b](https://github.com/ReVanced/revanced-api/commit/c25bc8b4ba2bd4bf1708f19dc8bc228a7f54d548))
# [1.6.0-dev.2](https://github.com/ReVanced/revanced-api/compare/v1.6.0-dev.1...v1.6.0-dev.2) (2024-12-20)
### Features
* Make some announcements schema fields nullable ([db22874](https://github.com/ReVanced/revanced-api/commit/db22874f063bae0c9e7f0c99a20cdf1b16addd89))
# [1.6.0-dev.1](https://github.com/ReVanced/revanced-api/compare/v1.5.0...v1.6.0-dev.1) (2024-11-23)
### Features
* Allow setting `Announcement.createdAt` when creating an announcement ([7f6e29d](https://github.com/ReVanced/revanced-api/commit/7f6e29de5205f63ac4aaea490c844b58e14000c8))
# [1.5.0](https://github.com/ReVanced/revanced-api/compare/v1.4.0...v1.5.0) (2024-11-06)
### Features
* Allow updating `createdAt` field for announcements ([58ba4cb](https://github.com/ReVanced/revanced-api/commit/58ba4cb11c789507826cd70ac548943a94da4223))
* Move spec url to versioned path ([e871b23](https://github.com/ReVanced/revanced-api/commit/e871b23210798723c34bce93c7567d8fbcf4e060))
* Simplify log pattern ([d5d9e04](https://github.com/ReVanced/revanced-api/commit/d5d9e04325fa93540be0438e7b51243e2aeeab3d))
# [1.5.0-dev.2](https://github.com/ReVanced/revanced-api/compare/v1.5.0-dev.1...v1.5.0-dev.2) (2024-11-06)
### Features
* Allow updating `createdAt` field for announcements ([58ba4cb](https://github.com/ReVanced/revanced-api/commit/58ba4cb11c789507826cd70ac548943a94da4223))
* Simplify log pattern ([d5d9e04](https://github.com/ReVanced/revanced-api/commit/d5d9e04325fa93540be0438e7b51243e2aeeab3d))
# [1.5.0-dev.1](https://github.com/ReVanced/revanced-api/compare/v1.4.0...v1.5.0-dev.1) (2024-11-06)
### Features
* Move spec url to versioned path ([e871b23](https://github.com/ReVanced/revanced-api/commit/e871b23210798723c34bce93c7567d8fbcf4e060))
* Simplify log pattern ([d5d9e04](https://github.com/ReVanced/revanced-api/commit/d5d9e04325fa93540be0438e7b51243e2aeeab3d))
# [1.5.0-dev.1](https://github.com/ReVanced/revanced-api/compare/v1.4.0...v1.5.0-dev.1) (2024-11-06)
### Features
* Move spec url to versioned path ([e871b23](https://github.com/ReVanced/revanced-api/commit/e871b23210798723c34bce93c7567d8fbcf4e060))
# [1.4.0](https://github.com/ReVanced/revanced-api/compare/v1.3.0...v1.4.0) (2024-11-06)
### Bug Fixes
* Add missing logging level environment variable to .env.example ([3b62120](https://github.com/ReVanced/revanced-api/commit/3b6212065a5cfb95c303b6d0551747ba1eb317f6))
* Use new patches file extension ([d42a3a3](https://github.com/ReVanced/revanced-api/commit/d42a3a393396a0f4e9085cda46e0af2c12b63cb1))
### Features
* Add URL and use friendly name for `APIContributable` ([a5498ab](https://github.com/ReVanced/revanced-api/commit/a5498aba2b99db89c28a65738cc58cc4c852c327))
* Allow versioning by arbitrary path string ([814d3c9](https://github.com/ReVanced/revanced-api/commit/814d3c946e31068e12e3886aa8beb3238ef126ae))
* Improve announcements API ([#192](https://github.com/ReVanced/revanced-api/issues/192)) ([56a00dd](https://github.com/ReVanced/revanced-api/commit/56a00ddb85f302d441f0b222a9902ea2c1c18897))
* Make backend configurable ([f91f3a6](https://github.com/ReVanced/revanced-api/commit/f91f3a65c5e07b5b58ccbff1d4b0a5ba9b15fc50))
* Remove "archived" query parameter ([8ad614e](https://github.com/ReVanced/revanced-api/commit/8ad614ef4fdaf45af87a3316ef4db7e7236fd64a))
* Remove deprecated routes and old API ([eca40a6](https://github.com/ReVanced/revanced-api/commit/eca40a69799240f7803aa8851eb3ee961937e4d6))
* Remove ReVanced Integrations ([f1c1092](https://github.com/ReVanced/revanced-api/commit/f1c10928ae3be1c6b1d675819755b3046fad70d8))
* Use tag name directly instead of ID ([fc40427](https://github.com/ReVanced/revanced-api/commit/fc40427fbaafb523045eb6f5285d90949b206b8b))
# [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)
### Features
* Remove "archived" query parameter ([8ad614e](https://github.com/ReVanced/revanced-api/commit/8ad614ef4fdaf45af87a3316ef4db7e7236fd64a))
* Use tag name directly instead of ID ([fc40427](https://github.com/ReVanced/revanced-api/commit/fc40427fbaafb523045eb6f5285d90949b206b8b))
# [1.4.0-dev.3](https://github.com/ReVanced/revanced-api/compare/v1.4.0-dev.2...v1.4.0-dev.3) (2024-11-01) # [1.4.0-dev.3](https://github.com/ReVanced/revanced-api/compare/v1.4.0-dev.2...v1.4.0-dev.3) (2024-11-01)

View File

@ -81,6 +81,7 @@ Some of the features ReVanced API include:
and links of the hoster of ReVanced API and links of the hoster of ReVanced API
- 🧩 **Patches**: Get the latest updates of ReVanced Patches, directly from ReVanced API - 🧩 **Patches**: Get the latest updates of ReVanced Patches, directly from ReVanced API
- 👥 **Contributors**: List all contributors involved in the project - 👥 **Contributors**: List all contributors involved in the project
- 🔄 **Backwards compatibility**: Proxy an old API for migration purposes and backwards compatibility
## 🚀 How to get started ## 🚀 How to get started

View File

@ -5,7 +5,6 @@
"branding": { "branding": {
"logo": "https://raw.githubusercontent.com/ReVanced/revanced-branding/main/assets/revanced-logo/revanced-logo.svg" "logo": "https://raw.githubusercontent.com/ReVanced/revanced-branding/main/assets/revanced-logo/revanced-logo.svg"
}, },
"status": "https://status.revanced.app",
"contact": { "contact": {
"email": "contact@revanced.app" "email": "contact@revanced.app"
}, },

View File

@ -1,9 +1,10 @@
api-version = "v1" api-version = 1
cors-allowed-hosts = [ cors-allowed-hosts = [
"revanced.app", "revanced.app",
"*.revanced.app" "*.revanced.app"
] ]
endpoint = "https://api.revanced.app" endpoint = "https://api.revanced.app"
old-api-endpoint = "https://old-api.revanced.app"
static-files-path = "static/root" static-files-path = "static/root"
versioned-static-files-path = "static/versioned" versioned-static-files-path = "static/versioned"
backend-service-name = "GitHub" backend-service-name = "GitHub"

View File

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

View File

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

View File

@ -51,6 +51,7 @@ fun Application.configureDependencies(
AuthenticationService(issuer, validityInMin, jwtSecret, authSHA256DigestString) AuthenticationService(issuer, validityInMin, jwtSecret, authSHA256DigestString)
} }
singleOf(::OldApiService)
singleOf(::AnnouncementService) singleOf(::AnnouncementService)
singleOf(::SignatureService) singleOf(::SignatureService)
singleOf(::PatchesService) singleOf(::PatchesService)

View File

@ -11,7 +11,7 @@ import org.koin.ktor.ext.get
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
fun Application.configureHTTP() { fun Application.configureHTTP() {
val configuration = get<ConfigurationRepository>() val configurationRepository = get<ConfigurationRepository>()
install(CORS) { install(CORS) {
HttpMethod.DefaultMethods.minus(HttpMethod.Options).forEach(::allowMethod) HttpMethod.DefaultMethods.minus(HttpMethod.Options).forEach(::allowMethod)
@ -22,7 +22,7 @@ fun Application.configureHTTP() {
allowCredentials = true allowCredentials = true
configuration.corsAllowedHosts.forEach { host -> configurationRepository.corsAllowedHosts.forEach { host ->
allowHost(host = host, schemes = listOf("https")) allowHost(host = host, schemes = listOf("https"))
} }
} }

View File

@ -2,7 +2,6 @@ package app.revanced.api.configuration
import app.revanced.api.command.applicationVersion import app.revanced.api.command.applicationVersion
import app.revanced.api.configuration.repository.ConfigurationRepository import app.revanced.api.configuration.repository.ConfigurationRepository
import io.bkbn.kompendium.core.attribute.KompendiumAttributes
import io.bkbn.kompendium.core.plugin.NotarizedApplication import io.bkbn.kompendium.core.plugin.NotarizedApplication
import io.bkbn.kompendium.json.schema.KotlinXSchemaConfigurator import io.bkbn.kompendium.json.schema.KotlinXSchemaConfigurator
import io.bkbn.kompendium.oas.OpenApiSpec import io.bkbn.kompendium.oas.OpenApiSpec
@ -13,22 +12,13 @@ import io.bkbn.kompendium.oas.info.License
import io.bkbn.kompendium.oas.security.BearerAuth import io.bkbn.kompendium.oas.security.BearerAuth
import io.bkbn.kompendium.oas.server.Server import io.bkbn.kompendium.oas.server.Server
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.response.* import org.koin.ktor.ext.get
import io.ktor.server.routing.*
import java.net.URI import java.net.URI
import org.koin.ktor.ext.get as koinGet
internal fun Application.configureOpenAPI() { internal fun Application.configureOpenAPI() {
val configuration = koinGet<ConfigurationRepository>() val configurationRepository = get<ConfigurationRepository>()
install(NotarizedApplication()) { install(NotarizedApplication()) {
openApiJson = {
route("/${configuration.apiVersion}/openapi.json") {
get {
call.respond(application.attributes[KompendiumAttributes.openApiSpec])
}
}
}
spec = OpenApiSpec( spec = OpenApiSpec(
info = Info( info = Info(
title = "ReVanced API", title = "ReVanced API",
@ -51,7 +41,7 @@ internal fun Application.configureOpenAPI() {
), ),
).apply { ).apply {
servers += Server( servers += Server(
url = URI(configuration.endpoint), url = URI(configurationRepository.endpoint),
description = "ReVanced API server", description = "ReVanced API server",
) )
} }

View File

@ -4,6 +4,7 @@ import app.revanced.api.configuration.repository.ConfigurationRepository
import app.revanced.api.configuration.routes.* import app.revanced.api.configuration.routes.*
import app.revanced.api.configuration.routes.announcementsRoute import app.revanced.api.configuration.routes.announcementsRoute
import app.revanced.api.configuration.routes.apiRoute import app.revanced.api.configuration.routes.apiRoute
import app.revanced.api.configuration.routes.oldApiRoute
import app.revanced.api.configuration.routes.patchesRoute import app.revanced.api.configuration.routes.patchesRoute
import io.bkbn.kompendium.core.routes.redoc import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.core.routes.swagger import io.bkbn.kompendium.core.routes.swagger
@ -18,7 +19,7 @@ internal fun Application.configureRouting() = routing {
installCache(5.minutes) installCache(5.minutes)
route("/${configuration.apiVersion}") { route("/v${configuration.apiVersion}") {
announcementsRoute() announcementsRoute()
patchesRoute() patchesRoute()
managerRoute() managerRoute()
@ -52,7 +53,9 @@ internal fun Application.configureRouting() = routing {
extensions("json", "asc") extensions("json", "asc")
} }
val specUrl = "/${configuration.apiVersion}/openapi.json" swagger(pageTitle = "ReVanced API", path = "/")
swagger(pageTitle = "ReVanced API", path = "/", specUrl = specUrl) redoc(pageTitle = "ReVanced API", path = "/redoc")
redoc(pageTitle = "ReVanced API", path = "/redoc", specUrl = specUrl)
// TODO: Remove, once migration period from v2 API is over (In 1-2 years).
oldApiRoute()
} }

View File

@ -1,11 +1,12 @@
package app.revanced.api.configuration.repository package app.revanced.api.configuration.repository
import app.revanced.api.configuration.ApiAnnouncement import app.revanced.api.configuration.schema.ApiAnnouncement
import app.revanced.api.configuration.ApiAnnouncementTag import app.revanced.api.configuration.schema.ApiAnnouncementTag
import app.revanced.api.configuration.ApiResponseAnnouncement import app.revanced.api.configuration.schema.ApiResponseAnnouncement
import app.revanced.api.configuration.ApiResponseAnnouncementId import app.revanced.api.configuration.schema.ApiResponseAnnouncementId
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.datetime.*
import org.jetbrains.exposed.dao.IntEntity import org.jetbrains.exposed.dao.IntEntity
import org.jetbrains.exposed.dao.IntEntityClass import org.jetbrains.exposed.dao.IntEntityClass
import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.EntityID
@ -14,11 +15,12 @@ import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
import org.jetbrains.exposed.sql.kotlin.datetime.datetime import org.jetbrains.exposed.sql.kotlin.datetime.datetime
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import java.time.LocalDateTime
internal class AnnouncementRepository(private val database: Database) { internal class AnnouncementRepository(private val database: Database) {
// This is better than doing a maxByOrNull { it.id } on every request. // This is better than doing a maxByOrNull { it.id } on every request.
private var latestAnnouncement: Announcement? = null private var latestAnnouncement: Announcement? = null
private val latestAnnouncementByTag = mutableMapOf<String, Announcement>() private val latestAnnouncementByTag = mutableMapOf<Int, Announcement>()
init { init {
runBlocking { runBlocking {
@ -38,23 +40,22 @@ internal class AnnouncementRepository(private val database: Database) {
private fun initializeLatestAnnouncements() { private fun initializeLatestAnnouncements() {
latestAnnouncement = Announcement.all().orderBy(Announcements.id to SortOrder.DESC).firstOrNull() latestAnnouncement = Announcement.all().orderBy(Announcements.id to SortOrder.DESC).firstOrNull()
Tag.all().map { it.name }.forEach(::updateLatestAnnouncementForTag) Tag.all().map { it.id.value }.forEach(::updateLatestAnnouncementForTag)
} }
private fun updateLatestAnnouncement(new: Announcement) { private fun updateLatestAnnouncement(new: Announcement) {
if (latestAnnouncement == null || latestAnnouncement!!.id.value <= new.id.value) { if (latestAnnouncement == null || latestAnnouncement!!.id.value <= new.id.value) {
latestAnnouncement = new latestAnnouncement = new
new.tags.forEach { tag -> latestAnnouncementByTag[tag.name] = new } new.tags.forEach { tag -> latestAnnouncementByTag[tag.id.value] = new }
} }
} }
private fun updateLatestAnnouncementForTag(tag: String) { private fun updateLatestAnnouncementForTag(tag: Int) {
val latestAnnouncementForTag = Tags.innerJoin(AnnouncementTags) val latestAnnouncementForTag = AnnouncementTags.select(AnnouncementTags.announcement)
.select(AnnouncementTags.announcement) .where { AnnouncementTags.tag eq tag }
.where { Tags.name eq tag } .map { it[AnnouncementTags.announcement] }
.orderBy(AnnouncementTags.announcement to SortOrder.DESC) .mapNotNull { Announcement.findById(it) }
.limit(1) .maxByOrNull { it.id }
.firstNotNullOfOrNull { Announcement.findById(it[AnnouncementTags.announcement]) }
latestAnnouncementForTag?.let { latestAnnouncementByTag[tag] = it } latestAnnouncementForTag?.let { latestAnnouncementByTag[tag] = it }
} }
@ -63,29 +64,42 @@ internal class AnnouncementRepository(private val database: Database) {
latestAnnouncement.toApiResponseAnnouncement() latestAnnouncement.toApiResponseAnnouncement()
} }
suspend fun latest(tags: Set<String>) = transaction { suspend fun latest(tags: Set<Int>) = transaction {
tags.mapNotNull { tag -> latestAnnouncementByTag[tag] }.toApiAnnouncement() tags.mapNotNull { tag -> latestAnnouncementByTag[tag] }.toApiAnnouncement()
} }
fun latestId() = latestAnnouncement?.id?.value.toApiResponseAnnouncementId() fun latestId() = latestAnnouncement?.id?.value.toApiResponseAnnouncementId()
fun latestId(tags: Set<String>) = tags.map { tag -> latestAnnouncementByTag[tag]?.id?.value }.toApiResponseAnnouncementId() fun latestId(tags: Set<Int>) =
tags.map { tag -> latestAnnouncementByTag[tag]?.id?.value }.toApiResponseAnnouncementId()
suspend fun paged(cursor: Int, count: Int, tags: Set<String>?) = transaction { suspend fun paged(cursor: Int, count: Int, tags: Set<Int>?, archived: Boolean) = transaction {
Announcement.find { Announcement.find {
fun idLessEq() = Announcements.id lessEq cursor fun idLessEq() = Announcements.id lessEq cursor
fun archivedAtIsNull() = Announcements.archivedAt.isNull()
fun archivedAtGreaterNow() = Announcements.archivedAt greater LocalDateTime.now().toKotlinLocalDateTime()
if (tags == null) { if (tags == null) {
idLessEq() if (archived) {
idLessEq()
} else {
idLessEq() and (archivedAtIsNull() or archivedAtGreaterNow())
}
} else { } else {
fun hasTags() = Announcements.id inSubQuery ( fun archivedAtGreaterOrNullOrTrue() = if (archived) {
AnnouncementTags.innerJoin(Tags) Op.TRUE
.select(AnnouncementTags.announcement) } else {
.withDistinct() archivedAtIsNull() or archivedAtGreaterNow()
.where { Tags.name inList tags } }
)
idLessEq() and hasTags() 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() }.orderBy(Announcements.id to SortOrder.DESC).limit(count).toApiAnnouncement()
} }
@ -99,16 +113,13 @@ internal class AnnouncementRepository(private val database: Database) {
author = new.author author = new.author
title = new.title title = new.title
content = new.content content = new.content
createdAt = new.createdAt
archivedAt = new.archivedAt archivedAt = new.archivedAt
level = new.level level = new.level
if (new.tags != null) { tags = SizedCollection(
tags = SizedCollection( new.tags.map { tag -> Tag.find { Tags.name eq tag }.firstOrNull() ?: Tag.new { name = tag } },
new.tags.map { tag -> Tag.find { Tags.name eq tag }.firstOrNull() ?: Tag.new { name = tag } }, )
)
}
}.apply { }.apply {
new.attachments?.map { attachmentUrl -> new.attachments.map { attachmentUrl ->
Attachment.new { Attachment.new {
url = attachmentUrl url = attachmentUrl
announcement = this@apply announcement = this@apply
@ -122,32 +133,27 @@ internal class AnnouncementRepository(private val database: Database) {
it.author = new.author it.author = new.author
it.title = new.title it.title = new.title
it.content = new.content it.content = new.content
it.createdAt = new.createdAt
it.archivedAt = new.archivedAt it.archivedAt = new.archivedAt
it.level = new.level it.level = new.level
if (new.tags != null) { // Get the old tags, create new tags if they don't exist,
// 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.
// and delete tags that are not in the new tags, after updating the announcement. val oldTags = it.tags.toList()
val oldTags = it.tags.toList() val updatedTags = new.tags.map { name ->
val updatedTags = new.tags.map { name -> Tag.find { Tags.name eq name }.firstOrNull() ?: Tag.new { this.name = name }
Tag.find { Tags.name eq name }.firstOrNull() ?: Tag.new { this.name = name } }
} it.tags = SizedCollection(updatedTags)
it.tags = SizedCollection(updatedTags) oldTags.forEach { tag ->
oldTags.forEach { tag -> if (tag in updatedTags || !tag.announcements.empty()) return@forEach
if (tag in updatedTags || !tag.announcements.empty()) return@forEach tag.delete()
tag.delete()
}
} }
// Delete old attachments and create new attachments. // Delete old attachments and create new attachments.
if (new.attachments != null) { it.attachments.forEach { attachment -> attachment.delete() }
it.attachments.forEach { attachment -> attachment.delete() } new.attachments.map { attachment ->
new.attachments.map { attachment -> Attachment.new {
Attachment.new { url = attachment
url = attachment announcement = it
announcement = it
}
} }
} }
}?.let(::updateLatestAnnouncement) ?: Unit }?.let(::updateLatestAnnouncement) ?: Unit
@ -159,7 +165,7 @@ internal class AnnouncementRepository(private val database: Database) {
// Delete the tag if no other announcements are referencing it. // Delete the tag if no other announcements are referencing it.
// One count means that the announcement is the only one referencing the tag. // One count means that the announcement is the only one referencing the tag.
announcement.tags.filter { tag -> tag.announcements.count() == 1L }.forEach { tag -> announcement.tags.filter { tag -> tag.announcements.count() == 1L }.forEach { tag ->
latestAnnouncementByTag -= tag.name latestAnnouncementByTag -= tag.id.value
tag.delete() tag.delete()
} }
@ -180,7 +186,8 @@ internal class AnnouncementRepository(private val database: Database) {
Tag.all().toList().toApiTag() Tag.all().toList().toApiTag()
} }
private suspend fun <T> transaction(statement: suspend Transaction.() -> T) = newSuspendedTransaction(Dispatchers.IO, database, statement = statement) private suspend fun <T> transaction(statement: suspend Transaction.() -> T) =
newSuspendedTransaction(Dispatchers.IO, database, statement = statement)
private object Announcements : IntIdTable() { private object Announcements : IntIdTable() {
val author = varchar("author", 32).nullable() val author = varchar("author", 32).nullable()
@ -243,7 +250,7 @@ internal class AnnouncementRepository(private val database: Database) {
title, title,
content, content,
attachments.map { it.url }, attachments.map { it.url },
tags.map { it.name }, tags.map { it.id.value },
createdAt, createdAt,
archivedAt, archivedAt,
level, level,
@ -252,7 +259,7 @@ internal class AnnouncementRepository(private val database: Database) {
private fun Iterable<Announcement>.toApiAnnouncement() = map { it.toApiResponseAnnouncement()!! } private fun Iterable<Announcement>.toApiAnnouncement() = map { it.toApiResponseAnnouncement()!! }
private fun Iterable<Tag>.toApiTag() = map { ApiAnnouncementTag(it.name) } private fun Iterable<Tag>.toApiTag() = map { ApiAnnouncementTag(it.id.value, it.name) }
private fun Int?.toApiResponseAnnouncementId() = this?.let { ApiResponseAnnouncementId(this) } private fun Int?.toApiResponseAnnouncementId() = this?.let { ApiResponseAnnouncementId(this) }

View File

@ -135,14 +135,12 @@ abstract class BackendRepository internal constructor(
* @property tag The tag of the release. * @property tag The tag of the release.
* @property assets The assets of the release. * @property assets The assets of the release.
* @property createdAt The date and time the release was created. * @property createdAt The date and time the release was created.
* @property prerelease Whether the release is a prerelease.
* @property releaseNote The release note of the release. * @property releaseNote The release note of the release.
*/ */
class BackendRelease( class BackendRelease(
val tag: String, val tag: String,
val releaseNote: String, val releaseNote: String,
val createdAt: LocalDateTime, val createdAt: LocalDateTime,
val prerelease: Boolean,
// Using a list instead of a set because set semantics are unnecessary here. // Using a list instead of a set because set semantics are unnecessary here.
val assets: List<BackendAsset>, val assets: List<BackendAsset>,
) { ) {
@ -182,13 +180,13 @@ abstract class BackendRepository internal constructor(
* *
* @param owner The owner of the repository. * @param owner The owner of the repository.
* @param repository The name of the repository. * @param repository The name of the repository.
* @param prerelease Whether to get a prerelease. * @param tag The tag of the release. If null, the latest release is returned.
* @return The release. * @return The release.
*/ */
abstract suspend fun release( abstract suspend fun release(
owner: String, owner: String,
repository: String, repository: String,
prerelease: Boolean, tag: String? = null,
): BackendOrganization.BackendRepository.BackendRelease ): BackendOrganization.BackendRepository.BackendRelease
/** /**

View File

@ -1,6 +1,6 @@
package app.revanced.api.configuration.repository package app.revanced.api.configuration.repository
import app.revanced.api.configuration.APIAbout import app.revanced.api.configuration.schema.APIAbout
import app.revanced.api.configuration.services.ManagerService import app.revanced.api.configuration.services.ManagerService
import app.revanced.api.configuration.services.PatchesService import app.revanced.api.configuration.services.PatchesService
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
@ -30,6 +30,7 @@ import kotlin.io.path.createDirectories
* @property apiVersion The version to use for the API. * @property apiVersion The version to use for the API.
* @property corsAllowedHosts The hosts allowed to make requests to the API. * @property corsAllowedHosts The hosts allowed to make requests to the API.
* @property endpoint The endpoint of 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 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 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] * @property about The path to the json file deserialized to [APIAbout]
@ -45,10 +46,12 @@ internal class ConfigurationRepository(
@SerialName("backend-service-name") @SerialName("backend-service-name")
val backendServiceName: String, val backendServiceName: String,
@SerialName("api-version") @SerialName("api-version")
val apiVersion: String = "v1", val apiVersion: Int = 1,
@SerialName("cors-allowed-hosts") @SerialName("cors-allowed-hosts")
val corsAllowedHosts: Set<String>, val corsAllowedHosts: Set<String>,
val endpoint: String, val endpoint: String,
@SerialName("old-api-endpoint")
val oldApiEndpoint: String,
@Serializable(with = PathSerializer::class) @Serializable(with = PathSerializer::class)
@SerialName("static-files-path") @SerialName("static-files-path")
val staticFilesPath: Path, val staticFilesPath: Path,

View File

@ -24,10 +24,10 @@ class GitHubBackendRepository : BackendRepository("https://api.github.com", "htt
override suspend fun release( override suspend fun release(
owner: String, owner: String,
repository: String, repository: String,
prerelease: Boolean, tag: String?,
): BackendRelease { ): BackendRelease {
val release: GitHubRelease = if (prerelease) { val release: GitHubRelease = if (tag != null) {
client.get(Releases(owner, repository)).body<List<GitHubRelease>>().first { it.prerelease } client.get(Releases.Tag(owner, repository, tag)).body()
} else { } else {
client.get(Releases.Latest(owner, repository)).body() client.get(Releases.Latest(owner, repository)).body()
} }
@ -36,7 +36,6 @@ class GitHubBackendRepository : BackendRepository("https://api.github.com", "htt
tag = release.tagName, tag = release.tagName,
releaseNote = release.body, releaseNote = release.body,
createdAt = release.createdAt.toLocalDateTime(TimeZone.UTC), createdAt = release.createdAt.toLocalDateTime(TimeZone.UTC),
prerelease = release.prerelease,
assets = release.assets.map { assets = release.assets.map {
BackendAsset( BackendAsset(
name = it.name, name = it.name,
@ -164,7 +163,6 @@ class GitHubOrganization {
// Using a list instead of a set because set semantics are unnecessary here. // Using a list instead of a set because set semantics are unnecessary here.
val assets: List<GitHubAsset>, val assets: List<GitHubAsset>,
val createdAt: Instant, val createdAt: Instant,
val prerelease: Boolean,
val body: String, val body: String,
) { ) {
@Serializable @Serializable
@ -202,8 +200,10 @@ class Organization {
@Resource("/repos/{owner}/{repo}/contributors") @Resource("/repos/{owner}/{repo}/contributors")
class Contributors(val owner: String, val repo: String, @SerialName("per_page") val perPage: Int = 100) class Contributors(val owner: String, val repo: String, @SerialName("per_page") val perPage: Int = 100)
@Resource("/repos/{owner}/{repo}/releases") class Releases {
class Releases(val owner: String, val repo: String) { @Resource("/repos/{owner}/{repo}/releases/tags/{tag}")
class Tag(val owner: String, val repo: String, val tag: String)
@Resource("/repos/{owner}/{repo}/releases/latest") @Resource("/repos/{owner}/{repo}/releases/latest")
class Latest(val owner: String, val repo: String) class Latest(val owner: String, val repo: String)
} }

View File

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

View File

@ -6,6 +6,7 @@ import app.revanced.api.configuration.installNoCache
import app.revanced.api.configuration.installNotarizedRoute import app.revanced.api.configuration.installNotarizedRoute
import app.revanced.api.configuration.repository.ConfigurationRepository import app.revanced.api.configuration.repository.ConfigurationRepository
import app.revanced.api.configuration.respondOrNotFound 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.ApiService
import app.revanced.api.configuration.services.AuthenticationService import app.revanced.api.configuration.services.AuthenticationService
import io.bkbn.kompendium.core.metadata.* import io.bkbn.kompendium.core.metadata.*
@ -183,7 +184,7 @@ private fun Route.installTokenRouteDocumentation() = installNotarizedRoute {
"username=\"ReVanced\", " + "username=\"ReVanced\", " +
"realm=\"ReVanced\", " + "realm=\"ReVanced\", " +
"nonce=\"abc123\", " + "nonce=\"abc123\", " +
"uri=\"/${configuration.apiVersion}/token\", " + "uri=\"/v${configuration.apiVersion}/token\", " +
"algorithm=SHA-256, " + "algorithm=SHA-256, " +
"response=\"yxz456\"", "response=\"yxz456\"",
), ),

View File

@ -1,12 +1,10 @@
package app.revanced.api.configuration.routes 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.installNotarizedRoute
import app.revanced.api.configuration.schema.ApiRelease
import app.revanced.api.configuration.schema.ApiReleaseVersion
import app.revanced.api.configuration.services.ManagerService import app.revanced.api.configuration.services.ManagerService
import io.bkbn.kompendium.core.metadata.GetInfo import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.oas.payload.Parameter
import io.ktor.http.* import io.ktor.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.plugins.ratelimit.* import io.ktor.server.plugins.ratelimit.*
@ -15,44 +13,41 @@ import io.ktor.server.routing.*
import org.koin.ktor.ext.get as koinGet import org.koin.ktor.ext.get as koinGet
internal fun Route.managerRoute() = route("manager") { 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<ManagerService>() val managerService = koinGet<ManagerService>()
installManagerRouteDocumentation() installManagerRouteDocumentation(deprecated)
rateLimit(RateLimitName("weak")) { rateLimit(RateLimitName("weak")) {
get { get {
val prerelease = call.parameters["prerelease"]?.toBoolean() ?: false call.respond(managerService.latestRelease())
call.respond(managerService.latestRelease(prerelease))
} }
route("version") { route("version") {
installManagerVersionRouteDocumentation() installManagerVersionRouteDocumentation(deprecated)
get { get {
val prerelease = call.parameters["prerelease"]?.toBoolean() ?: false call.respond(managerService.latestVersion())
call.respond(managerService.latestVersion(prerelease))
} }
} }
} }
} }
private val prereleaseParameter = Parameter( private fun Route.installManagerRouteDocumentation(deprecated: Boolean) = installNotarizedRoute {
name = "prerelease",
`in` = Parameter.Location.query,
schema = TypeDefinition.STRING,
description = "Whether to get the current manager prerelease",
required = false,
)
private fun Route.installManagerRouteDocumentation() = installNotarizedRoute {
tags = setOf("Manager") tags = setOf("Manager")
get = GetInfo.builder { get = GetInfo.builder {
if (deprecated) isDeprecated()
description("Get the current manager release") description("Get the current manager release")
summary("Get current manager release") summary("Get current manager release")
parameters(prereleaseParameter)
response { response {
description("The latest manager release") description("The latest manager release")
mediaTypes("application/json") mediaTypes("application/json")
@ -62,13 +57,13 @@ private fun Route.installManagerRouteDocumentation() = installNotarizedRoute {
} }
} }
private fun Route.installManagerVersionRouteDocumentation() = installNotarizedRoute { private fun Route.installManagerVersionRouteDocumentation(deprecated: Boolean) = installNotarizedRoute {
tags = setOf("Manager") tags = setOf("Manager")
get = GetInfo.builder { get = GetInfo.builder {
if (deprecated) isDeprecated()
description("Get the current manager release version") description("Get the current manager release version")
summary("Get current manager release version") summary("Get current manager release version")
parameters(prereleaseParameter)
response { response {
description("The current manager release version") description("The current manager release version")
mediaTypes("application/json") mediaTypes("application/json")

View File

@ -0,0 +1,19 @@
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<OldApiService>()
rateLimit(RateLimitName("weak")) {
route(Regex("/(v2|tools|contributors).*")) {
handle {
oldApiService.proxy(call)
}
}
}
}

View File

@ -1,14 +1,12 @@
package app.revanced.api.configuration.routes 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.installCache
import app.revanced.api.configuration.installNotarizedRoute 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 app.revanced.api.configuration.services.PatchesService
import io.bkbn.kompendium.core.metadata.GetInfo import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.oas.payload.Parameter
import io.ktor.http.* import io.ktor.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.plugins.ratelimit.* import io.ktor.server.plugins.ratelimit.*
@ -18,36 +16,39 @@ import kotlin.time.Duration.Companion.days
import org.koin.ktor.ext.get as koinGet import org.koin.ktor.ext.get as koinGet
internal fun Route.patchesRoute() = route("patches") { 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<PatchesService>() val patchesService = koinGet<PatchesService>()
installPatchesRouteDocumentation() installPatchesRouteDocumentation(deprecated)
rateLimit(RateLimitName("weak")) { rateLimit(RateLimitName("weak")) {
get { get {
val prerelease = call.parameters["prerelease"]?.toBoolean() ?: false call.respond(patchesService.latestRelease())
call.respond(patchesService.latestRelease(prerelease))
} }
route("version") { route("version") {
installPatchesVersionRouteDocumentation() installPatchesVersionRouteDocumentation(deprecated)
get { get {
val prerelease = call.parameters["prerelease"]?.toBoolean() ?: false call.respond(patchesService.latestVersion())
call.respond(patchesService.latestVersion(prerelease))
} }
} }
} }
rateLimit(RateLimitName("strong")) { rateLimit(RateLimitName("strong")) {
route("list") { route("list") {
installPatchesListRouteDocumentation() installPatchesListRouteDocumentation(deprecated)
get { get {
val prerelease = call.parameters["prerelease"]?.toBoolean() ?: false call.respondBytes(ContentType.Application.Json) { patchesService.list() }
call.respondBytes(ContentType.Application.Json) { patchesService.list(prerelease) }
} }
} }
} }
@ -56,7 +57,7 @@ internal fun Route.patchesRoute() = route("patches") {
route("keys") { route("keys") {
installCache(356.days) installCache(356.days)
installPatchesPublicKeyRouteDocumentation() installPatchesPublicKeyRouteDocumentation(deprecated)
get { get {
call.respond(patchesService.publicKey()) call.respond(patchesService.publicKey())
@ -65,21 +66,13 @@ internal fun Route.patchesRoute() = route("patches") {
} }
} }
private val prereleaseParameter = Parameter( private fun Route.installPatchesRouteDocumentation(deprecated: Boolean) = installNotarizedRoute {
name = "prerelease",
`in` = Parameter.Location.query,
schema = TypeDefinition.STRING,
description = "Whether to get the current patches prerelease",
required = false,
)
private fun Route.installPatchesRouteDocumentation() = installNotarizedRoute {
tags = setOf("Patches") tags = setOf("Patches")
get = GetInfo.builder { get = GetInfo.builder {
if (deprecated) isDeprecated()
description("Get the current patches release") description("Get the current patches release")
summary("Get current patches release") summary("Get current patches release")
parameters(prereleaseParameter)
response { response {
description("The current patches release") description("The current patches release")
mediaTypes("application/json") mediaTypes("application/json")
@ -89,13 +82,13 @@ private fun Route.installPatchesRouteDocumentation() = installNotarizedRoute {
} }
} }
private fun Route.installPatchesVersionRouteDocumentation() = installNotarizedRoute { private fun Route.installPatchesVersionRouteDocumentation(deprecated: Boolean) = installNotarizedRoute {
tags = setOf("Patches") tags = setOf("Patches")
get = GetInfo.builder { get = GetInfo.builder {
if (deprecated) isDeprecated()
description("Get the current patches release version") description("Get the current patches release version")
summary("Get current patches release version") summary("Get current patches release version")
parameters(prereleaseParameter)
response { response {
description("The current patches release version") description("The current patches release version")
mediaTypes("application/json") mediaTypes("application/json")
@ -105,13 +98,13 @@ private fun Route.installPatchesVersionRouteDocumentation() = installNotarizedRo
} }
} }
private fun Route.installPatchesListRouteDocumentation() = installNotarizedRoute { private fun Route.installPatchesListRouteDocumentation(deprecated: Boolean) = installNotarizedRoute {
tags = setOf("Patches") tags = setOf("Patches")
get = GetInfo.builder { get = GetInfo.builder {
if (deprecated) isDeprecated()
description("Get the list of patches from the current patches release") description("Get the list of patches from the current patches release")
summary("Get list of patches from current patches release") summary("Get list of patches from current patches release")
parameters(prereleaseParameter)
response { response {
description("The list of patches") description("The list of patches")
mediaTypes("application/json") mediaTypes("application/json")
@ -121,10 +114,11 @@ private fun Route.installPatchesListRouteDocumentation() = installNotarizedRoute
} }
} }
private fun Route.installPatchesPublicKeyRouteDocumentation() = installNotarizedRoute { private fun Route.installPatchesPublicKeyRouteDocumentation(deprecated: Boolean) = installNotarizedRoute {
tags = setOf("Patches") tags = setOf("Patches")
get = GetInfo.builder { get = GetInfo.builder {
if (deprecated) isDeprecated()
description("Get the public keys for verifying patches assets") description("Get the public keys for verifying patches assets")
summary("Get patches public keys") summary("Get patches public keys")
response { response {

View File

@ -1,9 +1,6 @@
package app.revanced.api.configuration package app.revanced.api.configuration.schema
import kotlinx.datetime.Clock
import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
interface ApiUser { interface ApiUser {
@ -63,10 +60,9 @@ class ApiAnnouncement(
val title: String, val title: String,
val content: String? = null, val content: String? = null,
// Using a list instead of a set because set semantics are unnecessary here. // Using a list instead of a set because set semantics are unnecessary here.
val attachments: List<String>? = null, val attachments: List<String> = emptyList(),
// Using a list instead of a set because set semantics are unnecessary here. // Using a list instead of a set because set semantics are unnecessary here.
val tags: List<String>? = null, val tags: List<String> = emptyList(),
val createdAt: LocalDateTime = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()),
val archivedAt: LocalDateTime? = null, val archivedAt: LocalDateTime? = null,
val level: Int = 0, val level: Int = 0,
) )
@ -78,9 +74,9 @@ class ApiResponseAnnouncement(
val title: String, val title: String,
val content: String? = null, val content: String? = null,
// Using a list instead of a set because set semantics are unnecessary here. // Using a list instead of a set because set semantics are unnecessary here.
val attachments: List<String>? = null, val attachments: List<String> = emptyList(),
// Using a list instead of a set because set semantics are unnecessary here. // Using a list instead of a set because set semantics are unnecessary here.
val tags: List<String>? = null, val tags: List<Int> = emptyList(),
val createdAt: LocalDateTime, val createdAt: LocalDateTime,
val archivedAt: LocalDateTime? = null, val archivedAt: LocalDateTime? = null,
val level: Int = 0, val level: Int = 0,
@ -98,6 +94,7 @@ class ApiAnnouncementArchivedAt(
@Serializable @Serializable
class ApiAnnouncementTag( class ApiAnnouncementTag(
val id: Int,
val name: String, val name: String,
) )
@ -123,7 +120,6 @@ class APIAbout(
// Using a list instead of a set because set semantics are unnecessary here. // Using a list instead of a set because set semantics are unnecessary here.
val socials: List<Social>?, val socials: List<Social>?,
val donations: Donations?, val donations: Donations?,
val status: String,
) { ) {
@Serializable @Serializable
class Branding( class Branding(

View File

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

View File

@ -1,8 +1,8 @@
package app.revanced.api.configuration.services package app.revanced.api.configuration.services
import app.revanced.api.configuration.*
import app.revanced.api.configuration.repository.BackendRepository import app.revanced.api.configuration.repository.BackendRepository
import app.revanced.api.configuration.repository.ConfigurationRepository import app.revanced.api.configuration.repository.ConfigurationRepository
import app.revanced.api.configuration.schema.*
import io.ktor.http.* import io.ktor.http.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async

View File

@ -1,6 +1,6 @@
package app.revanced.api.configuration.services package app.revanced.api.configuration.services
import app.revanced.api.configuration.ApiToken import app.revanced.api.configuration.schema.ApiToken
import com.auth0.jwt.JWT import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm import com.auth0.jwt.algorithms.Algorithm
import io.ktor.server.auth.* import io.ktor.server.auth.*

View File

@ -1,20 +1,19 @@
package app.revanced.api.configuration.services 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
import app.revanced.api.configuration.repository.BackendRepository.BackendOrganization.BackendRepository.BackendRelease.Companion.first import app.revanced.api.configuration.repository.BackendRepository.BackendOrganization.BackendRepository.BackendRelease.Companion.first
import app.revanced.api.configuration.repository.ConfigurationRepository import app.revanced.api.configuration.repository.ConfigurationRepository
import app.revanced.api.configuration.schema.ApiRelease
import app.revanced.api.configuration.schema.ApiReleaseVersion
internal class ManagerService( internal class ManagerService(
private val backendRepository: BackendRepository, private val backendRepository: BackendRepository,
private val configurationRepository: ConfigurationRepository, private val configurationRepository: ConfigurationRepository,
) { ) {
suspend fun latestRelease(prerelease: Boolean): ApiRelease { suspend fun latestRelease(): ApiRelease {
val managerRelease = backendRepository.release( val managerRelease = backendRepository.release(
configurationRepository.organization, configurationRepository.organization,
configurationRepository.manager.repository, configurationRepository.manager.repository,
prerelease,
) )
return ApiRelease( return ApiRelease(
@ -25,11 +24,10 @@ internal class ManagerService(
) )
} }
suspend fun latestVersion(prerelease: Boolean): ApiReleaseVersion { suspend fun latestVersion(): ApiReleaseVersion {
val managerRelease = backendRepository.release( val managerRelease = backendRepository.release(
configurationRepository.organization, configurationRepository.organization,
configurationRepository.manager.repository, configurationRepository.manager.repository,
prerelease,
) )
return ApiReleaseVersion(managerRelease.tag) return ApiReleaseVersion(managerRelease.tag)

View File

@ -0,0 +1,76 @@
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)
}
})
}
}

View File

@ -1,11 +1,11 @@
package app.revanced.api.configuration.services 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
import app.revanced.api.configuration.repository.BackendRepository.BackendOrganization.BackendRepository.BackendRelease.Companion.first import app.revanced.api.configuration.repository.BackendRepository.BackendOrganization.BackendRepository.BackendRelease.Companion.first
import app.revanced.api.configuration.repository.ConfigurationRepository 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.library.serializeTo
import app.revanced.patcher.patch.loadPatchesFromJar import app.revanced.patcher.patch.loadPatchesFromJar
import com.github.benmanes.caffeine.cache.Caffeine import com.github.benmanes.caffeine.cache.Caffeine
@ -19,11 +19,10 @@ internal class PatchesService(
private val backendRepository: BackendRepository, private val backendRepository: BackendRepository,
private val configurationRepository: ConfigurationRepository, private val configurationRepository: ConfigurationRepository,
) { ) {
suspend fun latestRelease(prerelease: Boolean): ApiRelease { suspend fun latestRelease(): ApiRelease {
val patchesRelease = backendRepository.release( val patchesRelease = backendRepository.release(
configurationRepository.organization, configurationRepository.organization,
configurationRepository.patches.repository, configurationRepository.patches.repository,
prerelease,
) )
return ApiRelease( return ApiRelease(
@ -35,11 +34,10 @@ internal class PatchesService(
) )
} }
suspend fun latestVersion(prerelease: Boolean): ApiReleaseVersion { suspend fun latestVersion(): ApiReleaseVersion {
val patchesRelease = backendRepository.release( val patchesRelease = backendRepository.release(
configurationRepository.organization, configurationRepository.organization,
configurationRepository.patches.repository, configurationRepository.patches.repository,
prerelease,
) )
return ApiReleaseVersion(patchesRelease.tag) return ApiReleaseVersion(patchesRelease.tag)
@ -50,11 +48,10 @@ internal class PatchesService(
.maximumSize(1) .maximumSize(1)
.build<String, ByteArray>() .build<String, ByteArray>()
suspend fun list(prerelease: Boolean): ByteArray { suspend fun list(): ByteArray {
val patchesRelease = backendRepository.release( val patchesRelease = backendRepository.release(
configurationRepository.organization, configurationRepository.organization,
configurationRepository.patches.repository, configurationRepository.patches.repository,
prerelease,
) )
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {

View File

@ -12,7 +12,7 @@ import java.security.MessageDigest
internal class SignatureService { internal class SignatureService {
private val signatureCache = Caffeine private val signatureCache = Caffeine
.newBuilder() .newBuilder()
.maximumSize(2) // 2 because currently only the latest release and prerelease patches are needed. .maximumSize(1) // 1 because currently only the latest patches is needed.
.build<ByteArray, Boolean>() // Hash -> Verified. .build<ByteArray, Boolean>() // Hash -> Verified.
fun verify( fun verify(

View File

@ -1,7 +1,7 @@
<configuration> <configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder> <encoder>
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} %-5level %msg%n</pattern> <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder> </encoder>
</appender> </appender>
<root level="\${LOG_LEVEL:-INFO}"> <root level="\${LOG_LEVEL:-INFO}">

View File

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