From 42f731854d0b91070bd7b075054d67d3d9e1d1b4 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 31 Jan 2024 03:01:07 +0100 Subject: [PATCH] feat: Add announcements API --- .env.example | 13 +- build.gradle.kts | 3 + configuration.example.toml | 2 +- gradle/libs.versions.toml | 16 +- .../app/revanced/api/backend/Backend.kt | 3 +- .../api/backend/github/GitHubBackend.kt | 2 + .../api/backend/github/api/ResponseSchema.kt | 3 +- .../revanced/api/command/StartAPICommand.kt | 5 +- .../app/revanced/api/modules/Database.kt | 160 ++++++++++++++++++ .../app/revanced/api/modules/Dependencies.kt | 73 ++++++++ .../revanced/api/{plugins => modules}/HTTP.kt | 2 +- .../app/revanced/api/modules/Routing.kt | 158 +++++++++++++++++ .../app/revanced/api/modules/Security.kt | 53 ++++++ .../api/{plugins => modules}/Serialization.kt | 2 +- .../app/revanced/api/plugins/Databases.kt | 49 ------ .../app/revanced/api/plugins/Dependencies.kt | 33 ---- .../app/revanced/api/plugins/Routing.kt | 88 ---------- .../app/revanced/api/plugins/Security.kt | 30 ---- .../app/revanced/api/plugins/UsersSchema.kt | 59 ------- .../app/revanced/api/schema/APISchema.kt | 39 +++-- .../kotlin/app/revanced/ApplicationTest.kt | 2 +- 21 files changed, 509 insertions(+), 286 deletions(-) create mode 100644 src/main/kotlin/app/revanced/api/modules/Database.kt create mode 100644 src/main/kotlin/app/revanced/api/modules/Dependencies.kt rename src/main/kotlin/app/revanced/api/{plugins => modules}/HTTP.kt (96%) create mode 100644 src/main/kotlin/app/revanced/api/modules/Routing.kt create mode 100644 src/main/kotlin/app/revanced/api/modules/Security.kt rename src/main/kotlin/app/revanced/api/{plugins => modules}/Serialization.kt (87%) delete mode 100644 src/main/kotlin/app/revanced/api/plugins/Databases.kt delete mode 100644 src/main/kotlin/app/revanced/api/plugins/Dependencies.kt delete mode 100644 src/main/kotlin/app/revanced/api/plugins/Routing.kt delete mode 100644 src/main/kotlin/app/revanced/api/plugins/Security.kt delete mode 100644 src/main/kotlin/app/revanced/api/plugins/UsersSchema.kt diff --git a/.env.example b/.env.example index 2bdac94..91a9819 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,13 @@ GITHUB_TOKEN= -CONFIG_FILE_PATH= \ No newline at end of file +CONFIG_FILE_PATH=configuration.toml + +DB_URL=jdbc:h2:./api.db +DB_USER= +DB_PASSWORD= + +JWT_SECRET= +JWT_ISSUER= +JWT_VALIDITY_IN_MIN= + +BASIC_USERNAME= +BASIC_PASSWORD= diff --git a/build.gradle.kts b/build.gradle.kts index 50cccda..b932b6f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -45,10 +45,13 @@ dependencies { implementation(libs.logback.classic) implementation(libs.exposed.core) implementation(libs.exposed.jdbc) + implementation(libs.exposed.dao) + implementation(libs.exposed.kotlin.datetime) implementation(libs.dotenv.kotlin) implementation(libs.ktoml.core) implementation(libs.ktoml.file) implementation(libs.picocli) + implementation(libs.kotlinx.datetime) testImplementation(libs.ktor.server.tests) testImplementation(libs.kotlin.test.junit) diff --git a/configuration.example.toml b/configuration.example.toml index 5935fd6..0fdadb0 100644 --- a/configuration.example.toml +++ b/configuration.example.toml @@ -2,4 +2,4 @@ organization = "org" patches-repository = "patches" integrations-repositories = ["integrations"] contributors-repositories = ["patches", "integrations"] -api-version = 1 +api-version = 1 \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 016787b..f5d3655 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,13 +1,14 @@ [versions] -kotlin="1.9.22" -logback="1.4.14" -exposed="0.41.1" -h2="2.1.214" -koin="3.5.3" -dotenv="6.4.1" +kotlin = "1.9.22" +logback = "1.4.14" +exposed = "0.41.1" +h2 = "2.2.224" +koin = "3.5.3" +dotenv = "6.4.1" ktor = "2.3.7" ktoml = "0.5.1" picocli = "4.7.3" +datetime = "0.5.0" [libraries] ktor-client-core = { module = "io.ktor:ktor-client-core" } @@ -31,12 +32,15 @@ h2 = { module = "com.h2database:h2", version.ref = "h2" } logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" } exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" } +exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" } +exposed-kotlin-datetime = { module = "org.jetbrains.exposed:exposed-kotlin-datetime", version.ref = "exposed" } dotenv-kotlin = { module = "io.github.cdimascio:dotenv-kotlin", version.ref = "dotenv" } ktor-server-tests = { module = "io.ktor:ktor-server-tests" } kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } ktoml-core = { module = "com.akuleshov7:ktoml-core", version.ref = "ktoml" } ktoml-file = { module = "com.akuleshov7:ktoml-file", version.ref = "ktoml" } picocli = { module = "info.picocli:picocli", version.ref = "picocli" } +kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" } [plugins] serilization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } diff --git a/src/main/kotlin/app/revanced/api/backend/Backend.kt b/src/main/kotlin/app/revanced/api/backend/Backend.kt index 6549c64..e231949 100644 --- a/src/main/kotlin/app/revanced/api/backend/Backend.kt +++ b/src/main/kotlin/app/revanced/api/backend/Backend.kt @@ -2,6 +2,7 @@ package app.revanced.api.backend import io.ktor.client.* import io.ktor.client.engine.okhttp.* +import kotlinx.datetime.LocalDateTime import kotlinx.serialization.Serializable /** @@ -89,7 +90,7 @@ abstract class Backend( class BackendRelease( val tag: String, val releaseNote: String, - val createdAt: String, + val createdAt: LocalDateTime, val assets: Set ) { /** diff --git a/src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt b/src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt index e024ce5..10dcfda 100644 --- a/src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt +++ b/src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt @@ -26,6 +26,8 @@ import kotlinx.coroutines.* import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonNamingStrategy +import org.koin.dsl.bind +import org.koin.dsl.module @OptIn(ExperimentalSerializationApi::class) class GitHubBackend(token: String? = null) : Backend({ diff --git a/src/main/kotlin/app/revanced/api/backend/github/api/ResponseSchema.kt b/src/main/kotlin/app/revanced/api/backend/github/api/ResponseSchema.kt index 26d56b5..9fe86ab 100644 --- a/src/main/kotlin/app/revanced/api/backend/github/api/ResponseSchema.kt +++ b/src/main/kotlin/app/revanced/api/backend/github/api/ResponseSchema.kt @@ -1,5 +1,6 @@ package app.revanced.api.backend.github.api +import kotlinx.datetime.LocalDateTime import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -40,7 +41,7 @@ class Response { class GitHubRelease( val tagName: String, val assets: Set, - val createdAt: String, + val createdAt: LocalDateTime, val body: String ) { @Serializable diff --git a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt index 0aa50ed..b4e3cd6 100644 --- a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt +++ b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt @@ -1,6 +1,6 @@ package app.revanced.api.command -import app.revanced.api.plugins.* +import app.revanced.api.modules.* import io.ktor.server.engine.* import io.ktor.server.netty.* import picocli.CommandLine @@ -30,11 +30,10 @@ internal object StartAPICommand : Runnable { workerGroupSize = 1 callGroupSize = 1 }) { + configureDependencies() configureHTTP() configureSerialization() - configureDatabases() configureSecurity() - configureDependencies() configureRouting() }.start(wait = true) } diff --git a/src/main/kotlin/app/revanced/api/modules/Database.kt b/src/main/kotlin/app/revanced/api/modules/Database.kt new file mode 100644 index 0000000..fcd0955 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/modules/Database.kt @@ -0,0 +1,160 @@ +package app.revanced.api.modules + +import app.revanced.api.modules.AnnouncementService.Attachments.announcement +import app.revanced.api.schema.APIResponseAnnouncement +import app.revanced.api.schema.APIAnnouncement +import app.revanced.api.schema.APILatestAnnouncement +import kotlinx.datetime.* +import org.jetbrains.exposed.sql.transactions.transaction +import org.jetbrains.exposed.dao.IntEntity +import org.jetbrains.exposed.dao.IntEntityClass +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.kotlin.datetime.datetime + + +class AnnouncementService(private val database: Database) { + private object Announcements : IntIdTable() { + val author = varchar("author", 32).nullable() + val title = varchar("title", 64) + val content = text("content").nullable() + val channel = varchar("channel", 16).nullable() + val createdAt = datetime("createdAt") + val archivedAt = datetime("archivedAt").nullable() + val level = integer("level") + } + + private object Attachments : IntIdTable() { + val url = varchar("url", 256) + val announcement = reference("announcement", Announcements, onDelete = ReferenceOption.CASCADE) + } + + class Announcement(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(Announcements) + + var author by Announcements.author + var title by Announcements.title + var content by Announcements.content + val attachments by Attachment referrersOn announcement + var channel by Announcements.channel + var createdAt by Announcements.createdAt + var archivedAt by Announcements.archivedAt + var level by Announcements.level + + fun api() = APIResponseAnnouncement( + id.value, + author, + title, + content, + attachments.map(Attachment::url).toSet(), + channel, + createdAt, + archivedAt, + level + ) + } + + class Attachment(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(Attachments) + + var url by Attachments.url + var announcement by Announcement referencedOn Attachments.announcement + } + + init { + transaction { + SchemaUtils.create(Announcements, Attachments) + } + } + + private fun transaction(block: Transaction.() -> T) = transaction(database, block) + + fun read() = transaction { + Announcement.all().map { it.api() }.toSet() + } + + fun read(channel: String) = transaction { + Announcement.find { Announcements.channel eq channel }.map { it.api() }.toSet() + } + + fun delete(id: Int) = transaction { + val announcement = Announcement.findById(id) ?: return@transaction + + announcement.delete() + } + + fun latest() = transaction { + Announcement.all().maxByOrNull { it.createdAt }?.api() + } + + fun latest(channel: String) = transaction { + Announcement.find { Announcements.channel eq channel }.maxByOrNull { it.createdAt }?.api() + } + + fun latestId() = transaction { + Announcement.all().maxByOrNull { it.createdAt }?.id?.value?.let { + APILatestAnnouncement(it) + } + } + + fun latestId(channel: String) = transaction { + Announcement.find { Announcements.channel eq channel }.maxByOrNull { it.createdAt }?.id?.value?.let { + APILatestAnnouncement(it) + } + } + + fun archive( + id: Int, + archivedAt: LocalDateTime? + ) = transaction { + Announcement.findById(id)?.apply { + this.archivedAt = archivedAt ?: java.time.LocalDateTime.now().toKotlinLocalDateTime() + } + } + + fun unarchive(id: Int) = transaction { + Announcement.findById(id)?.apply { + archivedAt = null + } + } + + fun new(new: APIAnnouncement) = transaction { + Announcement.new announcement@{ + author = new.author + title = new.title + content = new.content + channel = new.channel + createdAt = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) + archivedAt = new.archivedAt + level = new.level + }.also { newAnnouncement -> + new.attachmentUrls.map { + Attachment.new { + url = it + announcement = newAnnouncement + } + } + } + } + + fun update(id: Int, new: APIAnnouncement) = transaction { + Announcement.findById(id)?.apply { + author = new.author + title = new.title + content = new.content + channel = new.channel + archivedAt = new.archivedAt + level = new.level + + attachments.forEach(Attachment::delete) + new.attachmentUrls.map { + Attachment.new { + url = it + announcement = this@apply + } + } + } + } +} diff --git a/src/main/kotlin/app/revanced/api/modules/Dependencies.kt b/src/main/kotlin/app/revanced/api/modules/Dependencies.kt new file mode 100644 index 0000000..48ee6c7 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/modules/Dependencies.kt @@ -0,0 +1,73 @@ +package app.revanced.api.modules + +import app.revanced.api.backend.Backend +import app.revanced.api.backend.github.GitHubBackend +import app.revanced.api.schema.APIConfiguration +import com.akuleshov7.ktoml.Toml +import com.akuleshov7.ktoml.source.decodeFromStream +import io.github.cdimascio.dotenv.Dotenv +import io.ktor.server.application.* +import org.jetbrains.exposed.sql.Database +import org.koin.dsl.bind +import org.koin.dsl.module +import org.koin.ktor.plugin.Koin +import java.io.File + +fun Application.configureDependencies() { + install(Koin) { + modules( + globalModule, + gitHubBackendModule, + databaseModule, + authModule + ) + } +} + +val globalModule = module { + single { + Dotenv.load() + } + single { + val configFilePath = get().get("CONFIG_FILE_PATH")!! + Toml.decodeFromStream(File(configFilePath).inputStream()) + } +} + +val gitHubBackendModule = module { + single { + val token = get().get("GITHUB_TOKEN") + GitHubBackend(token) + } bind Backend::class +} + +val databaseModule = module { + single { + val dotenv = get() + + Database.connect( + url = dotenv.get("DB_URL"), + user = dotenv.get("DB_USER"), + password = dotenv.get("DB_PASSWORD"), + driver = "org.h2.Driver" + ) + } + factory { + AnnouncementService(get()) + } +} + +val authModule = module { + single { + val dotenv = get() + + val jwtSecret = dotenv.get("JWT_SECRET")!! + val issuer = dotenv.get("JWT_ISSUER")!! + val validityInMin = dotenv.get("JWT_VALIDITY_IN_MIN")!!.toInt() + + val basicUsername = dotenv.get("BASIC_USERNAME")!! + val basicPassword = dotenv.get("BASIC_PASSWORD")!! + + AuthService(issuer, validityInMin, jwtSecret, basicUsername, basicPassword) + } +} diff --git a/src/main/kotlin/app/revanced/api/plugins/HTTP.kt b/src/main/kotlin/app/revanced/api/modules/HTTP.kt similarity index 96% rename from src/main/kotlin/app/revanced/api/plugins/HTTP.kt rename to src/main/kotlin/app/revanced/api/modules/HTTP.kt index b46f168..590629b 100644 --- a/src/main/kotlin/app/revanced/api/plugins/HTTP.kt +++ b/src/main/kotlin/app/revanced/api/modules/HTTP.kt @@ -1,4 +1,4 @@ -package app.revanced.api.plugins +package app.revanced.api.modules import io.ktor.http.* import io.ktor.http.content.* diff --git a/src/main/kotlin/app/revanced/api/modules/Routing.kt b/src/main/kotlin/app/revanced/api/modules/Routing.kt new file mode 100644 index 0000000..18da381 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/modules/Routing.kt @@ -0,0 +1,158 @@ +package app.revanced.api.modules + +import app.revanced.api.backend.Backend +import app.revanced.api.schema.* +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.http.content.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import io.ktor.util.pipeline.* +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.datetime.toKotlinLocalDateTime +import java.time.LocalDateTime +import org.koin.ktor.ext.get as koinGet + +fun Application.configureRouting() { + val backend: Backend = koinGet() + val configuration: APIConfiguration = koinGet() + val announcementService: AnnouncementService = koinGet() + val authService: AuthService = koinGet() + + routing { + route("/v${configuration.apiVersion}") { + route("/announcements") { + suspend fun PipelineContext<*, ApplicationCall>.announcement( + block: AnnouncementService.() -> APIResponseAnnouncement? + ) = announcementService.block()?.let { call.respond(it) } + ?: call.respond(HttpStatusCode.NotFound) + + suspend fun PipelineContext<*, ApplicationCall>.announcementId( + block: AnnouncementService.() -> APILatestAnnouncement? + ) = announcementService.block()?.let { call.respond(it) } + ?: call.respond(HttpStatusCode.NotFound) + + suspend fun PipelineContext<*, ApplicationCall>.channel(block: suspend (String) -> Unit) = + block(call.parameters["channel"]!!) + + announcementService.new( + APIAnnouncement( + "author", + "title", + "content", + setOf("https://example.com"), + "channel", + LocalDateTime.now().toKotlinLocalDateTime(), + ) + ) + route("/{channel}/latest") { + get("/id") { channel { announcementId { latestId(it) } } } + + get { channel { announcement { latest(it) } } } + } + + get("/{channel}") { channel { call.respond(announcementService.read(it)) } } + + route("/latest") { + get("/id") { announcementId { latestId() } } + + get { announcement { latest() } } + } + + get { call.respond(announcementService.read()) } + + authenticate("jwt") { + suspend fun PipelineContext<*, ApplicationCall>.id(block: suspend (Int) -> Unit) = + call.parameters["id"]!!.toIntOrNull()?.let { block(it) } + ?: call.respond(HttpStatusCode.BadRequest) + + post { announcementService.new(call.receive()) } + + post("/{id}/archive") { + id { + val archivedAt = call.receiveNullable()?.archivedAt + announcementService.archive(it, archivedAt) + } + } + + post("/{id}/unarchive") { id { announcementService.unarchive(it) } } + + patch("/{id}") { id { announcementService.update(it, call.receive()) } } + + delete("/{id}") { id { announcementService.delete(it) } } + } + } + + route("/patches") { + route("latest") { + get { + val patches = backend.getRelease(configuration.organization, configuration.patchesRepository) + val integrations = configuration.integrationsRepositoryNames.map { + async { backend.getRelease(configuration.organization, it) } + }.awaitAll() + + val assets = (patches.assets + integrations.flatMap { it.assets }).filter { + it.downloadUrl.endsWith(".apk") || it.downloadUrl.endsWith(".jar") + }.map { APIAsset(it.downloadUrl) }.toSet() + + val release = APIRelease( + patches.tag, + patches.createdAt, + patches.releaseNote, + assets + ) + + call.respond(release) + } + + get("/version") { + val patches = backend.getRelease(configuration.organization, configuration.patchesRepository) + + val release = APIReleaseVersion(patches.tag) + + call.respond(release) + } + } + } + + staticResources("/", "/static/api") { + contentType { ContentType.Application.Json } + extensions("json") + } + + get("/contributors") { + val contributors = configuration.contributorsRepositoryNames.map { + async { + APIContributable( + it, + backend.getContributors(configuration.organization, it).map { + APIContributor(it.name, it.avatarUrl, it.url, it.contributions) + }.toSet() + ) + } + }.awaitAll() + + call.respond(contributors) + } + + get("/team") { + val team = backend.getMembers(configuration.organization).map { + APIMember(it.name, it.avatarUrl, it.url, it.gpgKeysUrl) + } + + call.respond(team) + } + + route("/ping") { + handle { + call.respond(HttpStatusCode.NoContent) + } + } + + authenticate("basic") { get("/token") { call.respond(authService.newToken()) } } + } + } +} diff --git a/src/main/kotlin/app/revanced/api/modules/Security.kt b/src/main/kotlin/app/revanced/api/modules/Security.kt new file mode 100644 index 0000000..0dd2dcb --- /dev/null +++ b/src/main/kotlin/app/revanced/api/modules/Security.kt @@ -0,0 +1,53 @@ +package app.revanced.api.modules + +import com.auth0.jwt.JWT +import com.auth0.jwt.algorithms.Algorithm +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.auth.jwt.* +import org.koin.ktor.ext.get +import java.util.* +import kotlin.time.Duration.Companion.minutes + +class AuthService( + private val issuer: String, + private val validityInMin: Int, + private val jwtSecret: String, + private val basicUsername: String, + private val basicPassword: String, +) { + val configureSecurity: Application.() -> Unit = { + install(Authentication) { + jwt("jwt") { + verifier( + JWT.require(Algorithm.HMAC256(jwtSecret)) + .withIssuer(issuer) + .build() + ) + validate { credential -> JWTPrincipal(credential.payload) } + } + + basic("basic") { + validate { credentials -> + if (credentials.name == basicUsername && credentials.password == basicPassword) { + UserIdPrincipal(credentials.name) + } else { + null + } + } + } + } + } + + fun newToken(): String { + return JWT.create() + .withIssuer(issuer) + .withExpiresAt(Date(System.currentTimeMillis() + validityInMin.minutes.inWholeMilliseconds)) + .sign(Algorithm.HMAC256(jwtSecret)) + } +} + +fun Application.configureSecurity() { + val configureSecurity = get().configureSecurity + configureSecurity() +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/api/plugins/Serialization.kt b/src/main/kotlin/app/revanced/api/modules/Serialization.kt similarity index 87% rename from src/main/kotlin/app/revanced/api/plugins/Serialization.kt rename to src/main/kotlin/app/revanced/api/modules/Serialization.kt index e2cb0d1..ef38558 100644 --- a/src/main/kotlin/app/revanced/api/plugins/Serialization.kt +++ b/src/main/kotlin/app/revanced/api/modules/Serialization.kt @@ -1,4 +1,4 @@ -package app.revanced.api.plugins +package app.revanced.api.modules import io.ktor.serialization.kotlinx.json.* import io.ktor.server.application.* diff --git a/src/main/kotlin/app/revanced/api/plugins/Databases.kt b/src/main/kotlin/app/revanced/api/plugins/Databases.kt deleted file mode 100644 index c023db1..0000000 --- a/src/main/kotlin/app/revanced/api/plugins/Databases.kt +++ /dev/null @@ -1,49 +0,0 @@ -package app.revanced.api.plugins - -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.request.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import org.jetbrains.exposed.sql.* - -fun Application.configureDatabases() { - val database = Database.connect( - url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", - user = "root", - driver = "org.h2.Driver", - password = "" - ) - val userService = UserService(database) - routing { - // Create user - post("/users") { - val user = call.receive() - val id = userService.create(user) - call.respond(HttpStatusCode.Created, id) - } - // Read user - get("/users/{id}") { - val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException("Invalid ID") - val user = userService.read(id) - if (user != null) { - call.respond(HttpStatusCode.OK, user) - } else { - call.respond(HttpStatusCode.NotFound) - } - } - // Update user - put("/users/{id}") { - val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException("Invalid ID") - val user = call.receive() - userService.update(id, user) - call.respond(HttpStatusCode.OK) - } - // Delete user - delete("/users/{id}") { - val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException("Invalid ID") - userService.delete(id) - call.respond(HttpStatusCode.OK) - } - } -} diff --git a/src/main/kotlin/app/revanced/api/plugins/Dependencies.kt b/src/main/kotlin/app/revanced/api/plugins/Dependencies.kt deleted file mode 100644 index 7667e3f..0000000 --- a/src/main/kotlin/app/revanced/api/plugins/Dependencies.kt +++ /dev/null @@ -1,33 +0,0 @@ -package app.revanced.api.plugins - -import app.revanced.api.schema.APIConfiguration -import app.revanced.api.backend.github.GitHubBackend -import com.akuleshov7.ktoml.Toml -import com.akuleshov7.ktoml.source.decodeFromStream -import io.github.cdimascio.dotenv.Dotenv -import io.ktor.server.application.* -import org.koin.dsl.module -import org.koin.ktor.plugin.Koin -import java.io.File - -fun Application.configureDependencies() { - - install(Koin) { - modules( - module { - single { - Dotenv.load() - } - single { - val configFilePath = get().get("CONFIG_FILE_PATH")!! - Toml.decodeFromStream(File(configFilePath).inputStream()) - } - single { - val token = get().get("GITHUB_TOKEN") - GitHubBackend(token) - } - } - ) - } -} - diff --git a/src/main/kotlin/app/revanced/api/plugins/Routing.kt b/src/main/kotlin/app/revanced/api/plugins/Routing.kt deleted file mode 100644 index 0138207..0000000 --- a/src/main/kotlin/app/revanced/api/plugins/Routing.kt +++ /dev/null @@ -1,88 +0,0 @@ -package app.revanced.api.plugins - -import app.revanced.api.backend.github.GitHubBackend -import app.revanced.api.schema.* -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.http.content.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import org.koin.ktor.ext.inject - -fun Application.configureRouting() { - val backend by inject() - val configuration by inject() - - routing { - route("/v${configuration.apiVersion}") { - route("/patches") { - route("latest") { - get { - val patches = backend.getRelease(configuration.organization, configuration.patchesRepository) - val integrations = configuration.integrationsRepositoryNames.map { - async { backend.getRelease(configuration.organization, it) } - }.awaitAll() - - val assets = (patches.assets + integrations.flatMap { it.assets }).filter { - it.downloadUrl.endsWith(".apk") || it.downloadUrl.endsWith(".jar") - }.map { APIAsset(it.downloadUrl) }.toSet() - - val release = APIRelease( - patches.tag, - patches.createdAt, - patches.releaseNote, - assets - ) - - call.respond(release) - } - - get("/version") { - val patches = backend.getRelease(configuration.organization, configuration.patchesRepository) - - val release = APIReleaseVersion(patches.tag) - - call.respond(release) - } - } - } - - get("/contributors") { - val contributors = configuration.contributorsRepositoryNames.map { - async { - APIContributable( - it, - backend.getContributors(configuration.organization, it).map { - APIContributor(it.name, it.avatarUrl, it.url, it.contributions) - }.toSet() - ) - } - }.awaitAll() - - call.respond(contributors) - } - - get("/members") { - val members = backend.getMembers(configuration.organization).map { - APIMember(it.name, it.avatarUrl, it.url, it.gpgKeysUrl) - } - - call.respond(members) - } - - route("/ping") { - handle { - call.respond(HttpStatusCode.NoContent) - } - } - - staticResources("/", "/static/api") { - contentType { ContentType.Application.Json } - extensions("json") - } - } - - } -} diff --git a/src/main/kotlin/app/revanced/api/plugins/Security.kt b/src/main/kotlin/app/revanced/api/plugins/Security.kt deleted file mode 100644 index 38bcf7f..0000000 --- a/src/main/kotlin/app/revanced/api/plugins/Security.kt +++ /dev/null @@ -1,30 +0,0 @@ -package app.revanced.api.plugins - -import com.auth0.jwt.JWT -import com.auth0.jwt.algorithms.Algorithm -import io.ktor.server.application.* -import io.ktor.server.auth.* -import io.ktor.server.auth.jwt.* - -fun Application.configureSecurity() { - // Please read the jwt property from the config file if you are using EngineMain - val jwtAudience = "jwt-audience" - val jwtDomain = "https://jwt-provider-domain/" - val jwtRealm = "ktor sample app" - val jwtSecret = "secret" - authentication { - jwt { - realm = jwtRealm - verifier( - JWT - .require(Algorithm.HMAC256(jwtSecret)) - .withAudience(jwtAudience) - .withIssuer(jwtDomain) - .build() - ) - validate { credential -> - if (credential.payload.audience.contains(jwtAudience)) JWTPrincipal(credential.payload) else null - } - } - } -} diff --git a/src/main/kotlin/app/revanced/api/plugins/UsersSchema.kt b/src/main/kotlin/app/revanced/api/plugins/UsersSchema.kt deleted file mode 100644 index 9867997..0000000 --- a/src/main/kotlin/app/revanced/api/plugins/UsersSchema.kt +++ /dev/null @@ -1,59 +0,0 @@ -package app.revanced.api.plugins - -import org.jetbrains.exposed.sql.transactions.transaction -import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction -import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq -import kotlinx.serialization.Serializable -import kotlinx.coroutines.Dispatchers -import org.jetbrains.exposed.sql.* - -@Serializable -data class ExposedUser(val name: String, val age: Int) -class UserService(private val database: Database) { - object Users : Table() { - val id = integer("id").autoIncrement() - val name = varchar("name", length = 50) - val age = integer("age") - - override val primaryKey = PrimaryKey(id) - } - - init { - transaction(database) { - SchemaUtils.create(Users) - } - } - - suspend fun dbQuery(block: suspend () -> T): T = - newSuspendedTransaction(Dispatchers.IO) { block() } - - suspend fun create(user: ExposedUser): Int = dbQuery { - Users.insert { - it[name] = user.name - it[age] = user.age - }[Users.id] - } - - suspend fun read(id: Int): ExposedUser? { - return dbQuery { - Users.select { Users.id eq id } - .map { ExposedUser(it[Users.name], it[Users.age]) } - .singleOrNull() - } - } - - suspend fun update(id: Int, user: ExposedUser) { - dbQuery { - Users.update({ Users.id eq id }) { - it[name] = user.name - it[age] = user.age - } - } - } - - suspend fun delete(id: Int) { - dbQuery { - Users.deleteWhere { Users.id.eq(id) } - } - } -} diff --git a/src/main/kotlin/app/revanced/api/schema/APISchema.kt b/src/main/kotlin/app/revanced/api/schema/APISchema.kt index 4432470..295278a 100644 --- a/src/main/kotlin/app/revanced/api/schema/APISchema.kt +++ b/src/main/kotlin/app/revanced/api/schema/APISchema.kt @@ -1,11 +1,12 @@ package app.revanced.api.schema +import kotlinx.datetime.LocalDateTime import kotlinx.serialization.Serializable @Serializable class APIRelease( val version: String, - val createdAt: String, + val createdAt: LocalDateTime, val changelog: String, val assets: Set ) @@ -56,18 +57,34 @@ class APIReleaseVersion( @Serializable class APIAnnouncement( - val id: Int, - val author: APIUser?, + val author: String? = null, val title: String, - val content: APIAnnouncementContent, - val channel: String, - val createdAt: String, - val archivedAt: String?, - val level: Int, + val content: String? = null, + val attachmentUrls: Set = emptySet(), + val channel: String? = null, + val archivedAt: LocalDateTime? = null, + val level: Int = 0 ) @Serializable -class APIAnnouncementContent( - val message: String, - val attachmentUrls: Set +class APIResponseAnnouncement( + val id: Int, + val author: String? = null, + val title: String, + val content: String? = null, + val attachmentUrls: Set = emptySet(), + val channel: String? = null, + val createdAt: LocalDateTime, + val archivedAt: LocalDateTime? = null, + val level: Int = 0 ) + +@Serializable +class APILatestAnnouncement( + val id: Int +) + +@Serializable +class APIAnnouncementArchivedAt( + val archivedAt: LocalDateTime +) \ No newline at end of file diff --git a/src/test/kotlin/app/revanced/ApplicationTest.kt b/src/test/kotlin/app/revanced/ApplicationTest.kt index c4ba45b..a38fa2b 100644 --- a/src/test/kotlin/app/revanced/ApplicationTest.kt +++ b/src/test/kotlin/app/revanced/ApplicationTest.kt @@ -1,6 +1,6 @@ package app.revanced -import app.revanced.api.plugins.configureRouting +import app.revanced.api.modules.configureRouting import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.*