feat: Implement more routes and add configuration

This commit is contained in:
oSumAtrIX 2024-01-29 03:18:31 +01:00
parent 8ae50b543e
commit 9999b242ad
No known key found for this signature in database
GPG Key ID: A9B3094ACDB604B4
15 changed files with 236 additions and 82 deletions

View File

@ -1,2 +1,2 @@
GITHUB_TOKEN=
API_VERSION=
CONFIG_FILE_PATH=

3
.gitignore vendored
View File

@ -36,4 +36,5 @@ out/
.vscode/
### Project ###
.env
.env
configuration.toml

View File

@ -42,6 +42,10 @@ dependencies {
implementation(libs.exposed.core)
implementation(libs.exposed.jdbc)
implementation(libs.dotenv.kotlin)
implementation(libs.ktoml.core)
implementation(libs.ktoml.file)
testImplementation(libs.ktor.server.tests)
testImplementation(libs.kotlin.test.junit)
}

View File

@ -0,0 +1,5 @@
organization = "org"
patches-repository = "patches"
integrations-repositories = ["integrations"]
contributors-repositories = ["patches", "integrations"]
api-version = 1

14
configuration.toml Normal file
View File

@ -0,0 +1,14 @@
organization = "revanced"
patches-repository = "revanced-patches"
integrations-repositories = [
"revanced-integrations"
]
contributors-repositories = [
"revanced-patcher",
"revanced-patches",
"revanced-integrations",
"revanced-website",
"revanced-cli",
"revanced-manager",
]
api-version = 1

View File

@ -6,6 +6,7 @@ h2="2.1.214"
koin="3.5.3"
dotenv="6.4.1"
ktor = "2.3.7"
ktoml = "0.5.1"
[libraries]
ktor-client-core = { module = "io.ktor:ktor-client-core" }
@ -34,6 +35,8 @@ exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "e
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" }
[plugins]
serilization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

View File

@ -0,0 +1,55 @@
package app.revanced.api
import kotlinx.serialization.Serializable
@Serializable
class APIRelease(
val version: String,
val createdAt: String,
val changelog: String,
val assets: Set<APIAsset>
)
interface APIUser {
val name: String
val avatarUrl: String
val url: String
}
@Serializable
class APIMember(
override val name: String,
override val avatarUrl: String,
override val url: String,
val gpgKeysUrl: String
) : APIUser
@Serializable
class APIContributor(
override val name: String,
override val avatarUrl: String,
override val url: String,
val contributions: Int,
) : APIUser
@Serializable
class APIContributable(
val name: String,
val contributors: Set<APIContributor>
)
@Serializable
class APIAsset(
val downloadUrl: String,
) {
val type = when {
downloadUrl.endsWith(".jar") -> "patches"
downloadUrl.endsWith(".apk") -> "integrations"
else -> "unknown"
}
}
@Serializable
class APIReleaseVersion(
val version: String
)

View File

