diff --git a/.env.example b/.env.example index 6129bbb..2bdac94 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,2 @@ GITHUB_TOKEN= -API_VERSION= \ No newline at end of file +CONFIG_FILE_PATH= \ No newline at end of file diff --git a/.gitignore b/.gitignore index d16fa9e..dd55c73 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,5 @@ out/ .vscode/ ### Project ### -.env \ No newline at end of file +.env +configuration.toml \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 822a45f..d1c7c33 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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) + } diff --git a/configuration.example.toml b/configuration.example.toml new file mode 100644 index 0000000..5935fd6 --- /dev/null +++ b/configuration.example.toml @@ -0,0 +1,5 @@ +organization = "org" +patches-repository = "patches" +integrations-repositories = ["integrations"] +contributors-repositories = ["patches", "integrations"] +api-version = 1 diff --git a/configuration.toml b/configuration.toml new file mode 100644 index 0000000..dcef633 --- /dev/null +++ b/configuration.toml @@ -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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ae96f6a..5074e91 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/src/main/kotlin/app/revanced/api/APISchema.kt b/src/main/kotlin/app/revanced/api/APISchema.kt new file mode 100644 index 0000000..5e2de99 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/APISchema.kt @@ -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 +) + +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 +) + +@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 +) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/api/Application.kt b/src/main/kotlin/app/revanced/api/Application.kt index d7dd38b..0b7c571 100644 --- a/src/main/kotlin/app/revanced/api/Application.kt +++ b/src/main/kotlin/app/revanced/api/Application.kt @@ -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 diff --git a/src/main/kotlin/app/revanced/api/ConfigurationSchema.kt b/src/main/kotlin/app/revanced/api/ConfigurationSchema.kt new file mode 100644 index 0000000..0a5979a --- /dev/null +++ b/src/main/kotlin/app/revanced/api/ConfigurationSchema.kt @@ -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, + @SerialName("contributors-repositories") + val contributorsRepositoryNames: Set, + @SerialName("api-version") + val apiVersion: Int = 1 +) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/api/backend/Backend.kt b/src/main/kotlin/app/revanced/api/backend/Backend.kt index efb1d02..6549c64 100644 --- a/src/main/kotlin/app/revanced/api/backend/Backend.kt +++ b/src/main/kotlin/app/revanced/api/backend/Backend.kt @@ -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 + class BackendOrganization( + val members: Set ) { /** * 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 + class BackendRepository( + val contributors: Set ) { /** * 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 + val assets: Set ) { /** * 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 + abstract suspend fun getContributors(owner: String, repository: String): Set /** * 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 + abstract suspend fun getMembers(organization: String): 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 db144f5..e024ce5 100644 --- a/src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt +++ b/src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt @@ -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 = 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 { - val contributors: Set = client.get(Contributors(owner, repository)).body() + override suspend fun getContributors( + owner: String, + repository: String + ): Set { + val contributors: Set = 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 { + override suspend fun getMembers(organization: String): Set { // Get the list of members of the organization. - val members: Set = client.get(Members(organization)).body>() + val members: Set = 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() + client.get(Request.User(member.login)).body() } } }.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", ) 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 e286b95..26d56b5 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,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, - val preReleases: Boolean, + val assets: Set, val createdAt: String, val body: String ) { @Serializable - class Asset( + class GitHubAsset( val browserDownloadUrl: String ) } diff --git a/src/main/kotlin/app/revanced/api/plugins/Dependencies.kt b/src/main/kotlin/app/revanced/api/plugins/Dependencies.kt index 3950ec9..7e0a3d3 100644 --- a/src/main/kotlin/app/revanced/api/plugins/Dependencies.kt +++ b/src/main/kotlin/app/revanced/api/plugins/Dependencies.kt @@ -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().get("GITHUB_TOKEN")) } + 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 index c14a7c3..1bd019f 100644 --- a/src/main/kotlin/app/revanced/api/plugins/Routing.kt +++ b/src/main/kotlin/app/revanced/api/plugins/Routing.kt @@ -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() - val dotenv by inject() + val configuration by inject() 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") } } diff --git a/src/main/resources/static/about.json b/src/main/resources/static/api/about similarity index 100% rename from src/main/resources/static/about.json rename to src/main/resources/static/api/about