diff --git a/build.gradle.kts b/build.gradle.kts index 8c3226d1..7a421557 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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) diff --git a/src/main/kotlin/app/revanced/api/command/MainCommand.kt b/src/main/kotlin/app/revanced/api/command/MainCommand.kt index 6b773879..c665597b 100644 --- a/src/main/kotlin/app/revanced/api/command/MainCommand.kt +++ b/src/main/kotlin/app/revanced/api/command/MainCommand.kt @@ -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) { CommandLine(MainCommand).execute(*args).let(System::exit) } @@ -10,15 +18,7 @@ fun main(args: Array) { 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", ) } diff --git a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt index 7131be4c..a220addb 100644 --- a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt +++ b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt @@ -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) } diff --git a/src/main/kotlin/app/revanced/api/configuration/Extensions.kt b/src/main/kotlin/app/revanced/api/configuration/Extensions.kt index e630bc68..d7546939 100644 --- a/src/main/kotlin/app/revanced/api/configuration/Extensions.kt +++ b/src/main/kotlin/app/revanced/api/configuration/Extensions.kt @@ -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) diff --git a/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt b/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt new file mode 100644 index 00000000..48bcb881 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt @@ -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() + } +} diff --git a/src/main/kotlin/app/revanced/api/configuration/Routing.kt b/src/main/kotlin/app/revanced/api/configuration/Routing.kt index d6dcff88..f20dc4c4 100644 --- a/src/main/kotlin/app/revanced/api/configuration/Routing.kt +++ b/src/main/kotlin/app/revanced/api/configuration/Routing.kt @@ -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() - install(CachingHeaders) { - options { _, _ -> - CachingOptions( - CacheControl.MaxAge(maxAgeSeconds = 5.minutes.inWholeSeconds.toInt()), - ) - } - } + installCache(5.minutes) route("/v${configuration.apiVersion}") { rootRoute() diff --git a/src/main/kotlin/app/revanced/api/configuration/Serialization.kt b/src/main/kotlin/app/revanced/api/configuration/Serialization.kt index 43d3eab5..ceb6182c 100644 --- a/src/main/kotlin/app/revanced/api/configuration/Serialization.kt +++ b/src/main/kotlin/app/revanced/api/configuration/Serialization.kt @@ -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 }, diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt index 7da8300e..eb8475e1 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt @@ -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) } } diff --git a/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt b/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt index 984eb178..814402c4 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt @@ -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,87 +28,343 @@ import org.koin.ktor.ext.get as koinGet internal fun Route.announcementsRoute() = route("announcements") { val announcementService = koinGet() - install(CachingHeaders) { - options { _, _ -> - CachingOptions( - CacheControl.MaxAge(maxAgeSeconds = 1.minutes.inWholeSeconds.toInt()), - ) + installCache(5.minutes) + + installAnnouncementsRouteDocumentation() + + rateLimit(RateLimitName("strong")) { + get { + call.respond(announcementService.all()) } } - rateLimit(RateLimitName("weak")) { + rateLimit(RateLimitName("strong")) { route("{channel}/latest") { - get("id") { - val channel: String by call.parameters - - call.respondOrNotFound(announcementService.latestId(channel)) - } + 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")) { - get("{channel}") { - val channel: String by call.parameters + route("{channel}") { + installChannelAnnouncementsRouteDocumentation() + + get { + val channel: String by call.parameters - call.respond(announcementService.all(channel)) + call.respond(announcementService.all(channel)) + } } } rateLimit(RateLimitName("strong")) { route("latest") { - get("id") { - call.respondOrNotFound(announcementService.latestId()) - } + installLatestAnnouncementRouteDocumentation() get { call.respondOrNotFound(announcementService.latest()) } - } - } - rateLimit(RateLimitName("strong")) { - get { - call.respond(announcementService.all()) + route("id") { + installLatestAnnouncementIdRouteDocumentation() + + get { + call.respondOrNotFound(announcementService.latestId()) + } + } } } rateLimit(RateLimitName("strong")) { authenticate("jwt") { - post { - announcementService.new(call.receive()) + post { announcement -> + announcementService.new(announcement) } - post("{id}/archive") { - val id: Int by call.parameters - val archivedAt = call.receiveNullable()?.archivedAt + route("{id}") { + installAnnouncementIdRouteDocumentation() - announcementService.archive(id, archivedAt) - } + patch { 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() + announcementService.delete(id) + } - announcementService.update(id, announcement) - } + route("archive") { + installAnnouncementArchiveRouteDocumentation() + + post { + val id: Int by call.parameters + val archivedAt = call.receiveNullable()?.archivedAt - delete("{id}") { - val id: Int by call.parameters + announcementService.archive(id, archivedAt) + } + } - announcementService.delete(id) + 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() + } + canRespond { + responseCode(HttpStatusCode.NotFound) + description("No announcement exists") + responseType() + } + } +} + +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() + } + canRespond { + responseCode(HttpStatusCode.NotFound) + description("No announcement exists") + responseType() + } + } +} + +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>() + } + } +} + +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() + } + } +} + +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() + } + } +} + +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() + description("The new announcement") + } + response { + description("When announcement was updated") + responseCode(HttpStatusCode.OK) + responseType() + } + } + + delete = DeleteInfo.builder { + description("Delete an announcement") + summary("Delete announcement") + response { + description("When the announcement was deleted") + responseCode(HttpStatusCode.OK) + responseType() + } + } +} + +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>() + } + } +} + +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() + } + canRespond { + responseCode(HttpStatusCode.NotFound) + description("The channel does not exist") + responseType() + } + } +} + +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() + } + canRespond { + responseCode(HttpStatusCode.NotFound) + description("The channel does not exist") + responseType() + } + } +} diff --git a/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt index 2b20b0b5..fe2d3302 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt @@ -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() + } + } +} + +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() + } + } +} + +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>() + } + } +} + +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>() + } + } +} + +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() + } + } +} diff --git a/src/main/kotlin/app/revanced/api/configuration/routes/PatchesRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routes/PatchesRoute.kt index a5811bfb..ab11b324 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routes/PatchesRoute.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/PatchesRoute.kt @@ -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() 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() + } + } +} + +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() + } + } +} + +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() + } + } +} diff --git a/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt b/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt index b6018e42..1dad95fa 100644 --- a/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt +++ b/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt @@ -98,7 +98,7 @@ class APIResponseAnnouncement( ) @Serializable -class APILatestAnnouncement( +class APIResponseAnnouncementId( val id: Int, ) diff --git a/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt b/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt index 80a3ddb0..7a0b4603 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt @@ -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() diff --git a/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt b/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt index bf408615..10b54cf3 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt @@ -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)