diff --git a/api/gateway/build.gradle.kts b/api/gateway/build.gradle.kts new file mode 100644 index 0000000..101843d --- /dev/null +++ b/api/gateway/build.gradle.kts @@ -0,0 +1,65 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.androidLibrary) + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.kotlinSerialization) +} + +kotlin { + jvm() + androidTarget { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } + + 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) + } + } +} + +dependencies { + coreLibraryDesugaring(libs.desugar) +} + +android { + namespace = "moe.lava.neon.api.gateway" + 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/commonMain/kotlin/moe/lava/neon/core/api/gateway/Capability.kt b/api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/Capability.kt similarity index 97% rename from core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/Capability.kt rename to api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/Capability.kt index 76347ee..ced4e64 100644 --- a/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/Capability.kt +++ b/api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/Capability.kt @@ -1,4 +1,4 @@ -package moe.lava.neon.core.api.gateway +package moe.lava.neon.api.gateway @Suppress("unused") object Capability { diff --git a/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/GatewayCloseReason.kt b/api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/GatewayCloseReason.kt similarity index 95% rename from core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/GatewayCloseReason.kt rename to api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/GatewayCloseReason.kt index 2e1a826..396752d 100644 --- a/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/GatewayCloseReason.kt +++ b/api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/GatewayCloseReason.kt @@ -1,4 +1,4 @@ -package moe.lava.neon.core.api.gateway +package moe.lava.neon.api.gateway import io.ktor.websocket.CloseReason diff --git a/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/GatewayHandler.kt b/api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/GatewayHandler.kt similarity index 83% rename from core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/GatewayHandler.kt rename to api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/GatewayHandler.kt index 9126635..1d1d1bc 100644 --- a/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/GatewayHandler.kt +++ b/api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/GatewayHandler.kt @@ -1,4 +1,4 @@ -package moe.lava.neon.core.api.gateway +package moe.lava.neon.api.gateway import co.touchlab.kermit.Logger import kotlinx.coroutines.CoroutineScope @@ -6,15 +6,15 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.serialization.ExperimentalSerializationApi -import moe.lava.neon.core.api.gateway.handlers.EventHandlers -import moe.lava.neon.core.repository.AuthRepository +import moe.lava.neon.api.gateway.handlers.Handler import kotlin.math.pow +import kotlin.reflect.KClass import kotlin.time.Duration.Companion.seconds -class GatewayHandler( - private val auth: AuthRepository, - private val eventHandlers: EventHandlers, -) { +typealias EventHandlers = Map, MutableList>> + +class GatewayHandler { + private val eventHandlers: EventHandlers = mutableMapOf() private val logger = Logger.withTag("neon.core.api.gateway/handler") private val scope = CoroutineScope(Dispatchers.IO) private var session: GatewaySession? = null @@ -23,13 +23,11 @@ class GatewayHandler( private var retryAttempts: Int = 0 @OptIn(ExperimentalSerializationApi::class) - suspend fun connect() { + suspend fun connect(token: String) { 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, @@ -59,7 +57,7 @@ class GatewayHandler( logger.d { "Reconnecting in ${dur.inWholeMilliseconds}ms" } delay(dur) retryAttempts += 1 - res = runCatching { connect() } + res = runCatching { connect(token) } res.exceptionOrNull()?.let { logger.e(it) { "Reconnect failed" } } diff --git a/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/GatewaySession.kt b/api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/GatewaySession.kt similarity index 91% rename from core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/GatewaySession.kt rename to api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/GatewaySession.kt index c2fc7b7..f0c83a0 100644 --- a/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/GatewaySession.kt +++ b/api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/GatewaySession.kt @@ -1,4 +1,4 @@ -package moe.lava.neon.core.api.gateway +package moe.lava.neon.api.gateway import co.touchlab.kermit.Logger import io.ktor.client.HttpClient @@ -22,15 +22,14 @@ import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import moe.lava.neon.core.api.ApiConstants -import moe.lava.neon.core.api.ApiConstants.json -import moe.lava.neon.core.api.gateway.handlers.EventHandlers +import moe.lava.neon.api.ApiConstants +import moe.lava.neon.api.ApiConstants.json import kotlin.random.Random import kotlin.time.Duration.Companion.milliseconds private val logger = Logger.withTag("neon.core.api.gateway/session") -class GatewaySession private constructor( +internal class GatewaySession private constructor( private var ws: DefaultClientWebSocketSession, private val token: String, private val handlers: EventHandlers, @@ -100,19 +99,28 @@ class GatewaySession private constructor( private suspend fun handlePayload(payload: Payload.Incoming<*>) { logger.d { payload.toString() } - when (val event = payload.d) { + val event = payload.d + when (event) { 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 -> handlers.ready.handle(event) { - resumeProps = it + is Event.Ready -> { + resumeProps = ResumeProperties( + sessionId = event.sessionId, + resumeGatewayUrl = event.resumeGatewayUrl, + lastSequence = 0, + ) onSuccess() } is Event.Resumed -> onSuccess() } + if (event is Event.Dispatch) { + val eventHandlers = handlers[event::class] ?: return + eventHandlers.forEach { it.handle(event) } + } } private suspend fun handleUnknownPayload(payload: Payload.Unknown) { diff --git a/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/Payloads.kt b/api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/Payloads.kt similarity index 95% rename from core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/Payloads.kt rename to api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/Payloads.kt index 446980b..e65fa3b 100644 --- a/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/Payloads.kt +++ b/api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/Payloads.kt @@ -1,9 +1,9 @@ -package moe.lava.neon.core.api.gateway +package moe.lava.neon.api.gateway import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonElement -import moe.lava.neon.core.api.ApiConstants -import moe.lava.neon.core.api.structures.User +import moe.lava.neon.api.ApiConstants +import moe.lava.neon.api.objects.User sealed interface Payload { val op: Int diff --git a/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/ResumeProperties.kt b/api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/ResumeProperties.kt similarity index 55% rename from core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/ResumeProperties.kt rename to api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/ResumeProperties.kt index 092497d..f962e3d 100644 --- a/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/ResumeProperties.kt +++ b/api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/ResumeProperties.kt @@ -1,6 +1,6 @@ -package moe.lava.neon.core.api.gateway +package moe.lava.neon.api.gateway -data class ResumeProperties( +internal data class ResumeProperties( val sessionId: String, val resumeGatewayUrl: String, val lastSequence: Int, diff --git a/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/SerializingExtensions.kt b/api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/SerializingExtensions.kt similarity index 83% rename from core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/SerializingExtensions.kt rename to api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/SerializingExtensions.kt index f95aaa3..78eab17 100644 --- a/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/SerializingExtensions.kt +++ b/api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/SerializingExtensions.kt @@ -1,12 +1,10 @@ -package moe.lava.neon.core.api.gateway +package moe.lava.neon.api.gateway import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.decodeFromJsonElement -import moe.lava.neon.core.api.ApiConstants +import moe.lava.neon.api.ApiConstants.json -private val json = ApiConstants.json - -fun T.pack(): Payload.Outgoing { +internal fun T.pack(): Payload.Outgoing { val opcode: Int = when (this) { is Event.Heartbeat -> 1 is Event.Identify -> 2 @@ -16,7 +14,7 @@ fun T.pack(): Payload.Outgoing { return Payload.Outgoing(op = opcode, d = this) } -fun Payload.Unknown.asIncoming() : Payload.WithSequence { +internal fun Payload.Unknown.asIncoming() : Payload.WithSequence { return when (op) { 0 -> when (t) { "READY" -> decode() 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 new file mode 100644 index 0000000..5c721fd --- /dev/null +++ b/api/gateway/src/commonMain/kotlin/moe/lava/neon/api/gateway/handlers/EventHandlers.kt @@ -0,0 +1,7 @@ +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 new file mode 100644 index 0000000..1587873 --- /dev/null +++ b/api/rest/build.gradle.kts @@ -0,0 +1,65 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.androidLibrary) + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.kotlinSerialization) +} + +kotlin { + jvm() + androidTarget { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } + + 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) + } + jvmMain.dependencies { + implementation(libs.ktor.client.okhttp) + } + androidMain.dependencies { + implementation(libs.ktor.client.okhttp) + } + } +} + +dependencies { + coreLibraryDesugaring(libs.desugar) +} + +android { + namespace = "moe.lava.neon.api.rest" + 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/commonMain/kotlin/moe/lava/neon/core/api/ApiClient.kt b/api/rest/src/commonMain/kotlin/moe/lava/neon/api/ApiClient.kt similarity index 81% rename from core/src/commonMain/kotlin/moe/lava/neon/core/api/ApiClient.kt rename to api/rest/src/commonMain/kotlin/moe/lava/neon/api/ApiClient.kt index 727b4e2..7122519 100644 --- a/core/src/commonMain/kotlin/moe/lava/neon/core/api/ApiClient.kt +++ b/api/rest/src/commonMain/kotlin/moe/lava/neon/api/ApiClient.kt @@ -1,4 +1,4 @@ -package moe.lava.neon.core.api +package moe.lava.neon.api import co.touchlab.kermit.Logger import io.ktor.client.HttpClient @@ -15,10 +15,8 @@ import io.ktor.http.userAgent import io.ktor.serialization.kotlinx.json.json import io.ktor.util.appendAll import kotlinx.serialization.ExperimentalSerializationApi -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 +import moe.lava.neon.common.captcha.CaptchaRequest +import moe.lava.neon.common.captcha.CaptchaResponse class ApiClient { private val logger = Logger.withTag("neon.core.api/client") @@ -81,20 +79,3 @@ class ApiClient { } } } - -@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/build.gradle.kts b/api/shared/build.gradle.kts new file mode 100644 index 0000000..23b67e6 --- /dev/null +++ b/api/shared/build.gradle.kts @@ -0,0 +1,50 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.androidLibrary) + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.kotlinSerialization) +} + +kotlin { + jvm() + androidTarget { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } + + sourceSets { + commonMain.dependencies { + implementation(libs.kotlinx.serialization.json) + } + } +} + +dependencies { + coreLibraryDesugaring(libs.desugar) +} + +android { + namespace = "moe.lava.neon.api" + 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/api/ApiConstants.android.kt b/api/shared/src/androidMain/kotlin/moe/lava/neon/api/ApiConstants.android.kt similarity index 60% rename from core/src/androidMain/kotlin/moe/lava/neon/core/api/ApiConstants.android.kt rename to api/shared/src/androidMain/kotlin/moe/lava/neon/api/ApiConstants.android.kt index 54b2c9c..37847a8 100644 --- a/core/src/androidMain/kotlin/moe/lava/neon/core/api/ApiConstants.android.kt +++ b/api/shared/src/androidMain/kotlin/moe/lava/neon/api/ApiConstants.android.kt @@ -1,12 +1,14 @@ -package moe.lava.neon.core.api +package moe.lava.neon.api +import android.annotation.SuppressLint +import android.os.Build import java.util.Locale -@Suppress("ConstantLocale") +@SuppressLint("ConstantLocale") internal actual val platformSuperProps = PlatformProps( - device = android.os.Build.DEVICE, + device = 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 = "${android.os.Build.VERSION.SDK_INT}", + osVersion = "${Build.VERSION.SDK_INT}", ) diff --git a/core/src/commonMain/kotlin/moe/lava/neon/core/api/ApiConstants.kt b/api/shared/src/commonMain/kotlin/moe/lava/neon/api/ApiConstants.kt similarity index 99% rename from core/src/commonMain/kotlin/moe/lava/neon/core/api/ApiConstants.kt rename to api/shared/src/commonMain/kotlin/moe/lava/neon/api/ApiConstants.kt index 42fc251..36258d9 100644 --- a/core/src/commonMain/kotlin/moe/lava/neon/core/api/ApiConstants.kt +++ b/api/shared/src/commonMain/kotlin/moe/lava/neon/api/ApiConstants.kt @@ -1,4 +1,4 @@ -package moe.lava.neon.core.api +package moe.lava.neon.api import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable diff --git a/core/src/commonMain/kotlin/moe/lava/neon/core/api/structures/Snowflake.kt b/api/shared/src/commonMain/kotlin/moe/lava/neon/api/objects/Snowflake.kt similarity index 80% rename from core/src/commonMain/kotlin/moe/lava/neon/core/api/structures/Snowflake.kt rename to api/shared/src/commonMain/kotlin/moe/lava/neon/api/objects/Snowflake.kt index 4d8dec5..3172955 100644 --- a/core/src/commonMain/kotlin/moe/lava/neon/core/api/structures/Snowflake.kt +++ b/api/shared/src/commonMain/kotlin/moe/lava/neon/api/objects/Snowflake.kt @@ -1,4 +1,4 @@ -package moe.lava.neon.core.api.structures +package moe.lava.neon.api.objects import kotlinx.serialization.Serializable import kotlinx.serialization.builtins.LongAsStringSerializer diff --git a/core/src/commonMain/kotlin/moe/lava/neon/core/api/structures/User.kt b/api/shared/src/commonMain/kotlin/moe/lava/neon/api/objects/User.kt similarity index 78% rename from core/src/commonMain/kotlin/moe/lava/neon/core/api/structures/User.kt rename to api/shared/src/commonMain/kotlin/moe/lava/neon/api/objects/User.kt index eb78a93..3168445 100644 --- a/core/src/commonMain/kotlin/moe/lava/neon/core/api/structures/User.kt +++ b/api/shared/src/commonMain/kotlin/moe/lava/neon/api/objects/User.kt @@ -1,4 +1,4 @@ -package moe.lava.neon.core.api.structures +package moe.lava.neon.api.objects import kotlinx.serialization.Serializable diff --git a/core/src/jvmMain/kotlin/moe/lava/neon/core/api/ApiConstants.jvm.kt b/api/shared/src/jvmMain/kotlin/moe/lava/neon/api/ApiConstants.jvm.kt similarity index 85% rename from core/src/jvmMain/kotlin/moe/lava/neon/core/api/ApiConstants.jvm.kt rename to api/shared/src/jvmMain/kotlin/moe/lava/neon/api/ApiConstants.jvm.kt index 4e2e603..70ff314 100644 --- a/core/src/jvmMain/kotlin/moe/lava/neon/core/api/ApiConstants.jvm.kt +++ b/api/shared/src/jvmMain/kotlin/moe/lava/neon/api/ApiConstants.jvm.kt @@ -1,7 +1,8 @@ -package moe.lava.neon.core.api +package moe.lava.neon.api import java.util.Locale +// TODO @Suppress("ConstantLocale") internal actual val platformSuperProps = PlatformProps( device = "", diff --git a/common/build.gradle.kts b/common/build.gradle.kts new file mode 100644 index 0000000..84aa4ff --- /dev/null +++ b/common/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.kotlinSerialization) +} + +kotlin { + jvm() + + sourceSets { + commonMain.dependencies { + implementation(libs.kotlinx.serialization.core) + } + } +} diff --git a/core/src/commonMain/kotlin/moe/lava/neon/core/api/captcha/CaptchaRequest.kt b/common/src/commonMain/kotlin/moe/lava/neon/common/captcha/CaptchaRequest.kt similarity index 89% rename from core/src/commonMain/kotlin/moe/lava/neon/core/api/captcha/CaptchaRequest.kt rename to common/src/commonMain/kotlin/moe/lava/neon/common/captcha/CaptchaRequest.kt index 4357d6d..9ded522 100644 --- a/core/src/commonMain/kotlin/moe/lava/neon/core/api/captcha/CaptchaRequest.kt +++ b/common/src/commonMain/kotlin/moe/lava/neon/common/captcha/CaptchaRequest.kt @@ -1,4 +1,4 @@ -package moe.lava.neon.core.api.captcha +package moe.lava.neon.common.captcha import kotlinx.serialization.Serializable diff --git a/core/src/commonMain/kotlin/moe/lava/neon/core/api/captcha/CaptchaResponse.kt b/common/src/commonMain/kotlin/moe/lava/neon/common/captcha/CaptchaResponse.kt similarity index 80% rename from core/src/commonMain/kotlin/moe/lava/neon/core/api/captcha/CaptchaResponse.kt rename to common/src/commonMain/kotlin/moe/lava/neon/common/captcha/CaptchaResponse.kt index 7bc9d59..85f93df 100644 --- a/core/src/commonMain/kotlin/moe/lava/neon/core/api/captcha/CaptchaResponse.kt +++ b/common/src/commonMain/kotlin/moe/lava/neon/common/captcha/CaptchaResponse.kt @@ -1,4 +1,4 @@ -package moe.lava.neon.core.api.captcha +package moe.lava.neon.common.captcha sealed class CaptchaResponse { data class Success(val token: String) : CaptchaResponse() diff --git a/core/build.gradle.kts b/core/build.gradle.kts index ed7c496..00d01b0 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -18,9 +18,10 @@ kotlin { sourceSets { commonMain.dependencies { + implementation(project(":api:gateway")) + implementation(project(":api:rest")) + implementation(project(":common")) implementation(libs.ktor.client.core) - implementation(libs.ktor.client.content.negotiation) - implementation(libs.ktor.client.websockets) implementation(libs.ktor.serialization.kotlinx.json) implementation(project.dependencies.platform(libs.koin.bom)) @@ -32,12 +33,6 @@ kotlin { commonTest.dependencies { implementation(libs.kotlin.test) } - jvmMain.dependencies { - implementation(libs.ktor.client.okhttp) - } - androidMain.dependencies { - implementation(libs.ktor.client.okhttp) - } } } 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 2ca64da..3492b1a 100644 --- a/core/src/commonMain/kotlin/moe/lava/neon/core/AppSettings.kt +++ b/core/src/commonMain/kotlin/moe/lava/neon/core/AppSettings.kt @@ -3,7 +3,7 @@ package moe.lava.neon.core import com.russhwolf.settings.Settings import com.russhwolf.settings.nullableString -class AppSettings { +internal class AppSettings { private val settings = Settings() var fingerprint by settings.nullableString() diff --git a/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/handlers/EventHandlers.kt b/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/handlers/EventHandlers.kt deleted file mode 100644 index 87504bf..0000000 --- a/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/handlers/EventHandlers.kt +++ /dev/null @@ -1,9 +0,0 @@ -package moe.lava.neon.core.api.gateway.handlers - -import moe.lava.neon.core.api.gateway.Event - -sealed interface Handler - -class EventHandlers( - val ready: ReadyHandler -) 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 deleted file mode 100644 index e3b2ef9..0000000 --- a/core/src/commonMain/kotlin/moe/lava/neon/core/api/gateway/handlers/ReadyHandler.kt +++ /dev/null @@ -1,18 +0,0 @@ -package moe.lava.neon.core.api.gateway.handlers - -import co.touchlab.kermit.Logger -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") - -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/core/src/commonMain/kotlin/moe/lava/neon/core/di/CoreModule.kt b/core/src/commonMain/kotlin/moe/lava/neon/core/di/CoreModule.kt index c16a33e..6e79c70 100644 --- a/core/src/commonMain/kotlin/moe/lava/neon/core/di/CoreModule.kt +++ b/core/src/commonMain/kotlin/moe/lava/neon/core/di/CoreModule.kt @@ -1,11 +1,11 @@ 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.api.ApiClient -import moe.lava.neon.core.api.gateway.GatewayHandler -import moe.lava.neon.core.api.gateway.handlers.EventHandlers -import moe.lava.neon.core.api.gateway.handlers.ReadyHandler 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 @@ -15,10 +15,9 @@ val coreModule = module { single() single() + single() + single() single() single() - - single() - single() } 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 9375cd2..e2ecb90 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 @@ -10,8 +10,8 @@ import io.ktor.client.request.setBody import io.ktor.http.ContentType import io.ktor.http.contentType import kotlinx.serialization.Serializable +import moe.lava.neon.api.ApiClient import moe.lava.neon.core.AppSettings -import moe.lava.neon.core.api.ApiClient @Serializable private data class ExperimentResponse( @@ -43,16 +43,15 @@ sealed class AuthResponse { // data class MFARequested() : AuthResponse() } -class AuthRepository( +class AuthRepository internal constructor( private val settings: AppSettings, private val api: ApiClient, ) { private val logger = Logger.withTag("neon.core.repo/auth") - var token by settings::token - private set + private var token by settings::token + private var fingerprint by settings::fingerprint - var fingerprint by settings::fingerprint - private set + val loggedIn get() = token != null suspend fun login( email: 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 new file mode 100644 index 0000000..6e5911f --- /dev/null +++ b/core/src/commonMain/kotlin/moe/lava/neon/core/repository/CaptchaRepository.kt @@ -0,0 +1,11 @@ +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 new file mode 100644 index 0000000..84f14f5 --- /dev/null +++ b/core/src/commonMain/kotlin/moe/lava/neon/core/repository/GatewayRepository.kt @@ -0,0 +1,18 @@ +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/gradle/libs.versions.toml b/gradle/libs.versions.toml index ef8ff17..f04b531 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,6 +23,7 @@ koin-bom = "4.2.0-RC1" koin-plugin = "0.3.0" kotlin = "2.3.0" kotlinx-coroutines = "1.10.2" +kotlinx-serialization = "1.10.0" ktor = "3.4.0" material3 = "1.11.0-alpha02" material3-adaptive = "1.3.0-alpha04" @@ -64,6 +65,8 @@ koin-test = { module = "io.insert-koin:koin-test" } 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-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } diff --git a/settings.gradle.kts b/settings.gradle.kts index e405915..0811cc7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -33,5 +33,9 @@ plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" } +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 31edb2d..d69d7f9 100644 --- a/ui/build.gradle.kts +++ b/ui/build.gradle.kts @@ -31,6 +31,7 @@ kotlin { implementation(libs.ktor.client.okhttp) } commonMain.dependencies { + implementation(project(":common")) implementation(project(":core")) implementation(libs.compose.components.resources) implementation(libs.compose.foundation) diff --git a/ui/src/androidMain/kotlin/moe/lava/neon/ui/CaptchaBinder.android.kt b/ui/src/androidMain/kotlin/moe/lava/neon/ui/CaptchaHandler.android.kt similarity index 96% rename from ui/src/androidMain/kotlin/moe/lava/neon/ui/CaptchaBinder.android.kt rename to ui/src/androidMain/kotlin/moe/lava/neon/ui/CaptchaHandler.android.kt index 6baa380..185be4a 100644 --- a/ui/src/androidMain/kotlin/moe/lava/neon/ui/CaptchaBinder.android.kt +++ b/ui/src/androidMain/kotlin/moe/lava/neon/ui/CaptchaHandler.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.core.api.ApiClient -import moe.lava.neon.core.api.captcha.CaptchaResponse +import moe.lava.neon.common.captcha.CaptchaRequest +import moe.lava.neon.common.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 CaptchaBinder(api: ApiClient) { +actual fun getCaptchaHandler(): suspend (CaptchaRequest) -> CaptchaResponse { val context = LocalContext.current val queue = MutableSharedFlow>() val scope = rememberCoroutineScope() @@ -67,7 +67,7 @@ actual fun CaptchaBinder(api: ApiClient) { } } - api.setCaptchaHandler { captcha -> + return { captcha -> val intent = Intent(context, HCaptchaActivity::class.java).apply { putExtra(EXTRA_SITE_KEY, captcha.captchaSitekey) putExtra(EXTRA_RQ_DATA, captcha.captchaRqdata) 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 c42a2cc..cc2f999 100644 --- a/ui/src/commonMain/kotlin/moe/lava/neon/ui/App.kt +++ b/ui/src/commonMain/kotlin/moe/lava/neon/ui/App.kt @@ -17,6 +17,7 @@ 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.screens.Login import moe.lava.neon.ui.screens.Sample import moe.lava.neon.ui.screens.chat.Chat @@ -67,12 +68,15 @@ fun App() { } val auth: AuthRepository = koinInject() - CaptchaBinder(koinInject()) + val captcha: CaptchaRepository = koinInject() + captcha.setHandler(getCaptchaHandler()) + MaterialExpressiveTheme( colorScheme = getColorScheme(), motionScheme = MotionScheme.expressive(), ) { - val init = if (auth.token != null) Route.Sample else Route.Login + 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( diff --git a/ui/src/commonMain/kotlin/moe/lava/neon/ui/CaptchaBinder.kt b/ui/src/commonMain/kotlin/moe/lava/neon/ui/CaptchaBinder.kt deleted file mode 100644 index f8e6966..0000000 --- a/ui/src/commonMain/kotlin/moe/lava/neon/ui/CaptchaBinder.kt +++ /dev/null @@ -1,7 +0,0 @@ -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 new file mode 100644 index 0000000..70b32bb --- /dev/null +++ b/ui/src/commonMain/kotlin/moe/lava/neon/ui/CaptchaHandler.kt @@ -0,0 +1,8 @@ +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/screens/Sample.kt b/ui/src/commonMain/kotlin/moe/lava/neon/ui/screens/Sample.kt index 64579ba..56f4151 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 @@ -21,8 +21,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import co.touchlab.kermit.Logger 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 @@ -84,26 +84,24 @@ fun Sample( class SampleViewModel( private val auth: AuthRepository, - private val gateway: GatewayHandler, + private val gateway: GatewayRepository, ) : ViewModel() { private val logger = Logger.withTag("neon.ui.screens/Sample") val token get() = auth.token fun connect() { viewModelScope.launch { - try { - gateway.connect() - } catch(e: Throwable) { - logger.e(e) { "Failed to connect to gateway: ${e.stackTraceToString()}" } + val exception = gateway.start().exceptionOrNull() + if (exception != null) { + logger.e(exception) { "Failed to connect to gateway: ${exception.stackTraceToString()}" } } } } fun disconnect() { viewModelScope.launch { - try { - gateway.disconnect() - } catch(e: Throwable) { - logger.e(e) { "Failed to connect to gateway: ${e.stackTraceToString()}" } + val exception = gateway.pause().exceptionOrNull() + if (exception != null) { + logger.e(exception) { "Failed to disconnect from gateway: ${exception.stackTraceToString()}" } } } } 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 deleted file mode 100644 index 397f534..0000000 --- a/ui/src/jvmMain/kotlin/moe/lava/neon/ui/CaptchaBinder.jvm.kt +++ /dev/null @@ -1,13 +0,0 @@ -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 new file mode 100644 index 0000000..56044e0 --- /dev/null +++ b/ui/src/jvmMain/kotlin/moe/lava/neon/ui/CaptchaHandler.jvm.kt @@ -0,0 +1,13 @@ +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()) + } +}