mirror of
https://github.com/ReVanced/revanced-api.git
synced 2026-01-19 09:23:57 +00:00
feat: Initialize project
This commit is contained in:
24
src/main/kotlin/app/revanced/api/Application.kt
Normal file
24
src/main/kotlin/app/revanced/api/Application.kt
Normal file
@@ -0,0 +1,24 @@
|
||||
package app.revanced.api
|
||||
|
||||
import app.revanced.api.plugins.*
|
||||
import io.github.cdimascio.dotenv.Dotenv
|
||||
import io.ktor.server.application.*
|
||||
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
|
||||
callGroupSize = 1
|
||||
}) {
|
||||
configureHTTP()
|
||||
configureSerialization()
|
||||
configureDatabases()
|
||||
configureSecurity()
|
||||
configureDependencies()
|
||||
configureRouting()
|
||||
}.start(wait = true)
|
||||
}
|
||||
140
src/main/kotlin/app/revanced/api/backend/Backend.kt
Normal file
140
src/main/kotlin/app/revanced/api/backend/Backend.kt
Normal file
@@ -0,0 +1,140 @@
|
||||
package app.revanced.api.backend
|
||||
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.okhttp.*
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* The backend of the application used to get data for the API.
|
||||
*
|
||||
* @param httpClientConfig The configuration of the HTTP client.
|
||||
*/
|
||||
abstract class Backend(
|
||||
httpClientConfig: HttpClientConfig<OkHttpConfig>.() -> Unit = {}
|
||||
) {
|
||||
protected val client: HttpClient = HttpClient(OkHttp, httpClientConfig)
|
||||
|
||||
/**
|
||||
* A user.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
interface User {
|
||||
val name: String
|
||||
val avatarUrl: String
|
||||
val profileUrl: String
|
||||
}
|
||||
|
||||
/**
|
||||
* An organization.
|
||||
*
|
||||
* @property members The members of the organization.
|
||||
*/
|
||||
class Organization(
|
||||
val members: Set<Member>
|
||||
) {
|
||||
/**
|
||||
* 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 bio The bio of the member.
|
||||
* @property gpgKeysUrl The URL to the GPG keys of the member.
|
||||
*/
|
||||
@Serializable
|
||||
class Member (
|
||||
override val name: String,
|
||||
override val avatarUrl: String,
|
||||
override val profileUrl: String,
|
||||
val bio: String?,
|
||||
val gpgKeysUrl: String?
|
||||
) : User
|
||||
|
||||
/**
|
||||
* A repository of an organization.
|
||||
*
|
||||
* @property contributors The contributors of the repository.
|
||||
*/
|
||||
class Repository(
|
||||
val contributors: Set<Contributor>
|
||||
) {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@Serializable
|
||||
class Contributor(
|
||||
override val name: String,
|
||||
override val avatarUrl: String,
|
||||
override val profileUrl: String
|
||||
) : User
|
||||
|
||||
/**
|
||||
* A release of a repository.
|
||||
*
|
||||
* @property tag The tag of the release.
|
||||
* @property assets The assets of the release.
|
||||
* @property createdAt The date and time the release was created.
|
||||
* @property releaseNote The release note of the release.
|
||||
*/
|
||||
@Serializable
|
||||
class Release(
|
||||
val tag: String,
|
||||
val releaseNote: String,
|
||||
val createdAt: String,
|
||||
val assets: Set<Asset>
|
||||
) {
|
||||
/**
|
||||
* An asset of a release.
|
||||
*
|
||||
* @property downloadUrl The URL to download the asset.
|
||||
*/
|
||||
@Serializable
|
||||
class Asset(
|
||||
val downloadUrl: String
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a release of a repository.
|
||||
*
|
||||
* @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
|
||||
|
||||
/**
|
||||
* Get the contributors of a repository.
|
||||
*
|
||||
* @param owner The owner of the repository.
|
||||
* @param repository The name of the repository.
|
||||
* @return The contributors.
|
||||
*/
|
||||
abstract suspend fun getContributors(owner: String, repository: String): Set<Organization.Repository.Contributor>
|
||||
|
||||
/**
|
||||
* Get the members of an organization.
|
||||
*
|
||||
* @param organization The name of the organization.
|
||||
* @return The members.
|
||||
*/
|
||||
abstract suspend fun getMembers(organization: String): Set<Organization.Member>
|
||||
}
|
||||
116
src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt
Normal file
116
src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt
Normal file
@@ -0,0 +1,116 @@
|
||||
package app.revanced.api.backend.github
|
||||
|
||||
import app.revanced.api.backend.Backend
|
||||
import app.revanced.api.backend.github.api.Request
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.plugins.*
|
||||
import io.ktor.client.plugins.auth.*
|
||||
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.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 io.ktor.client.plugins.resources.Resources
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonNamingStrategy
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
class GitHubBackend(token: String? = null) : Backend({
|
||||
install(HttpCache)
|
||||
install(Resources)
|
||||
install(ContentNegotiation) {
|
||||
json(Json {
|
||||
ignoreUnknownKeys = true
|
||||
namingStrategy = JsonNamingStrategy.SnakeCase
|
||||
})
|
||||
}
|
||||
|
||||
defaultRequest { url("https://api.github.com") }
|
||||
|
||||
token?.let {
|
||||
install(Auth) {
|
||||
bearer {
|
||||
loadTokens {
|
||||
BearerTokens(
|
||||
accessToken = it,
|
||||
refreshToken = "" // Required dummy value
|
||||
)
|
||||
}
|
||||
|
||||
sendWithoutRequest { true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}) {
|
||||
override suspend fun getRelease(
|
||||
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()
|
||||
}
|
||||
|
||||
return Organization.Repository.Release(
|
||||
tag = release.tagName,
|
||||
releaseNote = release.body,
|
||||
createdAt = release.createdAt,
|
||||
assets = release.assets.map {
|
||||
Organization.Repository.Release.Asset(
|
||||
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()
|
||||
|
||||
return contributors.map {
|
||||
Organization.Repository.Contributor(
|
||||
name = it.login,
|
||||
avatarUrl = it.avatarUrl,
|
||||
profileUrl = it.url
|
||||
)
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
override suspend fun getMembers(organization: String): Set<Organization.Member> {
|
||||
// Get the list of members of the organization.
|
||||
val members: Set<Member> = client.get(Members(organization)).body<Set<Member>>()
|
||||
|
||||
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>()
|
||||
}
|
||||
}
|
||||
}.awaitAll().map { user ->
|
||||
// Map the user back to a member.
|
||||
Organization.Member(
|
||||
name = user.login,
|
||||
avatarUrl = user.avatarUrl,
|
||||
profileUrl = user.url,
|
||||
bio = user.bio,
|
||||
gpgKeysUrl = "https://github.com/${user.login}.gpg",
|
||||
)
|
||||
}.toSet()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package app.revanced.api.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package app.revanced.api.backend.github.api
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
||||
class Response {
|
||||
interface IUser {
|
||||
val login: String
|
||||
val avatarUrl: String
|
||||
val url: String
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class User (
|
||||
override val login: String,
|
||||
override val avatarUrl: String,
|
||||
override val url: String,
|
||||
val bio: String?,
|
||||
) : IUser
|
||||
|
||||
class Organization {
|
||||
@Serializable
|
||||
class Member(
|
||||
override val login: String,
|
||||
override val avatarUrl: String,
|
||||
override val url: String,
|
||||
) : IUser
|
||||
|
||||
class Repository {
|
||||
@Serializable
|
||||
class Contributor(
|
||||
override val login: String,
|
||||
override val avatarUrl: String,
|
||||
override val url: String,
|
||||
) : IUser
|
||||
|
||||
@Serializable
|
||||
class Release(
|
||||
val tagName: String,
|
||||
val assets: Set<Asset>,
|
||||
val preReleases: Boolean,
|
||||
val createdAt: String,
|
||||
val body: String
|
||||
) {
|
||||
@Serializable
|
||||
class Asset(
|
||||
val browserDownloadUrl: String
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
49
src/main/kotlin/app/revanced/api/plugins/Databases.kt
Normal file
49
src/main/kotlin/app/revanced/api/plugins/Databases.kt
Normal file
@@ -0,0 +1,49 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
21
src/main/kotlin/app/revanced/api/plugins/Dependencies.kt
Normal file
21
src/main/kotlin/app/revanced/api/plugins/Dependencies.kt
Normal file
@@ -0,0 +1,21 @@
|
||||
package app.revanced.api.plugins
|
||||
|
||||
import app.revanced.api.backend.github.GitHubBackend
|
||||
import io.github.cdimascio.dotenv.Dotenv
|
||||
import io.ktor.server.application.*
|
||||
import org.koin.core.context.startKoin
|
||||
import org.koin.dsl.module
|
||||
import org.koin.ktor.ext.inject
|
||||
import org.koin.ktor.plugin.Koin
|
||||
|
||||
fun Application.configureDependencies() {
|
||||
|
||||
install(Koin) {
|
||||
modules(
|
||||
module {
|
||||
single { Dotenv.load() }
|
||||
single { GitHubBackend(get<Dotenv>().get("GITHUB_TOKEN")) }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
37
src/main/kotlin/app/revanced/api/plugins/HTTP.kt
Normal file
37
src/main/kotlin/app/revanced/api/plugins/HTTP.kt
Normal file
@@ -0,0 +1,37 @@
|
||||
package app.revanced.api.plugins
|
||||
|
||||
import io.ktor.http.*
|
||||
import io.ktor.http.content.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.plugins.cachingheaders.*
|
||||
import io.ktor.server.plugins.conditionalheaders.*
|
||||
import io.ktor.server.plugins.cors.routing.*
|
||||
import io.ktor.server.plugins.openapi.*
|
||||
import io.ktor.server.plugins.swagger.*
|
||||
import io.ktor.server.routing.*
|
||||
|
||||
fun Application.configureHTTP() {
|
||||
install(ConditionalHeaders)
|
||||
routing {
|
||||
swaggerUI(path = "openapi")
|
||||
}
|
||||
routing {
|
||||
openAPI(path = "openapi")
|
||||
}
|
||||
install(CORS) {
|
||||
allowMethod(HttpMethod.Options)
|
||||
allowMethod(HttpMethod.Put)
|
||||
allowMethod(HttpMethod.Delete)
|
||||
allowMethod(HttpMethod.Patch)
|
||||
allowHeader(HttpHeaders.Authorization)
|
||||
anyHost() // @TODO: Don't do this in production if possible. Try to limit it.
|
||||
}
|
||||
install(CachingHeaders) {
|
||||
options { _, outgoingContent ->
|
||||
when (outgoingContent.contentType?.withoutParameters()) {
|
||||
ContentType.Text.CSS -> CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 24 * 60 * 60))
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
45
src/main/kotlin/app/revanced/api/plugins/Routing.kt
Normal file
45
src/main/kotlin/app/revanced/api/plugins/Routing.kt
Normal file
@@ -0,0 +1,45 @@
|
||||
package app.revanced.api.plugins
|
||||
|
||||
import app.revanced.api.backend.github.GitHubBackend
|
||||
import io.github.cdimascio.dotenv.Dotenv
|
||||
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 org.koin.ktor.ext.inject
|
||||
|
||||
fun Application.configureRouting() {
|
||||
val backend by inject<GitHubBackend>()
|
||||
val dotenv by inject<Dotenv>()
|
||||
|
||||
routing {
|
||||
route("/v${dotenv.get("API_VERSION", "1")}") {
|
||||
route("/manager") {
|
||||
get("/contributors") {
|
||||
val contributors = backend.getContributors("revanced", "revanced-patches")
|
||||
|
||||
call.respond(contributors)
|
||||
}
|
||||
|
||||
get("/members") {
|
||||
val members = backend.getMembers("revanced")
|
||||
|
||||
call.respond(members)
|
||||
}
|
||||
}
|
||||
|
||||
route("/patches") {
|
||||
|
||||
}
|
||||
|
||||
route("/ping") {
|
||||
handle {
|
||||
call.respond(HttpStatusCode.NoContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
staticResources("/", "static")
|
||||
}
|
||||
}
|
||||
30
src/main/kotlin/app/revanced/api/plugins/Security.kt
Normal file
30
src/main/kotlin/app/revanced/api/plugins/Security.kt
Normal file
@@ -0,0 +1,30 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/main/kotlin/app/revanced/api/plugins/Serialization.kt
Normal file
11
src/main/kotlin/app/revanced/api/plugins/Serialization.kt
Normal file
@@ -0,0 +1,11 @@
|
||||
package app.revanced.api.plugins
|
||||
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.plugins.contentnegotiation.*
|
||||
|
||||
fun Application.configureSerialization() {
|
||||
install(ContentNegotiation) {
|
||||
json()
|
||||
}
|
||||
}
|
||||
59
src/main/kotlin/app/revanced/api/plugins/UsersSchema.kt
Normal file
59
src/main/kotlin/app/revanced/api/plugins/UsersSchema.kt
Normal file
@@ -0,0 +1,59 @@
|
||||
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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
12
src/main/resources/logback.xml
Normal file
12
src/main/resources/logback.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<root level="trace">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
<logger name="org.eclipse.jetty" level="INFO"/>
|
||||
<logger name="io.netty" level="INFO"/>
|
||||
</configuration>
|
||||
23
src/main/resources/openapi/documentation.yaml
Normal file
23
src/main/resources/openapi/documentation.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
openapi: "3.0.3"
|
||||
info:
|
||||
title: "Application API"
|
||||
description: "Application API"
|
||||
version: "1.0.0"
|
||||
servers:
|
||||
- url: "http://0.0.0.0:8080"
|
||||
paths:
|
||||
/:
|
||||
get:
|
||||
description: "Hello World!"
|
||||
responses:
|
||||
"200":
|
||||
description: "OK"
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: "string"
|
||||
examples:
|
||||
Example#1:
|
||||
value: "Hello World!"
|
||||
components:
|
||||
schemas:
|
||||
99
src/main/resources/static/about.json
Normal file
99
src/main/resources/static/about.json
Normal file
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"name": "ReVanced",
|
||||
"about": "ReVanced was born out of Vanced's discontinuation and it is our goal to continue the legacy of what Vanced left behind. Thanks to ReVanced Patcher, it's possible to create long-lasting patches for nearly any Android app. ReVanced's patching system is designed to allow patches to work on new versions of the apps automatically with bare minimum maintenance.",
|
||||
"branding":
|
||||
{
|
||||
"logo": "https://raw.githubusercontent.com/ReVanced/revanced-branding/main/assets/revanced-logo/revanced-logo.svg"
|
||||
},
|
||||
"contact":
|
||||
{
|
||||
"email": "contact@revanced.app"
|
||||
},
|
||||
"socials":
|
||||
[
|
||||
{
|
||||
"name": "Website",
|
||||
"url": "https://revanced.app",
|
||||
"preferred": true
|
||||
},
|
||||
{
|
||||
"name": "GitHub",
|
||||
"url": "https://github.com/revanced",
|
||||
"preferred": false
|
||||
},
|
||||
{
|
||||
"name": "Twitter",
|
||||
"url": "https://twitter.com/revancedapp",
|
||||
"preferred": false
|
||||
},
|
||||
{
|
||||
"name": "Discord",
|
||||
"url": "https://revanced.app/discord",
|
||||
"preferred": true
|
||||
},
|
||||
{
|
||||
"name": "Reddit",
|
||||
"url": "https://www.reddit.com/r/revancedapp",
|
||||
"preferred": false
|
||||
},
|
||||
{
|
||||
"name": "Telegram",
|
||||
"url": "https://t.me/app_revanced",
|
||||
"preferred": false
|
||||
},
|
||||
{
|
||||
"name": "YouTube",
|
||||
"url": "https://www.youtube.com/@ReVanced",
|
||||
"preferred": false
|
||||
}
|
||||
],
|
||||
"donations":
|
||||
{
|
||||
"wallets":
|
||||
[
|
||||
{
|
||||
"network": "Bitcoin",
|
||||
"currency_code": "BTC",
|
||||
"address": "bc1q4x8j6mt27y5gv0q625t8wkr87ruy8fprpy4v3f",
|
||||
"preferred": false
|
||||
},
|
||||
{
|
||||
"network": "Dogecoin",
|
||||
"currency_code": "DOGE",
|
||||
"address": "D8GH73rNjudgi6bS2krrXWEsU9KShedLXp",
|
||||
"preferred": true
|
||||
},
|
||||
{
|
||||
"network": "Ethereum",
|
||||
"currency_code": "ETH",
|
||||
"address": "0x7ab4091e00363654bf84B34151225742cd92FCE5",
|
||||
"preferred": false
|
||||
},
|
||||
{
|
||||
"network": "Litecoin",
|
||||
"currency_code": "LTC",
|
||||
"address": "LbJi8EuoDcwaZvykcKmcrM74jpjde23qJ2",
|
||||
"preferred": false
|
||||
},
|
||||
{
|
||||
"network": "Monero",
|
||||
"currency_code": "XMR",
|
||||
"address": "46YwWDbZD6jVptuk5mLHsuAmh1BnUMSjSNYacozQQEraWSQ93nb2yYVRHoMR6PmFYWEHsLHg9tr1cH5M8Rtn7YaaGQPCjSh",
|
||||
"preferred": false
|
||||
}
|
||||
],
|
||||
"links":
|
||||
[
|
||||
{
|
||||
"name": "Open Collective",
|
||||
"url": "https://opencollective.com/revanced",
|
||||
"preferred": true
|
||||
},
|
||||
{
|
||||
"name": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/ReVanced",
|
||||
"preferred": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
2
src/main/resources/static/robots.txt
Normal file
2
src/main/resources/static/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
21
src/test/kotlin/app/revanced/ApplicationTest.kt
Normal file
21
src/test/kotlin/app/revanced/ApplicationTest.kt
Normal file
@@ -0,0 +1,21 @@
|
||||
package app.revanced
|
||||
|
||||
import app.revanced.api.plugins.configureRouting
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.testing.*
|
||||
import kotlin.test.*
|
||||
|
||||
class ApplicationTest {
|
||||
@Test
|
||||
fun testRoot() = testApplication {
|
||||
application {
|
||||
configureRouting()
|
||||
}
|
||||
client.get("/").apply {
|
||||
assertEquals(HttpStatusCode.OK, status)
|
||||
assertEquals("Hello World!", bodyAsText())
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user