diff --git a/client-py/build.gradle.kts b/client-py/build.gradle.kts
new file mode 100644
index 0000000..0b281ee
--- /dev/null
+++ b/client-py/build.gradle.kts
@@ -0,0 +1,92 @@
+/*
+ * This file is part of Fhraise.
+ * Copyright (c) 2024 HSAS Foodies. All Rights Reserved.
+ *
+ * Fhraise is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * Fhraise is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with Fhraise. If not, see .
+ */
+
+import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
+import org.jetbrains.kotlin.gradle.plugin.mpp.SharedLibrary
+import xyz.xfqlittlefan.fhraise.buildsrc.projectVersion
+
+plugins {
+ alias(libs.plugins.kotlinMultiplatform)
+ alias(libs.plugins.kotlinSerialization)
+}
+
+group = "xyz.xfqlittlefan.fhraise"
+project.version = projectVersion
+
+kotlin {
+ val linuxTargets = mutableListOf()
+ val mingwTargets = mutableListOf()
+
+ linuxTargets += linuxArm64()
+ linuxTargets += linuxX64()
+
+ mingwTargets += mingwX64()
+
+ val defaultSharedLibConfigure: SharedLibrary.() -> Unit = {
+ export(project(":py"))
+ }
+
+ configure(linuxTargets) {
+ binaries {
+ sharedLib {
+ baseName = "fhraisepy"
+ defaultSharedLibConfigure()
+ }
+ }
+ }
+
+ configure(mingwTargets) {
+ binaries {
+ sharedLib {
+ baseName = "libfhraisepy"
+ defaultSharedLibConfigure()
+ }
+ }
+ }
+
+ applyDefaultHierarchyTemplate()
+
+ sourceSets {
+ val nativeMain by getting {
+ dependencies {
+ api(project(":py"))
+ implementation(libs.kotlinx.datetime)
+ implementation(libs.ktor.client.core)
+ implementation(libs.ktor.client.websockets)
+ implementation(libs.ktor.serialization.kotlinx.cbor)
+ }
+ }
+
+ val nonMingwMain by creating {
+ dependsOn(nativeMain)
+ dependencies {
+ implementation(libs.ktor.client.cio)
+ }
+ }
+
+ val mingwMain by getting {
+ dependencies {
+ implementation(libs.ktor.client.winhttp)
+ }
+ }
+
+ val linuxMain by getting {
+ dependsOn(nonMingwMain)
+ }
+ }
+}
diff --git a/client-py/src/nativeMain/kotlin/Client.kt b/client-py/src/nativeMain/kotlin/Client.kt
new file mode 100644
index 0000000..1a82a43
--- /dev/null
+++ b/client-py/src/nativeMain/kotlin/Client.kt
@@ -0,0 +1,110 @@
+/*
+ * This file is part of Fhraise.
+ * Copyright (c) 2024 HSAS Foodies. All Rights Reserved.
+ *
+ * Fhraise is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * Fhraise is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with Fhraise. If not, see .
+ */
+
+package xyz.xfqlittlefan.fhraise.py
+
+import io.ktor.client.*
+import io.ktor.client.plugins.websocket.*
+import io.ktor.serialization.kotlinx.*
+import kotlinx.cinterop.*
+import kotlinx.coroutines.*
+import kotlinx.coroutines.channels.Channel
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.cbor.Cbor
+import kotlin.coroutines.resume
+import kotlin.experimental.ExperimentalNativeApi
+
+class Client(private val host: String, private val port: UShort) {
+ private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
+ private val messageChannel = Channel(3)
+
+ @OptIn(ExperimentalForeignApi::class)
+ private val messageErrorChannel = Channel>(3)
+ private val resultChannel = Channel(3)
+
+ @OptIn(ExperimentalSerializationApi::class)
+ private val client = HttpClient {
+ install(WebSockets) {
+ contentConverter = KotlinxWebsocketSerializationConverter(Cbor)
+ }
+ }
+
+ @OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class)
+ fun connect(throwable: CPointer>?) = runBlocking {
+ logger.debug("Connecting to $host:$port.")
+ suspendCancellableCoroutine { continuation ->
+ logger.debug("Launching coroutine.")
+ scope.launch(Dispatchers.IO) {
+ runCatching(throwable) {
+ logger.debug("Starting connection.")
+ client.webSocket(host = host, port = port.toInt(), path = pyWsPath) {
+ continuation.resume(true)
+ logger.debug("Connected.")
+ while (true) {
+ logger.debug("Waiting for message...")
+ val receiveResult =
+ runCatching(null) { messageChannel.send(receiveDeserialized()) }.onFailure {
+ logger.error("Failed to receive message.")
+ it as ThrowableWrapper
+ messageErrorChannel.send(it.throwable)
+ messageChannel.send(null)
+ }
+ if (receiveResult.isFailure) continue
+ logger.debug("Waiting for result...")
+ sendSerialized(resultChannel.receive())
+ }
+ }
+ }.onFailure {
+ if (continuation.isActive) {
+ logger.error("Failed to connect.")
+ continuation.resume(false)
+ } else {
+ logger.error("Uncaught error in $this.")
+ throw it
+ }
+ }
+ }
+ }
+ }
+
+ @OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class)
+ fun receive(
+ type: CPointer>,
+ ref: CPointer,
+ throwable: CPointer>?,
+ getResult: CPointer CPointer<*>>>
+ ): Boolean {
+ val message = runBlocking { messageChannel.receive() }
+
+ if (message == null) {
+ return runBlocking {
+ throwable?.pointed?.value = messageErrorChannel.receive()
+ false
+ }
+ }
+
+ type.pointed.value = message::class.qualifiedName!!.cstrPtr
+ ref.pointed.value = StableRef.create(message).asCPointer()
+
+ return runBlocking {
+ runCatching(throwable) {
+ resultChannel.send(getResult().asStableRef().get())
+ }.isSuccess
+ }
+ }
+}
diff --git a/client-py/src/nativeMain/kotlin/Logger.kt b/client-py/src/nativeMain/kotlin/Logger.kt
new file mode 100644
index 0000000..a2940b2
--- /dev/null
+++ b/client-py/src/nativeMain/kotlin/Logger.kt
@@ -0,0 +1,50 @@
+/*
+ * This file is part of Fhraise.
+ * Copyright (c) 2024 HSAS Foodies. All Rights Reserved.
+ *
+ * Fhraise is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * Fhraise is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with Fhraise. If not, see .
+ */
+
+package xyz.xfqlittlefan.fhraise.py
+
+import kotlinx.datetime.Clock
+
+class Logger internal constructor(private val tag: Any) {
+ @Deprecated("This constructor is for calling from C code only.", level = DeprecationLevel.HIDDEN)
+ constructor(tag: String) : this(" $tag" as Any)
+
+ fun debug(message: String) {
+ println("Debug", message)
+ }
+
+ fun info(message: String) {
+ println("Info", message)
+ }
+
+ fun warn(message: String) {
+ println("Warn", message)
+ }
+
+ fun error(message: String) {
+ println("Error", message)
+ }
+
+ private fun println(level: String, message: String) {
+ message.split("\n").forEach {
+ println("${Clock.System.now()} [$tag] $level: $it")
+ }
+ }
+}
+
+internal val Any.logger: Logger get() = Logger(this::class.let { it.qualifiedName ?: it })
diff --git a/client-py/src/nativeMain/kotlin/Throwable.kt b/client-py/src/nativeMain/kotlin/Throwable.kt
new file mode 100644
index 0000000..3cc76bd
--- /dev/null
+++ b/client-py/src/nativeMain/kotlin/Throwable.kt
@@ -0,0 +1,95 @@
+/*
+ * This file is part of Fhraise.
+ * Copyright (c) 2024 HSAS Foodies. All Rights Reserved.
+ *
+ * Fhraise is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * Fhraise is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with Fhraise. If not, see .
+ */
+
+package xyz.xfqlittlefan.fhraise.py
+
+import kotlinx.cinterop.*
+import kotlin.experimental.ExperimentalNativeApi
+
+@OptIn(ExperimentalForeignApi::class)
+class ThrowableVar(rawPtr: NativePtr) : CStructVar(rawPtr) {
+ @Suppress("DEPRECATION")
+ companion object : Type(40, 8)
+
+ var type: CArrayPointer?
+ get() = memberAt>(0).value
+ set(value) {
+ memberAt>(0).value = value
+ }
+
+ var ref: COpaquePointer?
+ get() = memberAt(8).value
+ set(value) {
+ memberAt(8).value = value
+ }
+
+ var message: CArrayPointer?
+ get() = memberAt>(16).value
+ set(value) {
+ memberAt>(16).value = value
+ }
+
+ var stacktrace: CArrayPointer>?
+ get() = memberAt>>(24).value
+ set(value) {
+ memberAt>>(24).value = value
+ }
+
+ var stacktraceSize: Int?
+ get() = memberAt(32).value
+ set(value) {
+ memberAt(32).value = value ?: 0
+ }
+}
+
+@ExperimentalForeignApi
+internal class ThrowableWrapper(val throwable: CPointer) : Throwable()
+
+@ExperimentalNativeApi
+@ExperimentalForeignApi
+internal fun Throwable.wrapToC(): CPointer {
+ logger.debug("Wrapping throwable.")
+ logger.warn(getStackTrace().joinToString("\n"))
+
+ val throwable = nativeHeap.alloc()
+ throwable.type = this::class.qualifiedName?.cstrPtr
+ throwable.ref = StableRef.create(this).asCPointer()
+ throwable.message = this.message?.cstrPtr
+ val stacktraceList = mutableListOf()
+ this.getStackTrace().forEach {
+ stacktraceList.add(it.cstrPtr.pointed)
+ }
+ val stacktraceArray = nativeHeap.allocArrayOfPointersTo(stacktraceList)
+ throwable.stacktrace = stacktraceArray
+ throwable.stacktraceSize = stacktraceList.size
+ return throwable.ptr
+}
+
+@ExperimentalNativeApi
+@ExperimentalForeignApi
+internal inline fun runCatching(
+ throwablePtr: CPointer>?, block: () -> R
+) = runCatching(block).fold(onSuccess = {
+ Result.success(it)
+}, onFailure = {
+ val throwableVarPtr = it.wrapToC()
+ if (throwablePtr != null && throwablePtr.rawValue != nativeNullPtr) {
+ throwablePtr.pointed.value = throwableVarPtr
+ }
+ Result.failure(ThrowableWrapper(throwableVarPtr))
+})
diff --git a/client-py/src/nativeMain/kotlin/Types.kt b/client-py/src/nativeMain/kotlin/Types.kt
new file mode 100644
index 0000000..fe61a10
--- /dev/null
+++ b/client-py/src/nativeMain/kotlin/Types.kt
@@ -0,0 +1,30 @@
+/*
+ * This file is part of Fhraise.
+ * Copyright (c) 2024 HSAS Foodies. All Rights Reserved.
+ *
+ * Fhraise is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * Fhraise is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with Fhraise. If not, see .
+ */
+
+package xyz.xfqlittlefan.fhraise.py
+
+import kotlinx.cinterop.*
+
+@ExperimentalForeignApi
+internal val String.cstrPtr: CPointer
+ get() {
+ val cstr = cstr
+ val ptr = nativeHeap.allocArray(cstr.size)
+ cstr.place(ptr)
+ return ptr
+ }
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index cfeebf8..01d9592 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -3,6 +3,7 @@ kotlin = "1.9.23"
kotlinx-atomicfu = "0.23.2"
kotlinx-coroutines = "1.8.0"
kotlinx-datetime = "0.5.0"
+kotlinx-serilization = "1.6.3"
agp = "8.2.2"
androidx-core-splashscreen = "1.0.1"
androidx-window = "1.2.0"
@@ -30,6 +31,7 @@ kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-c
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
+kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serilization" }
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidx-core-splashscreen" }
androidx-window = { module = "androidx.window:window", version.ref = "androidx-window" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity-compose" }
@@ -42,6 +44,7 @@ decompose-extensions-compose = { module = "com.arkivanov.decompose:extensions-co
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" }
+ktor-client-winhttp = { module = "io.ktor:ktor-client-winhttp", version.ref = "ktor" }
ktor-client-resources = { module = "io.ktor:ktor-client-resources", version.ref = "ktor" }
ktor-client-auth = { module = "io.ktor:ktor-client-auth", version.ref = "ktor" }
ktor-client-contentNegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
diff --git a/py/build.gradle.kts b/py/build.gradle.kts
new file mode 100644
index 0000000..28da710
--- /dev/null
+++ b/py/build.gradle.kts
@@ -0,0 +1,45 @@
+/*
+ * This file is part of Fhraise.
+ * Copyright (c) 2024 HSAS Foodies. All Rights Reserved.
+ *
+ * Fhraise is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * Fhraise is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with Fhraise. If not, see .
+ */
+
+import xyz.xfqlittlefan.fhraise.buildsrc.projectVersion
+
+plugins {
+ alias(libs.plugins.kotlinMultiplatform)
+ alias(libs.plugins.kotlinSerialization)
+}
+
+group = "xyz.xfqlittlefan.fhraise"
+project.version = projectVersion
+
+kotlin {
+ jvm()
+
+ linuxArm64()
+ linuxX64()
+ mingwX64()
+
+ applyDefaultHierarchyTemplate()
+
+ sourceSets {
+ val commonMain by getting {
+ dependencies {
+ implementation(libs.kotlinx.serialization.core)
+ }
+ }
+ }
+}
diff --git a/py/src/commonMain/kotlin/Constants.kt b/py/src/commonMain/kotlin/Constants.kt
new file mode 100644
index 0000000..462da57
--- /dev/null
+++ b/py/src/commonMain/kotlin/Constants.kt
@@ -0,0 +1,21 @@
+/*
+ * This file is part of Fhraise.
+ * Copyright (c) 2024 HSAS Foodies. All Rights Reserved.
+ *
+ * Fhraise is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * Fhraise is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with Fhraise. If not, see .
+ */
+
+package xyz.xfqlittlefan.fhraise.py
+
+const val pyWsPath = "/internal/py/ws"
diff --git a/py/src/commonMain/kotlin/Message.kt b/py/src/commonMain/kotlin/Message.kt
new file mode 100644
index 0000000..636dd95
--- /dev/null
+++ b/py/src/commonMain/kotlin/Message.kt
@@ -0,0 +1,54 @@
+/*
+ * This file is part of Fhraise.
+ * Copyright (c) 2024 HSAS Foodies. All Rights Reserved.
+ *
+ * Fhraise is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * Fhraise is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with Fhraise. If not, see .
+ */
+
+package xyz.xfqlittlefan.fhraise.py
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+sealed class Message {
+ @Serializable
+ sealed class Register : Message() {
+ @Serializable
+ data class Frame(val callId: String, val content: ByteArray) : Register() {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || this::class != other::class) return false
+
+ other as Frame
+
+ if (callId != other.callId) return false
+ if (!content.contentEquals(other.content)) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = callId.hashCode()
+ result = 31 * result + content.contentHashCode()
+ return result
+ }
+ }
+
+ @Serializable
+ sealed class Result : Register() {
+ @Serializable
+ data object Success : Result()
+ }
+ }
+}
diff --git a/server/build.gradle.kts b/server/build.gradle.kts
index 84d86eb..ece21b1 100644
--- a/server/build.gradle.kts
+++ b/server/build.gradle.kts
@@ -34,6 +34,7 @@ application {
}
dependencies {
+ implementation(project(":py"))
implementation(projects.shared)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.ktor.server.core)
diff --git a/server/src/main/kotlin/Application.kt b/server/src/main/kotlin/Application.kt
index cc59701..84adbb2 100644
--- a/server/src/main/kotlin/Application.kt
+++ b/server/src/main/kotlin/Application.kt
@@ -41,6 +41,7 @@ import xyz.xfqlittlefan.fhraise.api.registerAppCodeVerification
import xyz.xfqlittlefan.fhraise.auth.cleanupUnverifiedUsersPreDay
import xyz.xfqlittlefan.fhraise.models.cleanupVerificationCodes
import xyz.xfqlittlefan.fhraise.proxy.proxyKeycloak
+import xyz.xfqlittlefan.fhraise.py.py
fun main() {
embeddedServer(CIO, port = defaultServerPort, host = "0.0.0.0", module = Application::module).start(wait = true)
@@ -72,6 +73,7 @@ fun Application.module() {
cleanupVerificationCodes()
routing {
+ py()
proxyKeycloak()
apiAuth()
apiOAuth()
diff --git a/server/src/main/kotlin/py/Py.kt b/server/src/main/kotlin/py/Py.kt
new file mode 100644
index 0000000..3448c7d
--- /dev/null
+++ b/server/src/main/kotlin/py/Py.kt
@@ -0,0 +1,29 @@
+/*
+ * This file is part of Fhraise.
+ * Copyright (c) 2024 HSAS Foodies. All Rights Reserved.
+ *
+ * Fhraise is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * Fhraise is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with Fhraise. If not, see .
+ */
+
+package xyz.xfqlittlefan.fhraise.py
+
+import io.ktor.server.routing.*
+import io.ktor.server.websocket.*
+
+fun Route.py() {
+ webSocket(pyWsPath) {
+ sendSerialized(Message.Register.Frame("id", "hello".encodeToByteArray()))
+ receiveDeserialized()
+ }
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 9c84498..2d6b78f 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -35,7 +35,11 @@ dependencyResolutionManagement {
}
}
+include(":shared")
+
include(":compose-app")
include(":server")
-include(":shared")
+include(":py")
+include(":client-py")
+
include(":keycloak-spi")