@ -7,8 +7,6 @@ import io.ktor.server.engine.*
import io.ktor.server.netty.*
fun main() {
Dotenv.load()
embeddedServer(Netty, port = 8080, host = "0.0.0.0", configure = {
connectionGroupSize = 1
workerGroupSize = 1

View File

@ -0,0 +1,17 @@
package app.revanced.api
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
class APIConfiguration(
val organization: String,
@SerialName("patches-repository")
val patchesRepository: String,
@SerialName("integrations-repositories")
val integrationsRepositoryNames: Set<String>,
@SerialName("contributors-repositories")
val contributorsRepositoryNames: Set<String>,
@SerialName("api-version")
val apiVersion: Int = 1
)

View File

@ -19,12 +19,12 @@ abstract class Backend(
*
* @property name The name of the user.
* @property avatarUrl The URL to the avatar of the user.
* @property profileUrl The URL to the profile of the user.
* @property url The URL to the profile of the user.
*/
interface User {
interface BackendUser {
val name: String
val avatarUrl: String
val profileUrl: String
val url: String
}
/**
@ -32,48 +32,50 @@ abstract class Backend(
*
* @property members The members of the organization.
*/
class Organization(
val members: Set<Member>
class BackendOrganization(
val members: Set<BackendMember>
) {
/**
* A member of an organization.
*
* @property name The name of the member.
* @property avatarUrl The URL to the avatar of the member.
* @property profileUrl The URL to the profile of the member.
* @property url The URL to the profile of the member.
* @property bio The bio of the member.
* @property gpgKeysUrl The URL to the GPG keys of the member.
*/
@Serializable
class Member (
class BackendMember (
override val name: String,
override val avatarUrl: String,
override val profileUrl: String,
override val url: String,
val bio: String?,
val gpgKeysUrl: String?
) : User
val gpgKeysUrl: String
) : BackendUser
/**
* A repository of an organization.
*
* @property contributors The contributors of the repository.
*/
class Repository(
val contributors: Set<Contributor>
class BackendRepository(
val contributors: Set<BackendContributor>
) {
/**
* A contributor of a repository.
*
* @property name The name of the contributor.
* @property avatarUrl The URL to the avatar of the contributor.
* @property profileUrl The URL to the profile of the contributor.
* @property url The URL to the profile of the contributor.
* @property contributions The number of contributions of the contributor.
*/
@Serializable
class Contributor(
class BackendContributor(
override val name: String,
override val avatarUrl: String,
override val profileUrl: String
) : User
override val url: String,
val contributions: Int
) : BackendUser
/**
* A release of a repository.
@ -84,11 +86,11 @@ abstract class Backend(
* @property releaseNote The release note of the release.
*/
@Serializable
class Release(
class BackendRelease(
val tag: String,
val releaseNote: String,
val createdAt: String,
val assets: Set<Asset>
val assets: Set<BackendAsset>
) {
/**
* An asset of a release.
@ -96,7 +98,7 @@ abstract class Backend(
* @property downloadUrl The URL to download the asset.
*/
@Serializable
class Asset(
class BackendAsset(
val downloadUrl: String
)
}
@ -109,17 +111,13 @@ abstract class Backend(
* @param owner The owner of the repository.
* @param repository The name of the repository.
* @param tag The tag of the release. If null, the latest release is returned.
* @param preRelease Whether to return a pre-release.
* If no pre-release exists, the latest release is returned.
* If tag is not null, this parameter is ignored.
* @return The release.
*/
abstract suspend fun getRelease(
owner: String,
repository: String,
tag: String? = null,
preRelease: Boolean = false
): Organization.Repository.Release
): BackendOrganization.BackendRepository.BackendRelease
/**
* Get the contributors of a repository.
@ -128,7 +126,7 @@ abstract class Backend(
* @param repository The name of the repository.
* @return The contributors.
*/
abstract suspend fun getContributors(owner: String, repository: String): Set<Organization.Repository.Contributor>
abstract suspend fun getContributors(owner: String, repository: String): Set<BackendOrganization.BackendRepository.BackendContributor>
/**
* Get the members of an organization.
@ -136,5 +134,5 @@ abstract class Backend(
* @param organization The name of the organization.
* @return The members.
*/
abstract suspend fun getMembers(organization: String): Set<Organization.Member>
abstract suspend fun getMembers(organization: String): Set<BackendOrganization.BackendMember>
}

View File

@ -9,13 +9,17 @@ import io.ktor.client.plugins.auth.providers.*
import io.ktor.client.plugins.cache.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.resources.*
import app.revanced.api.backend.Backend.BackendOrganization.BackendMember
import app.revanced.api.backend.Backend.BackendOrganization.BackendRepository.BackendRelease
import app.revanced.api.backend.Backend.BackendOrganization.BackendRepository.BackendContributor
import app.revanced.api.backend.Backend.BackendOrganization.BackendRepository.BackendRelease.BackendAsset
import app.revanced.api.backend.github.api.Request.Organization.Repository.Releases
import app.revanced.api.backend.github.api.Request.Organization.Repository.Contributors
import app.revanced.api.backend.github.api.Request.Organization.Members
import app.revanced.api.backend.github.api.Response
import app.revanced.api.backend.github.api.Response.Organization.Repository.Release
import app.revanced.api.backend.github.api.Response.Organization.Repository.Contributor
import app.revanced.api.backend.github.api.Response.Organization.Member
import app.revanced.api.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubRelease
import app.revanced.api.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubContributor
import app.revanced.api.backend.github.api.Response.GitHubOrganization.GitHubMember
import io.ktor.client.plugins.resources.Resources
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.*
@ -55,59 +59,58 @@ class GitHubBackend(token: String? = null) : Backend({
owner: String,
repository: String,
tag: String?,
preRelease: Boolean
): Organization.Repository.Release {
val release = if (preRelease) {
val releases: Set<Release> = client.get(Releases(owner, repository)).body()
releases.firstOrNull { it.preReleases } ?: releases.first() // Latest pre-release or latest release
} else {
client.get(
tag?.let { Releases.Tag(owner, repository, it) }
?: Releases.Latest(owner, repository)
).body()
}
): BackendRelease {
val release: GitHubRelease = if (tag != null)
client.get(Releases.Tag(owner, repository, tag)).body()
else
client.get(Releases.Latest(owner, repository)).body()
return Organization.Repository.Release(
return BackendRelease(
tag = release.tagName,
releaseNote = release.body,
createdAt = release.createdAt,
assets = release.assets.map {
Organization.Repository.Release.Asset(
BackendAsset(
downloadUrl = it.browserDownloadUrl
)
}.toSet()
)
}
override suspend fun getContributors(owner: String, repository: String): Set<Organization.Repository.Contributor> {
val contributors: Set<Contributor> = client.get(Contributors(owner, repository)).body()
override suspend fun getContributors(
owner: String,
repository: String
): Set<BackendContributor> {
val contributors: Set<GitHubContributor> = client.get(Contributors(owner, repository)).body()
return contributors.map {
Organization.Repository.Contributor(
BackendContributor(
name = it.login,
avatarUrl = it.avatarUrl,
profileUrl = it.url
url = it.url,
contributions = it.contributions
)
}.toSet()
}
override suspend fun getMembers(organization: String): Set<Organization.Member> {
override suspend fun getMembers(organization: String): Set<BackendMember> {
// Get the list of members of the organization.
val members: Set<Member> = client.get(Members(organization)).body<Set<Member>>()
val members: Set<GitHubMember> = client.get(Members(organization)).body()
return runBlocking(Dispatchers.Default) {
members.map { member ->
// Map the member to a user in order to get the bio.
async {
client.get(Request.User(member.login)).body<Response.User>()
client.get(Request.User(member.login)).body<Response.GitHubUser>()
}
}
}.awaitAll().map { user ->
// Map the user back to a member.
Organization.Member(
BackendMember(
name = user.login,
avatarUrl = user.avatarUrl,
profileUrl = user.url,
url = user.url,
bio = user.bio,
gpgKeysUrl = "https://github.com/${user.login}.gpg",
)

View File

@ -1,49 +1,50 @@
package app.revanced.api.backend.github.api
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
class Response {
interface IUser {
interface IGitHubUser {
val login: String
val avatarUrl: String
val url: String
}
@Serializable
class User (
class GitHubUser (
override val login: String,
override val avatarUrl: String,
override val url: String,
val bio: String?,
) : IUser
) : IGitHubUser
class Organization {
class GitHubOrganization {
@Serializable
class Member(
class GitHubMember(
override val login: String,
override val avatarUrl: String,
override val url: String,
) : IUser
) : IGitHubUser
class Repository {
class GitHubRepository {
@Serializable
class Contributor(
class GitHubContributor(
override val login: String,
override val avatarUrl: String,
override val url: String,
) : IUser
val contributions: Int,
) : IGitHubUser
@Serializable
class Release(
class GitHubRelease(
val tagName: String,
val assets: Set<Asset>,
val preReleases: Boolean,
val assets: Set<GitHubAsset>,
val createdAt: String,
val body: String
) {
@Serializable
class Asset(
class GitHubAsset(
val browserDownloadUrl: String
)
}

View File

@ -1,21 +1,37 @@
package app.revanced.api.plugins
import app.revanced.api.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 kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import org.koin.core.context.startKoin
import org.koin.dsl.module
import org.koin.ktor.ext.inject
import org.koin.ktor.plugin.Koin
import java.io.File
fun Application.configureDependencies() {
install(Koin) {
modules(
module {
single { Dotenv.load() }
single { GitHubBackend(get<Dotenv>().get("GITHUB_TOKEN")) }
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)
}
}
)
}
}

View File

@ -1,36 +1,74 @@
package app.revanced.api.plugins
import app.revanced.api.*
import app.revanced.api.backend.github.GitHubBackend
import io.github.cdimascio.dotenv.Dotenv
import io.ktor.client.utils.EmptyContent.contentType
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 dotenv by inject<Dotenv>()
val configuration by inject<APIConfiguration>()
routing {
route("/v${dotenv.get("API_VERSION", "1")}") {
route("/manager") {
get("/contributors") {
val contributors = backend.getContributors("revanced", "revanced-patches")
route("/v${configuration.apiVersion}") {
route("/patches") {
get {
val patches = backend.getRelease(configuration.organization, configuration.patchesRepository)
val integrations = configuration.integrationsRepositoryNames.map {
async { backend.getRelease(configuration.organization, it) }
}.awaitAll()
call.respond(contributors)
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("/members") {
val members = backend.getMembers("revanced")
get("/version") {
val patches = backend.getRelease(configuration.organization, configuration.patchesRepository)
call.respond(members)
val release = APIReleaseVersion(patches.tag)
call.respond(release)
}
}
route("/patches") {
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") {
@ -38,8 +76,9 @@ fun Application.configureRouting() {
call.respond(HttpStatusCode.NoContent)
}
}
staticResources("/", "/static/api") { contentType { ContentType.Application.Json } }
}
staticResources("/", "static")
}
}