mirror of
https://github.com/revanced/revanced-api.git
synced 2025-04-29 22:24:31 +02:00
feat: Add announcements API
This commit is contained in:
parent
af0b0865f4
commit
42f731854d
13
.env.example
13
.env.example
@ -1,2 +1,13 @@
|
||||
GITHUB_TOKEN=
|
||||
CONFIG_FILE_PATH=
|
||||
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=
|
||||
|
@ -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)
|
||||
|
@ -2,4 +2,4 @@ organization = "org"
|
||||
patches-repository = "patches"
|
||||
integrations-repositories = ["integrations"]
|
||||
contributors-repositories = ["patches", "integrations"]
|
||||
api-version = 1
|
||||
api-version = 1
|
@ -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" }
|
||||
|
@ -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<BackendAsset>
|
||||
) {
|
||||
/**
|
||||
|
@ -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({
|
||||
|
@ -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<GitHubAsset>,
|
||||
val createdAt: String,
|
||||
val createdAt: LocalDateTime,
|
||||
val body: String
|
||||
) {
|
||||
@Serializable
|
||||
|
@ -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)
|
||||
}
|
||||
|
160
src/main/kotlin/app/revanced/api/modules/Database.kt
Normal file
160
src/main/kotlin/app/revanced/api/modules/Database.kt
Normal file
@ -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<Int>) : IntEntity(id) {
|
||||
companion object : IntEntityClass<Announcement>(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<Int>) : IntEntity(id) {
|
||||
companion object : IntEntityClass<Attachment>(Attachments)
|
||||
|
||||
var url by Attachments.url
|
||||
var announcement by Announcement referencedOn Attachments.announcement
|
||||
}
|
||||
|
||||
init {
|
||||
transaction {
|
||||
SchemaUtils.create(Announcements, Attachments)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
73
src/main/kotlin/app/revanced/api/modules/Dependencies.kt
Normal file
73
src/main/kotlin/app/revanced/api/modules/Dependencies.kt
Normal file
@ -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<Dotenv>().get("CONFIG_FILE_PATH")!!
|
||||
Toml.decodeFromStream<APIConfiguration>(File(configFilePath).inputStream())
|
||||
}
|
||||
}
|
||||
|
||||
val gitHubBackendModule = module {
|
||||
single {
|
||||
val token = get<Dotenv>().get("GITHUB_TOKEN")
|
||||
GitHubBackend(token)
|
||||
} bind Backend::class
|
||||
}
|
||||
|
||||
val databaseModule = module {
|
||||
single {
|
||||
val dotenv = get<Dotenv>()
|
||||
|
||||
Database.connect(
|
||||
url = dotenv.get("DB_URL"),
|
||||
user = dotenv.get("DB_USER"),
|
||||
password = dotenv.get("DB_PASSWORD"),
|
||||
driver = "org.h2.Driver"
|
||||
)
|
||||
}
|
||||
factory<AnnouncementService> {
|
||||
AnnouncementService(get())
|
||||
}
|
||||
}
|
||||
|
||||
val authModule = module {
|
||||
single {
|
||||
val dotenv = get<Dotenv>()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package app.revanced.api.plugins
|
||||
package app.revanced.api.modules
|
||||
|
||||
import io.ktor.http.*
|
||||
import io.ktor.http.content.*
|
158
src/main/kotlin/app/revanced/api/modules/Routing.kt
Normal file
158
src/main/kotlin/app/revanced/api/modules/Routing.kt
Normal file
@ -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<APIAnnouncement>()) }
|
||||
|
||||
post("/{id}/archive") {
|
||||
id {
|
||||
val archivedAt = call.receiveNullable<APIAnnouncementArchivedAt>()?.archivedAt
|
||||
announcementService.archive(it, archivedAt)
|
||||
}
|
||||
}
|
||||
|
||||
post("/{id}/unarchive") { id { announcementService.unarchive(it) } }
|
||||
|
||||
patch("/{id}") { id { announcementService.update(it, call.receive<APIAnnouncement>()) } }
|
||||
|
||||
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()) } }
|
||||
}
|
||||
}
|
||||
}
|
53
src/main/kotlin/app/revanced/api/modules/Security.kt
Normal file
53
src/main/kotlin/app/revanced/api/modules/Security.kt
Normal file
@ -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<AuthService>().configureSecurity
|
||||
configureSecurity()
|
||||
}
|
@ -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.*
|
@ -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<ExposedUser>()
|
||||
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<ExposedUser>()
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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<Dotenv>().get("CONFIG_FILE_PATH")!!
|
||||
Toml.decodeFromStream<APIConfiguration>(File(configFilePath).inputStream())
|
||||
}
|
||||
single {
|
||||
val token = get<Dotenv>().get("GITHUB_TOKEN")
|
||||
GitHubBackend(token)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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<GitHubBackend>()
|
||||
val configuration by inject<APIConfiguration>()
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 <T> 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) }
|
||||
}
|
||||
}
|
||||
}
|
@ -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<APIAsset>
|
||||
)
|
||||
@ -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<String> = emptySet(),
|
||||
val channel: String? = null,
|
||||
val archivedAt: LocalDateTime? = null,
|
||||
val level: Int = 0
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class APIAnnouncementContent(
|
||||
val message: String,
|
||||
val attachmentUrls: Set<String>
|
||||
class APIResponseAnnouncement(
|
||||
val id: Int,
|
||||
val author: String? = null,
|
||||
val title: String,
|
||||
val content: String? = null,
|
||||
val attachmentUrls: Set<String> = 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
|
||||
)
|
@ -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.*
|
||||
|
Loading…
x
Reference in New Issue
Block a user