mirror of
https://github.com/revanced/revanced-api.git
synced 2025-04-29 22:24:31 +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.serialization.kotlinx.json)
|
||||
implementation(libs.koin.ktor)
|
||||
implementation("io.bkbn:kompendium-core:latest.release")
|
||||
implementation(libs.h2)
|
||||
implementation(libs.logback.classic)
|
||||
implementation(libs.exposed.core)
|
||||
|
@ -3,6 +3,14 @@ package app.revanced.api.command
|
||||
import picocli.CommandLine
|
||||
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>) {
|
||||
CommandLine(MainCommand).execute(*args).let(System::exit)
|
||||
}
|
||||
@ -10,15 +18,7 @@ fun main(args: Array<String>) {
|
||||
private object CLIVersionProvider : CommandLine.IVersionProvider {
|
||||
override fun getVersion() =
|
||||
arrayOf(
|
||||
MainCommand::class.java.getResourceAsStream(
|
||||
"/app/revanced/api/version.properties",
|
||||
)?.use { stream ->
|
||||
Properties().apply {
|
||||
load(stream)
|
||||
}.let {
|
||||
"ReVanced API v${it.getProperty("version")}"
|
||||
}
|
||||
} ?: "ReVanced API",
|
||||
"ReVanced API $applicationVersion",
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,6 @@
|
||||
package app.revanced.api.command
|
||||
|
||||
import app.revanced.api.configuration.configureDependencies
|
||||
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 app.revanced.api.configuration.*
|
||||
import io.ktor.server.engine.*
|
||||
import io.ktor.server.jetty.*
|
||||
import picocli.CommandLine
|
||||
@ -42,6 +38,7 @@ internal object StartAPICommand : Runnable {
|
||||
configureHTTP()
|
||||
configureSerialization()
|
||||
configureSecurity()
|
||||
configureOpenAPI()
|
||||
configureRouting()
|
||||
}.start(wait = true)
|
||||
}
|
||||
|
@ -1,7 +1,27 @@
|
||||
package app.revanced.api.configuration
|
||||
|
||||
import io.bkbn.kompendium.core.plugin.NotarizedRoute
|
||||
import io.ktor.http.*
|
||||
import io.ktor.http.content.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.plugins.cachingheaders.*
|
||||
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.patchesRoute
|
||||
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.plugins.cachingheaders.*
|
||||
import io.ktor.server.routing.*
|
||||
import org.koin.ktor.ext.get
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
@ -16,13 +13,7 @@ import kotlin.time.Duration.Companion.minutes
|
||||
internal fun Application.configureRouting() = routing {
|
||||
val configuration = get<ConfigurationRepository>()
|
||||
|
||||
install(CachingHeaders) {
|
||||
options { _, _ ->
|
||||
CachingOptions(
|
||||
CacheControl.MaxAge(maxAgeSeconds = 5.minutes.inWholeSeconds.toInt()),
|
||||
)
|
||||
}
|
||||
}
|
||||
installCache(5.minutes)
|
||||
|
||||
route("/v${configuration.apiVersion}") {
|
||||
rootRoute()
|
||||
|
@ -1,5 +1,6 @@
|
||||
package app.revanced.api.configuration
|
||||
|
||||
import io.bkbn.kompendium.oas.serialization.KompendiumSerializersModule
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.plugins.contentnegotiation.*
|
||||
@ -12,6 +13,7 @@ fun Application.configureSerialization() {
|
||||
install(ContentNegotiation) {
|
||||
json(
|
||||
Json {
|
||||
serializersModule = KompendiumSerializersModule.module
|
||||
namingStrategy = JsonNamingStrategy.SnakeCase
|
||||
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.schema.APIAnnouncement
|
||||
import app.revanced.api.configuration.schema.APILatestAnnouncement
|
||||
import app.revanced.api.configuration.schema.APIResponseAnnouncement
|
||||
import app.revanced.api.configuration.schema.APIResponseAnnouncementId
|
||||
import kotlinx.datetime.*
|
||||
import org.jetbrains.exposed.dao.IntEntity
|
||||
import org.jetbrains.exposed.dao.IntEntityClass
|
||||
@ -42,6 +42,8 @@ internal class AnnouncementRepository(private val database: Database) {
|
||||
announcement.delete()
|
||||
}
|
||||
|
||||
// TODO: These are inefficient, but I'm not sure how to make them more efficient.
|
||||
|
||||
fun latest() = transaction {
|
||||
AnnouncementEntity.all().maxByOrNull { it.createdAt }?.toApi()
|
||||
}
|
||||
@ -52,13 +54,13 @@ internal class AnnouncementRepository(private val database: Database) {
|
||||
|
||||
fun latestId() = transaction {
|
||||
AnnouncementEntity.all().maxByOrNull { it.createdAt }?.id?.value?.let {
|
||||
APILatestAnnouncement(it)
|
||||
APIResponseAnnouncementId(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun latestId(channel: String) = transaction {
|
||||
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
|
||||
|
||||
import app.revanced.api.configuration.installCache
|
||||
import app.revanced.api.configuration.installNotarizedRoute
|
||||
import app.revanced.api.configuration.respondOrNotFound
|
||||
import app.revanced.api.configuration.schema.APIAnnouncement
|
||||
import app.revanced.api.configuration.schema.APIAnnouncementArchivedAt
|
||||
import app.revanced.api.configuration.schema.APIResponseAnnouncement
|
||||
import app.revanced.api.configuration.schema.APIResponseAnnouncementId
|
||||
import app.revanced.api.configuration.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.content.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.plugins.cachingheaders.*
|
||||
import io.ktor.server.plugins.ratelimit.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
@ -20,49 +28,9 @@ import org.koin.ktor.ext.get as koinGet
|
||||
internal fun Route.announcementsRoute() = route("announcements") {
|
||||
val announcementService = koinGet<AnnouncementService>()
|
||||
|
||||
install(CachingHeaders) {
|
||||
options { _, _ ->
|
||||
CachingOptions(
|
||||
CacheControl.MaxAge(maxAgeSeconds = 1.minutes.inWholeSeconds.toInt()),
|
||||
)
|
||||
}
|
||||
}
|
||||
installCache(5.minutes)
|
||||
|
||||
rateLimit(RateLimitName("weak")) {
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
installAnnouncementsRouteDocumentation()
|
||||
|
||||
rateLimit(RateLimitName("strong")) {
|
||||
get {
|
||||
@ -70,37 +38,333 @@ internal fun Route.announcementsRoute() = route("announcements") {
|
||||
}
|
||||
}
|
||||
|
||||
rateLimit(RateLimitName("strong")) {
|
||||
route("{channel}/latest") {
|
||||
installLatestChannelAnnouncementRouteDocumentation()
|
||||
|
||||
get {
|
||||
val channel: String by call.parameters
|
||||
|
||||
call.respondOrNotFound(announcementService.latest(channel))
|
||||
}
|
||||
|
||||
route("id") {
|
||||
installLatestChannelAnnouncementIdRouteDocumentation()
|
||||
|
||||
get {
|
||||
val channel: String by call.parameters
|
||||
|
||||
call.respondOrNotFound(announcementService.latestId(channel))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rateLimit(RateLimitName("strong")) {
|
||||
route("{channel}") {
|
||||
installChannelAnnouncementsRouteDocumentation()
|
||||
|
||||
get {
|
||||
val channel: String by call.parameters
|
||||
|
||||
call.respond(announcementService.all(channel))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rateLimit(RateLimitName("strong")) {
|
||||
route("latest") {
|
||||
installLatestAnnouncementRouteDocumentation()
|
||||
|
||||
get {
|
||||
call.respondOrNotFound(announcementService.latest())
|
||||
}
|
||||
|
||||
route("id") {
|
||||
installLatestAnnouncementIdRouteDocumentation()
|
||||
|
||||
get {
|
||||
call.respondOrNotFound(announcementService.latestId())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rateLimit(RateLimitName("strong")) {
|
||||
authenticate("jwt") {
|
||||
post {
|
||||
announcementService.new(call.receive<APIAnnouncement>())
|
||||
post<APIAnnouncement> { announcement ->
|
||||
announcementService.new(announcement)
|
||||
}
|
||||
|
||||
post("{id}/archive") {
|
||||
val id: Int by call.parameters
|
||||
val archivedAt = call.receiveNullable<APIAnnouncementArchivedAt>()?.archivedAt
|
||||
route("{id}") {
|
||||
installAnnouncementIdRouteDocumentation()
|
||||
|
||||
announcementService.archive(id, archivedAt)
|
||||
}
|
||||
patch<APIAnnouncement> { announcement ->
|
||||
val id: Int by call.parameters
|
||||
|
||||
post("{id}/unarchive") {
|
||||
val id: Int by call.parameters
|
||||
announcementService.update(id, announcement)
|
||||
}
|
||||
|
||||
announcementService.unarchive(id)
|
||||
}
|
||||
delete {
|
||||
val id: Int by call.parameters
|
||||
|
||||
patch("{id}") {
|
||||
val id: Int by call.parameters
|
||||
val announcement = call.receive<APIAnnouncement>()
|
||||
announcementService.delete(id)
|
||||
}
|
||||
|
||||
announcementService.update(id, announcement)
|
||||
}
|
||||
route("archive") {
|
||||
installAnnouncementArchiveRouteDocumentation()
|
||||
|
||||
delete("{id}") {
|
||||
val id: Int by call.parameters
|
||||
post {
|
||||
val id: Int by call.parameters
|
||||
val archivedAt = call.receiveNullable<APIAnnouncementArchivedAt>()?.archivedAt
|
||||
|
||||
announcementService.delete(id)
|
||||
announcementService.archive(id, archivedAt)
|
||||
}
|
||||
}
|
||||
|
||||
route("unarchive") {
|
||||
installAnnouncementUnarchiveRouteDocumentation()
|
||||
|
||||
post {
|
||||
val id: Int by call.parameters
|
||||
|
||||
announcementService.unarchive(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Route.installLatestAnnouncementRouteDocumentation() = installNotarizedRoute {
|
||||
tags = setOf("Announcements")
|
||||
|
||||
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>()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Route.installLatestAnnouncementIdRouteDocumentation() = installNotarizedRoute {
|
||||
tags = setOf("Announcements")
|
||||
|
||||
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
|
||||
|
||||
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.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.AuthService
|
||||
import io.bkbn.kompendium.core.metadata.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.http.content.CachingOptions
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
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.response.*
|
||||
import io.ktor.server.routing.*
|
||||
@ -22,17 +26,19 @@ internal fun Route.rootRoute() {
|
||||
|
||||
rateLimit(RateLimitName("strong")) {
|
||||
authenticate("basic") {
|
||||
get("token") {
|
||||
call.respond(authService.newToken())
|
||||
route("token") {
|
||||
installTokenRouteDocumentation()
|
||||
|
||||
get {
|
||||
call.respond(authService.newToken())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
route("contributors") {
|
||||
install(CachingHeaders) {
|
||||
options { _, _ ->
|
||||
CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 1.days.inWholeSeconds.toInt()))
|
||||
}
|
||||
}
|
||||
installCache(1.days)
|
||||
|
||||
installContributorsRouteDocumentation()
|
||||
|
||||
get {
|
||||
call.respond(apiService.contributors())
|
||||
@ -40,11 +46,9 @@ internal fun Route.rootRoute() {
|
||||
}
|
||||
|
||||
route("team") {
|
||||
install(CachingHeaders) {
|
||||
options { _, _ ->
|
||||
CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 1.days.inWholeSeconds.toInt()))
|
||||
}
|
||||
}
|
||||
installCache(1.days)
|
||||
|
||||
installTeamRouteDocumentation()
|
||||
|
||||
get {
|
||||
call.respond(apiService.team())
|
||||
@ -53,20 +57,22 @@ internal fun Route.rootRoute() {
|
||||
}
|
||||
|
||||
route("ping") {
|
||||
install(CachingHeaders) {
|
||||
options { _, _ ->
|
||||
CachingOptions(CacheControl.NoCache(null))
|
||||
}
|
||||
}
|
||||
installNoCache()
|
||||
|
||||
handle {
|
||||
installPingRouteDocumentation()
|
||||
|
||||
head {
|
||||
call.respond(HttpStatusCode.NoContent)
|
||||
}
|
||||
}
|
||||
|
||||
rateLimit(RateLimitName("weak")) {
|
||||
get("backend/rate_limit") {
|
||||
call.respondOrNotFound(apiService.rateLimit())
|
||||
route("backend/rate_limit") {
|
||||
installRateLimitRouteDocumentation()
|
||||
|
||||
get {
|
||||
call.respondOrNotFound(apiService.rateLimit())
|
||||
}
|
||||
}
|
||||
|
||||
staticResources("/", "/app/revanced/api/static") {
|
||||
@ -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
|
||||
|
||||
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 io.bkbn.kompendium.core.metadata.GetInfo
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.plugins.ratelimit.*
|
||||
@ -12,20 +16,75 @@ internal fun Route.patchesRoute() = route("patches") {
|
||||
val patchesService = koinGet<PatchesService>()
|
||||
|
||||
route("latest") {
|
||||
installLatestPatchesRouteDocumentation()
|
||||
|
||||
rateLimit(RateLimitName("weak")) {
|
||||
get {
|
||||
call.respond(patchesService.latestRelease())
|
||||
}
|
||||
|
||||
get("version") {
|
||||
call.respond(patchesService.latestVersion())
|
||||
route("version") {
|
||||
installLatestPatchesVersionRouteDocumentation()
|
||||
|
||||
get {
|
||||
call.respond(patchesService.latestVersion())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rateLimit(RateLimitName("strong")) {
|
||||
get("list") {
|
||||
call.respondBytes(ContentType.Application.Json) { patchesService.list() }
|
||||
route("list") {
|
||||
installLatestPatchesListRouteDocumentation()
|
||||
|
||||
get {
|
||||
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
|
||||
class APILatestAnnouncement(
|
||||
class APIResponseAnnouncementId(
|
||||
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.schema.APIAnnouncement
|
||||
import app.revanced.api.configuration.schema.APILatestAnnouncement
|
||||
import app.revanced.api.configuration.schema.APIResponseAnnouncementId
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
|
||||
internal class AnnouncementService(
|
||||
private val announcementRepository: AnnouncementRepository,
|
||||
) {
|
||||
fun latestId(channel: String): APILatestAnnouncement? = announcementRepository.latestId(channel)
|
||||
fun latestId(): APILatestAnnouncement? = announcementRepository.latestId()
|
||||
fun latestId(channel: String): APIResponseAnnouncementId? = announcementRepository.latestId(channel)
|
||||
fun latestId(): APIResponseAnnouncementId? = announcementRepository.latestId()
|
||||
|
||||
fun latest(channel: String) = announcementRepository.latest(channel)
|
||||
fun latest() = announcementRepository.latest()
|
||||
|
@ -23,7 +23,7 @@ internal class ApiService(
|
||||
)
|
||||
}
|
||||
}
|
||||
}.awaitAll()
|
||||
}.awaitAll().toSet()
|
||||
|
||||
suspend fun team() = backendRepository.members(configurationRepository.organization).map { member ->
|
||||
APIMember(
|
||||
@ -41,7 +41,7 @@ internal class ApiService(
|
||||
},
|
||||
|
||||
)
|
||||
}
|
||||
}.toSet()
|
||||
|
||||
suspend fun rateLimit() = backendRepository.rateLimit()?.let {
|
||||
APIRateLimit(it.limit, it.remaining, it.reset)
|
||||
|
Loading…
x
Reference in New Issue
Block a user