refactor: split up core into multiple modules

This commit is contained in:
Cilly Leang 2026-02-05 01:05:02 +11:00
parent 2725342c3f
commit 0d84411f14
Signed by: cilly
GPG key ID: 6500251E087653C9
38 changed files with 344 additions and 149 deletions

View file

@ -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
}
}

View file

@ -1,4 +1,4 @@
package moe.lava.neon.core.api.gateway package moe.lava.neon.api.gateway
@Suppress("unused") @Suppress("unused")
object Capability { object Capability {

View file

@ -1,4 +1,4 @@
package moe.lava.neon.core.api.gateway package moe.lava.neon.api.gateway
import io.ktor.websocket.CloseReason import io.ktor.websocket.CloseReason

View file

@ -1,4 +1,4 @@
package moe.lava.neon.core.api.gateway package moe.lava.neon.api.gateway
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -6,15 +6,15 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import moe.lava.neon.core.api.gateway.handlers.EventHandlers import moe.lava.neon.api.gateway.handlers.Handler
import moe.lava.neon.core.repository.AuthRepository
import kotlin.math.pow import kotlin.math.pow
import kotlin.reflect.KClass
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
class GatewayHandler( typealias EventHandlers = Map<KClass<out Event.Dispatch>, MutableList<Handler<in Event.Dispatch>>>
private val auth: AuthRepository,
private val eventHandlers: EventHandlers, class GatewayHandler {
) { private val eventHandlers: EventHandlers = mutableMapOf()
private val logger = Logger.withTag("neon.core.api.gateway/handler") private val logger = Logger.withTag("neon.core.api.gateway/handler")
private val scope = CoroutineScope(Dispatchers.IO) private val scope = CoroutineScope(Dispatchers.IO)
private var session: GatewaySession? = null private var session: GatewaySession? = null
@ -23,13 +23,11 @@ class GatewayHandler(
private var retryAttempts: Int = 0 private var retryAttempts: Int = 0
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
suspend fun connect() { suspend fun connect(token: String) {
if (session != null) { if (session != null) {
logger.w(Throwable()) { "Attempted to connect, but client already connected, ignoring..." } logger.w(Throwable()) { "Attempted to connect, but client already connected, ignoring..." }
return return
} }
val token = auth.token
?: throw IllegalStateException("Tried to connect to gateway with no token")
session = GatewaySession.start( session = GatewaySession.start(
token = token, token = token,
@ -59,7 +57,7 @@ class GatewayHandler(
logger.d { "Reconnecting in ${dur.inWholeMilliseconds}ms" } logger.d { "Reconnecting in ${dur.inWholeMilliseconds}ms" }
delay(dur) delay(dur)
retryAttempts += 1 retryAttempts += 1
res = runCatching { connect() } res = runCatching { connect(token) }
res.exceptionOrNull()?.let { res.exceptionOrNull()?.let {
logger.e(it) { "Reconnect failed" } logger.e(it) { "Reconnect failed" }
} }

View file

@ -1,4 +1,4 @@
package moe.lava.neon.core.api.gateway package moe.lava.neon.api.gateway
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
@ -22,15 +22,14 @@ import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import moe.lava.neon.core.api.ApiConstants import moe.lava.neon.api.ApiConstants
import moe.lava.neon.core.api.ApiConstants.json import moe.lava.neon.api.ApiConstants.json
import moe.lava.neon.core.api.gateway.handlers.EventHandlers
import kotlin.random.Random import kotlin.random.Random
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
private val logger = Logger.withTag("neon.core.api.gateway/session") private val logger = Logger.withTag("neon.core.api.gateway/session")
class GatewaySession private constructor( internal class GatewaySession private constructor(
private var ws: DefaultClientWebSocketSession, private var ws: DefaultClientWebSocketSession,
private val token: String, private val token: String,
private val handlers: EventHandlers, private val handlers: EventHandlers,
@ -100,19 +99,28 @@ class GatewaySession private constructor(
private suspend fun handlePayload(payload: Payload.Incoming<*>) { private suspend fun handlePayload(payload: Payload.Incoming<*>) {
logger.d { payload.toString() } logger.d { payload.toString() }
when (val event = payload.d) { val event = payload.d
when (event) {
is Event.Heartbeat -> handleHeartbeat() is Event.Heartbeat -> handleHeartbeat()
is Event.Reconnect -> close(GatewayCloseReason.ServerReconnect) is Event.Reconnect -> close(GatewayCloseReason.ServerReconnect)
is Event.InvalidSession -> close(GatewayCloseReason.InvalidSession(event.resumable)) is Event.InvalidSession -> close(GatewayCloseReason.InvalidSession(event.resumable))
is Event.Hello -> handleHello(event) is Event.Hello -> handleHello(event)
is Event.HeartbeatAck -> { missedHeartbeats -= 1 } is Event.HeartbeatAck -> { missedHeartbeats -= 1 }
is Event.Ready -> handlers.ready.handle(event) { is Event.Ready -> {
resumeProps = it resumeProps = ResumeProperties(
sessionId = event.sessionId,
resumeGatewayUrl = event.resumeGatewayUrl,
lastSequence = 0,
)
onSuccess() onSuccess()
} }
is Event.Resumed -> 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) { private suspend fun handleUnknownPayload(payload: Payload.Unknown) {

View file

@ -1,9 +1,9 @@
package moe.lava.neon.core.api.gateway package moe.lava.neon.api.gateway
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement
import moe.lava.neon.core.api.ApiConstants import moe.lava.neon.api.ApiConstants
import moe.lava.neon.core.api.structures.User import moe.lava.neon.api.objects.User
sealed interface Payload { sealed interface Payload {
val op: Int val op: Int

View file

@ -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 sessionId: String,
val resumeGatewayUrl: String, val resumeGatewayUrl: String,
val lastSequence: Int, val lastSequence: Int,

View file

@ -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.JsonNull
import kotlinx.serialization.json.decodeFromJsonElement import kotlinx.serialization.json.decodeFromJsonElement
import moe.lava.neon.core.api.ApiConstants import moe.lava.neon.api.ApiConstants.json
private val json = ApiConstants.json internal fun <T : Event.Outgoing> T.pack(): Payload.Outgoing<T> {
fun <T : Event.Outgoing> T.pack(): Payload.Outgoing<T> {
val opcode: Int = when (this) { val opcode: Int = when (this) {
is Event.Heartbeat -> 1 is Event.Heartbeat -> 1
is Event.Identify -> 2 is Event.Identify -> 2
@ -16,7 +14,7 @@ fun <T : Event.Outgoing> T.pack(): Payload.Outgoing<T> {
return Payload.Outgoing(op = opcode, d = this) return Payload.Outgoing(op = opcode, d = this)
} }
fun Payload.Unknown.asIncoming() : Payload.WithSequence { internal fun Payload.Unknown.asIncoming() : Payload.WithSequence {
return when (op) { return when (op) {
0 -> when (t) { 0 -> when (t) {
"READY" -> decode<Event.Ready>() "READY" -> decode<Event.Ready>()

View file

@ -0,0 +1,7 @@
package moe.lava.neon.api.gateway.handlers
import moe.lava.neon.api.gateway.Event
sealed interface Handler<T: Event.Dispatch> {
suspend fun handle(event: T)
}

65
api/rest/build.gradle.kts Normal file
View file

@ -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
}
}

View file

@ -1,4 +1,4 @@
package moe.lava.neon.core.api package moe.lava.neon.api
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
@ -15,10 +15,8 @@ import io.ktor.http.userAgent
import io.ktor.serialization.kotlinx.json.json import io.ktor.serialization.kotlinx.json.json
import io.ktor.util.appendAll import io.ktor.util.appendAll
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json import moe.lava.neon.common.captcha.CaptchaRequest
import kotlinx.serialization.json.JsonNamingStrategy import moe.lava.neon.common.captcha.CaptchaResponse
import moe.lava.neon.core.api.captcha.CaptchaRequest
import moe.lava.neon.core.api.captcha.CaptchaResponse
class ApiClient { class ApiClient {
private val logger = Logger.withTag("neon.core.api/client") 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)
}
}

View file

@ -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
}
}

View file

@ -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 import java.util.Locale
@Suppress("ConstantLocale") @SuppressLint("ConstantLocale")
internal actual val platformSuperProps = PlatformProps( 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) // 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) // .toLanguageTag() is close, but returns too much junk (e.g. en-AU-u-fw-mon)
systemLocale = Locale.getDefault().language, systemLocale = Locale.getDefault().language,
osVersion = "${android.os.Build.VERSION.SDK_INT}", osVersion = "${Build.VERSION.SDK_INT}",
) )

View file

@ -1,4 +1,4 @@
package moe.lava.neon.core.api package moe.lava.neon.api
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable

View file

@ -1,4 +1,4 @@
package moe.lava.neon.core.api.structures package moe.lava.neon.api.objects
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.LongAsStringSerializer import kotlinx.serialization.builtins.LongAsStringSerializer

View file

@ -1,4 +1,4 @@
package moe.lava.neon.core.api.structures package moe.lava.neon.api.objects
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable

View file

@ -1,7 +1,8 @@
package moe.lava.neon.core.api package moe.lava.neon.api
import java.util.Locale import java.util.Locale
// TODO
@Suppress("ConstantLocale") @Suppress("ConstantLocale")
internal actual val platformSuperProps = PlatformProps( internal actual val platformSuperProps = PlatformProps(
device = "", device = "",

14
common/build.gradle.kts Normal file
View file

@ -0,0 +1,14 @@
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
}
kotlin {
jvm()
sourceSets {
commonMain.dependencies {
implementation(libs.kotlinx.serialization.core)
}
}
}

View file

@ -1,4 +1,4 @@
package moe.lava.neon.core.api.captcha package moe.lava.neon.common.captcha
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable

View file

@ -1,4 +1,4 @@
package moe.lava.neon.core.api.captcha package moe.lava.neon.common.captcha
sealed class CaptchaResponse { sealed class CaptchaResponse {
data class Success(val token: String) : CaptchaResponse() data class Success(val token: String) : CaptchaResponse()

View file

@ -18,9 +18,10 @@ kotlin {
sourceSets { sourceSets {
commonMain.dependencies { commonMain.dependencies {
implementation(project(":api:gateway"))
implementation(project(":api:rest"))
implementation(project(":common"))
implementation(libs.ktor.client.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.ktor.serialization.kotlinx.json)
implementation(project.dependencies.platform(libs.koin.bom)) implementation(project.dependencies.platform(libs.koin.bom))
@ -32,12 +33,6 @@ kotlin {
commonTest.dependencies { commonTest.dependencies {
implementation(libs.kotlin.test) implementation(libs.kotlin.test)
} }
jvmMain.dependencies {
implementation(libs.ktor.client.okhttp)
}
androidMain.dependencies {
implementation(libs.ktor.client.okhttp)
}
} }
} }

View file

@ -3,7 +3,7 @@ package moe.lava.neon.core
import com.russhwolf.settings.Settings import com.russhwolf.settings.Settings
import com.russhwolf.settings.nullableString import com.russhwolf.settings.nullableString
class AppSettings { internal class AppSettings {
private val settings = Settings() private val settings = Settings()
var fingerprint by settings.nullableString() var fingerprint by settings.nullableString()

View file

@ -1,9 +0,0 @@
package moe.lava.neon.core.api.gateway.handlers
import moe.lava.neon.core.api.gateway.Event
sealed interface Handler<T: Event.Incoming>
class EventHandlers(
val ready: ReadyHandler
)

View file

@ -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<Event.Ready> {
fun handle(event: Event.Ready, updateResumeProps: (ResumeProperties) -> Unit) {
logger.i { "Received payload $event" }
updateResumeProps(ResumeProperties(
sessionId = event.sessionId,
resumeGatewayUrl = event.resumeGatewayUrl,
lastSequence = 0,
))
}
}

View file

@ -1,11 +1,11 @@
package moe.lava.neon.core.di 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.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.AuthRepository
import moe.lava.neon.core.repository.CaptchaRepository
import moe.lava.neon.core.repository.GatewayRepository
import moe.lava.neon.core.repository.UserRepository import moe.lava.neon.core.repository.UserRepository
import org.koin.dsl.module import org.koin.dsl.module
import org.koin.plugin.module.dsl.single import org.koin.plugin.module.dsl.single
@ -15,10 +15,9 @@ val coreModule = module {
single<AppSettings>() single<AppSettings>()
single<AuthRepository>() single<AuthRepository>()
single<CaptchaRepository>()
single<GatewayRepository>()
single<UserRepository>() single<UserRepository>()
single<GatewayHandler>() single<GatewayHandler>()
single<ReadyHandler>()
single<EventHandlers>()
} }

View file

@ -10,8 +10,8 @@ import io.ktor.client.request.setBody
import io.ktor.http.ContentType import io.ktor.http.ContentType
import io.ktor.http.contentType import io.ktor.http.contentType
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import moe.lava.neon.api.ApiClient
import moe.lava.neon.core.AppSettings import moe.lava.neon.core.AppSettings
import moe.lava.neon.core.api.ApiClient
@Serializable @Serializable
private data class ExperimentResponse( private data class ExperimentResponse(
@ -43,16 +43,15 @@ sealed class AuthResponse {
// data class MFARequested() : AuthResponse() // data class MFARequested() : AuthResponse()
} }
class AuthRepository( class AuthRepository internal constructor(
private val settings: AppSettings, private val settings: AppSettings,
private val api: ApiClient, private val api: ApiClient,
) { ) {
private val logger = Logger.withTag("neon.core.repo/auth") private val logger = Logger.withTag("neon.core.repo/auth")
var token by settings::token private var token by settings::token
private set private var fingerprint by settings::fingerprint
var fingerprint by settings::fingerprint val loggedIn get() = token != null
private set
suspend fun login( suspend fun login(
email: String, email: String,

View file

@ -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)
}

View file

@ -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<Unit> = runCatching {
val token = settings.token
?: throw IllegalArgumentException("Tried to start gateway with no token")
gateway.connect(token)
}
suspend fun pause() = runCatching { gateway.disconnect() }
}

View file

@ -23,6 +23,7 @@ koin-bom = "4.2.0-RC1"
koin-plugin = "0.3.0" koin-plugin = "0.3.0"
kotlin = "2.3.0" kotlin = "2.3.0"
kotlinx-coroutines = "1.10.2" kotlinx-coroutines = "1.10.2"
kotlinx-serialization = "1.10.0"
ktor = "3.4.0" ktor = "3.4.0"
material3 = "1.11.0-alpha02" material3 = "1.11.0-alpha02"
material3-adaptive = "1.3.0-alpha04" 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 = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", 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-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-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-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }

View file

@ -33,5 +33,9 @@ plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" 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(":core")
include(":ui") include(":ui")

View file

@ -31,6 +31,7 @@ kotlin {
implementation(libs.ktor.client.okhttp) implementation(libs.ktor.client.okhttp)
} }
commonMain.dependencies { commonMain.dependencies {
implementation(project(":common"))
implementation(project(":core")) implementation(project(":core"))
implementation(libs.compose.components.resources) implementation(libs.compose.components.resources)
implementation(libs.compose.foundation) implementation(libs.compose.foundation)

View file

@ -19,8 +19,8 @@ import com.hcaptcha.sdk.HCaptchaVerifyParams
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import moe.lava.neon.core.api.ApiClient import moe.lava.neon.common.captcha.CaptchaRequest
import moe.lava.neon.core.api.captcha.CaptchaResponse import moe.lava.neon.common.captcha.CaptchaResponse
private val logger = Logger.withTag("neon.ui.app/captcha") 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" private const val EXTRA_RESULT_ERROR = "extra_result_error"
@Composable @Composable
actual fun CaptchaBinder(api: ApiClient) { actual fun getCaptchaHandler(): suspend (CaptchaRequest) -> CaptchaResponse {
val context = LocalContext.current val context = LocalContext.current
val queue = MutableSharedFlow<Pair<String, CaptchaResponse>>() val queue = MutableSharedFlow<Pair<String, CaptchaResponse>>()
val scope = rememberCoroutineScope() 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 { val intent = Intent(context, HCaptchaActivity::class.java).apply {
putExtra(EXTRA_SITE_KEY, captcha.captchaSitekey) putExtra(EXTRA_SITE_KEY, captcha.captchaSitekey)
putExtra(EXTRA_RQ_DATA, captcha.captchaRqdata) putExtra(EXTRA_RQ_DATA, captcha.captchaRqdata)

View file

@ -17,6 +17,7 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic import kotlinx.serialization.modules.polymorphic
import moe.lava.neon.core.repository.AuthRepository 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.Login
import moe.lava.neon.ui.screens.Sample import moe.lava.neon.ui.screens.Sample
import moe.lava.neon.ui.screens.chat.Chat import moe.lava.neon.ui.screens.chat.Chat
@ -67,12 +68,15 @@ fun App() {
} }
val auth: AuthRepository = koinInject() val auth: AuthRepository = koinInject()
CaptchaBinder(koinInject()) val captcha: CaptchaRepository = koinInject()
captcha.setHandler(getCaptchaHandler())
MaterialExpressiveTheme( MaterialExpressiveTheme(
colorScheme = getColorScheme(), colorScheme = getColorScheme(),
motionScheme = MotionScheme.expressive(), 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 backStack = rememberNavBackStack(config, Route.Sample)
val threePaneStrategy = rememberThreePaneSceneStrategy<NavKey>() val threePaneStrategy = rememberThreePaneSceneStrategy<NavKey>()
NavDisplay( NavDisplay(

View file

@ -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)

View file

@ -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

View file

@ -21,8 +21,8 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import kotlinx.coroutines.launch 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.AuthRepository
import moe.lava.neon.core.repository.GatewayRepository
import moe.lava.neon.resources.Res import moe.lava.neon.resources.Res
import moe.lava.neon.resources.compose_multiplatform import moe.lava.neon.resources.compose_multiplatform
import moe.lava.neon.ui.Greeting import moe.lava.neon.ui.Greeting
@ -84,26 +84,24 @@ fun Sample(
class SampleViewModel( class SampleViewModel(
private val auth: AuthRepository, private val auth: AuthRepository,
private val gateway: GatewayHandler, private val gateway: GatewayRepository,
) : ViewModel() { ) : ViewModel() {
private val logger = Logger.withTag("neon.ui.screens/Sample") private val logger = Logger.withTag("neon.ui.screens/Sample")
val token get() = auth.token val token get() = auth.token
fun connect() { fun connect() {
viewModelScope.launch { viewModelScope.launch {
try { val exception = gateway.start().exceptionOrNull()
gateway.connect() if (exception != null) {
} catch(e: Throwable) { logger.e(exception) { "Failed to connect to gateway: ${exception.stackTraceToString()}" }
logger.e(e) { "Failed to connect to gateway: ${e.stackTraceToString()}" }
} }
} }
} }
fun disconnect() { fun disconnect() {
viewModelScope.launch { viewModelScope.launch {
try { val exception = gateway.pause().exceptionOrNull()
gateway.disconnect() if (exception != null) {
} catch(e: Throwable) { logger.e(exception) { "Failed to disconnect from gateway: ${exception.stackTraceToString()}" }
logger.e(e) { "Failed to connect to gateway: ${e.stackTraceToString()}" }
} }
} }
} }

View file

@ -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())
}
}

View file

@ -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())
}
}