mirror of
https://github.com/revanced/revanced-api.git
synced 2025-04-30 06:34:36 +02:00
feat: Add OpenAPI docs and cache to routes
This commit is contained in:
parent
205bcde77a
commit
6ea63be490
@ -67,6 +67,7 @@ dependencies {
|
|||||||
implementation(libs.ktor.server.jetty)
|
implementation(libs.ktor.server.jetty)
|
||||||
implementation(libs.ktor.serialization.kotlinx.json)
|
implementation(libs.ktor.serialization.kotlinx.json)
|
||||||
implementation(libs.koin.ktor)
|
implementation(libs.koin.ktor)
|
||||||
|
implementation("io.bkbn:kompendium-core:latest.release")
|
||||||
implementation(libs.h2)
|
implementation(libs.h2)
|
||||||
implementation(libs.logback.classic)
|
implementation(libs.logback.classic)
|
||||||
implementation(libs.exposed.core)
|
implementation(libs.exposed.core)
|
||||||
|
@ -3,6 +3,14 @@ package app.revanced.api.command
|
|||||||
import picocli.CommandLine
|
import picocli.CommandLine
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
internal val applicationVersion = MainCommand::class.java.getResourceAsStream(
|
||||||
|
"/app/revanced/api/version.properties",
|
||||||
|
)?.use { stream ->
|
||||||
|
Properties().apply {
|
||||||
|
load(stream)
|
||||||
|
}.getProperty("version")
|
||||||
|
} ?: "v0.0.0"
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
CommandLine(MainCommand).execute(*args).let(System::exit)
|
CommandLine(MainCommand).execute(*args).let(System::exit)
|
||||||
}
|
}
|
||||||
@ -10,15 +18,7 @@ fun main(args: Array<String>) {
|
|||||||
private object CLIVersionProvider : CommandLine.IVersionProvider {
|
private object CLIVersionProvider : CommandLine.IVersionProvider {
|
||||||
override fun getVersion() =
|
override fun getVersion() =
|
||||||
arrayOf(
|
arrayOf(
|
||||||
MainCommand::class.java.getResourceAsStream(
|
"ReVanced API $applicationVersion",
|
||||||
"/app/revanced/api/version.properties",
|
|
||||||
)?.use { stream ->
|
|
||||||
Properties().apply {
|
|
||||||
load(stream)
|
|
||||||
}.let {
|
|
||||||
"ReVanced API v${it.getProperty("version")}"
|
|
||||||
}
|
|
||||||
} ?: "ReVanced API",
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
package app.revanced.api.command
|
package app.revanced.api.command
|
||||||
|
|
||||||
import app.revanced.api.configuration.configureDependencies
|
import app.revanced.api.configuration.*
|
||||||
import app.revanced.api.configuration.configureHTTP
|
|
||||||
import app.revanced.api.configuration.configureRouting
|
|
||||||
import app.revanced.api.configuration.configureSecurity
|
|
||||||
import app.revanced.api.configuration.configureSerialization
|
|
||||||
import io.ktor.server.engine.*
|
import io.ktor.server.engine.*
|
||||||
import io.ktor.server.jetty.*
|
import io.ktor.server.jetty.*
|
||||||
import picocli.CommandLine
|
import picocli.CommandLine
|
||||||
@ -42,6 +38,7 @@ internal object StartAPICommand : Runnable {
|
|||||||
configureHTTP()
|
configureHTTP()
|
||||||
configureSerialization()
|
configureSerialization()
|
||||||
configureSecurity()
|
configureSecurity()
|
||||||
|
configureOpenAPI()
|
||||||
configureRouting()
|
configureRouting()
|
||||||
}.start(wait = true)
|
}.start(wait = true)
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,27 @@
|
|||||||
package app.revanced.api.configuration
|
package app.revanced.api.configuration
|
||||||
|
|
||||||
|
import io.bkbn.kompendium.core.plugin.NotarizedRoute
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
|
import io.ktor.http.content.*
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.plugins.cachingheaders.*
|
||||||
import io.ktor.server.response.*
|
import io.ktor.server.response.*
|
||||||
|
import kotlin.time.Duration
|
||||||
|
|
||||||
suspend fun ApplicationCall.respondOrNotFound(value: Any?) = respond(value ?: HttpStatusCode.NotFound)
|
internal suspend fun ApplicationCall.respondOrNotFound(value: Any?) = respond(value ?: HttpStatusCode.NotFound)
|
||||||
|
|
||||||
|
internal fun ApplicationCallPipeline.installCache(maxAge: Duration) =
|
||||||
|
installCache(CacheControl.MaxAge(maxAgeSeconds = maxAge.inWholeSeconds.toInt()))
|
||||||
|
|
||||||
|
internal fun ApplicationCallPipeline.installNoCache() =
|
||||||
|
installCache(CacheControl.NoCache(null))
|
||||||
|
|
||||||
|
internal fun ApplicationCallPipeline.installCache(cacheControl: CacheControl) =
|
||||||
|
install(CachingHeaders) {
|
||||||
|
options { _, _ ->
|
||||||
|
CachingOptions(cacheControl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun ApplicationCallPipeline.installNotarizedRoute(configure: NotarizedRoute.Config.() -> Unit = {}) =
|
||||||
|
install(NotarizedRoute(), configure)
|
||||||
|
52
src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt
Normal file
52
src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package app.revanced.api.configuration
|
||||||
|
|
||||||
|
import app.revanced.api.command.applicationVersion
|
||||||
|
import io.bkbn.kompendium.core.plugin.NotarizedApplication
|
||||||
|
import io.bkbn.kompendium.json.schema.KotlinXSchemaConfigurator
|
||||||
|
import io.bkbn.kompendium.oas.OpenApiSpec
|
||||||
|
import io.bkbn.kompendium.oas.component.Components
|
||||||
|
import io.bkbn.kompendium.oas.info.Contact
|
||||||
|
import io.bkbn.kompendium.oas.info.Info
|
||||||
|
import io.bkbn.kompendium.oas.info.License
|
||||||
|
import io.bkbn.kompendium.oas.security.BasicAuth
|
||||||
|
import io.bkbn.kompendium.oas.security.BearerAuth
|
||||||
|
import io.bkbn.kompendium.oas.server.Server
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
internal fun Application.configureOpenAPI() {
|
||||||
|
install(NotarizedApplication()) {
|
||||||
|
spec = {
|
||||||
|
OpenApiSpec(
|
||||||
|
info = Info(
|
||||||
|
title = "Revanced API",
|
||||||
|
version = applicationVersion,
|
||||||
|
description = "API server for ReVanced.",
|
||||||
|
contact = Contact(
|
||||||
|
name = "ReVanced",
|
||||||
|
url = URI("https://revanced.app"),
|
||||||
|
email = "contact@revanced.app",
|
||||||
|
),
|
||||||
|
license = License(
|
||||||
|
name = "AGPLv3",
|
||||||
|
url = URI("https://github.com/ReVanced/revanced-api/blob/main/LICENSE"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
components = Components(
|
||||||
|
securitySchemes = mutableMapOf(
|
||||||
|
"bearer" to BearerAuth(),
|
||||||
|
"basic" to BasicAuth(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
).apply {
|
||||||
|
servers += Server(
|
||||||
|
url = URI("https://api.revanced.app"),
|
||||||
|
description = "ReVanced API server.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
schemaConfigurator = KotlinXSchemaConfigurator()
|
||||||
|
}
|
||||||
|
}
|
@ -5,10 +5,7 @@ import app.revanced.api.configuration.routes.announcementsRoute
|
|||||||
import app.revanced.api.configuration.routes.oldApiRoute
|
import app.revanced.api.configuration.routes.oldApiRoute
|
||||||
import app.revanced.api.configuration.routes.patchesRoute
|
import app.revanced.api.configuration.routes.patchesRoute
|
||||||
import app.revanced.api.configuration.routes.rootRoute
|
import app.revanced.api.configuration.routes.rootRoute
|
||||||
import io.ktor.http.*
|
|
||||||
import io.ktor.http.content.*
|
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
import io.ktor.server.plugins.cachingheaders.*
|
|
||||||
import io.ktor.server.routing.*
|
import io.ktor.server.routing.*
|
||||||
import org.koin.ktor.ext.get
|
import org.koin.ktor.ext.get
|
||||||
import kotlin.time.Duration.Companion.minutes
|
import kotlin.time.Duration.Companion.minutes
|
||||||
@ -16,13 +13,7 @@ import kotlin.time.Duration.Companion.minutes
|
|||||||
internal fun Application.configureRouting() = routing {
|
internal fun Application.configureRouting() = routing {
|
||||||
val configuration = get<ConfigurationRepository>()
|
val configuration = get<ConfigurationRepository>()
|
||||||
|
|
||||||
install(CachingHeaders) {
|
installCache(5.minutes)
|
||||||
options { _, _ ->
|
|
||||||
CachingOptions(
|
|
||||||
CacheControl.MaxAge(maxAgeSeconds = 5.minutes.inWholeSeconds.toInt()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
route("/v${configuration.apiVersion}") {
|
route("/v${configuration.apiVersion}") {
|
||||||
rootRoute()
|
rootRoute()
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package app.revanced.api.configuration
|
package app.revanced.api.configuration
|
||||||
|
|
||||||
|
import io.bkbn.kompendium.oas.serialization.KompendiumSerializersModule
|
||||||
import io.ktor.serialization.kotlinx.json.*
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
import io.ktor.server.plugins.contentnegotiation.*
|
import io.ktor.server.plugins.contentnegotiation.*
|
||||||
@ -12,6 +13,7 @@ fun Application.configureSerialization() {
|
|||||||
install(ContentNegotiation) {
|
install(ContentNegotiation) {
|
||||||
json(
|
json(
|
||||||
Json {
|
Json {
|
||||||
|
serializersModule = KompendiumSerializersModule.module
|
||||||
namingStrategy = JsonNamingStrategy.SnakeCase
|
namingStrategy = JsonNamingStrategy.SnakeCase
|
||||||
explicitNulls = false
|
explicitNulls = false
|
||||||
},
|
},
|
||||||
|
@ -2,8 +2,8 @@ package app.revanced.api.configuration.repository
|
|||||||
|
|
||||||
import app.revanced.api.configuration.repository.AnnouncementRepository.AttachmentTable.announcement
|
import app.revanced.api.configuration.repository.AnnouncementRepository.AttachmentTable.announcement
|
||||||
import app.revanced.api.configuration.schema.APIAnnouncement
|
import app.revanced.api.configuration.schema.APIAnnouncement
|
||||||
import app.revanced.api.configuration.schema.APILatestAnnouncement
|
|
||||||
import app.revanced.api.configuration.schema.APIResponseAnnouncement
|
import app.revanced.api.configuration.schema.APIResponseAnnouncement
|
||||||
|
import app.revanced.api.configuration.schema.APIResponseAnnouncementId
|
||||||
import kotlinx.datetime.*
|
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
|
||||||
@ -42,6 +42,8 @@ internal class AnnouncementRepository(private val database: Database) {
|
|||||||
announcement.delete()
|
announcement.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: These are inefficient, but I'm not sure how to make them more efficient.
|
||||||
|
|
||||||
fun latest() = transaction {
|
fun latest() = transaction {
|
||||||
AnnouncementEntity.all().maxByOrNull { it.createdAt }?.toApi()
|
AnnouncementEntity.all().maxByOrNull { it.createdAt }?.toApi()
|
||||||
}
|
}
|
||||||
@ -52,13 +54,13 @@ internal class AnnouncementRepository(private val database: Database) {
|
|||||||
|
|
||||||
fun latestId() = transaction {
|
fun latestId() = transaction {
|
||||||
AnnouncementEntity.all().maxByOrNull { it.createdAt }?.id?.value?.let {
|
AnnouncementEntity.all().maxByOrNull { it.createdAt }?.id?.value?.let {
|
||||||
APILatestAnnouncement(it)
|
APIResponseAnnouncementId(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun latestId(channel: String) = transaction {
|
fun latestId(channel: String) = transaction {
|
||||||
AnnouncementEntity.find { AnnouncementTable.channel eq channel }.maxByOrNull { it.createdAt }?.id?.value?.let {
|
AnnouncementEntity.find { AnnouncementTable.channel eq channel }.maxByOrNull { it.createdAt }?.id?.value?.let {
|
||||||
APILatestAnnouncement(it)
|
APIResponseAnnouncementId(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,14 +1,22 @@
|
|||||||
package app.revanced.api.configuration.routes
|
package app.revanced.api.configuration.routes
|
||||||
|
|
||||||
|
import app.revanced.api.configuration.installCache
|
||||||
|
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.APIAnnouncement
|
||||||
import app.revanced.api.configuration.schema.APIAnnouncementArchivedAt
|
import app.revanced.api.configuration.schema.APIAnnouncementArchivedAt
|
||||||
|
import app.revanced.api.configuration.schema.APIResponseAnnouncement
|
||||||
|
import app.revanced.api.configuration.schema.APIResponseAnnouncementId
|
||||||
import app.revanced.api.configuration.services.AnnouncementService
|
import app.revanced.api.configuration.services.AnnouncementService
|
||||||
|
import io.bkbn.kompendium.core.metadata.DeleteInfo
|
||||||
|
import io.bkbn.kompendium.core.metadata.GetInfo
|
||||||
|
import io.bkbn.kompendium.core.metadata.PatchInfo
|
||||||
|
import io.bkbn.kompendium.core.metadata.PostInfo
|
||||||
|
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
|
||||||
|
import io.bkbn.kompendium.oas.payload.Parameter
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import io.ktor.http.content.*
|
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
import io.ktor.server.auth.*
|
import io.ktor.server.auth.*
|
||||||
import io.ktor.server.plugins.cachingheaders.*
|
|
||||||
import io.ktor.server.plugins.ratelimit.*
|
import io.ktor.server.plugins.ratelimit.*
|
||||||
import io.ktor.server.request.*
|
import io.ktor.server.request.*
|
||||||
import io.ktor.server.response.*
|
import io.ktor.server.response.*
|
||||||
@ -20,49 +28,9 @@ import org.koin.ktor.ext.get as koinGet
|
|||||||
internal fun Route.announcementsRoute() = route("announcements") {
|
internal fun Route.announcementsRoute() = route("announcements") {
|
||||||
val announcementService = koinGet<AnnouncementService>()
|
val announcementService = koinGet<AnnouncementService>()
|
||||||
|
|
||||||
install(CachingHeaders) {
|
installCache(5.minutes)
|
||||||
options { _, _ ->
|
|
||||||
CachingOptions(
|
|
||||||
CacheControl.MaxAge(maxAgeSeconds = 1.minutes.inWholeSeconds.toInt()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rateLimit(RateLimitName("weak")) {
|
installAnnouncementsRouteDocumentation()
|
||||||
route("{channel}/latest") {
|
|
||||||
get("id") {
|
|
||||||
val channel: String by call.parameters
|
|
||||||
|
|
||||||
call.respondOrNotFound(announcementService.latestId(channel))
|
|
||||||
}
|
|
||||||
|
|
||||||
get {
|
|
||||||
val channel: String by call.parameters
|
|
||||||
|
|
||||||
call.respondOrNotFound(announcementService.latest(channel))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rateLimit(RateLimitName("strong")) {
|
|
||||||
get("{channel}") {
|
|
||||||
val channel: String by call.parameters
|
|
||||||
|
|
||||||
call.respond(announcementService.all(channel))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rateLimit(RateLimitName("strong")) {
|
|
||||||
route("latest") {
|
|
||||||
get("id") {
|
|
||||||
call.respondOrNotFound(announcementService.latestId())
|
|
||||||
}
|
|
||||||
|
|
||||||
get {
|
|
||||||
call.respondOrNotFound(announcementService.latest())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rateLimit(RateLimitName("strong")) {
|
rateLimit(RateLimitName("strong")) {
|
||||||
get {
|
get {
|
||||||
@ -71,36 +39,332 @@ internal fun Route.announcementsRoute() = route("announcements") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rateLimit(RateLimitName("strong")) {
|
rateLimit(RateLimitName("strong")) {
|
||||||
authenticate("jwt") {
|
route("{channel}/latest") {
|
||||||
post {
|
installLatestChannelAnnouncementRouteDocumentation()
|
||||||
announcementService.new(call.receive<APIAnnouncement>())
|
|
||||||
|
get {
|
||||||
|
val channel: String by call.parameters
|
||||||
|
|
||||||
|
call.respondOrNotFound(announcementService.latest(channel))
|
||||||
}
|
}
|
||||||
|
|
||||||
post("{id}/archive") {
|
route("id") {
|
||||||
|
installLatestChannelAnnouncementIdRouteDocumentation()
|
||||||
|
|
||||||
|
get {
|
||||||
|
val channel: String by call.parameters
|
||||||
|
|
||||||
|
call.respondOrNotFound(announcementService.latestId(channel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rateLimit(RateLimitName("strong")) {
|
||||||
|
route("{channel}") {
|
||||||
|
installChannelAnnouncementsRouteDocumentation()
|
||||||
|
|
||||||
|
get {
|
||||||
|
val channel: String by call.parameters
|
||||||
|
|
||||||
|
call.respond(announcementService.all(channel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rateLimit(RateLimitName("strong")) {
|
||||||
|
route("latest") {
|
||||||
|
installLatestAnnouncementRouteDocumentation()
|
||||||
|
|
||||||
|
get {
|
||||||
|
call.respondOrNotFound(announcementService.latest())
|
||||||
|
}
|
||||||
|
|
||||||
|
route("id") {
|
||||||
|
installLatestAnnouncementIdRouteDocumentation()
|
||||||
|
|
||||||
|
get {
|
||||||
|
call.respondOrNotFound(announcementService.latestId())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rateLimit(RateLimitName("strong")) {
|
||||||
|
authenticate("jwt") {
|
||||||
|
post<APIAnnouncement> { announcement ->
|
||||||
|
announcementService.new(announcement)
|
||||||
|
}
|
||||||
|
|
||||||
|
route("{id}") {
|
||||||
|
installAnnouncementIdRouteDocumentation()
|
||||||
|
|
||||||
|
patch<APIAnnouncement> { announcement ->
|
||||||
|
val id: Int by call.parameters
|
||||||
|
|
||||||
|
announcementService.update(id, announcement)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete {
|
||||||
|
val id: Int by call.parameters
|
||||||
|
|
||||||
|
announcementService.delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
route("archive") {
|
||||||
|
installAnnouncementArchiveRouteDocumentation()
|
||||||
|
|
||||||
|
post {
|
||||||
val id: Int by call.parameters
|
val id: Int by call.parameters
|
||||||
val archivedAt = call.receiveNullable<APIAnnouncementArchivedAt>()?.archivedAt
|
val archivedAt = call.receiveNullable<APIAnnouncementArchivedAt>()?.archivedAt
|
||||||
|
|
||||||
announcementService.archive(id, archivedAt)
|
announcementService.archive(id, archivedAt)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
post("{id}/unarchive") {
|
route("unarchive") {
|
||||||
|
installAnnouncementUnarchiveRouteDocumentation()
|
||||||
|
|
||||||
|
post {
|
||||||
val id: Int by call.parameters
|
val id: Int by call.parameters
|
||||||
|
|
||||||
announcementService.unarchive(id)
|
announcementService.unarchive(id)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
patch("{id}") {
|
private fun Route.installLatestAnnouncementRouteDocumentation() = installNotarizedRoute {
|
||||||
val id: Int by call.parameters
|
tags = setOf("Announcements")
|
||||||
val announcement = call.receive<APIAnnouncement>()
|
|
||||||
|
|
||||||
announcementService.update(id, announcement)
|
get = GetInfo.builder {
|
||||||
|
description("Get the latest announcement")
|
||||||
|
summary("Get latest announcement")
|
||||||
|
response {
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
mediaTypes("application/json")
|
||||||
|
description("The latest announcement")
|
||||||
|
responseType<APIResponseAnnouncement>()
|
||||||
|
}
|
||||||
|
canRespond {
|
||||||
|
responseCode(HttpStatusCode.NotFound)
|
||||||
|
description("No announcement exists")
|
||||||
|
responseType<Unit>()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
delete("{id}") {
|
private fun Route.installLatestAnnouncementIdRouteDocumentation() = installNotarizedRoute {
|
||||||
val id: Int by call.parameters
|
tags = setOf("Announcements")
|
||||||
|
|
||||||
announcementService.delete(id)
|
get = GetInfo.builder {
|
||||||
|
description("Get the id of the latest announcement")
|
||||||
|
summary("Get id of latest announcement")
|
||||||
|
response {
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
mediaTypes("application/json")
|
||||||
|
description("The id of the latest announcement")
|
||||||
|
responseType<APIResponseAnnouncementId>()
|
||||||
|
}
|
||||||
|
canRespond {
|
||||||
|
responseCode(HttpStatusCode.NotFound)
|
||||||
|
description("No announcement exists")
|
||||||
|
responseType<Unit>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Route.installChannelAnnouncementsRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("Announcements")
|
||||||
|
|
||||||
|
parameters = listOf(
|
||||||
|
Parameter(
|
||||||
|
name = "channel",
|
||||||
|
`in` = Parameter.Location.path,
|
||||||
|
schema = TypeDefinition.STRING,
|
||||||
|
description = "The channel to get the announcements from",
|
||||||
|
required = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
get = GetInfo.builder {
|
||||||
|
description("Get the announcements from a channel")
|
||||||
|
summary("Get announcements from channel")
|
||||||
|
response {
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
mediaTypes("application/json")
|
||||||
|
description("The announcements in the channel")
|
||||||
|
responseType<Set<APIResponseAnnouncement>>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Route.installAnnouncementArchiveRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("Announcements")
|
||||||
|
|
||||||
|
parameters = listOf(
|
||||||
|
Parameter(
|
||||||
|
name = "id",
|
||||||
|
`in` = Parameter.Location.path,
|
||||||
|
schema = TypeDefinition.INT,
|
||||||
|
description = "The id of the announcement to archive",
|
||||||
|
required = true,
|
||||||
|
),
|
||||||
|
Parameter(
|
||||||
|
name = "archivedAt",
|
||||||
|
`in` = Parameter.Location.query,
|
||||||
|
schema = TypeDefinition.STRING,
|
||||||
|
description = "The date and time the announcement to be archived",
|
||||||
|
required = false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
post = PostInfo.builder {
|
||||||
|
description("Archive an announcement")
|
||||||
|
summary("Archive announcement")
|
||||||
|
response {
|
||||||
|
description("When the announcement was archived")
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
responseType<Unit>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Route.installAnnouncementUnarchiveRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("Announcements")
|
||||||
|
|
||||||
|
parameters = listOf(
|
||||||
|
Parameter(
|
||||||
|
name = "id",
|
||||||
|
`in` = Parameter.Location.path,
|
||||||
|
schema = TypeDefinition.INT,
|
||||||
|
description = "The id of the announcement to unarchive",
|
||||||
|
required = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
post = PostInfo.builder {
|
||||||
|
description("Unarchive an announcement")
|
||||||
|
summary("Unarchive announcement")
|
||||||
|
response {
|
||||||
|
description("When announcement was unarchived")
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
responseType<Unit>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Route.installAnnouncementIdRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("Announcements")
|
||||||
|
|
||||||
|
parameters = listOf(
|
||||||
|
Parameter(
|
||||||
|
name = "id",
|
||||||
|
`in` = Parameter.Location.path,
|
||||||
|
schema = TypeDefinition.INT,
|
||||||
|
description = "The id of the announcement to update",
|
||||||
|
required = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
patch = PatchInfo.builder {
|
||||||
|
description("Update an announcement")
|
||||||
|
summary("Update announcement")
|
||||||
|
request {
|
||||||
|
requestType<APIAnnouncement>()
|
||||||
|
description("The new announcement")
|
||||||
|
}
|
||||||
|
response {
|
||||||
|
description("When announcement was updated")
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
responseType<Unit>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete = DeleteInfo.builder {
|
||||||
|
description("Delete an announcement")
|
||||||
|
summary("Delete announcement")
|
||||||
|
response {
|
||||||
|
description("When the announcement was deleted")
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
responseType<Unit>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Route.installAnnouncementsRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("Announcements")
|
||||||
|
|
||||||
|
get = GetInfo.builder {
|
||||||
|
description("Get the announcements")
|
||||||
|
summary("Get announcement")
|
||||||
|
response {
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
mediaTypes("application/json")
|
||||||
|
description("The announcements")
|
||||||
|
responseType<Set<APIResponseAnnouncement>>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Route.installLatestChannelAnnouncementRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("Announcements")
|
||||||
|
|
||||||
|
parameters = listOf(
|
||||||
|
Parameter(
|
||||||
|
name = "channel",
|
||||||
|
`in` = Parameter.Location.path,
|
||||||
|
schema = TypeDefinition.STRING,
|
||||||
|
description = "The channel to get the latest announcement from",
|
||||||
|
required = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
get = GetInfo.builder {
|
||||||
|
description("Get the latest announcement from a channel")
|
||||||
|
summary("Get latest channel announcement")
|
||||||
|
response {
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
mediaTypes("application/json")
|
||||||
|
description("The latest announcement in the channel")
|
||||||
|
responseType<APIResponseAnnouncement>()
|
||||||
|
}
|
||||||
|
canRespond {
|
||||||
|
responseCode(HttpStatusCode.NotFound)
|
||||||
|
description("The channel does not exist")
|
||||||
|
responseType<Unit>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Route.installLatestChannelAnnouncementIdRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("Announcements")
|
||||||
|
|
||||||
|
parameters = listOf(
|
||||||
|
Parameter(
|
||||||
|
name = "channel",
|
||||||
|
`in` = Parameter.Location.path,
|
||||||
|
schema = TypeDefinition.STRING,
|
||||||
|
description = "The channel to get the latest announcement id from",
|
||||||
|
required = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
get = GetInfo.builder {
|
||||||
|
description("Get the id of the latest announcement from a channel")
|
||||||
|
summary("Get id of latest announcement from channel")
|
||||||
|
response {
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
mediaTypes("application/json")
|
||||||
|
description("The id of the latest announcement from the channel")
|
||||||
|
responseType<APIResponseAnnouncementId>()
|
||||||
|
}
|
||||||
|
canRespond {
|
||||||
|
responseCode(HttpStatusCode.NotFound)
|
||||||
|
description("The channel does not exist")
|
||||||
|
responseType<Unit>()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,19 @@
|
|||||||
package app.revanced.api.configuration.routes
|
package app.revanced.api.configuration.routes
|
||||||
|
|
||||||
|
import app.revanced.api.configuration.installCache
|
||||||
|
import app.revanced.api.configuration.installNoCache
|
||||||
|
import app.revanced.api.configuration.installNotarizedRoute
|
||||||
import app.revanced.api.configuration.respondOrNotFound
|
import app.revanced.api.configuration.respondOrNotFound
|
||||||
|
import app.revanced.api.configuration.schema.APIContributable
|
||||||
|
import app.revanced.api.configuration.schema.APIMember
|
||||||
|
import app.revanced.api.configuration.schema.APIRateLimit
|
||||||
import app.revanced.api.configuration.services.ApiService
|
import app.revanced.api.configuration.services.ApiService
|
||||||
import app.revanced.api.configuration.services.AuthService
|
import app.revanced.api.configuration.services.AuthService
|
||||||
|
import io.bkbn.kompendium.core.metadata.*
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import io.ktor.http.content.CachingOptions
|
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
import io.ktor.server.auth.*
|
import io.ktor.server.auth.*
|
||||||
import io.ktor.server.http.content.*
|
import io.ktor.server.http.content.*
|
||||||
import io.ktor.server.plugins.cachingheaders.*
|
|
||||||
import io.ktor.server.plugins.cors.routing.*
|
|
||||||
import io.ktor.server.plugins.ratelimit.*
|
import io.ktor.server.plugins.ratelimit.*
|
||||||
import io.ktor.server.response.*
|
import io.ktor.server.response.*
|
||||||
import io.ktor.server.routing.*
|
import io.ktor.server.routing.*
|
||||||
@ -22,17 +26,19 @@ internal fun Route.rootRoute() {
|
|||||||
|
|
||||||
rateLimit(RateLimitName("strong")) {
|
rateLimit(RateLimitName("strong")) {
|
||||||
authenticate("basic") {
|
authenticate("basic") {
|
||||||
get("token") {
|
route("token") {
|
||||||
|
installTokenRouteDocumentation()
|
||||||
|
|
||||||
|
get {
|
||||||
call.respond(authService.newToken())
|
call.respond(authService.newToken())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
route("contributors") {
|
route("contributors") {
|
||||||
install(CachingHeaders) {
|
installCache(1.days)
|
||||||
options { _, _ ->
|
|
||||||
CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 1.days.inWholeSeconds.toInt()))
|
installContributorsRouteDocumentation()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get {
|
get {
|
||||||
call.respond(apiService.contributors())
|
call.respond(apiService.contributors())
|
||||||
@ -40,11 +46,9 @@ internal fun Route.rootRoute() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
route("team") {
|
route("team") {
|
||||||
install(CachingHeaders) {
|
installCache(1.days)
|
||||||
options { _, _ ->
|
|
||||||
CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 1.days.inWholeSeconds.toInt()))
|
installTeamRouteDocumentation()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get {
|
get {
|
||||||
call.respond(apiService.team())
|
call.respond(apiService.team())
|
||||||
@ -53,21 +57,23 @@ internal fun Route.rootRoute() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
route("ping") {
|
route("ping") {
|
||||||
install(CachingHeaders) {
|
installNoCache()
|
||||||
options { _, _ ->
|
|
||||||
CachingOptions(CacheControl.NoCache(null))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handle {
|
installPingRouteDocumentation()
|
||||||
|
|
||||||
|
head {
|
||||||
call.respond(HttpStatusCode.NoContent)
|
call.respond(HttpStatusCode.NoContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rateLimit(RateLimitName("weak")) {
|
rateLimit(RateLimitName("weak")) {
|
||||||
get("backend/rate_limit") {
|
route("backend/rate_limit") {
|
||||||
|
installRateLimitRouteDocumentation()
|
||||||
|
|
||||||
|
get {
|
||||||
call.respondOrNotFound(apiService.rateLimit())
|
call.respondOrNotFound(apiService.rateLimit())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
staticResources("/", "/app/revanced/api/static") {
|
staticResources("/", "/app/revanced/api/static") {
|
||||||
contentType { ContentType.Application.Json }
|
contentType { ContentType.Application.Json }
|
||||||
@ -75,3 +81,77 @@ internal fun Route.rootRoute() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Route.installRateLimitRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("API")
|
||||||
|
|
||||||
|
get = GetInfo.builder {
|
||||||
|
description("Get the rate limit of the backend")
|
||||||
|
summary("Get rate limit of backend")
|
||||||
|
response {
|
||||||
|
description("The rate limit of the backend")
|
||||||
|
mediaTypes("application/json")
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
responseType<APIRateLimit>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Route.installPingRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("API")
|
||||||
|
|
||||||
|
head = HeadInfo.builder {
|
||||||
|
description("Ping the server")
|
||||||
|
summary("Ping")
|
||||||
|
response {
|
||||||
|
description("The server is reachable")
|
||||||
|
responseCode(HttpStatusCode.NoContent)
|
||||||
|
responseType<Unit>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Route.installTeamRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("API")
|
||||||
|
|
||||||
|
get = GetInfo.builder {
|
||||||
|
description("Get the list of team members")
|
||||||
|
summary("Get team members")
|
||||||
|
response {
|
||||||
|
description("The list of team members")
|
||||||
|
mediaTypes("application/json")
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
responseType<Set<APIMember>>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Route.installContributorsRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("API")
|
||||||
|
|
||||||
|
get = GetInfo.builder {
|
||||||
|
description("Get the list of contributors")
|
||||||
|
summary("Get contributors")
|
||||||
|
response {
|
||||||
|
description("The list of contributors")
|
||||||
|
mediaTypes("application/json")
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
responseType<Set<APIContributable>>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Route.installTokenRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("API")
|
||||||
|
|
||||||
|
get = GetInfo.builder {
|
||||||
|
description("Get a new authorization token")
|
||||||
|
summary("Get authorization token")
|
||||||
|
response {
|
||||||
|
description("The authorization token")
|
||||||
|
mediaTypes("application/json")
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
responseType<String>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
package app.revanced.api.configuration.routes
|
package app.revanced.api.configuration.routes
|
||||||
|
|
||||||
|
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.PatchesService
|
import app.revanced.api.configuration.services.PatchesService
|
||||||
|
import io.bkbn.kompendium.core.metadata.GetInfo
|
||||||
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.*
|
||||||
@ -12,20 +16,75 @@ internal fun Route.patchesRoute() = route("patches") {
|
|||||||
val patchesService = koinGet<PatchesService>()
|
val patchesService = koinGet<PatchesService>()
|
||||||
|
|
||||||
route("latest") {
|
route("latest") {
|
||||||
|
installLatestPatchesRouteDocumentation()
|
||||||
|
|
||||||
rateLimit(RateLimitName("weak")) {
|
rateLimit(RateLimitName("weak")) {
|
||||||
get {
|
get {
|
||||||
call.respond(patchesService.latestRelease())
|
call.respond(patchesService.latestRelease())
|
||||||
}
|
}
|
||||||
|
|
||||||
get("version") {
|
route("version") {
|
||||||
|
installLatestPatchesVersionRouteDocumentation()
|
||||||
|
|
||||||
|
get {
|
||||||
call.respond(patchesService.latestVersion())
|
call.respond(patchesService.latestVersion())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rateLimit(RateLimitName("strong")) {
|
rateLimit(RateLimitName("strong")) {
|
||||||
get("list") {
|
route("list") {
|
||||||
|
installLatestPatchesListRouteDocumentation()
|
||||||
|
|
||||||
|
get {
|
||||||
call.respondBytes(ContentType.Application.Json) { patchesService.list() }
|
call.respondBytes(ContentType.Application.Json) { patchesService.list() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Route.installLatestPatchesRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("Patches")
|
||||||
|
|
||||||
|
get = GetInfo.builder {
|
||||||
|
description("Get the latest patches release")
|
||||||
|
summary("Get latest patches release")
|
||||||
|
response {
|
||||||
|
description("The latest patches release")
|
||||||
|
mediaTypes("application/json")
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
responseType<APIRelease>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Route.installLatestPatchesVersionRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("Patches")
|
||||||
|
|
||||||
|
get = GetInfo.builder {
|
||||||
|
description("Get the latest patches release version")
|
||||||
|
summary("Get latest patches release version")
|
||||||
|
response {
|
||||||
|
description("The latest patches release version")
|
||||||
|
mediaTypes("application/json")
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
responseType<APIReleaseVersion>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Route.installLatestPatchesListRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("Patches")
|
||||||
|
|
||||||
|
get = GetInfo.builder {
|
||||||
|
description("Get the list of patches from the latest patches release")
|
||||||
|
summary("Get list of patches from latest patches release")
|
||||||
|
response {
|
||||||
|
description("The list of patches")
|
||||||
|
mediaTypes("application/json")
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
responseType<String>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -98,7 +98,7 @@ class APIResponseAnnouncement(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class APILatestAnnouncement(
|
class APIResponseAnnouncementId(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -2,14 +2,14 @@ package app.revanced.api.configuration.services
|
|||||||
|
|
||||||
import app.revanced.api.configuration.repository.AnnouncementRepository
|
import app.revanced.api.configuration.repository.AnnouncementRepository
|
||||||
import app.revanced.api.configuration.schema.APIAnnouncement
|
import app.revanced.api.configuration.schema.APIAnnouncement
|
||||||
import app.revanced.api.configuration.schema.APILatestAnnouncement
|
import app.revanced.api.configuration.schema.APIResponseAnnouncementId
|
||||||
import kotlinx.datetime.LocalDateTime
|
import kotlinx.datetime.LocalDateTime
|
||||||
|
|
||||||
internal class AnnouncementService(
|
internal class AnnouncementService(
|
||||||
private val announcementRepository: AnnouncementRepository,
|
private val announcementRepository: AnnouncementRepository,
|
||||||
) {
|
) {
|
||||||
fun latestId(channel: String): APILatestAnnouncement? = announcementRepository.latestId(channel)
|
fun latestId(channel: String): APIResponseAnnouncementId? = announcementRepository.latestId(channel)
|
||||||
fun latestId(): APILatestAnnouncement? = announcementRepository.latestId()
|
fun latestId(): APIResponseAnnouncementId? = announcementRepository.latestId()
|
||||||
|
|
||||||
fun latest(channel: String) = announcementRepository.latest(channel)
|
fun latest(channel: String) = announcementRepository.latest(channel)
|
||||||
fun latest() = announcementRepository.latest()
|
fun latest() = announcementRepository.latest()
|
||||||
|
@ -23,7 +23,7 @@ internal class ApiService(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.awaitAll()
|
}.awaitAll().toSet()
|
||||||
|
|
||||||
suspend fun team() = backendRepository.members(configurationRepository.organization).map { member ->
|
suspend fun team() = backendRepository.members(configurationRepository.organization).map { member ->
|
||||||
APIMember(
|
APIMember(
|
||||||
@ -41,7 +41,7 @@ internal class ApiService(
|
|||||||
},
|
},
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}.toSet()
|
||||||
|
|
||||||
suspend fun rateLimit() = backendRepository.rateLimit()?.let {
|
suspend fun rateLimit() = backendRepository.rateLimit()?.let {
|
||||||
APIRateLimit(it.limit, it.remaining, it.reset)
|
APIRateLimit(it.limit, it.remaining, it.reset)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user