chore: Merge branch dev to main (#196)

This pull request will Merge branch `dev` to `main`.
This commit is contained in:
oSumAtrIX 2025-02-04 01:12:19 +01:00 committed by GitHub
commit 989094309f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 125 additions and 51 deletions

View File

@ -1,3 +1,25 @@
# [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) # [1.5.0](https://github.com/ReVanced/revanced-api/compare/v1.4.0...v1.5.0) (2024-11-06)

View File

@ -5,6 +5,7 @@
"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,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.5.0 version = 1.6.0-dev.3

View File

@ -1,6 +1,9 @@
package app.revanced.api.configuration package app.revanced.api.configuration
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 {
@ -60,10 +63,10 @@ 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> = emptyList(), val attachments: List<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 tags: List<String> = emptyList(), val tags: List<String>? = null,
val createdAt: LocalDateTime, 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,
) )
@ -75,9 +78,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> = emptyList(), val attachments: List<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 tags: List<String> = emptyList(), val tags: List<String>? = null,
val createdAt: LocalDateTime, val createdAt: LocalDateTime,
val archivedAt: LocalDateTime? = null, val archivedAt: LocalDateTime? = null,
val level: Int = 0, val level: Int = 0,
@ -120,6 +123,7 @@ 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

@ -69,8 +69,7 @@ internal class AnnouncementRepository(private val database: Database) {
fun latestId() = latestAnnouncement?.id?.value.toApiResponseAnnouncementId() fun latestId() = latestAnnouncement?.id?.value.toApiResponseAnnouncementId()
fun latestId(tags: Set<String>) = fun latestId(tags: Set<String>) = tags.map { tag -> latestAnnouncementByTag[tag]?.id?.value }.toApiResponseAnnouncementId()
tags.map { tag -> latestAnnouncementByTag[tag]?.id?.value }.toApiResponseAnnouncementId()
suspend fun paged(cursor: Int, count: Int, tags: Set<String>?) = transaction { suspend fun paged(cursor: Int, count: Int, tags: Set<String>?) = transaction {
Announcement.find { Announcement.find {
@ -100,13 +99,16 @@ 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
@ -124,6 +126,7 @@ internal class AnnouncementRepository(private val database: Database) {
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()
@ -135,8 +138,10 @@ internal class AnnouncementRepository(private val database: Database) {
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 {
@ -144,6 +149,7 @@ internal class AnnouncementRepository(private val database: Database) {
announcement = it announcement = it
} }
} }
}
}?.let(::updateLatestAnnouncement) ?: Unit }?.let(::updateLatestAnnouncement) ?: Unit
} }
@ -174,8 +180,7 @@ internal class AnnouncementRepository(private val database: Database) {
Tag.all().toList().toApiTag() Tag.all().toList().toApiTag()
} }
private suspend fun <T> transaction(statement: suspend Transaction.() -> T) = private suspend fun <T> transaction(statement: suspend Transaction.() -> T) = newSuspendedTransaction(Dispatchers.IO, database, statement = statement)
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()

View File

