From 2ce1f4d37f3e893f6c5921ce017bcda0a1a8d7e1 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 6 Jun 2024 23:20:21 +0200 Subject: [PATCH] feat: Add GPG key to team members --- .../api/configuration/Dependencies.kt | 4 +- .../revanced/api/configuration/Extensions.kt | 7 + .../api/configuration/Serialization.kt | 1 + .../{backend => }/BackendRepository.kt | 23 +- .../repository/GitHubBackendRepository.kt | 203 ++++++++++++++++++ .../backend/github/GitHubBackendRepository.kt | 83 ------- .../repository/backend/github/api/Request.kt | 27 --- .../repository/backend/github/api/Response.kt | 52 ----- .../routing/routes/Announcements.kt | 17 +- .../api/configuration/schema/APISchema.kt | 8 +- .../api/configuration/services/ApiService.kt | 24 ++- .../configuration/services/PatchesService.kt | 2 +- 12 files changed, 261 insertions(+), 190 deletions(-) create mode 100644 src/main/kotlin/app/revanced/api/configuration/Extensions.kt rename src/main/kotlin/app/revanced/api/configuration/repository/{backend => }/BackendRepository.kt (89%) create mode 100644 src/main/kotlin/app/revanced/api/configuration/repository/GitHubBackendRepository.kt delete mode 100644 src/main/kotlin/app/revanced/api/configuration/repository/backend/github/GitHubBackendRepository.kt delete mode 100644 src/main/kotlin/app/revanced/api/configuration/repository/backend/github/api/Request.kt delete mode 100644 src/main/kotlin/app/revanced/api/configuration/repository/backend/github/api/Response.kt diff --git a/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt b/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt index 7328ea82..0fbed12d 100644 --- a/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt +++ b/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt @@ -1,9 +1,9 @@ package app.revanced.api.configuration import app.revanced.api.configuration.repository.AnnouncementRepository +import app.revanced.api.configuration.repository.BackendRepository import app.revanced.api.configuration.repository.ConfigurationRepository -import app.revanced.api.configuration.repository.backend.BackendRepository -import app.revanced.api.configuration.repository.backend.github.GitHubBackendRepository +import app.revanced.api.configuration.repository.GitHubBackendRepository import app.revanced.api.configuration.services.AnnouncementService import app.revanced.api.configuration.services.ApiService import app.revanced.api.configuration.services.AuthService diff --git a/src/main/kotlin/app/revanced/api/configuration/Extensions.kt b/src/main/kotlin/app/revanced/api/configuration/Extensions.kt new file mode 100644 index 00000000..e630bc68 --- /dev/null +++ b/src/main/kotlin/app/revanced/api/configuration/Extensions.kt @@ -0,0 +1,7 @@ +package app.revanced.api.configuration + +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.response.* + +suspend fun ApplicationCall.respondOrNotFound(value: Any?) = respond(value ?: HttpStatusCode.NotFound) diff --git a/src/main/kotlin/app/revanced/api/configuration/Serialization.kt b/src/main/kotlin/app/revanced/api/configuration/Serialization.kt index 4e9f7ed0..43d3eab5 100644 --- a/src/main/kotlin/app/revanced/api/configuration/Serialization.kt +++ b/src/main/kotlin/app/revanced/api/configuration/Serialization.kt @@ -13,6 +13,7 @@ fun Application.configureSerialization() { json( Json { namingStrategy = JsonNamingStrategy.SnakeCase + explicitNulls = false }, ) } diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/backend/BackendRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/BackendRepository.kt similarity index 89% rename from src/main/kotlin/app/revanced/api/configuration/repository/backend/BackendRepository.kt rename to src/main/kotlin/app/revanced/api/configuration/repository/BackendRepository.kt index 279c354d..a024b511 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/backend/BackendRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/BackendRepository.kt @@ -1,8 +1,7 @@ -package app.revanced.api.configuration.repository.backend +package app.revanced.api.configuration.repository import io.ktor.client.* import kotlinx.datetime.LocalDateTime -import kotlinx.serialization.Serializable /** * The backend of the application used to get data for the API. @@ -40,7 +39,7 @@ abstract class BackendRepository internal constructor( * @property avatarUrl The URL to the avatar 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. + * @property gpgKeys The GPG key of the member. */ @Serializable class BackendMember( @@ -48,8 +47,19 @@ abstract class BackendRepository internal constructor( override val avatarUrl: String, override val url: String, val bio: String?, - val gpgKeysUrl: String, - ) : BackendUser + val gpgKeys: GpgKeys, + ) : BackendUser { + /** + * The GPG keys of a member. + * + * @property ids The IDs of the GPG keys. + * @property url The URL to the GPG master key. + */ + class GpgKeys( + val ids: Set, + val url: String, + ) + } /** * A repository of an organization. @@ -67,7 +77,6 @@ abstract class BackendRepository internal constructor( * @property url The URL to the profile of the contributor. * @property contributions The number of contributions of the contributor. */ - @Serializable class BackendContributor( override val name: String, override val avatarUrl: String, @@ -83,7 +92,6 @@ abstract class BackendRepository internal constructor( * @property createdAt The date and time the release was created. * @property releaseNote The release note of the release. */ - @Serializable class BackendRelease( val tag: String, val releaseNote: String, @@ -95,7 +103,6 @@ abstract class BackendRepository internal constructor( * * @property downloadUrl The URL to download the asset. */ - @Serializable class BackendAsset( val downloadUrl: String, ) diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/GitHubBackendRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/GitHubBackendRepository.kt new file mode 100644 index 00000000..8084fb0d --- /dev/null +++ b/src/main/kotlin/app/revanced/api/configuration/repository/GitHubBackendRepository.kt @@ -0,0 +1,203 @@ +package app.revanced.api.configuration.repository + +import app.revanced.api.configuration.repository.BackendRepository.BackendOrganization.BackendMember +import app.revanced.api.configuration.repository.BackendRepository.BackendOrganization.BackendRepository.BackendContributor +import app.revanced.api.configuration.repository.BackendRepository.BackendOrganization.BackendRepository.BackendRelease +import app.revanced.api.configuration.repository.BackendRepository.BackendOrganization.BackendRepository.BackendRelease.BackendAsset +import app.revanced.api.configuration.repository.GitHubOrganization.GitHubRepository.GitHubContributor +import app.revanced.api.configuration.repository.GitHubOrganization.GitHubRepository.GitHubRelease +import app.revanced.api.configuration.repository.Organization.Repository.Contributors +import app.revanced.api.configuration.repository.Organization.Repository.Releases +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.plugins.resources.* +import io.ktor.resources.* +import kotlinx.coroutines.* +import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import kotlinx.serialization.Serializable + +class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) { + override suspend fun release( + owner: String, + repository: String, + tag: String?, + ): BackendRelease { + val release: GitHubRelease = if (tag != null) { + client.get(Releases.Tag(owner, repository, tag)).body() + } else { + client.get(Releases.Latest(owner, repository)).body() + } + + return BackendRelease( + tag = release.tagName, + releaseNote = release.body, + createdAt = release.createdAt.toLocalDateTime(TimeZone.UTC), + assets = release.assets.map { + BackendAsset(downloadUrl = it.browserDownloadUrl) + }.toSet(), + ) + } + + override suspend fun contributors( + owner: String, + repository: String, + ): Set { + val contributors: Set = client.get( + Contributors( + owner, + repository, + ), + ).body() + + return contributors.map { + BackendContributor( + name = it.login, + avatarUrl = it.avatarUrl, + url = it.htmlUrl, + contributions = it.contributions, + ) + }.toSet() + } + + override suspend fun members(organization: String): Set { + // Get the list of members of the organization. + val members: Set = client.get(Organization.Members(organization)).body() + + return coroutineScope { + members.map { member -> + async { + awaitAll( + async { + // Get the user. + client.get(User(member.login)).body() + }, + async { + // Get the GPG key of the user. + client.get(User.GpgKeys(member.login)).body>() + }, + ) + } + } + }.awaitAll().map { responses -> + val user = responses[0] as GitHubUser + + @Suppress("UNCHECKED_CAST") + val gpgKeys = responses[1] as Set + + BackendMember( + name = user.login, + avatarUrl = user.avatarUrl, + url = user.htmlUrl, + bio = user.bio, + gpgKeys = + BackendMember.GpgKeys( + ids = gpgKeys.map { it.keyId }.toSet(), + url = "https://api.github.com/users/${user.login}.gpg", + ), + ) + }.toSet() + } + + override suspend fun rateLimit(): BackendRateLimit { + val rateLimit: GitHubRateLimit = client.get(RateLimit()).body() + + return BackendRateLimit( + limit = rateLimit.rate.limit, + remaining = rateLimit.rate.remaining, + reset = Instant.fromEpochSeconds(rateLimit.rate.reset).toLocalDateTime(TimeZone.UTC), + ) + } +} + +interface IGitHubUser { + val login: String + val avatarUrl: String + val htmlUrl: String +} + +@Serializable +class GitHubUser( + override val login: String, + override val avatarUrl: String, + override val htmlUrl: String, + val bio: String?, +) : IGitHubUser { + @Serializable + class GitHubGpgKey( + val keyId: String, + ) +} + +class GitHubOrganization { + @Serializable + class GitHubMember( + override val login: String, + override val avatarUrl: String, + override val htmlUrl: String, + ) : IGitHubUser + + class GitHubRepository { + @Serializable + class GitHubContributor( + override val login: String, + override val avatarUrl: String, + override val htmlUrl: String, + val contributions: Int, + ) : IGitHubUser + + @Serializable + class GitHubRelease( + val tagName: String, + val assets: Set, + val createdAt: Instant, + val body: String, + ) { + @Serializable + class GitHubAsset( + val browserDownloadUrl: String, + ) + } + } +} + +@Serializable +class GitHubRateLimit( + val rate: Rate, +) { + @Serializable + class Rate( + val limit: Int, + val remaining: Int, + val reset: Long, + ) +} + +@Resource("/users/{login}") +class User(val login: String) { + @Resource("/users/{login}/gpg_keys") + class GpgKeys(val login: String) +} + +class Organization { + @Resource("/orgs/{org}/members") + class Members(val org: String) + + class Repository { + @Resource("/repos/{owner}/{repo}/contributors") + class Contributors(val owner: String, val repo: String) + + @Resource("/repos/{owner}/{repo}/releases") + class Releases(val owner: String, val repo: String) { + @Resource("/repos/{owner}/{repo}/releases/tags/{tag}") + class Tag(val owner: String, val repo: String, val tag: String) + + @Resource("/repos/{owner}/{repo}/releases/latest") + class Latest(val owner: String, val repo: String) + } + } +} + +@Resource("/rate_limit") +class RateLimit diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/backend/github/GitHubBackendRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/backend/github/GitHubBackendRepository.kt deleted file mode 100644 index cb22d58a..00000000 --- a/src/main/kotlin/app/revanced/api/configuration/repository/backend/github/GitHubBackendRepository.kt +++ /dev/null @@ -1,83 +0,0 @@ -package app.revanced.api.configuration.repository.backend.github - -import app.revanced.api.configuration.repository.backend.BackendRepository -import app.revanced.api.configuration.repository.backend.BackendRepository.BackendOrganization.BackendMember -import app.revanced.api.configuration.repository.backend.BackendRepository.BackendOrganization.BackendRepository.BackendContributor -import app.revanced.api.configuration.repository.backend.BackendRepository.BackendOrganization.BackendRepository.BackendRelease -import app.revanced.api.configuration.repository.backend.BackendRepository.BackendOrganization.BackendRepository.BackendRelease.BackendAsset -import app.revanced.api.configuration.repository.backend.github.api.Request -import app.revanced.api.configuration.repository.backend.github.api.Request.Organization.Members -import app.revanced.api.configuration.repository.backend.github.api.Request.Organization.Repository.Contributors -import app.revanced.api.configuration.repository.backend.github.api.Request.Organization.Repository.Releases -import app.revanced.api.configuration.repository.backend.github.api.Response -import app.revanced.api.configuration.repository.backend.github.api.Response.GitHubOrganization.GitHubMember -import app.revanced.api.configuration.repository.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubContributor -import app.revanced.api.configuration.repository.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubRelease -import io.ktor.client.* -import io.ktor.client.call.* -import io.ktor.client.plugins.resources.* -import kotlinx.coroutines.* -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime - -class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) { - override suspend fun release( - owner: String, - repository: String, - tag: String?, - ): BackendRelease { - val release: GitHubRelease = if (tag != null) { - client.get(Releases.Tag(owner, repository, tag)).body() - } else { - client.get(Releases.Latest(owner, repository)).body() - } - - return BackendRelease( - tag = release.tagName, - releaseNote = release.body, - createdAt = release.createdAt.toLocalDateTime(TimeZone.UTC), - assets = release.assets.map { - BackendAsset(downloadUrl = it.browserDownloadUrl) - }.toSet(), - ) - } - - override suspend fun contributors( - owner: String, - repository: String, - ): Set { - val contributors: Set = client.get(Contributors(owner, repository)).body() - - return contributors.map { - BackendContributor( - name = it.login, - avatarUrl = it.avatarUrl, - url = it.url, - contributions = it.contributions, - ) - }.toSet() - } - - override suspend fun members(organization: String): Set { - // Get the list of members of the organization. - 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() - } - } - }.awaitAll().map { user -> - // Map the user back to a member. - BackendMember( - name = user.login, - avatarUrl = user.avatarUrl, - url = user.url, - bio = user.bio, - gpgKeysUrl = "https://github.com/${user.login}.gpg", - ) - }.toSet() - } -} diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/backend/github/api/Request.kt b/src/main/kotlin/app/revanced/api/configuration/repository/backend/github/api/Request.kt deleted file mode 100644 index 7eb98972..00000000 --- a/src/main/kotlin/app/revanced/api/configuration/repository/backend/github/api/Request.kt +++ /dev/null @@ -1,27 +0,0 @@ -package app.revanced.api.configuration.repository.backend.github.api - -import io.ktor.resources.* - -class Request { - @Resource("/users/{username}") - class User(val username: String) - - class Organization { - @Resource("/orgs/{org}/members") - class Members(val org: String) - - class Repository { - @Resource("/repos/{owner}/{repo}/contributors") - class Contributors(val owner: String, val repo: String) - - @Resource("/repos/{owner}/{repo}/releases") - class Releases(val owner: String, val repo: String) { - @Resource("/repos/{owner}/{repo}/releases/tags/{tag}") - class Tag(val owner: String, val repo: String, val tag: String) - - @Resource("/repos/{owner}/{repo}/releases/latest") - class Latest(val owner: String, val repo: String) - } - } - } -} diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/backend/github/api/Response.kt b/src/main/kotlin/app/revanced/api/configuration/repository/backend/github/api/Response.kt deleted file mode 100644 index 0128ee45..00000000 --- a/src/main/kotlin/app/revanced/api/configuration/repository/backend/github/api/Response.kt +++ /dev/null @@ -1,52 +0,0 @@ -package app.revanced.api.configuration.repository.backend.github.api - -import kotlinx.datetime.Instant -import kotlinx.serialization.Serializable - -class Response { - interface IGitHubUser { - val login: String - val avatarUrl: String - val url: String - } - - @Serializable - class GitHubUser( - override val login: String, - override val avatarUrl: String, - override val url: String, - val bio: String?, - ) : IGitHubUser - - class GitHubOrganization { - @Serializable - class GitHubMember( - override val login: String, - override val avatarUrl: String, - override val url: String, - ) : IGitHubUser - - class GitHubRepository { - @Serializable - class GitHubContributor( - override val login: String, - override val avatarUrl: String, - override val url: String, - val contributions: Int, - ) : IGitHubUser - - @Serializable - class GitHubRelease( - val tagName: String, - val assets: Set, - val createdAt: Instant, - val body: String, - ) { - @Serializable - class GitHubAsset( - val browserDownloadUrl: String, - ) - } - } - } -} diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt b/src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt index 560e4dd6..0e968568 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt @@ -1,9 +1,9 @@ package app.revanced.api.configuration.routing.routes +import app.revanced.api.configuration.respondOrNotFound import app.revanced.api.configuration.schema.APIAnnouncement import app.revanced.api.configuration.schema.APIAnnouncementArchivedAt import app.revanced.api.configuration.services.AnnouncementService -import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.auth.* import io.ktor.server.request.* @@ -19,17 +19,13 @@ internal fun Route.announcementsRoute() = route("announcements") { get("id") { val channel: String by call.parameters - call.respond( - announcementService.latestId(channel) ?: return@get call.respond(HttpStatusCode.NotFound), - ) + call.respondOrNotFound(announcementService.latestId(channel)) } get { val channel: String by call.parameters - call.respond( - announcementService.latest(channel) ?: return@get call.respond(HttpStatusCode.NotFound), - ) + call.respondOrNotFound(announcementService.latest(channel)) } } @@ -41,11 +37,11 @@ internal fun Route.announcementsRoute() = route("announcements") { route("latest") { get("id") { - call.respond(announcementService.latestId() ?: return@get call.respond(HttpStatusCode.NotFound)) + call.respondOrNotFound(announcementService.latestId()) } get { - call.respond(announcementService.latest() ?: return@get call.respond(HttpStatusCode.NotFound)) + call.respondOrNotFound(announcementService.latest()) } } @@ -73,8 +69,9 @@ internal fun Route.announcementsRoute() = route("announcements") { patch("{id}") { val id: Int by call.parameters + val announcement = call.receive() - announcementService.update(id, call.receive()) + announcementService.update(id, announcement) } delete("{id}") { diff --git a/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt b/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt index 86584600..11d8446b 100644 --- a/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt +++ b/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt @@ -23,9 +23,15 @@ class APIMember( override val name: String, override val avatarUrl: String, override val url: String, - val gpgKeysUrl: String, + val gpgKey: APIGpgKey?, ) : APIUser +@Serializable +class APIGpgKey( + val id: String, + val url: String, +) + @Serializable class APIContributor( override val name: String, diff --git a/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt b/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt index 7e25b694..940c6e5a 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt @@ -1,10 +1,8 @@ package app.revanced.api.configuration.services +import app.revanced.api.configuration.repository.BackendRepository import app.revanced.api.configuration.repository.ConfigurationRepository -import app.revanced.api.configuration.repository.backend.BackendRepository -import app.revanced.api.configuration.schema.APIContributable -import app.revanced.api.configuration.schema.APIContributor -import app.revanced.api.configuration.schema.APIMember +import app.revanced.api.configuration.schema.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -27,7 +25,21 @@ internal class ApiService( } }.awaitAll() - suspend fun team() = backendRepository.members(configurationRepository.organization).map { - APIMember(it.name, it.avatarUrl, it.url, it.gpgKeysUrl) + suspend fun team() = backendRepository.members(configurationRepository.organization).map { member -> + APIMember( + member.name, + member.avatarUrl, + member.url, + if (member.gpgKeys.ids.isNotEmpty()) { + APIGpgKey( + // Must choose one of the GPG keys, because it does not make sense to have multiple GPG keys for the API. + member.gpgKeys.ids.first(), + member.gpgKeys.url, + ) + } else { + null + }, + + ) } } diff --git a/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt b/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt index 2281eed3..591e4fe7 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt @@ -1,7 +1,7 @@ package app.revanced.api.configuration.services +import app.revanced.api.configuration.repository.BackendRepository import app.revanced.api.configuration.repository.ConfigurationRepository -import app.revanced.api.configuration.repository.backend.BackendRepository import app.revanced.api.configuration.schema.APIAsset import app.revanced.api.configuration.schema.APIRelease import app.revanced.api.configuration.schema.APIReleaseVersion