diff --git a/android/build.gradle.kts b/android/build.gradle.kts deleted file mode 100644 index 2f262b1..0000000 --- a/android/build.gradle.kts +++ /dev/null @@ -1,63 +0,0 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - alias(libs.plugins.androidApplication) - alias(libs.plugins.composeMultiplatform) - alias(libs.plugins.composeCompiler) -} - -kotlin { - target { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) - } - } - - dependencies { - implementation(projects.ui) - implementation(libs.compose.ui.tooling.preview) - implementation(libs.androidx.activity.compose) - - implementation(libs.androidx.appcompat) - implementation(libs.hcaptcha.compose) - - implementation(libs.ktor.client.okhttp) - - implementation(project.dependencies.platform(libs.koin.bom)) - implementation(libs.koin.compose) - } -} - -dependencies { - debugImplementation(libs.compose.ui.tooling) - coreLibraryDesugaring(libs.desugar) -} - -android { - namespace = "moe.lava.neon" - compileSdk = libs.versions.android.compileSdk.get().toInt() - - defaultConfig { - applicationId = "moe.lava.neon" - minSdk = libs.versions.android.minSdk.get().toInt() - targetSdk = libs.versions.android.targetSdk.get().toInt() - versionCode = 1 - versionName = "1.0" - } - packaging { - resources { - excludes += "/META-INF/{AL2.0,LGPL2.1}" - } - } - buildTypes { - getByName("release") { - isMinifyEnabled = false - signingConfig = signingConfigs.getByName("debug") - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - isCoreLibraryDesugaringEnabled = true - } -} diff --git a/api/gateway/build.gradle.kts b/api/gateway/build.gradle.kts deleted file mode 100644 index 75a3458..0000000 --- a/api/gateway/build.gradle.kts +++ /dev/null @@ -1,41 +0,0 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - alias(libs.plugins.androidMultiplatformLibrary) - alias(libs.plugins.kotlinMultiplatform) - alias(libs.plugins.kotlinSerialization) -} - -kotlin { - androidLibrary { - namespace = "moe.lava.neon.api.gateway" - compileSdk = libs.versions.android.compileSdk.get().toInt() - - compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) - } - } - - jvm() - - sourceSets { - commonMain.dependencies { - implementation(project(":api:shared")) - - implementation(libs.kermit) - implementation(libs.ktor.client.core) - implementation(libs.ktor.client.content.negotiation) - implementation(libs.ktor.client.websockets) - implementation(libs.ktor.serialization.kotlinx.json) - } - commonTest.dependencies { - implementation(libs.kotlin.test) - } - jvmMain.dependencies { - implementation(libs.ktor.client.okhttp) - } - androidMain.dependencies { - implementation(libs.ktor.client.okhttp) - } - } -} diff --git a/api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/handlers/EventHandlers.kt b/api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/handlers/EventHandlers.kt deleted file mode 100644 index 5c721fd..0000000 --- a/api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/handlers/EventHandlers.kt +++ /dev/null @@ -1,7 +0,0 @@ -package moe.lava.neon.api.gateway.handlers - -import moe.lava.neon.api.gateway.Event - -sealed interface Handler { - suspend fun handle(event: T) -} diff --git a/api/rest/build.gradle.kts b/api/rest/build.gradle.kts deleted file mode 100644 index 028e304..0000000 --- a/api/rest/build.gradle.kts +++ /dev/null @@ -1,59 +0,0 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - alias(libs.plugins.androidMultiplatformLibrary) - alias(libs.plugins.kotest) - alias(libs.plugins.kotlinMultiplatform) - alias(libs.plugins.kotlinSerialization) - alias(libs.plugins.ksp) -} - -kotlin { - androidLibrary { - namespace = "moe.lava.neon.api.rest" - compileSdk = libs.versions.android.compileSdk.get().toInt() - - compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) - } - } - - jvm() - - sourceSets { - commonMain.dependencies { - implementation(project(":api:shared")) - implementation(project(":common")) - - implementation(libs.kermit) - implementation(libs.ktor.client.core) - implementation(libs.ktor.client.content.negotiation) - implementation(libs.ktor.serialization.kotlinx.json) - } - commonTest.dependencies { - implementation(libs.kotlin.test) - implementation(libs.kotest.assertions) - implementation(libs.kotest.framework) - implementation(libs.kotest.property) - implementation(libs.ktor.client.mock) - } - jvmMain.dependencies { - implementation(libs.ktor.client.okhttp) - } - jvmTest.dependencies { - implementation(libs.kotest.runner.junit5) - } - androidMain.dependencies { - implementation(libs.ktor.client.okhttp) - } - } -} - -tasks.named("jvmTest") { - useJUnitPlatform() -} - -//tasks.withType().configureEach { -// logger.lifecycle("UP-TO-DATE check for $name is disabled, forcing it to run.") -// outputs.upToDateWhen { false } -//} diff --git a/api/rest/src/commonMain/kotlin/moe/lava/neon/api/ApiResponse.kt b/api/rest/src/commonMain/kotlin/moe/lava/neon/api/ApiResponse.kt deleted file mode 100644 index f9c69c1..0000000 --- a/api/rest/src/commonMain/kotlin/moe/lava/neon/api/ApiResponse.kt +++ /dev/null @@ -1,15 +0,0 @@ -package moe.lava.neon.api - -import io.ktor.client.call.body -import io.ktor.client.statement.HttpResponse -import io.ktor.util.reflect.TypeInfo -import io.ktor.util.reflect.typeInfo - -class ApiResponse( - val response: HttpResponse, - private val bodyType: TypeInfo, -) { - suspend fun body() = response.body(bodyType) as T -} - -inline fun HttpResponse.wrap() = ApiResponse(this, typeInfo()) diff --git a/api/rest/src/commonMain/kotlin/moe/lava/neon/api/endpoints/Auth.kt b/api/rest/src/commonMain/kotlin/moe/lava/neon/api/endpoints/Auth.kt deleted file mode 100644 index ed00a39..0000000 --- a/api/rest/src/commonMain/kotlin/moe/lava/neon/api/endpoints/Auth.kt +++ /dev/null @@ -1,46 +0,0 @@ -package moe.lava.neon.api.endpoints - -import io.ktor.client.request.get -import io.ktor.client.request.header -import io.ktor.client.request.parameter -import io.ktor.client.request.post -import io.ktor.client.request.setBody -import kotlinx.serialization.Serializable -import moe.lava.neon.api.ApiClient -import moe.lava.neon.api.wrap - -@Serializable -data class ExperimentResponse( - val fingerprint: String, -) - -@Serializable -private data class LoginRequest( - val login: String, - val password: String, - val undelete: Boolean = false, - val loginSource: String? = null, - val giftCodeSkuId: String? = null, -) - -@Serializable -data class LoginResponse( - val userId: String, - val token: String, - val userSettings: UserSettings, -) { - @Serializable - data class UserSettings(val locale: String, val theme: String) -} - -suspend fun ApiClient.getExperiments() = client.get("experiments") { - parameter("with_guild_experiments", "true") -}.wrap() - -suspend fun ApiClient.login(email: String, password: String, fingerprint: String) = client.post("auth/login") { - header("X-Fingerprint", fingerprint) - setBody(LoginRequest( - login = email, - password = password, - )) -}.wrap() diff --git a/api/rest/src/commonTest/kotlin/moe/lava/neon/tests/api/CaptchaTest.kt b/api/rest/src/commonTest/kotlin/moe/lava/neon/tests/api/CaptchaTest.kt deleted file mode 100644 index 5c01a25..0000000 --- a/api/rest/src/commonTest/kotlin/moe/lava/neon/tests/api/CaptchaTest.kt +++ /dev/null @@ -1,30 +0,0 @@ -package moe.lava.neon.tests.api - -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import moe.lava.neon.api.ApiClient -import moe.lava.neon.api.endpoints.getExperiments -import moe.lava.neon.common.captcha.CaptchaResponse - -class CaptchaTest : FunSpec({ - val mock = DiscordApiMock() - val client = ApiClient(mock.engine, false) - - val (captchaReq, captchaRes) = mock.generateCaptcha() - mock.isCaptchaEnabled = true - - test("captcha should not be handled") { - val res = client.getExperiments().response - res.status.value shouldBe 400 - } - - test("captcha should be handled") { - client.setCaptchaHandler { req -> - captchaRes - .takeIf { req == captchaReq } - ?: CaptchaResponse.Failed(Throwable()) - } - val res = client.getExperiments().response - res.status.value shouldBe 200 - } -}) diff --git a/api/rest/src/commonTest/kotlin/moe/lava/neon/tests/api/DiscordApiMock.kt b/api/rest/src/commonTest/kotlin/moe/lava/neon/tests/api/DiscordApiMock.kt deleted file mode 100644 index ba65bc1..0000000 --- a/api/rest/src/commonTest/kotlin/moe/lava/neon/tests/api/DiscordApiMock.kt +++ /dev/null @@ -1,105 +0,0 @@ -package moe.lava.neon.tests.api - -import io.kotest.property.Arb -import io.kotest.property.arbitrary.long -import io.kotest.property.arbitrary.next -import io.kotest.property.arbitrary.single -import io.kotest.property.arbitrary.string -import io.kotest.property.arbitrary.stringPattern -import io.ktor.client.engine.mock.MockEngine -import io.ktor.client.engine.mock.MockRequestHandleScope -import io.ktor.client.engine.mock.respond -import io.ktor.client.engine.mock.respondError -import io.ktor.client.request.HttpResponseData -import io.ktor.http.HttpHeaders -import io.ktor.http.HttpStatusCode -import io.ktor.http.content.TextContent -import io.ktor.http.headersOf -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonNamingStrategy -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import moe.lava.neon.common.captcha.CaptchaRequest -import moe.lava.neon.common.captcha.CaptchaResponse -import moe.lava.neon.tests.api.mock.AuthResponse - -@OptIn(ExperimentalSerializationApi::class) -private val JsonWithSnakecase = Json { - namingStrategy = JsonNamingStrategy.SnakeCase -} -private val JsonHeader = headersOf(HttpHeaders.ContentType, "application/json") - -val idArb = Arb.long(1e18.toLong(), 1e20.toLong()) -// Good enough -val tokenArb = Arb.stringPattern("(mfa\\.[a-zA-Z0-9_-]{20,})|([a-zA-Z0-9_-]{23,28}\\.[a-zA-Z0-9_-]{6,7}\\.[a-zA-Z0-9_-]{38})") - -class DiscordApiMock { - private val fingerprints = mutableListOf() - private val logins = mutableMapOf() - private var captcha: Pair = generateCaptcha() - - var isCaptchaEnabled = false - - fun createLogin(email: String, password: String) { - logins[email] = password - } - - fun generateCaptcha(): Pair { - val req = CaptchaRequest( - listOf(Arb.string().single()), - Arb.string().single(), - Arb.string().single(), - Arb.string().single(), - Arb.string().single(), - Arb.string().single(), - true, - ) - val res = CaptchaResponse.Success(Arb.string().single()) - captcha = req to res - return req to res - } - - val engine = MockEngine { req -> - if (!req.url.toString().startsWith("https://discord.com/api/v9")) { - return@MockEngine respondError(HttpStatusCode.NotFound) - } - if (isCaptchaEnabled) { - if (req.headers["X-Captcha-Key"] != captcha.second.token) { - return@MockEngine respondJson(JsonWithSnakecase.encodeToString(captcha.first), HttpStatusCode.BadRequest) - } - } - val path = req.url.encodedPath.replaceFirst("/api/v9", "") - return@MockEngine when (path) { - "/experiments" -> { - val fp = Arb.string(18..20, "123456789").single() - fingerprints.add(fp) - respondJson(AuthResponse.Experiments(fp)) - } - "/auth/login" -> { - val body = req.body as? TextContent - ?: return@MockEngine badReq("No body") - val json = Json.parseToJsonElement(body.text).jsonObject - val login = json["login"]?.jsonPrimitive?.content - ?: return@MockEngine badReq("No login") - val password = json["password"]?.jsonPrimitive?.content - ?: return@MockEngine badReq("No password") - - if (logins[login] != password) { - return@MockEngine badReq("Unknown credentials") - } - - respondJson(AuthResponse.Login(idArb.next(), tokenArb.next())) - } - else -> respondError(HttpStatusCode.NotFound) - } - } - - @Suppress("NOTHING_TO_INLINE") - private inline fun MockRequestHandleScope.badReq(msg: String): HttpResponseData = - respondError(HttpStatusCode.BadRequest, content = "[Neon] $msg") - - @Suppress("NOTHING_TO_INLINE") - private inline fun MockRequestHandleScope.respondJson(content: String, status: HttpStatusCode = HttpStatusCode.OK): HttpResponseData = - respond(content = content, status = status, headers = JsonHeader) -} diff --git a/api/rest/src/commonTest/kotlin/moe/lava/neon/tests/api/EndpointTestFactory.kt b/api/rest/src/commonTest/kotlin/moe/lava/neon/tests/api/EndpointTestFactory.kt deleted file mode 100644 index f5cf9ee..0000000 --- a/api/rest/src/commonTest/kotlin/moe/lava/neon/tests/api/EndpointTestFactory.kt +++ /dev/null @@ -1,84 +0,0 @@ -package moe.lava.neon.tests.api - -import io.kotest.assertions.throwables.shouldNotThrowAny -import io.kotest.assertions.withClue -import io.kotest.core.spec.style.FunSpec -import io.kotest.core.spec.style.funSpec -import io.kotest.core.spec.style.scopes.FunSpecContainerScope -import io.kotest.matchers.booleans.shouldBeTrue -import io.kotest.matchers.collections.shouldBeOneOf -import io.kotest.matchers.collections.shouldContainAllInAnyOrder -import io.kotest.matchers.nulls.shouldNotBeNull -import io.kotest.matchers.shouldBe -import io.ktor.client.statement.request -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import moe.lava.neon.api.ApiResponse -import kotlin.io.encoding.Base64 - -fun endpointTestFactory( - endpoint: String, - testName: String?, - response: suspend () -> ApiResponse, - also: suspend FunSpecContainerScope.(response: ApiResponse) -> Unit, -) = funSpec { - context(testName ?: endpoint) { - val res = response() - val req = res.response.request - - test("has correct base url") { - req.url.toString().startsWith("https://discord.com/api/v9/") shouldBe true - } - test("uses correct endpoint") { - req.url.encodedPath.replace("/api/v9/", "") shouldBe endpoint - } - test("has valid super props") { - val props = req.headers["X-Super-Properties"] - props.shouldNotBeNull() - val decoded = withClue("should be decodable") { - shouldNotThrowAny { - Base64.decode(props).decodeToString() - } - } - val parsed = withClue("should be parsable") { - shouldNotThrowAny { - Json.parseToJsonElement(decoded).jsonObject - } - } - withClue("has props") { - parsed.keys shouldContainAllInAnyOrder setOf( - "os", - "browser", - "browser_user_agent", - "browser_version", - "client_build_number", - "release_channel", - "system_locale", - ) - } - val userAgent = withClue("has valid user agent") { - val agent = parsed["browser_user_agent"]?.jsonPrimitive - agent?.isString.shouldBeTrue() - agent.content - } - withClue("has matching user agent") { - userAgent shouldBeOneOf setOf(req.headers["User-Agent"], "") - } - } - test("has correct body") { - shouldNotThrowAny { res.body() } - } - also(res) - } -} - -fun FunSpec.withFactory( - testName: String? = null, - endpoint: String, - response: suspend () -> ApiResponse, - also: suspend FunSpecContainerScope.(response: ApiResponse) -> Unit = {}, -) { - include(endpointTestFactory(endpoint, testName, response, also)) -} - diff --git a/api/rest/src/commonTest/kotlin/moe/lava/neon/tests/api/endpoints/AuthTest.kt b/api/rest/src/commonTest/kotlin/moe/lava/neon/tests/api/endpoints/AuthTest.kt deleted file mode 100644 index 760c244..0000000 --- a/api/rest/src/commonTest/kotlin/moe/lava/neon/tests/api/endpoints/AuthTest.kt +++ /dev/null @@ -1,54 +0,0 @@ -package moe.lava.neon.tests.api.endpoints - -import io.kotest.assertions.throwables.shouldNotThrowAny -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import io.ktor.client.statement.request -import io.ktor.http.content.TextContent -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import moe.lava.neon.api.ApiClient -import moe.lava.neon.api.endpoints.getExperiments -import moe.lava.neon.api.endpoints.login -import moe.lava.neon.tests.api.DiscordApiMock -import moe.lava.neon.tests.api.withFactory - -class AuthTest : FunSpec({ - val mock = DiscordApiMock() - val client = ApiClient(mock.engine, false) - - var fp: String? = null - withFactory( - testName = "get experiments", - endpoint = "experiments", - response = client::getExperiments - ) { res -> - val body = res.body() - fp = body.fingerprint - } - - val email = "hello@example.com" - val password = "supersecurepassword" - mock.createLogin(email, password) - withFactory( - testName = "login with real creds", - endpoint = "auth/login", - response = { client.login(email, password, fp!!) } - ) { res -> - val req = res.response.request - val headers = req.headers - test("has correct fingerprint") { - headers["X-Fingerprint"] shouldBe fp - } - context("has correct body") { - val body = shouldNotThrowAny { Json.parseToJsonElement((req.content as TextContent).text).jsonObject } - test("has correct login") { - body["login"]?.jsonPrimitive?.content shouldBe email - } - test("has correct password") { - body["password"]?.jsonPrimitive?.content shouldBe password - } - } - } -}) diff --git a/api/rest/src/commonTest/kotlin/moe/lava/neon/tests/api/mock/AuthResponse.kt b/api/rest/src/commonTest/kotlin/moe/lava/neon/tests/api/mock/AuthResponse.kt deleted file mode 100644 index 5ec810d..0000000 --- a/api/rest/src/commonTest/kotlin/moe/lava/neon/tests/api/mock/AuthResponse.kt +++ /dev/null @@ -1,6 +0,0 @@ -package moe.lava.neon.tests.api.mock - -object AuthResponse { - fun Experiments(fp: String) = """{"fingerprint":"$fp","assignments":[[3643362751,0,1,-1,0,4342,0,0,null,null,null],[1428438599,0,1,-1,2,3832,0,0,null,null,null],[1567199723,0,1,-1,1,1775,0,0,null,null,null],[4179344527,3,1,-1,0,3584,0,0,null,null,null],[1814483290,0,1,-1,0,6281,0,0,null,null,null],[4221006726,0,1,-1,0,4318,0,0,null,null,null],[1398673921,1,1,-1,0,4649,0,0,null,null,null],[1034661306,0,1,-1,0,1792,0,0,null,null,null],[3124003316,1,1,-1,0,1427,0,0,null,null,null],[2676348506,0,1,-1,0,4412,0,0,null,null,null],[4136574802,0,1,-1,1,4337,0,0,null,null,null],[4049571159,0,1,-1,0,504,0,0,null,null,null],[2539540256,0,1,-1,3,25,0,0,null,null,null],[1549543958,2,1,-1,0,6992,0,0,null,null,null],[1333727,0,1,-1,0,4978,0,0,null,null,null],[3029387945,1,1,-1,0,8670,0,0,null,null,null],[738080167,0,1,-1,1,9441,0,0,null,null,null],[3283745071,0,1,-1,1,9135,0,0,null,null,null],[373531156,0,3,-1,0,7954,0,0,null,null,null],[1617749743,0,1,-1,0,8281,0,0,null,null,null],[288968706,0,1,-1,3,7867,0,0,null,null,null],[2091202574,0,1,-1,0,8443,0,0,null,null,null],[4265918989,0,1,-1,0,2580,0,0,null,null,null],[1083932689,0,1,-1,0,6167,0,0,null,null,null],[1884426471,0,1,-1,0,5258,0,0,null,null,null],[2180379513,0,1,-1,4,5727,0,0,null,null,null],[759064140,0,1,-1,0,6050,0,0,null,null,null],[1680860120,0,1,-1,1,6633,0,0,null,null,null],[151550492,0,1,-1,2,102,0,1,null,null,null],[2054293512,0,0,-1,0,193,0,1,null,null,null],[2848826960,0,1,-1,0,7093,0,0,null,null,null],[3775594731,3,1,-1,0,1418,0,0,null,null,null],[878040044,1,1,-1,0,6738,0,1,null,null,null],[2990331215,0,1,-1,0,2786,0,0,null,null,null],[996399186,2,1,-1,0,5255,0,0,null,null,null],[3173338335,0,0,-1,0,178,0,1,null,null,null],[640084831,3,1,-1,0,5284,0,0,null,null,null],[1112953678,0,1,-1,0,3640,0,0,null,null,null],[4285324985,0,1,-1,0,8618,0,0,null,null,null],[4206392105,4,1,-1,0,5758,0,0,null,null,null],[1714347921,0,1,-1,0,2255,0,0,null,null,null],[3936291300,3,1,-1,0,3408,0,0,null,null,null],[2660711063,0,1,-1,0,8317,0,0,null,null,null],[1644303758,0,1,-1,0,2365,0,1,null,null,null],[114771571,0,1,-1,0,796,0,0,null,null,null],[437074334,5,1,-1,2,2482,0,1,null,null,null],[1978990512,3,1,-1,0,9101,0,0,null,null,null],[3378028029,3,2,-1,0,1997,0,1,null,null,null],[1046173986,0,1,-1,0,9264,0,0,null,null,null],[1757800499,1,1,-1,0,9654,0,0,null,null,null],[2849514387,0,1,-1,0,275,0,1,null,null,null],[2613104049,0,1,-1,0,8339,0,1,null,null,null],[2482010813,0,1,-1,0,4372,0,1,null,null,null],[1778984745,0,1,-1,0,1685,0,1,null,null,null],[2870923171,0,1,-1,0,101,0,0,null,null,null],[1598219105,1,2,-1,0,3159,0,1,null,null,null],[641666131,1,1,-1,0,2501,0,0,null,null,null]]}""" - fun Login(userId: Long, token: String) = """{"user_id":"$userId","token":"$token","user_settings":{"locale":"en-US","theme":"dark"}}""" -} diff --git a/api/shared/build.gradle.kts b/api/shared/build.gradle.kts deleted file mode 100644 index 6b1f922..0000000 --- a/api/shared/build.gradle.kts +++ /dev/null @@ -1,26 +0,0 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - alias(libs.plugins.androidMultiplatformLibrary) - alias(libs.plugins.kotlinMultiplatform) - alias(libs.plugins.kotlinSerialization) -} - -kotlin { - androidLibrary { - namespace = "moe.lava.neon.api" - compileSdk = libs.versions.android.compileSdk.get().toInt() - - compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) - } - } - - jvm() - - sourceSets { - commonMain.dependencies { - implementation(libs.kotlinx.serialization.json) - } - } -} diff --git a/build.gradle.kts b/build.gradle.kts index 62884ba..8c3efbb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,14 +2,12 @@ plugins { // this is necessary to avoid the plugins to be loaded multiple times // in each subproject's classloader alias(libs.plugins.androidApplication) apply false - alias(libs.plugins.androidMultiplatformLibrary) apply false + alias(libs.plugins.androidLibrary) apply false alias(libs.plugins.composeHotReload) apply false alias(libs.plugins.composeMultiplatform) apply false alias(libs.plugins.composeCompiler) apply false - alias(libs.plugins.koinCompiler) apply false - alias(libs.plugins.kotest) apply false alias(libs.plugins.kotlinMultiplatform) apply false alias(libs.plugins.kotlinSerialization) apply false - alias(libs.plugins.ksp) apply false + alias(libs.plugins.metro) apply false alias(libs.plugins.sqldelight) apply false } diff --git a/common/build.gradle.kts b/common/build.gradle.kts deleted file mode 100644 index 84aa4ff..0000000 --- a/common/build.gradle.kts +++ /dev/null @@ -1,14 +0,0 @@ -plugins { - alias(libs.plugins.kotlinMultiplatform) - alias(libs.plugins.kotlinSerialization) -} - -kotlin { - jvm() - - sourceSets { - commonMain.dependencies { - implementation(libs.kotlinx.serialization.core) - } - } -} diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 97f594e..78e5ec2 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -1,33 +1,27 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { - alias(libs.plugins.androidMultiplatformLibrary) - alias(libs.plugins.koinCompiler) + alias(libs.plugins.androidLibrary) alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.kotlinSerialization) + alias(libs.plugins.metro) alias(libs.plugins.sqldelight) } kotlin { - androidLibrary { - namespace = "moe.lava.neon.core" - compileSdk = libs.versions.android.compileSdk.get().toInt() - + jvm() + androidTarget { compilerOptions { jvmTarget.set(JvmTarget.JVM_11) } } - jvm() - sourceSets { commonMain.dependencies { - implementation(project(":api:gateway")) - implementation(project(":api:rest")) - implementation(project(":common")) - - implementation(project.dependencies.platform(libs.koin.bom)) - implementation(libs.koin.core) + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.client.websockets) + implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.kermit) implementation(libs.settings) @@ -35,6 +29,40 @@ kotlin { commonTest.dependencies { implementation(libs.kotlin.test) } + jvmMain.dependencies { + implementation(libs.ktor.client.okhttp) + } + androidMain.dependencies { + implementation(libs.ktor.client.okhttp) + } + } +} + +dependencies { + coreLibraryDesugaring(libs.desugar) +} + +android { + namespace = "moe.lava.neon.core" + compileSdk = libs.versions.android.compileSdk.get().toInt() + + defaultConfig { + minSdk = libs.versions.android.minSdk.get().toInt() + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } + buildTypes { + getByName("release") { + isMinifyEnabled = false + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + isCoreLibraryDesugaringEnabled = true } } diff --git a/core/src/androidMain/kotlin/moe/lava/neon/core/Platform.android.kt b/core/src/androidMain/kotlin/moe/lava/neon/core/Platform.android.kt new file mode 100644 index 0000000..544fc61 --- /dev/null +++ b/core/src/androidMain/kotlin/moe/lava/neon/core/Platform.android.kt @@ -0,0 +1,9 @@ +package moe.lava.neon.core + +import android.os.Build + +class AndroidPlatform : Platform { + override val name: String = "Android ${Build.VERSION.SDK_INT}" +} + +actual fun getPlatform(): Platform = AndroidPlatform() diff --git a/api/shared/src/androidMain/kotlin/moe/lava/neon/api/ApiConstants.android.kt b/core/src/androidMain/kotlin/moe/lava/neon/core/api/ApiConstants.android.kt similarity index 60% rename from api/shared/src/androidMain/kotlin/moe/lava/neon/api/ApiConstants.android.kt rename to core/src/androidMain/kotlin/moe/lava/neon/core/api/ApiConstants.android.kt index 37847a8..54b2c9c 100644 --- a/api/shared/src/androidMain/kotlin/moe/lava/neon/api/ApiConstants.android.kt +++ b/core/src/androidMain/kotlin/moe/lava/neon/core/api/ApiConstants.android.kt @@ -1,14 +1,12 @@ -package moe.lava.neon.api +package moe.lava.neon.core.api -import android.annotation.SuppressLint -import android.os.Build import java.util.Locale -@SuppressLint("ConstantLocale") +@Suppress("ConstantLocale") internal actual val platformSuperProps = PlatformProps( - device = Build.DEVICE, + device = android.os.Build.DEVICE, // TODO: this only outputs language but not country (e.g. en instead of en-AU) // .toLanguageTag() is close, but returns too much junk (e.g. en-AU-u-fw-mon) systemLocale = Locale.getDefault().language, - osVersion = "${Build.VERSION.SDK_INT}", + osVersion = "${android.os.Build.VERSION.SDK_INT}", ) diff --git a/core/src/commonMain/kotlin/moe/lava/neon/core/AppSettings.kt b/core/src/commonMain/kotlin/moe/lava/neon/core/AppSettings.kt index 3492b1a..5d5040b 100644 --- a/core/src/commonMain/kotlin/moe/lava/neon/core/AppSettings.kt +++ b/core/src/commonMain/kotlin/moe/lava/neon/core/AppSettings.kt @@ -2,8 +2,13 @@ package moe.lava.neon.core import com.russhwolf.settings.Settings import com.russhwolf.settings.nullableString +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn -internal class AppSettings { +@SingleIn(AppScope::class) +@Inject +class AppSettings { private val settings = Settings() var fingerprint by settings.nullableString() diff --git a/core/src/commonMain/kotlin/moe/lava/neon/core/Platform.kt b/core/src/commonMain/kotlin/moe/lava/neon/core/Platform.kt new file mode 100644 index 0000000..aae09e6 --- /dev/null +++ b/core/src/commonMain/kotlin/moe/lava/neon/core/Platform.kt @@ -0,0 +1,7 @@ +package moe.lava.neon.core + +interface Platform { + val name: String +} + +expect fun getPlatform(): Platform diff --git a/api/rest/src/commonMain/kotlin/moe/lava/neon/api/ApiClient.kt b/core/src/commonMain/kotlin/moe/lava/neon/core/api/ApiClient.kt similarity index 76% rename from api/rest/src/commonMain/kotlin/moe/lava/neon/api/ApiClient.kt rename to core/src/commonMain/kotlin/moe/lava/neon/core/api/ApiClient.kt index ffbdc86..d34dbf3 100644 --- a/api/rest/src/commonMain/kotlin/moe/lava/neon/api/ApiClient.kt +++ b/core/src/commonMain/kotlin/moe/lava/neon/core/api/ApiClient.kt @@ -1,9 +1,11 @@ -package moe.lava.neon.api +package moe.lava.neon.core.api import co.touchlab.kermit.Logger +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import io.ktor.client.HttpClient import io.ktor.client.call.body -import io.ktor.client.engine.HttpClientEngine import io.ktor.client.plugins.HttpSend import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.cookies.HttpCookies @@ -12,21 +14,18 @@ import io.ktor.client.plugins.plugin import io.ktor.client.plugins.websocket.WebSockets import io.ktor.client.request.header import io.ktor.client.statement.bodyAsText -import io.ktor.http.ContentType -import io.ktor.http.contentType import io.ktor.http.userAgent import io.ktor.serialization.kotlinx.json.json import io.ktor.util.appendAll import kotlinx.serialization.ExperimentalSerializationApi -import moe.lava.neon.common.captcha.CaptchaRequest -import moe.lava.neon.common.captcha.CaptchaResponse - -class ApiClient internal constructor( - engine: HttpClientEngine, - assertSuccess: Boolean, -) { - constructor() : this(HttpClient().engine, true) +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonNamingStrategy +import moe.lava.neon.core.api.captcha.CaptchaRequest +import moe.lava.neon.core.api.captcha.CaptchaResponse +@SingleIn(AppScope::class) +@Inject +class ApiClient { private val logger = Logger.withTag("neon.core.api/client") private var captchaHandler: (suspend (CaptchaRequest) -> CaptchaResponse)? = null @@ -36,8 +35,8 @@ class ApiClient internal constructor( } @OptIn(ExperimentalSerializationApi::class) - internal val client = HttpClient(engine) { - expectSuccess = assertSuccess + val client = HttpClient { + expectSuccess = true install(ContentNegotiation) { json(ApiConstants.json) } @@ -45,7 +44,6 @@ class ApiClient internal constructor( install(HttpCookies) defaultRequest { url("https://discord.com/api/v9/") - contentType(ContentType.Application.Json) userAgent(ApiConstants.userAgent) headers.appendAll(ApiConstants.baseHeaders) } @@ -53,7 +51,6 @@ class ApiClient internal constructor( plugin(HttpSend).intercept { req -> logger.d { "Intercepting ${req.url.buildString()}" } val call = execute(req) - logger.d { "recv ${call.response.bodyAsText()}" } if (call.response.status.value != 400) return@intercept call logger.d { "Found 400 response: ${call.response.bodyAsText()}" } val captchaRequest = runCatching { call.response.body() } @@ -89,3 +86,20 @@ class ApiClient internal constructor( } } } + +@OptIn(ExperimentalSerializationApi::class) +fun buildApiClient() = HttpClient { + expectSuccess = true + install(ContentNegotiation) { + json(Json { + namingStrategy = JsonNamingStrategy.SnakeCase + ignoreUnknownKeys = true + }) + } + install(WebSockets) + install(HttpCookies) + defaultRequest { + url("https://discord.com/api/v9/") + headers.appendAll(ApiConstants.baseHeaders) + } +} diff --git a/api/shared/src/commonMain/kotlin/moe/lava/neon/api/ApiConstants.kt b/core/src/commonMain/kotlin/moe/lava/neon/core/api/ApiConstants.kt similarity index 97% rename from api/shared/src/commonMain/kotlin/moe/lava/neon/api/ApiConstants.kt rename to core/src/commonMain/kotlin/moe/lava/neon/core/api/ApiConstants.kt index dbbe8f2..42fc251 100644 --- a/api/shared/src/commonMain/kotlin/moe/lava/neon/api/ApiConstants.kt +++ b/core/src/commonMain/kotlin/moe/lava/neon/core/api/ApiConstants.kt @@ -1,4 +1,4 @@ -package moe.lava.neon.api +package moe.lava.neon.core.api import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable @@ -24,14 +24,7 @@ internal data class PlatformProps( internal expect val platformSuperProps: PlatformProps object ApiConstants { - @OptIn(ExperimentalSerializationApi::class) - val json = Json { - namingStrategy = JsonNamingStrategy.SnakeCase - ignoreUnknownKeys = true - encodeDefaults = true - } - - val superProps = Base64.encode(json.encodeToString(SuperProperties()).encodeToByteArray()) + val superProps = Base64.encode(Json.encodeToString(SuperProperties()).encodeToByteArray()) val baseHeaders = mapOf( "X-Debug-Options" to "bugReporterEnabled", "X-Discord-Locale" to "en-US", @@ -41,6 +34,13 @@ object ApiConstants { const val userAgent = "Discord-Android/311020;RNA" const val gatewayUserAgent = "okhttp/4.12.0" + @OptIn(ExperimentalSerializationApi::class) + val json = Json { + namingStrategy = JsonNamingStrategy.SnakeCase + ignoreUnknownKeys = true + encodeDefaults = true + } + // TODO: Desktop uses separate properties @Suppress("PropertyName") @Serializable diff --git a/common/src/commonMain/kotlin/moe/lava/neon/common/captcha/CaptchaRequest.kt b/core/src/commonMain/kotlin/moe/lava/neon/core/api/captcha/CaptchaRequest.kt similarity index 89% rename from common/src/commonMain/kotlin/moe/lava/neon/common/captcha/CaptchaRequest.kt rename to core/src/commonMain/kotlin/moe/lava/neon/core/api/captcha/CaptchaRequest.kt index 9ded522..4357d6d 100644 --- a/common/src/commonMain/kotlin/moe/lava/neon/common/captcha/CaptchaRequest.kt +++ b/core/src/commonMain/kotlin/moe/lava/neon/core/api/captcha/CaptchaRequest.kt @@ -1,4 +1,4 @@ -package moe.lava.neon.common.captcha +package moe.lava.neon.core.api.captcha import kotlinx.serialization.Serializable diff --git a/common/src/commonMain/kotlin/moe/lava/neon/common/captcha/CaptchaResponse.kt b/core/src/commonMain/kotlin/moe/lava/neon/core/api/captcha/CaptchaResponse.kt similarity index 80% rename from common/src/commonMain/kotlin/moe/lava/neon/common/captcha/CaptchaResponse.kt rename to core/src/commonMain/kotlin/moe/lava/neon/core/api/captcha/CaptchaResponse.kt index 85f93df..7bc9d59 100644 --- a/common/src/commonMain/kotlin/moe/lava/neon/common/captcha/CaptchaResponse.kt +++ b/core/src/commonMain/kotlin/moe/lava/neon/core/api/captcha/CaptchaResponse.kt @@ -1,4 +1,4 @@ -package moe.lava.neon.common.captcha +package moe.lava.neon.core.api.captcha sealed class CaptchaResponse { data class Success(val token: String) : CaptchaResponse() diff --git a/api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/Capability.kt b/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/Capability.kt similarity index 97% rename from api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/Capability.kt rename to core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/Capability.kt index ced4e64..76347ee 100644 --- a/api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/Capability.kt +++ b/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/Capability.kt @@ -1,4 +1,4 @@ -package moe.lava.neon.api.gateway +package moe.lava.neon.core.api.gateway @Suppress("unused") object Capability { diff --git a/api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/GatewayCloseReason.kt b/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/GatewayCloseReason.kt similarity index 95% rename from api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/GatewayCloseReason.kt rename to core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/GatewayCloseReason.kt index 396752d..2e1a826 100644 --- a/api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/GatewayCloseReason.kt +++ b/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/GatewayCloseReason.kt @@ -1,4 +1,4 @@ -package moe.lava.neon.api.gateway +package moe.lava.neon.core.api.gateway import io.ktor.websocket.CloseReason diff --git a/api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/GatewayHandler.kt b/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/GatewayHandler.kt similarity index 81% rename from api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/GatewayHandler.kt rename to core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/GatewayHandler.kt index 41054be..40dc00e 100644 --- a/api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/GatewayHandler.kt +++ b/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/GatewayHandler.kt @@ -1,22 +1,22 @@ -package moe.lava.neon.api.gateway +package moe.lava.neon.core.api.gateway import co.touchlab.kermit.Logger +import dev.zacsweers.metro.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.launch import kotlinx.serialization.ExperimentalSerializationApi -import moe.lava.neon.api.gateway.handlers.Handler +import moe.lava.neon.core.di.EventHandlerGraph +import moe.lava.neon.core.repository.AuthRepository import kotlin.math.pow -import kotlin.reflect.KClass import kotlin.time.Duration.Companion.seconds -class GatewayHandler { - private val mEvents = MutableSharedFlow() - val events = mEvents.asSharedFlow() - +@Inject +class GatewayHandler( + private val auth: AuthRepository, + private val handlers: EventHandlerGraph, +) { private val logger = Logger.withTag("neon.core.api.gateway/handler") private val scope = CoroutineScope(Dispatchers.IO) private var session: GatewaySession? = null @@ -25,20 +25,22 @@ class GatewayHandler { private var retryAttempts: Int = 0 @OptIn(ExperimentalSerializationApi::class) - suspend fun connect(token: String) { + suspend fun connect() { if (session != null) { logger.w(Throwable()) { "Attempted to connect, but client already connected, ignoring..." } return } + val token = auth.token + ?: throw IllegalStateException("Tried to connect to gateway with no token") session = GatewaySession.start( token = token, + eventHandlers = handlers, resumeProps = resumeProps, onSuccess = { logger.d { "Successful session start" } retryAttempts = 0 }, - onDispatch = { scope.launch { mEvents.emit(it) } }, onDestroy = { reason, resumeProps -> session = null @@ -59,7 +61,7 @@ class GatewayHandler { logger.d { "Reconnecting in ${dur.inWholeMilliseconds}ms" } delay(dur) retryAttempts += 1 - res = runCatching { connect(token) } + res = runCatching { connect() } res.exceptionOrNull()?.let { logger.e(it) { "Reconnect failed" } } diff --git a/api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/GatewaySession.kt b/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/GatewaySession.kt similarity index 90% rename from api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/GatewaySession.kt rename to core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/GatewaySession.kt index ce40047..1ac638b 100644 --- a/api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/GatewaySession.kt +++ b/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/GatewaySession.kt @@ -1,4 +1,4 @@ -package moe.lava.neon.api.gateway +package moe.lava.neon.core.api.gateway import co.touchlab.kermit.Logger import io.ktor.client.HttpClient @@ -22,19 +22,20 @@ import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import moe.lava.neon.api.ApiConstants -import moe.lava.neon.api.ApiConstants.json +import moe.lava.neon.core.api.ApiConstants +import moe.lava.neon.core.api.ApiConstants.json +import moe.lava.neon.core.di.EventHandlerGraph import kotlin.random.Random import kotlin.time.Duration.Companion.milliseconds private val logger = Logger.withTag("neon.core.api.gateway/session") -internal class GatewaySession private constructor( +class GatewaySession private constructor( private var ws: DefaultClientWebSocketSession, private val token: String, + private val handlers: EventHandlerGraph, private val scope: CoroutineScope, private var resumeProps: ResumeProperties?, - private val onDispatch: (Event.Dispatch) -> Unit, private val onDestroy: (GatewayCloseReason, ResumeProperties?) -> Unit, private val onSuccess: () -> Unit, ) { @@ -45,13 +46,13 @@ internal class GatewaySession private constructor( companion object { suspend fun start( token: String, + eventHandlers: EventHandlerGraph, client: HttpClient = HttpClient { install(HttpCookies) install(WebSockets) }, scope: CoroutineScope = CoroutineScope(Dispatchers.IO), resumeProps: ResumeProperties? = null, - onDispatch: (Event.Dispatch) -> Unit, onDestroy: (GatewayCloseReason, ResumeProperties?) -> Unit, onSuccess: () -> Unit, ): GatewaySession { @@ -66,7 +67,7 @@ internal class GatewaySession private constructor( } } - return GatewaySession(ws, token, scope, resumeProps, onDispatch, onDestroy, onSuccess) + return GatewaySession(ws, token, eventHandlers, scope, resumeProps, onDestroy, onSuccess) } } @@ -99,27 +100,19 @@ internal class GatewaySession private constructor( private suspend fun handlePayload(payload: Payload.Incoming<*>) { logger.d { payload.toString() } - val event = payload.d - when (event) { + when (val event = payload.d) { is Event.Heartbeat -> handleHeartbeat() is Event.Reconnect -> close(GatewayCloseReason.ServerReconnect) is Event.InvalidSession -> close(GatewayCloseReason.InvalidSession(event.resumable)) is Event.Hello -> handleHello(event) is Event.HeartbeatAck -> { missedHeartbeats -= 1 } - is Event.Ready -> { - resumeProps = ResumeProperties( - sessionId = event.sessionId, - resumeGatewayUrl = event.resumeGatewayUrl, - lastSequence = 0, - ) + is Event.Ready -> handlers.ready.handle(event) { + resumeProps = it onSuccess() } is Event.Resumed -> onSuccess() } - if (event is Event.Dispatch) { - onDispatch(event) - } } private suspend fun handleUnknownPayload(payload: Payload.Unknown) { diff --git a/api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/Payloads.kt b/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/Payloads.kt similarity index 95% rename from api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/Payloads.kt rename to core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/Payloads.kt index e65fa3b..446980b 100644 --- a/api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/Payloads.kt +++ b/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/Payloads.kt @@ -1,9 +1,9 @@ -package moe.lava.neon.api.gateway +package moe.lava.neon.core.api.gateway import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonElement -import moe.lava.neon.api.ApiConstants -import moe.lava.neon.api.objects.User +import moe.lava.neon.core.api.ApiConstants +import moe.lava.neon.core.api.structures.User sealed interface Payload { val op: Int diff --git a/api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/ResumeProperties.kt b/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/ResumeProperties.kt similarity index 55% rename from api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/ResumeProperties.kt rename to core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/ResumeProperties.kt index f962e3d..092497d 100644 --- a/api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/ResumeProperties.kt +++ b/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/ResumeProperties.kt @@ -1,6 +1,6 @@ -package moe.lava.neon.api.gateway +package moe.lava.neon.core.api.gateway -internal data class ResumeProperties( +data class ResumeProperties( val sessionId: String, val resumeGatewayUrl: String, val lastSequence: Int, diff --git a/api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/SerializingExtensions.kt b/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/SerializingExtensions.kt similarity index 83% rename from api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/SerializingExtensions.kt rename to core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/SerializingExtensions.kt index 78eab17..f95aaa3 100644 --- a/api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/SerializingExtensions.kt +++ b/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/SerializingExtensions.kt @@ -1,10 +1,12 @@ -package moe.lava.neon.api.gateway +package moe.lava.neon.core.api.gateway import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.decodeFromJsonElement -import moe.lava.neon.api.ApiConstants.json +import moe.lava.neon.core.api.ApiConstants -internal fun T.pack(): Payload.Outgoing { +private val json = ApiConstants.json + +fun T.pack(): Payload.Outgoing { val opcode: Int = when (this) { is Event.Heartbeat -> 1 is Event.Identify -> 2 @@ -14,7 +16,7 @@ internal fun T.pack(): Payload.Outgoing { return Payload.Outgoing(op = opcode, d = this) } -internal fun Payload.Unknown.asIncoming() : Payload.WithSequence { +fun Payload.Unknown.asIncoming() : Payload.WithSequence { return when (op) { 0 -> when (t) { "READY" -> decode() diff --git a/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/handlers/Handler.kt b/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/handlers/Handler.kt new file mode 100644 index 0000000..5f6a6ac --- /dev/null +++ b/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/handlers/Handler.kt @@ -0,0 +1,5 @@ +package moe.lava.neon.core.api.gateway.handlers + +import moe.lava.neon.core.api.gateway.Event + +sealed interface Handler diff --git a/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/handlers/ReadyHandler.kt b/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/handlers/ReadyHandler.kt new file mode 100644 index 0000000..f9b5afc --- /dev/null +++ b/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/handlers/ReadyHandler.kt @@ -0,0 +1,20 @@ +package moe.lava.neon.core.api.gateway.handlers + +import co.touchlab.kermit.Logger +import dev.zacsweers.metro.Inject +import moe.lava.neon.core.api.gateway.Event +import moe.lava.neon.core.api.gateway.ResumeProperties + +private val logger = Logger.withTag("neon.core.api.events/ready") + +@Inject +class ReadyHandler : Handler { + fun handle(event: Event.Ready, updateResumeProps: (ResumeProperties) -> Unit) { + logger.i { "Received payload $event" } + updateResumeProps(ResumeProperties( + sessionId = event.sessionId, + resumeGatewayUrl = event.resumeGatewayUrl, + lastSequence = 0, + )) + } +} diff --git a/api/shared/src/commonMain/kotlin/moe/lava/neon/api/objects/Snowflake.kt b/core/src/commonMain/kotlin/moe/lava/neon/core/api/structures/Snowflake.kt similarity index 80% rename from api/shared/src/commonMain/kotlin/moe/lava/neon/api/objects/Snowflake.kt rename to core/src/commonMain/kotlin/moe/lava/neon/core/api/structures/Snowflake.kt index 3172955..4d8dec5 100644 --- a/api/shared/src/commonMain/kotlin/moe/lava/neon/api/objects/Snowflake.kt +++ b/core/src/commonMain/kotlin/moe/lava/neon/core/api/structures/Snowflake.kt @@ -1,4 +1,4 @@ -package moe.lava.neon.api.objects +package moe.lava.neon.core.api.structures import kotlinx.serialization.Serializable import kotlinx.serialization.builtins.LongAsStringSerializer diff --git a/api/shared/src/commonMain/kotlin/moe/lava/neon/api/objects/User.kt b/core/src/commonMain/kotlin/moe/lava/neon/core/api/structures/User.kt similarity index 78% rename from api/shared/src/commonMain/kotlin/moe/lava/neon/api/objects/User.kt rename to core/src/commonMain/kotlin/moe/lava/neon/core/api/structures/User.kt index 3168445..eb78a93 100644 --- a/api/shared/src/commonMain/kotlin/moe/lava/neon/api/objects/User.kt +++ b/core/src/commonMain/kotlin/moe/lava/neon/core/api/structures/User.kt @@ -1,4 +1,4 @@ -package moe.lava.neon.api.objects +package moe.lava.neon.core.api.structures import kotlinx.serialization.Serializable diff --git a/core/src/commonMain/kotlin/moe/lava/neon/core/di/AppGraph.kt b/core/src/commonMain/kotlin/moe/lava/neon/core/di/AppGraph.kt new file mode 100644 index 0000000..1b38b5a --- /dev/null +++ b/core/src/commonMain/kotlin/moe/lava/neon/core/di/AppGraph.kt @@ -0,0 +1,18 @@ +package moe.lava.neon.core.di + +import dev.zacsweers.metro.GraphExtension +import moe.lava.neon.core.AppSettings +import moe.lava.neon.core.api.ApiClient +import moe.lava.neon.core.repository.AuthRepository +import moe.lava.neon.core.repository.UserRepository + +@GraphExtension +interface AppGraph { + val api: ApiClient + val settings: AppSettings + + val auth: AuthRepository + val users: UserRepository + + val gatewayHandlers: EventHandlerGraph +} diff --git a/core/src/commonMain/kotlin/moe/lava/neon/core/di/CoreModule.kt b/core/src/commonMain/kotlin/moe/lava/neon/core/di/CoreModule.kt deleted file mode 100644 index 600b7f2..0000000 --- a/core/src/commonMain/kotlin/moe/lava/neon/core/di/CoreModule.kt +++ /dev/null @@ -1,23 +0,0 @@ -package moe.lava.neon.core.di - -import moe.lava.neon.api.ApiClient -import moe.lava.neon.api.gateway.GatewayHandler -import moe.lava.neon.core.AppSettings -import moe.lava.neon.core.repository.AuthRepository -import moe.lava.neon.core.repository.CaptchaRepository -import moe.lava.neon.core.repository.GatewayRepository -import moe.lava.neon.core.repository.UserRepository -import org.koin.dsl.module -import org.koin.plugin.module.dsl.single - -val coreModule = module { - factory { ApiClient() } - single() - - single() - single() - single() - single() - - single() -} diff --git a/core/src/commonMain/kotlin/moe/lava/neon/core/di/EventHandlerGraph.kt b/core/src/commonMain/kotlin/moe/lava/neon/core/di/EventHandlerGraph.kt new file mode 100644 index 0000000..dfa03dd --- /dev/null +++ b/core/src/commonMain/kotlin/moe/lava/neon/core/di/EventHandlerGraph.kt @@ -0,0 +1,12 @@ +package moe.lava.neon.core.di + +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.GraphExtension +import moe.lava.neon.core.api.gateway.handlers.ReadyHandler + +@GraphExtension +@ContributesTo(AppScope::class) +interface EventHandlerGraph { + val ready: ReadyHandler +} diff --git a/core/src/commonMain/kotlin/moe/lava/neon/core/repository/AuthRepository.kt b/core/src/commonMain/kotlin/moe/lava/neon/core/repository/AuthRepository.kt index 0137c4e..93bbae6 100644 --- a/core/src/commonMain/kotlin/moe/lava/neon/core/repository/AuthRepository.kt +++ b/core/src/commonMain/kotlin/moe/lava/neon/core/repository/AuthRepository.kt @@ -1,50 +1,86 @@ package moe.lava.neon.core.repository import co.touchlab.kermit.Logger -import moe.lava.neon.api.ApiClient -import moe.lava.neon.api.endpoints.getExperiments -import moe.lava.neon.api.endpoints.login +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.client.request.header +import io.ktor.client.request.parameter +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.ContentType +import io.ktor.http.contentType +import kotlinx.serialization.Serializable import moe.lava.neon.core.AppSettings +import moe.lava.neon.core.api.ApiClient + +@Serializable +private data class ExperimentResponse( + val fingerprint: String, +) + +@Serializable +private data class LoginRequest( + val login: String, + val password: String, + val undelete: Boolean = false, + val loginSource: String? = null, + val giftCodeSkuId: String? = null, +) + +@Serializable +private data class LoginResponse( + val userId: String, + val token: String, + val userSettings: UserSettings, +) { + @Serializable + data class UserSettings(val locale: String, val theme: String) +} sealed class AuthResponse { - // TODO: Specify all possible error types here - data class Failed(val error: Throwable) : AuthResponse() data class Success(val token: String) : AuthResponse() // TODO // data class MFARequested() : AuthResponse() } -class AuthRepository internal constructor( +@Inject +@SingleIn(AppScope::class) +class AuthRepository( private val settings: AppSettings, private val api: ApiClient, ) { private val logger = Logger.withTag("neon.core.repo/auth") - private var token by settings::token - private var fingerprint by settings::fingerprint + var token by settings::token + private set - val loggedIn get() = token != null + var fingerprint by settings::fingerprint + private set suspend fun login( email: String, password: String, ): AuthResponse { - try { - if (fingerprint == null) { - fingerprint = api.getExperiments().body().fingerprint - } - - val login = api.login( - email = email, - password = password, - fingerprint = fingerprint!!, - ).body() - logger.i { "Login success $login" } - this.token = login.token - - return AuthResponse.Success(login.token) - } catch (e: Throwable) { - return AuthResponse.Failed(e) + if (fingerprint == null) { + fingerprint = api.client.get("experiments") { + parameter("with_guild_experiments", "true") + }.body().fingerprint } + + val res = api.client.post("auth/login") { + header("X-Fingerprint", fingerprint) + contentType(ContentType.Application.Json) + setBody(LoginRequest( + login = email, + password = password, + )) + } + val body = res.body() + logger.i { "Login success $body" } + this.token = body.token + return AuthResponse.Success(body.token) } fun login(token: String): String { diff --git a/core/src/commonMain/kotlin/moe/lava/neon/core/repository/CaptchaRepository.kt b/core/src/commonMain/kotlin/moe/lava/neon/core/repository/CaptchaRepository.kt deleted file mode 100644 index 6e5911f..0000000 --- a/core/src/commonMain/kotlin/moe/lava/neon/core/repository/CaptchaRepository.kt +++ /dev/null @@ -1,11 +0,0 @@ -package moe.lava.neon.core.repository - -import moe.lava.neon.api.ApiClient -import moe.lava.neon.common.captcha.CaptchaRequest -import moe.lava.neon.common.captcha.CaptchaResponse - -class CaptchaRepository( - private val api: ApiClient, -) { - fun setHandler(handler: suspend (CaptchaRequest) -> CaptchaResponse) = api.setCaptchaHandler(handler) -} diff --git a/core/src/commonMain/kotlin/moe/lava/neon/core/repository/GatewayRepository.kt b/core/src/commonMain/kotlin/moe/lava/neon/core/repository/GatewayRepository.kt deleted file mode 100644 index 84f14f5..0000000 --- a/core/src/commonMain/kotlin/moe/lava/neon/core/repository/GatewayRepository.kt +++ /dev/null @@ -1,18 +0,0 @@ -package moe.lava.neon.core.repository - -import moe.lava.neon.api.gateway.GatewayHandler -import moe.lava.neon.core.AppSettings - -class GatewayRepository internal constructor( - private val gateway: GatewayHandler, - private val settings: AppSettings, -) { - suspend fun start(): Result = runCatching { - val token = settings.token - ?: throw IllegalArgumentException("Tried to start gateway with no token") - - gateway.connect(token) - } - - suspend fun pause() = runCatching { gateway.disconnect() } -} diff --git a/core/src/commonMain/kotlin/moe/lava/neon/core/repository/UserRepository.kt b/core/src/commonMain/kotlin/moe/lava/neon/core/repository/UserRepository.kt index 1392010..53ff0c6 100644 --- a/core/src/commonMain/kotlin/moe/lava/neon/core/repository/UserRepository.kt +++ b/core/src/commonMain/kotlin/moe/lava/neon/core/repository/UserRepository.kt @@ -1,4 +1,10 @@ package moe.lava.neon.core.repository +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn + +@Inject +@SingleIn(AppScope::class) class UserRepository { } diff --git a/core/src/jvmMain/kotlin/moe/lava/neon/core/Platform.jvm.kt b/core/src/jvmMain/kotlin/moe/lava/neon/core/Platform.jvm.kt new file mode 100644 index 0000000..509cf56 --- /dev/null +++ b/core/src/jvmMain/kotlin/moe/lava/neon/core/Platform.jvm.kt @@ -0,0 +1,7 @@ +package moe.lava.neon.core + +class JVMPlatform : Platform { + override val name: String = "Java ${System.getProperty("java.version")}" +} + +actual fun getPlatform(): Platform = JVMPlatform() diff --git a/api/shared/src/jvmMain/kotlin/moe/lava/neon/api/ApiConstants.jvm.kt b/core/src/jvmMain/kotlin/moe/lava/neon/core/api/ApiConstants.jvm.kt similarity index 85% rename from api/shared/src/jvmMain/kotlin/moe/lava/neon/api/ApiConstants.jvm.kt rename to core/src/jvmMain/kotlin/moe/lava/neon/core/api/ApiConstants.jvm.kt index 70ff314..4e2e603 100644 --- a/api/shared/src/jvmMain/kotlin/moe/lava/neon/api/ApiConstants.jvm.kt +++ b/core/src/jvmMain/kotlin/moe/lava/neon/core/api/ApiConstants.jvm.kt @@ -1,8 +1,7 @@ -package moe.lava.neon.api +package moe.lava.neon.core.api import java.util.Locale -// TODO @Suppress("ConstantLocale") internal actual val platformSuperProps = PlatformProps( device = "", diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 189f3c9..9e1d141 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] #noinspection AndroidGradlePluginVersion -agp = "9.0.0" +agp = "8.13.2" android-compileSdk = "36" android-minSdk = "24" android-targetSdk = "36" @@ -19,16 +19,12 @@ desugar = "2.1.5" hcaptcha = "4.4.0" junit = "4.13.2" kermit = "2.0.8" -koin-bom = "4.2.0-RC1" -koin-plugin = "0.3.0" -kotest = "6.1.2" kotlin = "2.3.0" kotlinx-coroutines = "1.10.2" -kotlinx-serialization = "1.10.0" -ksp = "2.3.4" ktor = "3.4.0" material3 = "1.11.0-alpha02" material3-adaptive = "1.3.0-alpha04" +metro = "0.10.2" settings = "1.3.0" sqldelight = "2.2.1" @@ -58,38 +54,24 @@ desugar = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desuga hcaptcha-compose = { module = "com.github.hCaptcha.hcaptcha-android-sdk:compose-sdk", version.ref = "hcaptcha" } junit = { module = "junit:junit", version.ref = "junit" } kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } -koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin-bom" } -koin-compose = { module = "io.insert-koin:koin-compose" } -koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel" } -koin-compose-navigation3 = { module = "io.insert-koin:koin-compose-navigation3" } -koin-core = { module = "io.insert-koin:koin-core" } -koin-test = { module = "io.insert-koin:koin-test" } -kotest-assertions = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" } -kotest-framework = { module = "io.kotest:kotest-framework-engine", version.ref = "kotest" } -kotest-property = { module = "io.kotest:kotest-property", version.ref = "kotest" } -kotest-runner-junit5 = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } -kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" } -kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } -ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } ktor-client-websockets = { module = "io.ktor:ktor-client-websockets", version.ref = "ktor" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } +metrox-viewmodel-compose = { module = "dev.zacsweers.metro:metrox-viewmodel-compose", version.ref = "metro" } settings = { module = "com.russhwolf:multiplatform-settings-no-arg", version.ref = "settings" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } -androidMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" } +androidLibrary = { id = "com.android.library", version.ref = "agp" } composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } composeHotReload = { id = "org.jetbrains.compose.hot-reload", version.ref = "compose-hot-reload" } composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" } -koinCompiler = { id = "io.insert-koin.compiler.plugin", version.ref = "koin-plugin" } -kotest = { id = "io.kotest", version.ref = "kotest" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } -ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +metro = { id = "dev.zacsweers.metro", version.ref = "metro" } sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2e11132..d4081da 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/settings.gradle.kts b/settings.gradle.kts index 2b18bad..e405915 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -33,10 +33,5 @@ plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" } -include(":android") -include(":api:gateway") -include(":api:rest") -include(":api:shared") -include(":common") include(":core") include(":ui") diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts index dfd7b9b..ed73203 100644 --- a/ui/build.gradle.kts +++ b/ui/build.gradle.kts @@ -4,29 +4,22 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.kotlinSerialization) - alias(libs.plugins.androidMultiplatformLibrary) + alias(libs.plugins.androidApplication) alias(libs.plugins.composeMultiplatform) alias(libs.plugins.composeCompiler) alias(libs.plugins.composeHotReload) - alias(libs.plugins.koinCompiler) + alias(libs.plugins.metro) } kotlin { - androidLibrary { - namespace = "moe.lava.neon.ui" - compileSdk = libs.versions.android.compileSdk.get().toInt() - + androidTarget { compilerOptions { jvmTarget.set(JvmTarget.JVM_11) } - - androidResources { - enable = true - } } - + jvm() - + sourceSets { androidMain.dependencies { implementation(libs.compose.ui.tooling.preview) @@ -38,7 +31,6 @@ kotlin { implementation(libs.ktor.client.okhttp) } commonMain.dependencies { - implementation(project(":common")) implementation(project(":core")) implementation(libs.compose.components.resources) implementation(libs.compose.foundation) @@ -62,10 +54,8 @@ kotlin { implementation(libs.kermit) - implementation(project.dependencies.platform(libs.koin.bom)) - implementation(libs.koin.compose) - implementation(libs.koin.compose.viewmodel) - implementation(libs.koin.compose.navigation3) + implementation(libs.metrox.viewmodel.compose) + } commonTest.dependencies { implementation(libs.kotlin.test) @@ -79,8 +69,38 @@ kotlin { } } +android { + namespace = "moe.lava.neon" + compileSdk = libs.versions.android.compileSdk.get().toInt() + + defaultConfig { + applicationId = "moe.lava.neon" + minSdk = libs.versions.android.minSdk.get().toInt() + targetSdk = libs.versions.android.targetSdk.get().toInt() + versionCode = 1 + versionName = "1.0" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } + buildTypes { + getByName("release") { + isMinifyEnabled = false + signingConfig = signingConfigs.getByName("debug") + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + isCoreLibraryDesugaringEnabled = true + } +} + dependencies { - androidRuntimeClasspath(libs.compose.ui.tooling) + debugImplementation(libs.compose.ui.tooling) + coreLibraryDesugaring(libs.desugar) } compose.desktop { diff --git a/android/src/main/AndroidManifest.xml b/ui/src/androidMain/AndroidManifest.xml similarity index 100% rename from android/src/main/AndroidManifest.xml rename to ui/src/androidMain/AndroidManifest.xml diff --git a/android/src/main/kotlin/moe/lava/neon/MainActivity.kt b/ui/src/androidMain/kotlin/moe/lava/neon/MainActivity.kt similarity index 65% rename from android/src/main/kotlin/moe/lava/neon/MainActivity.kt rename to ui/src/androidMain/kotlin/moe/lava/neon/MainActivity.kt index cc4947f..45b77df 100644 --- a/android/src/main/kotlin/moe/lava/neon/MainActivity.kt +++ b/ui/src/androidMain/kotlin/moe/lava/neon/MainActivity.kt @@ -4,23 +4,23 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview import moe.lava.neon.ui.App -import moe.lava.neon.ui.di.initKoin -import org.koin.android.ext.koin.androidContext -import org.koin.android.ext.koin.androidLogger class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) - initKoin { - androidContext(this@MainActivity) - androidLogger() - } - setContent { App() } } } + +@Preview +@Composable +fun AppAndroidPreview() { + App() +} diff --git a/ui/src/androidMain/kotlin/moe/lava/neon/ui/CaptchaHandler.android.kt b/ui/src/androidMain/kotlin/moe/lava/neon/ui/CaptchaBinder.android.kt similarity index 96% rename from ui/src/androidMain/kotlin/moe/lava/neon/ui/CaptchaHandler.android.kt rename to ui/src/androidMain/kotlin/moe/lava/neon/ui/CaptchaBinder.android.kt index 185be4a..6baa380 100644 --- a/ui/src/androidMain/kotlin/moe/lava/neon/ui/CaptchaHandler.android.kt +++ b/ui/src/androidMain/kotlin/moe/lava/neon/ui/CaptchaBinder.android.kt @@ -19,8 +19,8 @@ import com.hcaptcha.sdk.HCaptchaVerifyParams import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import moe.lava.neon.common.captcha.CaptchaRequest -import moe.lava.neon.common.captcha.CaptchaResponse +import moe.lava.neon.core.api.ApiClient +import moe.lava.neon.core.api.captcha.CaptchaResponse private val logger = Logger.withTag("neon.ui.app/captcha") @@ -31,7 +31,7 @@ private const val EXTRA_RESULT_TOKEN = "extra_result_token" private const val EXTRA_RESULT_ERROR = "extra_result_error" @Composable -actual fun getCaptchaHandler(): suspend (CaptchaRequest) -> CaptchaResponse { +actual fun CaptchaBinder(api: ApiClient) { val context = LocalContext.current val queue = MutableSharedFlow>() val scope = rememberCoroutineScope() @@ -67,7 +67,7 @@ actual fun getCaptchaHandler(): suspend (CaptchaRequest) -> CaptchaResponse { } } - return { captcha -> + api.setCaptchaHandler { captcha -> val intent = Intent(context, HCaptchaActivity::class.java).apply { putExtra(EXTRA_SITE_KEY, captcha.captchaSitekey) putExtra(EXTRA_RQ_DATA, captcha.captchaRqdata) diff --git a/android/src/main/res/drawable-v24/ic_launcher_foreground.xml b/ui/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml similarity index 100% rename from android/src/main/res/drawable-v24/ic_launcher_foreground.xml rename to ui/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml diff --git a/android/src/main/res/drawable/ic_launcher_background.xml b/ui/src/androidMain/res/drawable/ic_launcher_background.xml similarity index 100% rename from android/src/main/res/drawable/ic_launcher_background.xml rename to ui/src/androidMain/res/drawable/ic_launcher_background.xml diff --git a/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/ui/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 100% rename from android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to ui/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml diff --git a/android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/ui/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml similarity index 100% rename from android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml rename to ui/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml diff --git a/android/src/main/res/mipmap-hdpi/ic_launcher.png b/ui/src/androidMain/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from android/src/main/res/mipmap-hdpi/ic_launcher.png rename to ui/src/androidMain/res/mipmap-hdpi/ic_launcher.png diff --git a/android/src/main/res/mipmap-hdpi/ic_launcher_round.png b/ui/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png similarity index 100% rename from android/src/main/res/mipmap-hdpi/ic_launcher_round.png rename to ui/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png diff --git a/android/src/main/res/mipmap-mdpi/ic_launcher.png b/ui/src/androidMain/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from android/src/main/res/mipmap-mdpi/ic_launcher.png rename to ui/src/androidMain/res/mipmap-mdpi/ic_launcher.png diff --git a/android/src/main/res/mipmap-mdpi/ic_launcher_round.png b/ui/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png similarity index 100% rename from android/src/main/res/mipmap-mdpi/ic_launcher_round.png rename to ui/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png diff --git a/android/src/main/res/mipmap-xhdpi/ic_launcher.png b/ui/src/androidMain/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from android/src/main/res/mipmap-xhdpi/ic_launcher.png rename to ui/src/androidMain/res/mipmap-xhdpi/ic_launcher.png diff --git a/android/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/ui/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png similarity index 100% rename from android/src/main/res/mipmap-xhdpi/ic_launcher_round.png rename to ui/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png diff --git a/android/src/main/res/mipmap-xxhdpi/ic_launcher.png b/ui/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from android/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to ui/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png diff --git a/android/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/ui/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png similarity index 100% rename from android/src/main/res/mipmap-xxhdpi/ic_launcher_round.png rename to ui/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png diff --git a/android/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/ui/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from android/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to ui/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/ui/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png similarity index 100% rename from android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png rename to ui/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/android/src/main/res/values/strings.xml b/ui/src/androidMain/res/values/strings.xml similarity index 100% rename from android/src/main/res/values/strings.xml rename to ui/src/androidMain/res/values/strings.xml diff --git a/ui/src/commonMain/kotlin/moe/lava/neon/ui/App.kt b/ui/src/commonMain/kotlin/moe/lava/neon/ui/App.kt index cc2f999..a24fac4 100644 --- a/ui/src/commonMain/kotlin/moe/lava/neon/ui/App.kt +++ b/ui/src/commonMain/kotlin/moe/lava/neon/ui/App.kt @@ -5,6 +5,7 @@ import androidx.compose.material3.MaterialExpressiveTheme import androidx.compose.material3.MotionScheme import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider @@ -13,11 +14,12 @@ import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator import androidx.navigation3.ui.NavDisplay import androidx.savedstate.serialization.SavedStateConfiguration import co.touchlab.kermit.Logger +import dev.zacsweers.metro.createGraph +import dev.zacsweers.metrox.viewmodel.LocalMetroViewModelFactory import kotlinx.serialization.Serializable import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.polymorphic -import moe.lava.neon.core.repository.AuthRepository -import moe.lava.neon.core.repository.CaptchaRepository +import moe.lava.neon.ui.di.AppUiGraph import moe.lava.neon.ui.screens.Login import moe.lava.neon.ui.screens.Sample import moe.lava.neon.ui.screens.chat.Chat @@ -27,7 +29,6 @@ import moe.lava.neon.ui.screens.navigator.NavigatorModel import moe.lava.neon.ui.screens.navigator.NavigatorPreviewProvider import moe.lava.neon.ui.util.ThreePaneSceneStrategy import moe.lava.neon.ui.util.rememberThreePaneSceneStrategy -import org.koin.compose.koinInject import kotlin.system.exitProcess object Route { @@ -67,81 +68,81 @@ fun App() { exitProcess(1) } - val auth: AuthRepository = koinInject() - val captcha: CaptchaRepository = koinInject() - captcha.setHandler(getCaptchaHandler()) - - MaterialExpressiveTheme( - colorScheme = getColorScheme(), - motionScheme = MotionScheme.expressive(), - ) { - val init = if (auth.loggedIn) Route.Sample else Route.Login -// val backStack = rememberNavBackStack(config, init) - val backStack = rememberNavBackStack(config, Route.Sample) - val threePaneStrategy = rememberThreePaneSceneStrategy() - NavDisplay( - backStack = backStack, - entryDecorators = listOf( - rememberSaveableStateHolderNavEntryDecorator(), - rememberViewModelStoreNavEntryDecorator(), - ), - onBack = { backStack.removeLastOrNull() }, - sceneStrategy = threePaneStrategy, - entryProvider = entryProvider { - entry { - Login( - onSuccess = { - backStack.clear() - backStack.add(Route.Sample) - } - ) - } - entry { - Sample( - navTest = { - backStack.add(Route.Navigator(it)) - backStack.add(Route.Chat) - backStack.add(Route.MembersList) - }, - onRequestLogout = { - backStack.clear() - backStack.add(Route.Login) - } - ) - } - - entry( - metadata = ThreePaneSceneStrategy.listPane() - ) { key -> - if (key.left) { - Navigator( - NavigatorPreviewProvider.base2.copy( - guildNavPosition = NavigatorModel.GuildNavPosition.LeftSidebar - ) - ) - } else { - Navigator( - NavigatorPreviewProvider.base2.copy( - guildNavPosition = NavigatorModel.GuildNavPosition.BottomSheet - ) + val uiGraph = createGraph() + val graph = uiGraph.core + CaptchaBinder(graph.api) + CompositionLocalProvider(LocalMetroViewModelFactory provides uiGraph.metroViewModelFactory) { + MaterialExpressiveTheme( + colorScheme = getColorScheme(), + motionScheme = MotionScheme.expressive(), + ) { + val init = if (graph.auth.token != null) Route.Sample else Route.Login + val backStack = rememberNavBackStack(config, Route.Sample) + val threePaneStrategy = rememberThreePaneSceneStrategy() + NavDisplay( + backStack = backStack, + entryDecorators = listOf( + rememberSaveableStateHolderNavEntryDecorator(), + rememberViewModelStoreNavEntryDecorator(), + ), + onBack = { backStack.removeLastOrNull() }, + sceneStrategy = threePaneStrategy, + entryProvider = entryProvider { + entry { + Login( + onSuccess = { + backStack.clear() + backStack.add(Route.Sample) + } + ) + } + entry { + Sample( + navTest = { + backStack.add(Route.Navigator(it)) + backStack.add(Route.Chat) + backStack.add(Route.MembersList) + }, + onRequestLogout = { + backStack.clear() + backStack.add(Route.Login) + } ) } - } - entry( - metadata = ThreePaneSceneStrategy.detailPane() - ) { - Chat( - onOpenMembers = { backStack.add(Route.MembersList) } - ) - } + entry( + metadata = ThreePaneSceneStrategy.listPane() + ) { key -> + if (key.left) { + Navigator( + NavigatorPreviewProvider.base2.copy( + guildNavPosition = NavigatorModel.GuildNavPosition.LeftSidebar + ) + ) + } else { + Navigator( + NavigatorPreviewProvider.base2.copy( + guildNavPosition = NavigatorModel.GuildNavPosition.BottomSheet + ) + ) + } + } - entry( - metadata = ThreePaneSceneStrategy.extraPane() - ) { - MembersList() + entry( + metadata = ThreePaneSceneStrategy.detailPane() + ) { + Chat( + onOpenMembers = { backStack.add(Route.MembersList) } + ) + } + + entry( + metadata = ThreePaneSceneStrategy.extraPane() + ) { + MembersList() + } } - } - ) + ) + } } } diff --git a/ui/src/commonMain/kotlin/moe/lava/neon/ui/CaptchaBinder.kt b/ui/src/commonMain/kotlin/moe/lava/neon/ui/CaptchaBinder.kt new file mode 100644 index 0000000..f8e6966 --- /dev/null +++ b/ui/src/commonMain/kotlin/moe/lava/neon/ui/CaptchaBinder.kt @@ -0,0 +1,7 @@ +package moe.lava.neon.ui + +import androidx.compose.runtime.Composable +import moe.lava.neon.core.api.ApiClient + +@Composable +expect fun CaptchaBinder(api: ApiClient) diff --git a/ui/src/commonMain/kotlin/moe/lava/neon/ui/CaptchaHandler.kt b/ui/src/commonMain/kotlin/moe/lava/neon/ui/CaptchaHandler.kt deleted file mode 100644 index 70b32bb..0000000 --- a/ui/src/commonMain/kotlin/moe/lava/neon/ui/CaptchaHandler.kt +++ /dev/null @@ -1,8 +0,0 @@ -package moe.lava.neon.ui - -import androidx.compose.runtime.Composable -import moe.lava.neon.common.captcha.CaptchaRequest -import moe.lava.neon.common.captcha.CaptchaResponse - -@Composable -expect fun getCaptchaHandler(): suspend (CaptchaRequest) -> CaptchaResponse diff --git a/ui/src/commonMain/kotlin/moe/lava/neon/ui/Greeting.kt b/ui/src/commonMain/kotlin/moe/lava/neon/ui/Greeting.kt new file mode 100644 index 0000000..63612da --- /dev/null +++ b/ui/src/commonMain/kotlin/moe/lava/neon/ui/Greeting.kt @@ -0,0 +1,11 @@ +package moe.lava.neon.ui + +import moe.lava.neon.core.getPlatform + +class Greeting { + private val platform = getPlatform() + + fun greet(): String { + return "Hello, ${platform.name}!" + } +} diff --git a/ui/src/commonMain/kotlin/moe/lava/neon/ui/di/AppUiGraph.kt b/ui/src/commonMain/kotlin/moe/lava/neon/ui/di/AppUiGraph.kt new file mode 100644 index 0000000..344555a --- /dev/null +++ b/ui/src/commonMain/kotlin/moe/lava/neon/ui/di/AppUiGraph.kt @@ -0,0 +1,29 @@ +package moe.lava.neon.ui.di + +import androidx.lifecycle.ViewModel +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.DependencyGraph +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.Provider +import dev.zacsweers.metro.SingleIn +import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactory +import dev.zacsweers.metrox.viewmodel.MetroViewModelFactory +import dev.zacsweers.metrox.viewmodel.ViewModelAssistedFactory +import dev.zacsweers.metrox.viewmodel.ViewModelGraph +import moe.lava.neon.core.di.AppGraph +import kotlin.reflect.KClass + +@DependencyGraph(AppScope::class) +interface AppUiGraph : ViewModelGraph { + val core: AppGraph +} + +@Inject +@ContributesBinding(AppScope::class) +@SingleIn(AppScope::class) +class AppViewModelFactory( + override val viewModelProviders: Map, Provider>, + override val assistedFactoryProviders: Map, Provider>, + override val manualAssistedFactoryProviders: Map, Provider>, +) : MetroViewModelFactory() diff --git a/ui/src/commonMain/kotlin/moe/lava/neon/ui/di/KoinInit.kt b/ui/src/commonMain/kotlin/moe/lava/neon/ui/di/KoinInit.kt deleted file mode 100644 index ca4a856..0000000 --- a/ui/src/commonMain/kotlin/moe/lava/neon/ui/di/KoinInit.kt +++ /dev/null @@ -1,13 +0,0 @@ -package moe.lava.neon.ui.di - -import org.koin.core.KoinApplication -import org.koin.core.context.startKoin -import org.koin.dsl.KoinAppDeclaration -import org.koin.dsl.includes - -fun initKoin(config: KoinAppDeclaration? = null): KoinApplication { - return startKoin { - includes(config) - modules(uiModule) - } -} diff --git a/ui/src/commonMain/kotlin/moe/lava/neon/ui/di/UiModule.kt b/ui/src/commonMain/kotlin/moe/lava/neon/ui/di/UiModule.kt deleted file mode 100644 index fd25012..0000000 --- a/ui/src/commonMain/kotlin/moe/lava/neon/ui/di/UiModule.kt +++ /dev/null @@ -1,13 +0,0 @@ -package moe.lava.neon.ui.di - -import moe.lava.neon.core.di.coreModule -import moe.lava.neon.ui.screens.LoginViewModel -import moe.lava.neon.ui.screens.SampleViewModel -import org.koin.dsl.module -import org.koin.plugin.module.dsl.viewModel - -val uiModule = module { - includes(coreModule) - viewModel() - viewModel() -} diff --git a/ui/src/commonMain/kotlin/moe/lava/neon/ui/screens/Login.kt b/ui/src/commonMain/kotlin/moe/lava/neon/ui/screens/Login.kt index 5e0078d..8ef0335 100644 --- a/ui/src/commonMain/kotlin/moe/lava/neon/ui/screens/Login.kt +++ b/ui/src/commonMain/kotlin/moe/lava/neon/ui/screens/Login.kt @@ -23,6 +23,11 @@ import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel import co.touchlab.kermit.Logger +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesIntoMap +import dev.zacsweers.metro.Inject +import dev.zacsweers.metrox.viewmodel.ViewModelKey +import dev.zacsweers.metrox.viewmodel.metroViewModel import kotlinx.coroutines.launch import moe.lava.neon.core.repository.AuthRepository import moe.lava.neon.core.repository.AuthResponse @@ -30,13 +35,12 @@ import moe.lava.neon.resources.Res import moe.lava.neon.resources.visibility import moe.lava.neon.resources.visibility_off import org.jetbrains.compose.resources.painterResource -import org.koin.compose.viewmodel.koinViewModel @Composable fun Login( onSuccess: () -> Unit, ) { - val viewModel: LoginViewModel = koinViewModel() + val viewModel: LoginViewModel = metroViewModel() val scope = rememberCoroutineScope() Column( @@ -109,6 +113,9 @@ fun Login( } } +@Inject +@ViewModelKey(LoginViewModel::class) +@ContributesIntoMap(AppScope::class) class LoginViewModel( private val auth: AuthRepository ) : ViewModel() { @@ -122,13 +129,13 @@ class LoginViewModel( } suspend fun login(email: String, password: String): LoginResult { - return when (val res = auth.login(email, password)) { - is AuthResponse.Success -> LoginResult.Success - is AuthResponse.Failed -> { - val e = res.error - logger.e(e) { "Login failed" } - LoginResult.Failed(e.toString()) + return try { + when (val res = auth.login(email, password)) { + is AuthResponse.Success -> LoginResult.Success } + } catch(e: Throwable) { + logger.e(e) { "Login failed" } + LoginResult.Failed(e.toString()) } } } diff --git a/ui/src/commonMain/kotlin/moe/lava/neon/ui/screens/Sample.kt b/ui/src/commonMain/kotlin/moe/lava/neon/ui/screens/Sample.kt index 73467fc..2a5531f 100644 --- a/ui/src/commonMain/kotlin/moe/lava/neon/ui/screens/Sample.kt +++ b/ui/src/commonMain/kotlin/moe/lava/neon/ui/screens/Sample.kt @@ -20,20 +20,25 @@ import androidx.compose.ui.Modifier import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import co.touchlab.kermit.Logger +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesIntoMap +import dev.zacsweers.metro.Inject +import dev.zacsweers.metrox.viewmodel.ViewModelKey +import dev.zacsweers.metrox.viewmodel.metroViewModel import kotlinx.coroutines.launch +import moe.lava.neon.core.api.gateway.GatewayHandler import moe.lava.neon.core.repository.AuthRepository -import moe.lava.neon.core.repository.GatewayRepository import moe.lava.neon.resources.Res import moe.lava.neon.resources.compose_multiplatform +import moe.lava.neon.ui.Greeting import org.jetbrains.compose.resources.painterResource -import org.koin.compose.viewmodel.koinViewModel @Composable fun Sample( navTest: (Boolean) -> Unit, onRequestLogout: () -> Unit, ) { - val viewModel: SampleViewModel = koinViewModel() + val viewModel: SampleViewModel = metroViewModel() var showContent by remember { mutableStateOf(false) } Column( modifier = Modifier @@ -52,12 +57,14 @@ fun Sample( Text("Click me (bottom!") } AnimatedVisibility(showContent) { + val greeting = remember { Greeting().greet() } Column( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, ) { Image(painterResource(Res.drawable.compose_multiplatform), null) - Text("Logged in: ${viewModel.loggedIn}") + Text("Compose: $greeting") + Text("Passed token: ${viewModel.token?.slice(0..10)}...") } } Button(onClick = { @@ -79,26 +86,31 @@ fun Sample( } } +@Inject +@ViewModelKey(SampleViewModel::class) +@ContributesIntoMap(AppScope::class) class SampleViewModel( private val auth: AuthRepository, - private val gateway: GatewayRepository, + private val gateway: GatewayHandler, ) : ViewModel() { private val logger = Logger.withTag("neon.ui.screens/Sample") - val loggedIn by auth::loggedIn + val token get() = auth.token fun connect() { viewModelScope.launch { - val exception = gateway.start().exceptionOrNull() - if (exception != null) { - logger.e(exception) { "Failed to connect to gateway: ${exception.stackTraceToString()}" } + try { + gateway.connect() + } catch(e: Throwable) { + logger.e(e) { "Failed to connect to gateway: ${e.stackTraceToString()}" } } } } fun disconnect() { viewModelScope.launch { - val exception = gateway.pause().exceptionOrNull() - if (exception != null) { - logger.e(exception) { "Failed to disconnect from gateway: ${exception.stackTraceToString()}" } + try { + gateway.disconnect() + } catch(e: Throwable) { + logger.e(e) { "Failed to connect to gateway: ${e.stackTraceToString()}" } } } } diff --git a/ui/src/jvmMain/kotlin/moe/lava/neon/main.kt b/ui/src/jvmMain/kotlin/moe/lava/neon/main.kt index 9c3c679..0db2ea1 100644 --- a/ui/src/jvmMain/kotlin/moe/lava/neon/main.kt +++ b/ui/src/jvmMain/kotlin/moe/lava/neon/main.kt @@ -6,16 +6,12 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import moe.lava.neon.ui.App -import moe.lava.neon.ui.di.initKoin // The UI is designed with touchscreens in mind; on desktop elements may look gigantic // So scale them down a bit const val scaleFactor = 0.75f fun main() = application { - initKoin { - printLogger() - } Window( onCloseRequest = ::exitApplication, title = "Neon", diff --git a/ui/src/jvmMain/kotlin/moe/lava/neon/ui/CaptchaBinder.jvm.kt b/ui/src/jvmMain/kotlin/moe/lava/neon/ui/CaptchaBinder.jvm.kt new file mode 100644 index 0000000..397f534 --- /dev/null +++ b/ui/src/jvmMain/kotlin/moe/lava/neon/ui/CaptchaBinder.jvm.kt @@ -0,0 +1,13 @@ +package moe.lava.neon.ui + +import androidx.compose.runtime.Composable +import moe.lava.neon.core.api.ApiClient +import moe.lava.neon.core.api.captcha.CaptchaResponse + +@Composable +// TODO +actual fun CaptchaBinder(api: ApiClient) { + api.setCaptchaHandler { + CaptchaResponse.Failed(NotImplementedError()) + } +} diff --git a/ui/src/jvmMain/kotlin/moe/lava/neon/ui/CaptchaHandler.jvm.kt b/ui/src/jvmMain/kotlin/moe/lava/neon/ui/CaptchaHandler.jvm.kt deleted file mode 100644 index 56044e0..0000000 --- a/ui/src/jvmMain/kotlin/moe/lava/neon/ui/CaptchaHandler.jvm.kt +++ /dev/null @@ -1,13 +0,0 @@ -package moe.lava.neon.ui - -import androidx.compose.runtime.Composable -import moe.lava.neon.common.captcha.CaptchaRequest -import moe.lava.neon.common.captcha.CaptchaResponse - -@Composable -// TODO -actual fun getCaptchaHandler(): suspend (CaptchaRequest) -> CaptchaResponse { - return { - CaptchaResponse.Failed(NotImplementedError()) - } -} diff --git a/android/src/main/res/values/styles.xml b/ui/src/main/res/values/styles.xml similarity index 100% rename from android/src/main/res/values/styles.xml rename to ui/src/main/res/values/styles.xml