refactor: split up core into multiple modules
This commit is contained in:
parent
2725342c3f
commit
0d84411f14
38 changed files with 344 additions and 149 deletions
65
api/gateway/build.gradle.kts
Normal file
65
api/gateway/build.gradle.kts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -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" }
|
||||||
}
|
}
|
||||||
|
|
@ -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) {
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -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>()
|
||||||
|
|
@ -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
65
api/rest/build.gradle.kts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
50
api/shared/build.gradle.kts
Normal file
50
api/shared/build.gradle.kts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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}",
|
||||||
)
|
)
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -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
14
common/build.gradle.kts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
|
alias(libs.plugins.kotlinSerialization)
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvm()
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
commonMain.dependencies {
|
||||||
|
implementation(libs.kotlinx.serialization.core)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
)
|
|
||||||
|
|
@ -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,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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>()
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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() }
|
||||||
|
}
|
||||||
|
|
@ -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" }
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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()}" }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
13
ui/src/jvmMain/kotlin/moe/lava/neon/ui/CaptchaHandler.jvm.kt
Normal file
13
ui/src/jvmMain/kotlin/moe/lava/neon/ui/CaptchaHandler.jvm.kt
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue