Compare commits

...

28 Commits

Author SHA1 Message Date
semantic-release-bot
01d86ebba8 chore: Release v1.6.0 [skip ci]
# [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](8a957cd797))
* Add support for prereleases ([c25bc8b](c25bc8b4ba))
* Allow setting `Announcement.createdAt` when creating an announcement ([7f6e29d](7f6e29de52))
* Make some announcements schema fields nullable ([db22874](db22874f06))
2025-02-04 00:14:22 +00:00
oSumAtrIX
989094309f
chore: Merge branch dev to main (#196)
This pull request will Merge branch `dev` to `main`.
2025-02-04 01:12:19 +01:00
semantic-release-bot
5b447aa62d chore: Release v1.6.0-dev.3 [skip ci]
# [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](8a957cd797))
* Add support for prereleases ([c25bc8b](c25bc8b4ba))
2024-12-25 10:52:04 +00:00
oSumAtrIX
c25bc8b4ba
feat: Add support for prereleases 2024-12-25 11:50:06 +01:00
oSumAtrIX
8a957cd797
feat: Add status page link to about 2024-12-25 11:50:05 +01:00
semantic-release-bot
712ab3be8c chore: Release v1.6.0-dev.2 [skip ci]
# [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](db22874f06))
2024-12-20 23:27:48 +00:00
oSumAtrIX
db22874f06
feat: Make some announcements schema fields nullable 2024-12-21 00:25:26 +01:00
semantic-release-bot
5d5533a920 chore: Release v1.6.0-dev.1 [skip ci]
# [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](7f6e29de52))
2024-11-23 18:26:28 +00:00
oSumAtrIX
7f6e29de52
feat: Allow setting Announcement.createdAt when creating an announcement 2024-11-13 22:23:05 +01:00
semantic-release-bot
48469d32c2 chore: Release v1.5.0 [skip ci]
# [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](58ba4cb11c))
* Move spec url to versioned path ([e871b23](e871b23210))
* Simplify log pattern ([d5d9e04](d5d9e04325))
2024-11-06 21:17:51 +00:00
oSumAtrIX
7f9159fef1
chore: Merge branch dev to main(#195) 2024-11-06 22:15:06 +01:00
semantic-release-bot
b063b4daf2 chore: Release v1.5.0-dev.2 [skip ci]
# [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](58ba4cb11c))
* Simplify log pattern ([d5d9e04](d5d9e04325))
2024-11-06 15:36:26 +00:00
oSumAtrIX
58ba4cb11c
feat: Allow updating createdAt field for announcements 2024-11-06 16:34:18 +01:00
semantic-release-bot
55e3774f07 chore: Release v1.5.0-dev.1 [skip ci]
# [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](e871b23210))
* Simplify log pattern ([d5d9e04](d5d9e04325))
2024-11-06 04:41:55 +00:00
oSumAtrIX
d5d9e04325
feat: Simplify log pattern 2024-11-06 05:39:39 +01:00
semantic-release-bot
6c8153ba98 chore: Release v1.5.0-dev.1 [skip ci]
# [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](e871b23210))
2024-11-06 04:32:10 +00:00
oSumAtrIX
e871b23210
feat: Move spec url to versioned path 2024-11-06 05:30:09 +01:00
semantic-release-bot
e22ec16e40 chore: Release v1.4.0 [skip ci]
# [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](3b6212065a))
* Use new patches file extension ([d42a3a3](d42a3a3933))

### Features

* Add URL and use friendly name for `APIContributable` ([a5498ab](a5498aba2b))
* Allow versioning by arbitrary path string ([814d3c9](814d3c946e))
* Improve announcements API ([#192](https://github.com/ReVanced/revanced-api/issues/192)) ([56a00dd](56a00ddb85))
* Make backend configurable ([f91f3a6](f91f3a65c5))
* Remove "archived" query parameter ([8ad614e](8ad614ef4f))
* Remove deprecated routes and old API ([eca40a6](eca40a6979))
* Remove ReVanced Integrations ([f1c1092](f1c10928ae))
* Use tag name directly instead of ID ([fc40427](fc40427fba))
2024-11-06 04:06:21 +00:00
oSumAtrIX
440fbbc6c2
chore: Merge branch dev to main (#194) 2024-11-06 05:04:05 +01:00
semantic-release-bot
f74012993e 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](814d3c946e))
* Remove deprecated routes and old API ([eca40a6](eca40a6979))
2024-11-06 03:39:26 +00:00
oSumAtrIX
814d3c946e
feat: Allow versioning by arbitrary path string 2024-11-06 04:37:10 +01:00
oSumAtrIX
eca40a6979
feat: Remove deprecated routes and old API
Backwards compatibility should be and is now handled by the reverse proxy.
2024-11-06 04:37:10 +01:00
oSumAtrIX
0b66fc2bca
refactor: Move file to upper package 2024-11-06 01:38:02 +01:00
semantic-release-bot
1a09b028b7 chore: Release v1.4.0-dev.5 [skip ci]
# [1.4.0-dev.5](https://github.com/ReVanced/revanced-api/compare/v1.4.0-dev.4...v1.4.0-dev.5) (2024-11-05)
2024-11-05 19:01:39 +00:00
oSumAtrIX
0ddbf5beda
build(Needs bump): Bump dependencies 2024-11-05 19:58:20 +01:00
semantic-release-bot
bf41fa1596 chore: Release v1.4.0-dev.4 [skip ci]
# [1.4.0-dev.4](https://github.com/ReVanced/revanced-api/compare/v1.4.0-dev.3...v1.4.0-dev.4) (2024-11-01)

### Features

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

View File

@ -1,3 +1,105 @@
# [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,7 +81,6 @@ 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,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,10 +1,9 @@
api-version = 1 api-version = "v1"
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.4.0-dev.3 version = 1.6.0

View File

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

View File

@ -1,6 +1,9 @@
package app.revanced.api.configuration.schema 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,9 +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 = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()),
val archivedAt: LocalDateTime? = null, val archivedAt: LocalDateTime? = null,
val level: Int = 0, val level: Int = 0,
) )
@ -74,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<Int> = 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,
@ -94,7 +98,6 @@ class ApiAnnouncementArchivedAt(
@Serializable @Serializable
class ApiAnnouncementTag( class ApiAnnouncementTag(
val id: Int,
val name: String, val name: String,
) )
@ -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

@ -51,7 +51,6 @@ 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 configurationRepository = get<ConfigurationRepository>() val configuration = 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
configurationRepository.corsAllowedHosts.forEach { host -> configuration.corsAllowedHosts.forEach { host ->
allowHost(host = host, schemes = listOf("https")) allowHost(host = host, schemes = listOf("https"))
} }
} }

View File

@ -2,6 +2,7 @@ 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
@ -12,13 +13,22 @@ 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 org.koin.ktor.ext.get import io.ktor.server.response.*
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 configurationRepository = get<ConfigurationRepository>() val configuration = koinGet<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",
@ -41,7 +51,7 @@ internal fun Application.configureOpenAPI() {
), ),
).apply { ).apply {
servers += Server( servers += Server(
url = URI(configurationRepository.endpoint), url = URI(configuration.endpoint),
description = "ReVanced API server", description = "ReVanced API server",
) )
} }

View File

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

View File

@ -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

@ -1,6 +1,6 @@
package app.revanced.api.configuration.repository 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.ManagerService
import app.revanced.api.configuration.services.PatchesService import app.revanced.api.configuration.services.PatchesService
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
@ -30,7 +30,6 @@ 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]
@ -46,12 +45,10 @@ 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: Int = 1, val apiVersion: String = "v1",
@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,
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

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

View File

@ -6,7 +6,6 @@ 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.*
@ -184,7 +183,7 @@ private fun Route.installTokenRouteDocumentation() = installNotarizedRoute {
"username=\"ReVanced\", " + "username=\"ReVanced\", " +
"realm=\"ReVanced\", " + "realm=\"ReVanced\", " +
"nonce=\"abc123\", " + "nonce=\"abc123\", " +
"uri=\"/v${configuration.apiVersion}/token\", " + "uri=\"/${configuration.apiVersion}/token\", " +
"algorithm=SHA-256, " + "algorithm=SHA-256, " +
"response=\"yxz456\"", "response=\"yxz456\"",
), ),

View File

@ -1,10 +1,12 @@
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.*
@ -13,41 +15,44 @@ 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(deprecated) installManagerRouteDocumentation()
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(deprecated) installManagerVersionRouteDocumentation()
get { get {
call.respond(managerService.latestVersion()) val prerelease = call.parameters["prerelease"]?.toBoolean() ?: false
call.respond(managerService.latestVersion(prerelease))
} }
} }
} }
} }
private fun Route.installManagerRouteDocumentation(deprecated: Boolean) = installNotarizedRoute { 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 {
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")
@ -57,13 +62,13 @@ private fun Route.installManagerRouteDocumentation(deprecated: Boolean) = instal
} }
} }
private fun Route.installManagerVersionRouteDocumentation(deprecated: Boolean) = installNotarizedRoute { private fun Route.installManagerVersionRouteDocumentation() = 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

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

View File

@ -1,12 +1,14 @@
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.*
@ -16,39 +18,36 @@ 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(deprecated) installPatchesRouteDocumentation()
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(deprecated) installPatchesVersionRouteDocumentation()
get { get {
call.respond(patchesService.latestVersion()) val prerelease = call.parameters["prerelease"]?.toBoolean() ?: false
call.respond(patchesService.latestVersion(prerelease))
} }
} }
} }
rateLimit(RateLimitName("strong")) { rateLimit(RateLimitName("strong")) {
route("list") { route("list") {
installPatchesListRouteDocumentation(deprecated) 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,7 +56,7 @@ private fun Route.configure(deprecated: Boolean = false) {
route("keys") { route("keys") {
installCache(356.days) installCache(356.days)
installPatchesPublicKeyRouteDocumentation(deprecated) installPatchesPublicKeyRouteDocumentation()
get { get {
call.respond(patchesService.publicKey()) call.respond(patchesService.publicKey())
@ -66,13 +65,21 @@ private fun Route.configure(deprecated: Boolean = false) {
} }
} }
private fun Route.installPatchesRouteDocumentation(deprecated: Boolean) = installNotarizedRoute { 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 {
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")
@ -82,13 +89,13 @@ private fun Route.installPatchesRouteDocumentation(deprecated: Boolean) = instal
} }
} }
private fun Route.installPatchesVersionRouteDocumentation(deprecated: Boolean) = installNotarizedRoute { private fun Route.installPatchesVersionRouteDocumentation() = 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")
@ -98,13 +105,13 @@ private fun Route.installPatchesVersionRouteDocumentation(deprecated: Boolean) =
} }
} }
private fun Route.installPatchesListRouteDocumentation(deprecated: Boolean) = installNotarizedRoute { private fun Route.installPatchesListRouteDocumentation() = 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")
@ -114,11 +121,10 @@ private fun Route.installPatchesListRouteDocumentation(deprecated: Boolean) = in
} }
} }
private fun Route.installPatchesPublicKeyRouteDocumentation(deprecated: Boolean) = installNotarizedRoute { private fun Route.installPatchesPublicKeyRouteDocumentation() = 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,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<Int>) = announcementRepository.latest(tags) suspend fun latest(tags: Set<String>) = announcementRepository.latest(tags)
suspend fun latest() = announcementRepository.latest() suspend fun latest() = announcementRepository.latest()
fun latestId(tags: Set<Int>) = announcementRepository.latestId(tags) fun latestId(tags: Set<String>) = announcementRepository.latestId(tags)
fun latestId() = announcementRepository.latestId() fun latestId() = announcementRepository.latestId()
suspend fun paged(cursor: Int, limit: Int, tags: Set<Int>?, archived: Boolean) = suspend fun paged(cursor: Int, limit: Int, tags: Set<String>?) =
announcementRepository.paged(cursor, limit, tags, archived) announcementRepository.paged(cursor, limit, tags)
suspend fun get(id: Int) = announcementRepository.get(id) suspend fun get(id: Int) = announcementRepository.get(id)

View File

@ -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.schema.ApiToken import app.revanced.api.configuration.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,19 +1,20 @@
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(): 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

@ -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)
}
})
}
}

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,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

@ -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} [%thread] %-5level %logger{36} - %msg%n</pattern> <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} %-5level %msg%n</pattern>
</encoder> </encoder>
</appender> </appender>
<root level="\${LOG_LEVEL:-INFO}"> <root level="\${LOG_LEVEL:-INFO}">

View File

@ -1,12 +1,14 @@
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
@ -84,27 +86,22 @@ private object AnnouncementServiceTest {
announcementService.new(ApiAnnouncement(title = "2", tags = listOf("tag1", "tag3"))) announcementService.new(ApiAnnouncement(title = "2", tags = listOf("tag1", "tag3")))
announcementService.new(ApiAnnouncement(title = "3", tags = listOf("tag1", "tag4"))) announcementService.new(ApiAnnouncement(title = "3", tags = listOf("tag1", "tag4")))
val tag2 = announcementService.tags().find { it.name == "tag2" }!!.id assert(announcementService.latest(setOf("tag2")).first().title == "1")
assert(announcementService.latest(setOf(tag2)).first().title == "1") assert(announcementService.latest(setOf("tag3")).last().title == "2")
val tag3 = announcementService.tags().find { it.name == "tag3" }!!.id val announcement2and3 = announcementService.latest(setOf("tag1", "tag3"))
assert(announcementService.latest(setOf(tag3)).last().title == "2")
val tag1and3 =
announcementService.tags().filter { it.name == "tag1" || it.name == "tag3" }.map { it.id }.toSet()
val announcement2and3 = announcementService.latest(tag1and3)
assert(announcement2and3.size == 2) assert(announcement2and3.size == 2)
assert(announcement2and3.any { it.title == "2" }) assert(announcement2and3.any { it.title == "2" })
assert(announcement2and3.any { it.title == "3" }) assert(announcement2and3.any { it.title == "3" })
announcementService.delete(announcementService.latestId()!!.id) announcementService.delete(announcementService.latestId()!!.id)
assert(announcementService.latest(tag1and3).first().title == "2") assert(announcementService.latest(setOf("tag1", "tag3")).first().title == "2")
announcementService.delete(announcementService.latestId()!!.id) announcementService.delete(announcementService.latestId()!!.id)
assert(announcementService.latest(tag1and3).first().title == "1") assert(announcementService.latest(setOf("tag1", "tag3")).first().title == "1")
announcementService.delete(announcementService.latestId()!!.id) announcementService.delete(announcementService.latestId()!!.id)
assert(announcementService.latest(tag1and3).isEmpty()) assert(announcementService.latest(setOf("tag1", "tag3")).isEmpty())
assert(announcementService.tags().isEmpty()) assert(announcementService.tags().isEmpty())
} }
@ -138,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" })
@ -147,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
@ -156,11 +153,11 @@ private object AnnouncementServiceTest {
announcementService.new(ApiAnnouncement(title = "title$it")) announcementService.new(ApiAnnouncement(title = "title$it"))
} }
val announcements = announcementService.paged(Int.MAX_VALUE, 5, null, true) val announcements = announcementService.paged(Int.MAX_VALUE, 5, null)
assertEquals(5, announcements.size, "Returns correct number of announcements") assertEquals(5, announcements.size, "Returns correct number of announcements")
assertEquals("title9", announcements.first().title, "Starts from the latest announcement") assertEquals("title9", announcements.first().title, "Starts from the latest announcement")
val announcements2 = announcementService.paged(5, 5, null, true) val announcements2 = announcementService.paged(5, 5, null)
assertEquals(5, announcements2.size, "Returns correct number of announcements when starting from the cursor") assertEquals(5, announcements2.size, "Returns correct number of announcements when starting from the cursor")
assertEquals("title4", announcements2.first().title, "Starts from the cursor") assertEquals("title4", announcements2.first().title, "Starts from the cursor")
@ -183,10 +180,7 @@ private object AnnouncementServiceTest {
val tags = announcementService.tags() val tags = announcementService.tags()
assertEquals(5, tags.size, "Returns correct number of newly created tags") assertEquals(5, tags.size, "Returns correct number of newly created tags")
val announcements3 = announcementService.paged(5, 5, setOf(tags[1].id), true) val announcements3 = announcementService.paged(5, 5, setOf(tags[1].name))
assertEquals(4, announcements3.size, "Filters announcements by tag") assertEquals(4, announcements3.size, "Filters announcements by tag")
val announcements4 = announcementService.paged(Int.MAX_VALUE, 10, null, false)
assertEquals(8, announcements4.size, "Filters out archived announcements")
} }
} }