@ -135,12 +135,14 @@ 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>,
) { ) {
@ -180,13 +182,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 tag The tag of the release. If null, the latest release is returned. * @param prerelease Whether to get a prerelease.
* @return The release. * @return The release.
*/ */
abstract suspend fun release( abstract suspend fun release(
owner: String, owner: String,
repository: String, repository: String,
tag: String? = null, prerelease: Boolean,
): BackendOrganization.BackendRepository.BackendRelease ): BackendOrganization.BackendRepository.BackendRelease
/** /**

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,
tag: String?, prerelease: Boolean,
): BackendRelease { ): BackendRelease {
val release: GitHubRelease = if (tag != null) { val release: GitHubRelease = if (prerelease) {
client.get(Releases.Tag(owner, repository, tag)).body() client.get(Releases(owner, repository)).body<List<GitHubRelease>>().first { it.prerelease }
} else { } else {
client.get(Releases.Latest(owner, repository)).body() client.get(Releases.Latest(owner, repository)).body()
} }
@ -36,6 +36,7 @@ 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,
@ -163,6 +164,7 @@ 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
@ -200,10 +202,8 @@ 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)
class Releases { @Resource("/repos/{owner}/{repo}/releases")
@Resource("/repos/{owner}/{repo}/releases/tags/{tag}") class Releases(val owner: String, val repo: String) {
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

@ -5,6 +5,8 @@ import app.revanced.api.configuration.ApiReleaseVersion
import app.revanced.api.configuration.installNotarizedRoute import app.revanced.api.configuration.installNotarizedRoute
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.*
@ -19,25 +21,38 @@ internal fun Route.managerRoute() = route("manager") {
rateLimit(RateLimitName("weak")) { rateLimit(RateLimitName("weak")) {
get { get {
call.respond(managerService.latestRelease()) val prerelease = call.parameters["prerelease"]?.toBoolean() ?: false
call.respond(managerService.latestRelease(prerelease))
} }
route("version") { route("version") {
installManagerVersionRouteDocumentation() installManagerVersionRouteDocumentation()
get { get {
call.respond(managerService.latestVersion()) val prerelease = call.parameters["prerelease"]?.toBoolean() ?: false
call.respond(managerService.latestVersion(prerelease))
} }
} }
} }
} }
private val prereleaseParameter = Parameter(
name = "prerelease",
`in` = Parameter.Location.query,
schema = TypeDefinition.STRING,
description = "Whether to get the current manager prerelease",
required = false,
)
private fun Route.installManagerRouteDocumentation() = installNotarizedRoute { private fun Route.installManagerRouteDocumentation() = installNotarizedRoute {
tags = setOf("Manager") tags = setOf("Manager")
get = GetInfo.builder { get = GetInfo.builder {
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")
@ -53,6 +68,7 @@ private fun Route.installManagerVersionRouteDocumentation() = installNotarizedRo
get = GetInfo.builder { get = GetInfo.builder {
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

@ -7,6 +7,8 @@ import app.revanced.api.configuration.installCache
import app.revanced.api.configuration.installNotarizedRoute import app.revanced.api.configuration.installNotarizedRoute
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.*
@ -22,14 +24,18 @@ internal fun Route.patchesRoute() = route("patches") {
rateLimit(RateLimitName("weak")) { rateLimit(RateLimitName("weak")) {
get { get {
call.respond(patchesService.latestRelease()) val prerelease = call.parameters["prerelease"]?.toBoolean() ?: false
call.respond(patchesService.latestRelease(prerelease))
} }
route("version") { route("version") {
installPatchesVersionRouteDocumentation() installPatchesVersionRouteDocumentation()
get { get {
call.respond(patchesService.latestVersion()) val prerelease = call.parameters["prerelease"]?.toBoolean() ?: false
call.respond(patchesService.latestVersion(prerelease))
} }
} }
} }
@ -39,7 +45,9 @@ internal fun Route.patchesRoute() = route("patches") {
installPatchesListRouteDocumentation() installPatchesListRouteDocumentation()
get { get {
call.respondBytes(ContentType.Application.Json) { patchesService.list() } val prerelease = call.parameters["prerelease"]?.toBoolean() ?: false
call.respondBytes(ContentType.Application.Json) { patchesService.list(prerelease) }
} }
} }
} }
@ -57,12 +65,21 @@ internal fun Route.patchesRoute() = route("patches") {
} }
} }
private val prereleaseParameter = Parameter(
name = "prerelease",
`in` = Parameter.Location.query,
schema = TypeDefinition.STRING,
description = "Whether to get the current patches prerelease",
required = false,
)
private fun Route.installPatchesRouteDocumentation() = installNotarizedRoute { private fun Route.installPatchesRouteDocumentation() = installNotarizedRoute {
tags = setOf("Patches") tags = setOf("Patches")
get = GetInfo.builder { get = GetInfo.builder {
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")
@ -78,6 +95,7 @@ private fun Route.installPatchesVersionRouteDocumentation() = installNotarizedRo
get = GetInfo.builder { get = GetInfo.builder {
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")
@ -93,6 +111,7 @@ private fun Route.installPatchesListRouteDocumentation() = installNotarizedRoute
get = GetInfo.builder { get = GetInfo.builder {
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")

View File

@ -10,10 +10,11 @@ internal class ManagerService(
private val backendRepository: BackendRepository, private val backendRepository: BackendRepository,
private val configurationRepository: ConfigurationRepository, private val configurationRepository: ConfigurationRepository,
) { ) {
suspend fun latestRelease(): ApiRelease { suspend fun latestRelease(prerelease: Boolean): ApiRelease {
val managerRelease = backendRepository.release( val managerRelease = backendRepository.release(
configurationRepository.organization, configurationRepository.organization,
configurationRepository.manager.repository, configurationRepository.manager.repository,
prerelease,
) )
return ApiRelease( return ApiRelease(
@ -24,10 +25,11 @@ internal class ManagerService(
) )
} }
suspend fun latestVersion(): ApiReleaseVersion { suspend fun latestVersion(prerelease: Boolean): 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

@ -19,10 +19,11 @@ internal class PatchesService(
private val backendRepository: BackendRepository, private val backendRepository: BackendRepository,
private val configurationRepository: ConfigurationRepository, private val configurationRepository: ConfigurationRepository,
) { ) {
suspend fun latestRelease(): ApiRelease { suspend fun latestRelease(prerelease: Boolean): ApiRelease {
val patchesRelease = backendRepository.release( val patchesRelease = backendRepository.release(
configurationRepository.organization, configurationRepository.organization,
configurationRepository.patches.repository, configurationRepository.patches.repository,
prerelease,
) )
return ApiRelease( return ApiRelease(
@ -34,10 +35,11 @@ internal class PatchesService(
) )
} }
suspend fun latestVersion(): ApiReleaseVersion { suspend fun latestVersion(prerelease: Boolean): 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)
@ -48,10 +50,11 @@ internal class PatchesService(
.maximumSize(1) .maximumSize(1)
.build<String, ByteArray>() .build<String, ByteArray>()
suspend fun list(): ByteArray { suspend fun list(prerelease: Boolean): 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(1) // 1 because currently only the latest patches is needed. .maximumSize(2) // 2 because currently only the latest release and prerelease patches are needed.
.build<ByteArray, Boolean>() // Hash -> Verified. .build<ByteArray, Boolean>() // Hash -> Verified.
fun verify( fun verify(

View File

@ -135,7 +135,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 +144,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