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
No known key found for this signature in database
GPG Key ID: A9B3094ACDB604B4
14 changed files with 592 additions and 124 deletions

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

View File

@ -98,7 +98,7 @@ class APIResponseAnnouncement(
)
@Serializable
class APILatestAnnouncement(
class APIResponseAnnouncementId(
val id: Int,
)

View File

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

View File

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