feat: Add OpenAPI docs and cache to routes

This commit is contained in:
oSumAtrIX
2024-06-09 01:28:33 +02:00
parent 205bcde77a
commit 6ea63be490
14 changed files with 592 additions and 124 deletions

View File

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

View File

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

View File

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