From 79ffd46a70ffedcfe81c6d405a382943c2defb72 Mon Sep 17 00:00:00 2001 From: Lanius Trolling Date: Tue, 17 Dec 2024 21:41:27 -0500 Subject: [PATCH] Update dependencies, fix stuff, and move custom fonts to VFS --- .idea/artifacts/map_view_jvm.xml | 8 + .idea/artifacts/map_view_map.xml | 8 + .idea/gradle.xml | 1 + .idea/kotlinc.xml | 2 +- build.gradle.kts | 194 +- gradle/wrapper/gradle-wrapper.properties | 2 +- map-view/build.gradle.kts | 83 + .../kotlin/info/mechyrdia/mapviewer/assets.kt | 0 .../kotlin/info/mechyrdia/mapviewer/codec.kt | 0 .../kotlin/info/mechyrdia/mapviewer/data.kt | 0 .../info/mechyrdia/mapviewer/entryPoint.kt | 0 .../info/mechyrdia/mapviewer/history.kt | 0 .../kotlin/info/mechyrdia/mapviewer/math.kt | 0 .../kotlin/info/mechyrdia/mapviewer/modal.kt | 0 .../kotlin/info/mechyrdia/mapviewer/render.kt | 0 .../kotlin/info/mechyrdia/mapviewer/search.kt | 0 .../kotlin/info/mechyrdia/mapviewer/utils.kt | 0 settings.gradle.kts | 1 + .../font/kishari-language-alphabet.woff | Bin 2800 -> 0 bytes .../font/pokhval-language-alphabet.woff | Bin 2884 -> 0 bytes .../font/thedish-language-alphabet.woff | Bin 4940 -> 0 bytes .../font/tylan-language-alphabet-3.woff | Bin 4692 -> 0 bytes .../kotlin/info/mechyrdia/BlockingCode.kt | 0 .../kotlin/info/mechyrdia/Configuration.kt | 132 +- .../kotlin/info/mechyrdia/Factbooks.kt | 3 +- .../kotlin/info/mechyrdia/JSON.kt | 0 .../kotlin/info/mechyrdia/Utils.kt | 8 + .../info/mechyrdia/auth/NationStates.kt} | 0 .../info/mechyrdia/auth/SessionStorage.kt | 0 .../kotlin/info/mechyrdia/auth/Sessions.kt | 0 .../kotlin/info/mechyrdia/auth/ViewsLogin.kt | 0 .../kotlin/info/mechyrdia/auth/WebDav.kt | 0 .../kotlin/info/mechyrdia/data/Bson.kt | 0 .../kotlin/info/mechyrdia/data/Comments.kt | 0 .../kotlin/info/mechyrdia/data/Data.kt | 0 .../kotlin/info/mechyrdia/data/DataFiles.kt | 0 .../info/mechyrdia/data/MigrateFiles.kt | 0 .../info/mechyrdia/data/MigrateFilesSerial.kt | 0 .../kotlin/info/mechyrdia/data/Nations.kt | 0 .../info/mechyrdia/data/ViewComments.kt | 0 .../info/mechyrdia/data/ViewsComment.kt | 0 .../kotlin/info/mechyrdia/data/ViewsFiles.kt | 10 +- .../kotlin/info/mechyrdia/data/ViewsUser.kt | 0 .../kotlin/info/mechyrdia/data/Visits.kt | 0 .../kotlin/info/mechyrdia/data/Xml.kt | 0 .../kotlin/info/mechyrdia/lore/April1st.kt | 0 .../info/mechyrdia/lore/ArticleListing.kt | 0 .../info/mechyrdia/lore/ArticleTitles.kt | 0 .../info/mechyrdia/lore/AssetCaching.kt | 0 .../info/mechyrdia/lore/AssetCompression.kt | 0 .../info/mechyrdia/lore/AssetHashing.kt | 0 .../kotlin/info/mechyrdia/lore/FileData.kt | 0 .../kotlin/info/mechyrdia/lore/FontAssets.kt | 70 + .../info/mechyrdia/lore/FontDrawing.kt} | 0 .../kotlin/info/mechyrdia/lore/HttpUtils.kt | 0 .../info/mechyrdia/lore/ParserBuilder.kt | 0 .../kotlin/info/mechyrdia/lore/ParserHtml.kt | 0 .../kotlin/info/mechyrdia/lore/ParserLexer.kt | 0 .../info/mechyrdia/lore/ParserLexerAsync.kt | 0 .../kotlin/info/mechyrdia/lore/ParserPlain.kt | 0 .../info/mechyrdia/lore/ParserPreprocess.kt | 0 .../mechyrdia/lore/ParserPreprocessInclude.kt | 0 .../mechyrdia/lore/ParserPreprocessJson.kt | 0 .../mechyrdia/lore/ParserPreprocessMath.kt | 0 .../kotlin/info/mechyrdia/lore/ParserRaw.kt | 0 .../kotlin/info/mechyrdia/lore/ParserRobot.kt | 0 .../kotlin/info/mechyrdia/lore/ParserTree.kt | 0 .../kotlin/info/mechyrdia/lore/ParserUtils.kt | 0 .../kotlin/info/mechyrdia/lore/ViewBar.kt | 0 .../kotlin/info/mechyrdia/lore/ViewMap.kt | 0 .../kotlin/info/mechyrdia/lore/ViewNav.kt | 6 +- .../kotlin/info/mechyrdia/lore/ViewOg.kt | 0 .../kotlin/info/mechyrdia/lore/ViewTpl.kt | 5 +- .../kotlin/info/mechyrdia/lore/ViewsError.kt | 0 .../kotlin/info/mechyrdia/lore/ViewsLore.kt | 12 +- .../kotlin/info/mechyrdia/lore/ViewsPrefs.kt | 0 .../kotlin/info/mechyrdia/lore/ViewsQuote.kt | 0 .../kotlin/info/mechyrdia/lore/ViewsRobots.kt | 0 .../kotlin/info/mechyrdia/lore/ViewsRss.kt | 0 .../kotlin/info/mechyrdia/robot/RobotApi.kt | 0 .../kotlin/info/mechyrdia/robot/RobotCodec.kt | 0 .../kotlin/info/mechyrdia/robot/RobotFiles.kt | 0 .../info/mechyrdia/robot/RobotRateLimiter.kt | 0 .../info/mechyrdia/robot/RobotSchema.kt | 0 .../info/mechyrdia/robot/RobotService.kt | 942 ++++---- .../kotlin/info/mechyrdia/robot/RobotSse.kt | 0 .../info/mechyrdia/robot/RobotUserLimiter.kt | 0 .../kotlin/info/mechyrdia/robot/ViewsRobot.kt | 4 +- .../info/mechyrdia/route/ResourceBodies.kt | 0 .../info/mechyrdia/route/ResourceCsrf.kt | 0 .../info/mechyrdia/route/ResourceHandler.kt | 36 +- .../info/mechyrdia/route/ResourceMultipart.kt | 5 +- .../info/mechyrdia/route/ResourceTypes.kt | 113 +- .../info/mechyrdia/route/ResourceWebDav.kt | 1 - src/{jvmMain => main}/resources/logback.xml | 0 .../resources/static/admin.css | 0 .../resources/static/admin.js | 0 .../static/font/DejaVuSans-Bold.woff | Bin .../static/font/DejaVuSans-BoldOblique.woff | Bin .../static/font/DejaVuSans-Oblique.woff | Bin .../resources/static/font/DejaVuSans.woff | Bin .../static/font/JetBrainsMono-ExtraBold.woff | Bin .../font/JetBrainsMono-ExtraBoldItalic.woff | Bin .../static/font/JetBrainsMono-Medium.woff | Bin .../font/JetBrainsMono-MediumItalic.woff | Bin .../resources/static/font/Oxanium-Bold.woff | Bin .../static/font/Oxanium-ExtraBold.woff | Bin .../static/font/Oxanium-Regular.woff | Bin .../static/font/Oxanium-SemiBold.woff | Bin .../static/images/external-link-dark.png | Bin .../resources/static/images/external-link.png | Bin .../resources/static/images/icon.png | Bin .../resources/static/images/icon.svg | 0 .../resources/static/init.js | 17 +- .../resources/static/obj-viewer/hammer.min.js | 0 .../static/obj-viewer/three-examples.js | 0 .../resources/static/obj-viewer/three.js | 0 .../resources/static/raw.css | 0 .../resources/static/style.css | 72 - stuff/mintah_table.txt | 2027 +++++++++++++++++ stuff/mintah_table_script.py | 121 + 121 files changed, 3043 insertions(+), 840 deletions(-) create mode 100644 .idea/artifacts/map_view_jvm.xml create mode 100644 .idea/artifacts/map_view_map.xml create mode 100644 map-view/build.gradle.kts rename {src => map-view/src}/mapMain/kotlin/info/mechyrdia/mapviewer/assets.kt (100%) rename {src => map-view/src}/mapMain/kotlin/info/mechyrdia/mapviewer/codec.kt (100%) rename {src => map-view/src}/mapMain/kotlin/info/mechyrdia/mapviewer/data.kt (100%) rename {src => map-view/src}/mapMain/kotlin/info/mechyrdia/mapviewer/entryPoint.kt (100%) rename {src => map-view/src}/mapMain/kotlin/info/mechyrdia/mapviewer/history.kt (100%) rename {src => map-view/src}/mapMain/kotlin/info/mechyrdia/mapviewer/math.kt (100%) rename {src => map-view/src}/mapMain/kotlin/info/mechyrdia/mapviewer/modal.kt (100%) rename {src => map-view/src}/mapMain/kotlin/info/mechyrdia/mapviewer/render.kt (100%) rename {src => map-view/src}/mapMain/kotlin/info/mechyrdia/mapviewer/search.kt (100%) rename {src => map-view/src}/mapMain/kotlin/info/mechyrdia/mapviewer/utils.kt (100%) delete mode 100644 src/jvmMain/resources/static/font/kishari-language-alphabet.woff delete mode 100644 src/jvmMain/resources/static/font/pokhval-language-alphabet.woff delete mode 100644 src/jvmMain/resources/static/font/thedish-language-alphabet.woff delete mode 100644 src/jvmMain/resources/static/font/tylan-language-alphabet-3.woff rename src/{jvmMain => main}/kotlin/info/mechyrdia/BlockingCode.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/Configuration.kt (96%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/Factbooks.kt (99%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/JSON.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/Utils.kt (69%) rename src/{jvmMain/kotlin/info/mechyrdia/auth/nationstates.kt => main/kotlin/info/mechyrdia/auth/NationStates.kt} (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/auth/SessionStorage.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/auth/Sessions.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/auth/ViewsLogin.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/auth/WebDav.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/data/Bson.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/data/Comments.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/data/Data.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/data/DataFiles.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/data/MigrateFiles.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/data/MigrateFilesSerial.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/data/Nations.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/data/ViewComments.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/data/ViewsComment.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/data/ViewsFiles.kt (97%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/data/ViewsUser.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/data/Visits.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/data/Xml.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/lore/April1st.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/lore/ArticleListing.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/lore/ArticleTitles.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/lore/AssetCaching.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/lore/AssetCompression.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/lore/AssetHashing.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/lore/FileData.kt (100%) create mode 100644 src/main/kotlin/info/mechyrdia/lore/FontAssets.kt rename src/{jvmMain/kotlin/info/mechyrdia/lore/Fonts.kt => main/kotlin/info/mechyrdia/lore/FontDrawing.kt} (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/lore/HttpUtils.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/lore/ParserBuilder.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/lore/ParserHtml.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/lore/ParserLexer.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/lore/ParserLexerAsync.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/lore/ParserPlain.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/lore/ParserPreprocess.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/lore/ParserPreprocessInclude.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/lore/ParserPreprocessJson.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/lore/ParserPreprocessMath.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/lore/ParserRaw.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/lore/ParserRobot.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/lore/ParserTree.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/lore/ParserUtils.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/lore/ViewBar.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/lore/ViewMap.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/lore/ViewNav.kt (95%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/lore/ViewOg.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/lore/ViewTpl.kt (96%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/lore/ViewsError.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/lore/ViewsLore.kt (93%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/lore/ViewsPrefs.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/lore/ViewsQuote.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/lore/ViewsRobots.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/lore/ViewsRss.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/robot/RobotApi.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/robot/RobotCodec.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/robot/RobotFiles.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/robot/RobotRateLimiter.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/robot/RobotSchema.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/robot/RobotService.kt (96%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/robot/RobotSse.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/robot/RobotUserLimiter.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/robot/ViewsRobot.kt (96%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/route/ResourceBodies.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/route/ResourceCsrf.kt (100%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/route/ResourceHandler.kt (81%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/route/ResourceMultipart.kt (92%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/route/ResourceTypes.kt (79%) rename src/{jvmMain => main}/kotlin/info/mechyrdia/route/ResourceWebDav.kt (99%) rename src/{jvmMain => main}/resources/logback.xml (100%) rename src/{jvmMain => main}/resources/static/admin.css (100%) rename src/{jvmMain => main}/resources/static/admin.js (100%) rename src/{jvmMain => main}/resources/static/font/DejaVuSans-Bold.woff (100%) rename src/{jvmMain => main}/resources/static/font/DejaVuSans-BoldOblique.woff (100%) rename src/{jvmMain => main}/resources/static/font/DejaVuSans-Oblique.woff (100%) rename src/{jvmMain => main}/resources/static/font/DejaVuSans.woff (100%) rename src/{jvmMain => main}/resources/static/font/JetBrainsMono-ExtraBold.woff (100%) rename src/{jvmMain => main}/resources/static/font/JetBrainsMono-ExtraBoldItalic.woff (100%) rename src/{jvmMain => main}/resources/static/font/JetBrainsMono-Medium.woff (100%) rename src/{jvmMain => main}/resources/static/font/JetBrainsMono-MediumItalic.woff (100%) rename src/{jvmMain => main}/resources/static/font/Oxanium-Bold.woff (100%) rename src/{jvmMain => main}/resources/static/font/Oxanium-ExtraBold.woff (100%) rename src/{jvmMain => main}/resources/static/font/Oxanium-Regular.woff (100%) rename src/{jvmMain => main}/resources/static/font/Oxanium-SemiBold.woff (100%) rename src/{jvmMain => main}/resources/static/images/external-link-dark.png (100%) rename src/{jvmMain => main}/resources/static/images/external-link.png (100%) rename src/{jvmMain => main}/resources/static/images/icon.png (100%) rename src/{jvmMain => main}/resources/static/images/icon.svg (100%) rename src/{jvmMain => main}/resources/static/init.js (99%) rename src/{jvmMain => main}/resources/static/obj-viewer/hammer.min.js (100%) rename src/{jvmMain => main}/resources/static/obj-viewer/three-examples.js (100%) rename src/{jvmMain => main}/resources/static/obj-viewer/three.js (100%) rename src/{jvmMain => main}/resources/static/raw.css (100%) rename src/{jvmMain => main}/resources/static/style.css (90%) create mode 100644 stuff/mintah_table.txt create mode 100644 stuff/mintah_table_script.py diff --git a/.idea/artifacts/map_view_jvm.xml b/.idea/artifacts/map_view_jvm.xml new file mode 100644 index 0000000..27851cb --- /dev/null +++ b/.idea/artifacts/map_view_jvm.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/map-view/build/libs + + + + + \ No newline at end of file diff --git a/.idea/artifacts/map_view_map.xml b/.idea/artifacts/map_view_map.xml new file mode 100644 index 0000000..5f13dd9 --- /dev/null +++ b/.idea/artifacts/map_view_map.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/map-view/build/libs + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 08d6d55..5332bcf 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -11,6 +11,7 @@ diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index fe63bb6..bb44937 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 72c2a88..aa3fb20 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,10 +3,6 @@ import com.nixxcode.jvmbrotli.common.BrotliLoader import com.nixxcode.jvmbrotli.enc.BrotliOutputStream import com.nixxcode.jvmbrotli.enc.Encoder import groovy.json.JsonSlurper -import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension -import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpack -import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig -import org.jetbrains.kotlin.gradle.targets.js.webpack.WebpackDevtool import java.util.concurrent.CountDownLatch import java.util.concurrent.Executors import java.util.zip.GZIPOutputStream @@ -28,9 +24,10 @@ buildscript { plugins { java - kotlin("multiplatform") version "2.0.10" - kotlin("plugin.serialization") version "2.0.10" - id("com.github.johnrengelman.shadow") version "7.1.2" + kotlin("jvm") version "2.1.0" + kotlin("plugin.serialization") version "2.1.0" + kotlin("multiplatform") version "2.1.0" apply false + id("com.github.johnrengelman.shadow") version "8.1.1" application } @@ -44,137 +41,65 @@ val isDevMode by project.extra { (configFile["isDevMode"] as? Boolean) ?: false } -val browserWebpackSuffix by project.extra { - if (isDevMode) - "BrowserDevelopmentWebpack" - else - "BrowserProductionWebpack" -} - -fun KotlinMultiplatformExtension.jsConfigured(name: String) { - val isDevMode: Boolean by project.extra - - js(name) { - browser { - val fileName = "$name.js" - - commonWebpackConfig { - outputFileName = fileName - if (isDevMode) { - mode = KotlinWebpackConfig.Mode.DEVELOPMENT - devtool = WebpackDevtool.SOURCE_MAP - sourceMaps = true - } else { - mode = KotlinWebpackConfig.Mode.PRODUCTION - devtool = null - sourceMaps = false - } - } - - webpackTask { - mainOutputFileName.set(fileName) - if (isDevMode) { - mode = KotlinWebpackConfig.Mode.DEVELOPMENT - devtool = WebpackDevtool.SOURCE_MAP - sourceMaps = true - } else { - mode = KotlinWebpackConfig.Mode.PRODUCTION - sourceMaps = false - } - } - } - - binaries.executable() - } -} - repositories { mavenCentral() } -kotlin { - jsConfigured("map") - jvmToolchain(17) - jvm("jvm") { - withJava() - } +dependencies { + implementation(kotlin("stdlib")) + implementation(kotlin("stdlib-jdk7")) + implementation(kotlin("stdlib-jdk8")) + implementation(kotlin("reflect")) - sourceSets { - all { - languageSettings { - optIn("kotlin.RequiresOptIn") - } - } - - val mapMain by getting { - dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") - implementation("org.jetbrains.kotlinx:kotlinx-html-js:0.11.0") - - implementation(project(":externals")) - } - } - - val jvmMain by getting { - dependencies { - implementation(kotlin("stdlib")) - implementation(kotlin("stdlib-jdk7")) - implementation(kotlin("stdlib-jdk8")) - implementation(kotlin("reflect")) - - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.1") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.8.1") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactive:1.8.1") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.7.1") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.7.1") - - implementation("io.ktor:ktor-server-core-jvm:2.3.12") - implementation("io.ktor:ktor-server-cio-jvm:2.3.12") - - implementation("io.ktor:ktor-server-auto-head-response:2.3.12") - implementation("io.ktor:ktor-server-caching-headers:2.3.12") - implementation("io.ktor:ktor-server-call-id:2.3.12") - implementation("io.ktor:ktor-server-call-logging:2.3.12") - implementation("io.ktor:ktor-server-conditional-headers:2.3.12") - implementation("io.ktor:ktor-server-content-negotiation:2.3.12") - implementation("io.ktor:ktor-server-default-headers:2.3.12") - implementation("io.ktor:ktor-server-forwarded-header:2.3.12") - implementation("io.ktor:ktor-server-html-builder:2.3.12") - implementation("io.ktor:ktor-server-resources:2.3.12") - implementation("io.ktor:ktor-server-sessions-jvm:2.3.12") - implementation("io.ktor:ktor-server-status-pages:2.3.12") - implementation("io.ktor:ktor-server-websockets:2.3.12") - - implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.12") - - implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.11.0") - - implementation("org.apache.groovy:groovy-jsr223:4.0.22") - - implementation(files("libs/nsapi4j.jar")) - - implementation("com.aventrix.jnanoid:jnanoid:2.0.0") - implementation("org.mongodb:mongodb-driver-kotlin-coroutine:5.0.0") - implementation("org.mongodb:bson-kotlinx:5.0.0") - - implementation("org.slf4j:slf4j-api:2.0.7") - implementation("ch.qos.logback:logback-classic:1.4.14") - - implementation("com.aallam.ktoken:ktoken:0.4.0") - - implementation("io.ktor:ktor-client-core:2.3.12") - implementation("io.ktor:ktor-client-java:2.3.12") - implementation("io.ktor:ktor-client-content-negotiation:2.3.12") - implementation("io.ktor:ktor-client-logging:2.3.12") - - implementation(project(":fontparser")) - } - } - } + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.9.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.9.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactive:1.9.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.7.3") + + implementation("io.ktor:ktor-server-core-jvm:3.0.2") + implementation("io.ktor:ktor-server-cio-jvm:3.0.2") + + implementation("io.ktor:ktor-server-auto-head-response:3.0.2") + implementation("io.ktor:ktor-server-caching-headers:3.0.2") + implementation("io.ktor:ktor-server-call-id:3.0.2") + implementation("io.ktor:ktor-server-call-logging:3.0.2") + implementation("io.ktor:ktor-server-conditional-headers:3.0.2") + implementation("io.ktor:ktor-server-content-negotiation:3.0.2") + implementation("io.ktor:ktor-server-default-headers:3.0.2") + implementation("io.ktor:ktor-server-forwarded-header:3.0.2") + implementation("io.ktor:ktor-server-html-builder:3.0.2") + implementation("io.ktor:ktor-server-resources:3.0.2") + implementation("io.ktor:ktor-server-sessions-jvm:3.0.2") + implementation("io.ktor:ktor-server-status-pages:3.0.2") + implementation("io.ktor:ktor-server-websockets:3.0.2") + + implementation("io.ktor:ktor-serialization-kotlinx-json:3.0.2") + + implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.11.0") + + implementation("org.apache.groovy:groovy-jsr223:4.0.22") + + implementation(files("libs/nsapi4j.jar")) + + implementation("com.aventrix.jnanoid:jnanoid:2.0.0") + implementation("org.mongodb:mongodb-driver-kotlin-coroutine:5.0.0") + implementation("org.mongodb:bson-kotlinx:5.0.0") + + implementation("org.slf4j:slf4j-api:2.0.7") + implementation("ch.qos.logback:logback-classic:1.4.14") + + implementation("com.aallam.ktoken:ktoken:0.4.0") + + implementation("io.ktor:ktor-client-core:3.0.2") + implementation("io.ktor:ktor-client-java:3.0.2") + implementation("io.ktor:ktor-client-content-negotiation:3.0.2") + implementation("io.ktor:ktor-client-logging:3.0.2") + + implementation(project(":fontparser")) } -tasks.named("jvmProcessResources") { +tasks.named("processResources") { doLast { val pool = Executors.newWorkStealingPool() @@ -212,16 +137,9 @@ application { mainClass.set("info.mechyrdia.Factbooks") } -fun Task.buildJsAsset(name: String) { - val browserWebpackSuffix: String by project.extra - val browserWebpackTask by tasks.named("$name$browserWebpackSuffix") - dependsOn(browserWebpackTask) -} - tasks.withType { mergeServiceFiles() exclude { it.name == "module-info.class" } - buildJsAsset("map") } tasks.register("migrateToGridFs", JavaExec::class) { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 02c0802..ba9ccfe 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists \ No newline at end of file diff --git a/map-view/build.gradle.kts b/map-view/build.gradle.kts new file mode 100644 index 0000000..715e444 --- /dev/null +++ b/map-view/build.gradle.kts @@ -0,0 +1,83 @@ +import groovy.json.JsonSlurper +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig +import org.jetbrains.kotlin.gradle.targets.js.webpack.WebpackDevtool + +plugins { + kotlin("multiplatform") + kotlin("plugin.serialization") +} + +group = "info.mechyrdia" + +val configFile by project.extra { + (JsonSlurper().parse(File(rootDir, "config.json")) as Map<*, *>).mapKeys { (k, _) -> k.toString() } +} + +val isDevMode by project.extra { + (configFile["isDevMode"] as? Boolean) ?: false +} + +fun KotlinMultiplatformExtension.jsConfigured(name: String) { + val isDevMode: Boolean by project.extra + + js(name) { + browser { + val fileName = "$name.js" + + commonWebpackConfig { + outputFileName = fileName + if (isDevMode) { + mode = KotlinWebpackConfig.Mode.DEVELOPMENT + devtool = WebpackDevtool.SOURCE_MAP + sourceMaps = true + } else { + mode = KotlinWebpackConfig.Mode.PRODUCTION + devtool = null + sourceMaps = false + } + } + + webpackTask { + mainOutputFileName.set(fileName) + if (isDevMode) { + mode = KotlinWebpackConfig.Mode.DEVELOPMENT + devtool = WebpackDevtool.SOURCE_MAP + sourceMaps = true + } else { + mode = KotlinWebpackConfig.Mode.PRODUCTION + sourceMaps = false + } + } + } + + binaries.executable() + } +} + +repositories { + mavenCentral() +} + +kotlin { + jsConfigured("map") + jvm("jvm") {} + + sourceSets { + all { + languageSettings { + optIn("kotlin.RequiresOptIn") + } + } + + val mapMain by getting { + dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-html-js:0.11.0") + + implementation(project(":externals")) + } + } + } +} diff --git a/src/mapMain/kotlin/info/mechyrdia/mapviewer/assets.kt b/map-view/src/mapMain/kotlin/info/mechyrdia/mapviewer/assets.kt similarity index 100% rename from src/mapMain/kotlin/info/mechyrdia/mapviewer/assets.kt rename to map-view/src/mapMain/kotlin/info/mechyrdia/mapviewer/assets.kt diff --git a/src/mapMain/kotlin/info/mechyrdia/mapviewer/codec.kt b/map-view/src/mapMain/kotlin/info/mechyrdia/mapviewer/codec.kt similarity index 100% rename from src/mapMain/kotlin/info/mechyrdia/mapviewer/codec.kt rename to map-view/src/mapMain/kotlin/info/mechyrdia/mapviewer/codec.kt diff --git a/src/mapMain/kotlin/info/mechyrdia/mapviewer/data.kt b/map-view/src/mapMain/kotlin/info/mechyrdia/mapviewer/data.kt similarity index 100% rename from src/mapMain/kotlin/info/mechyrdia/mapviewer/data.kt rename to map-view/src/mapMain/kotlin/info/mechyrdia/mapviewer/data.kt diff --git a/src/mapMain/kotlin/info/mechyrdia/mapviewer/entryPoint.kt b/map-view/src/mapMain/kotlin/info/mechyrdia/mapviewer/entryPoint.kt similarity index 100% rename from src/mapMain/kotlin/info/mechyrdia/mapviewer/entryPoint.kt rename to map-view/src/mapMain/kotlin/info/mechyrdia/mapviewer/entryPoint.kt diff --git a/src/mapMain/kotlin/info/mechyrdia/mapviewer/history.kt b/map-view/src/mapMain/kotlin/info/mechyrdia/mapviewer/history.kt similarity index 100% rename from src/mapMain/kotlin/info/mechyrdia/mapviewer/history.kt rename to map-view/src/mapMain/kotlin/info/mechyrdia/mapviewer/history.kt diff --git a/src/mapMain/kotlin/info/mechyrdia/mapviewer/math.kt b/map-view/src/mapMain/kotlin/info/mechyrdia/mapviewer/math.kt similarity index 100% rename from src/mapMain/kotlin/info/mechyrdia/mapviewer/math.kt rename to map-view/src/mapMain/kotlin/info/mechyrdia/mapviewer/math.kt diff --git a/src/mapMain/kotlin/info/mechyrdia/mapviewer/modal.kt b/map-view/src/mapMain/kotlin/info/mechyrdia/mapviewer/modal.kt similarity index 100% rename from src/mapMain/kotlin/info/mechyrdia/mapviewer/modal.kt rename to map-view/src/mapMain/kotlin/info/mechyrdia/mapviewer/modal.kt diff --git a/src/mapMain/kotlin/info/mechyrdia/mapviewer/render.kt b/map-view/src/mapMain/kotlin/info/mechyrdia/mapviewer/render.kt similarity index 100% rename from src/mapMain/kotlin/info/mechyrdia/mapviewer/render.kt rename to map-view/src/mapMain/kotlin/info/mechyrdia/mapviewer/render.kt diff --git a/src/mapMain/kotlin/info/mechyrdia/mapviewer/search.kt b/map-view/src/mapMain/kotlin/info/mechyrdia/mapviewer/search.kt similarity index 100% rename from src/mapMain/kotlin/info/mechyrdia/mapviewer/search.kt rename to map-view/src/mapMain/kotlin/info/mechyrdia/mapviewer/search.kt diff --git a/src/mapMain/kotlin/info/mechyrdia/mapviewer/utils.kt b/map-view/src/mapMain/kotlin/info/mechyrdia/mapviewer/utils.kt similarity index 100% rename from src/mapMain/kotlin/info/mechyrdia/mapviewer/utils.kt rename to map-view/src/mapMain/kotlin/info/mechyrdia/mapviewer/utils.kt diff --git a/settings.gradle.kts b/settings.gradle.kts index a67fe34..a171981 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,6 +1,7 @@ rootProject.name = "factbooks" +include("map-view") include("externals") include("fontparser") //include("fightgame") diff --git a/src/jvmMain/resources/static/font/kishari-language-alphabet.woff b/src/jvmMain/resources/static/font/kishari-language-alphabet.woff deleted file mode 100644 index 949b56e59d03df617a1b1c3ccf29e3b71ea8c477..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2800 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%0D3pt|>s696O_2$V7(9#3h!9Pv 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 M6ZjGrDZaAxAL#f4ApigX diff --git a/src/jvmMain/resources/static/font/tylan-language-alphabet-3.woff b/src/jvmMain/resources/static/font/tylan-language-alphabet-3.woff deleted file mode 100644 index 313bb3f7368d8b934a464fc799c08210cc4d198c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4692 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 - get() = Id(Configuration.Current.ownerNation) - -const val MainDomainName = "https://mechyrdia.info" +package info.mechyrdia + +import info.mechyrdia.data.Id +import info.mechyrdia.data.NationData +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import java.io.File + +@Serializable +sealed class FileStorageConfig { + @Serializable + @SerialName("flat") + data class Flat(val baseDir: String) : FileStorageConfig() + + @Serializable + @SerialName("gridFS") + data object GridFs : FileStorageConfig() +} + +@Serializable +data class OpenAiConfig( + val token: String, + val orgId: String, + val project: String? = null, + val assistantModel: String, + val assistantName: String = "Natural-language Universal Knowledge Engine", + val assistantInstructions: String = "You are a helpful interactive encyclopedia, able to answer questions with information from the provided files", + val assistantTemperature: Double = 1.0, +) + +@Serializable +data class Configuration( + val host: String = "127.0.0.1", + val port: Int = 8080, + + val isDevMode: Boolean = false, + + val storage: FileStorageConfig = FileStorageConfig.Flat(".."), + + val dbName: String = "nslore", + val dbConn: String = "mongodb://localhost:27017", + + val ownerNation: String = "mechyrdia", + val emergencyPassword: String? = null, + + val openAi: OpenAiConfig? = null, +) { + companion object { + val Current: Configuration by lazy { + val file = File(System.getProperty("info.mechyrdia.configpath", "./config.json")) + if (!file.isFile) { + if (file.exists()) + file.deleteRecursively() + + file.writeText(JsonFileCodec.encodeToString(serializer(), Configuration()), Charsets.UTF_8) + } + + JsonFileCodec.decodeFromString(serializer(), file.readText(Charsets.UTF_8)) + } + } +} + +val OwnerNationId: Id + get() = Id(Configuration.Current.ownerNation) + +const val MainDomainName = "https://mechyrdia.info" diff --git a/src/jvmMain/kotlin/info/mechyrdia/Factbooks.kt b/src/main/kotlin/info/mechyrdia/Factbooks.kt similarity index 99% rename from src/jvmMain/kotlin/info/mechyrdia/Factbooks.kt rename to src/main/kotlin/info/mechyrdia/Factbooks.kt index 2d5eee5..5df15c3 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/Factbooks.kt +++ b/src/main/kotlin/info/mechyrdia/Factbooks.kt @@ -51,7 +51,7 @@ import io.ktor.server.plugins.cachingheaders.CachingHeaders import io.ktor.server.plugins.callid.CallId import io.ktor.server.plugins.callid.callId import io.ktor.server.plugins.callid.callIdMdc -import io.ktor.server.plugins.callloging.CallLogging +import io.ktor.server.plugins.calllogging.CallLogging import io.ktor.server.plugins.conditionalheaders.ConditionalHeaders import io.ktor.server.plugins.contentnegotiation.ContentNegotiation import io.ktor.server.plugins.defaultheaders.DefaultHeaders @@ -250,6 +250,7 @@ fun Application.factbooks() { get() get() + get() get() get() get() diff --git a/src/jvmMain/kotlin/info/mechyrdia/JSON.kt b/src/main/kotlin/info/mechyrdia/JSON.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/JSON.kt rename to src/main/kotlin/info/mechyrdia/JSON.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/Utils.kt b/src/main/kotlin/info/mechyrdia/Utils.kt similarity index 69% rename from src/jvmMain/kotlin/info/mechyrdia/Utils.kt rename to src/main/kotlin/info/mechyrdia/Utils.kt index 3bfd683..8afb104 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/Utils.kt +++ b/src/main/kotlin/info/mechyrdia/Utils.kt @@ -3,3 +3,11 @@ package info.mechyrdia fun Iterable.concat(delimiter: String = "", prefix: String = "", suffix: String = "") = joinToString(separator = delimiter, prefix = prefix, postfix = suffix) fun Iterable.concat(delimiter: String = "", prefix: String = "", suffix: String = "", converter: (T) -> String = Any?::toString) = joinToString(separator = delimiter, prefix = prefix, postfix = suffix, transform = converter) + +fun R.concatenated(iterable: Iterable, delimiter: R.() -> Unit, body: R.(T) -> Unit) { + for ((i, item) in iterable.withIndex()) { + if (i > 0) + delimiter() + body(item) + } +} diff --git a/src/jvmMain/kotlin/info/mechyrdia/auth/nationstates.kt b/src/main/kotlin/info/mechyrdia/auth/NationStates.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/auth/nationstates.kt rename to src/main/kotlin/info/mechyrdia/auth/NationStates.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/auth/SessionStorage.kt b/src/main/kotlin/info/mechyrdia/auth/SessionStorage.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/auth/SessionStorage.kt rename to src/main/kotlin/info/mechyrdia/auth/SessionStorage.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/auth/Sessions.kt b/src/main/kotlin/info/mechyrdia/auth/Sessions.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/auth/Sessions.kt rename to src/main/kotlin/info/mechyrdia/auth/Sessions.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/auth/ViewsLogin.kt b/src/main/kotlin/info/mechyrdia/auth/ViewsLogin.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/auth/ViewsLogin.kt rename to src/main/kotlin/info/mechyrdia/auth/ViewsLogin.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/auth/WebDav.kt b/src/main/kotlin/info/mechyrdia/auth/WebDav.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/auth/WebDav.kt rename to src/main/kotlin/info/mechyrdia/auth/WebDav.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/Bson.kt b/src/main/kotlin/info/mechyrdia/data/Bson.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/data/Bson.kt rename to src/main/kotlin/info/mechyrdia/data/Bson.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/Comments.kt b/src/main/kotlin/info/mechyrdia/data/Comments.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/data/Comments.kt rename to src/main/kotlin/info/mechyrdia/data/Comments.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/Data.kt b/src/main/kotlin/info/mechyrdia/data/Data.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/data/Data.kt rename to src/main/kotlin/info/mechyrdia/data/Data.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/DataFiles.kt b/src/main/kotlin/info/mechyrdia/data/DataFiles.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/data/DataFiles.kt rename to src/main/kotlin/info/mechyrdia/data/DataFiles.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/MigrateFiles.kt b/src/main/kotlin/info/mechyrdia/data/MigrateFiles.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/data/MigrateFiles.kt rename to src/main/kotlin/info/mechyrdia/data/MigrateFiles.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/MigrateFilesSerial.kt b/src/main/kotlin/info/mechyrdia/data/MigrateFilesSerial.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/data/MigrateFilesSerial.kt rename to src/main/kotlin/info/mechyrdia/data/MigrateFilesSerial.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/Nations.kt b/src/main/kotlin/info/mechyrdia/data/Nations.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/data/Nations.kt rename to src/main/kotlin/info/mechyrdia/data/Nations.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/ViewComments.kt b/src/main/kotlin/info/mechyrdia/data/ViewComments.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/data/ViewComments.kt rename to src/main/kotlin/info/mechyrdia/data/ViewComments.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/ViewsComment.kt b/src/main/kotlin/info/mechyrdia/data/ViewsComment.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/data/ViewsComment.kt rename to src/main/kotlin/info/mechyrdia/data/ViewsComment.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/ViewsFiles.kt b/src/main/kotlin/info/mechyrdia/data/ViewsFiles.kt similarity index 97% rename from src/jvmMain/kotlin/info/mechyrdia/data/ViewsFiles.kt rename to src/main/kotlin/info/mechyrdia/data/ViewsFiles.kt index 5b3663a..c10039c 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/data/ViewsFiles.kt +++ b/src/main/kotlin/info/mechyrdia/data/ViewsFiles.kt @@ -11,15 +11,13 @@ import info.mechyrdia.route.installCsrfToken import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode import io.ktor.http.content.PartData -import io.ktor.http.content.streamProvider import io.ktor.http.defaultForFileExtension import io.ktor.server.application.ApplicationCall import io.ktor.server.html.respondHtml import io.ktor.server.plugins.MissingRequestParameterException import io.ktor.server.response.respond import io.ktor.server.response.respondBytes -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext +import io.ktor.utils.io.toByteArray import kotlinx.html.* fun Map.sortedAsFiles() = toList() @@ -72,7 +70,7 @@ private fun UL.render(path: StoragePath, childNodes: Map, call p { form(action = call.href(Root.Admin.Vfs.MkDir(path.elements)), method = FormMethod.post) { installCsrfToken(call = call) - textInput { + textInput(name = "directory") { placeholder = "new-dir" } +Entities.nbsp @@ -288,7 +286,7 @@ suspend fun ApplicationCall.adminUploadFile(path: StoragePath, part: PartData.Fi val name = part.originalFileName ?: throw MissingRequestParameterException("originalFileName") val filePath = path / name - val content = withContext(Dispatchers.IO) { part.streamProvider().readAllBytes() } + val content = part.provider().toByteArray() if (FileStorage.instance.writeFile(filePath, content)) redirectHref(Root.Admin.Vfs.View(filePath.elements), HttpStatusCode.SeeOther) else @@ -296,7 +294,7 @@ suspend fun ApplicationCall.adminUploadFile(path: StoragePath, part: PartData.Fi } suspend fun ApplicationCall.adminOverwriteFile(path: StoragePath, part: PartData.FileItem) { - if (FileStorage.instance.writeFile(path, withContext(Dispatchers.IO) { part.streamProvider().readAllBytes() })) + if (FileStorage.instance.writeFile(path, part.provider().toByteArray())) redirectHref(Root.Admin.Vfs.View(path.elements), HttpStatusCode.SeeOther) else respond(HttpStatusCode.Conflict) diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/ViewsUser.kt b/src/main/kotlin/info/mechyrdia/data/ViewsUser.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/data/ViewsUser.kt rename to src/main/kotlin/info/mechyrdia/data/ViewsUser.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/Visits.kt b/src/main/kotlin/info/mechyrdia/data/Visits.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/data/Visits.kt rename to src/main/kotlin/info/mechyrdia/data/Visits.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/Xml.kt b/src/main/kotlin/info/mechyrdia/data/Xml.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/data/Xml.kt rename to src/main/kotlin/info/mechyrdia/data/Xml.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/April1st.kt b/src/main/kotlin/info/mechyrdia/lore/April1st.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/April1st.kt rename to src/main/kotlin/info/mechyrdia/lore/April1st.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ArticleListing.kt b/src/main/kotlin/info/mechyrdia/lore/ArticleListing.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/ArticleListing.kt rename to src/main/kotlin/info/mechyrdia/lore/ArticleListing.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ArticleTitles.kt b/src/main/kotlin/info/mechyrdia/lore/ArticleTitles.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/ArticleTitles.kt rename to src/main/kotlin/info/mechyrdia/lore/ArticleTitles.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/AssetCaching.kt b/src/main/kotlin/info/mechyrdia/lore/AssetCaching.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/AssetCaching.kt rename to src/main/kotlin/info/mechyrdia/lore/AssetCaching.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/AssetCompression.kt b/src/main/kotlin/info/mechyrdia/lore/AssetCompression.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/AssetCompression.kt rename to src/main/kotlin/info/mechyrdia/lore/AssetCompression.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/AssetHashing.kt b/src/main/kotlin/info/mechyrdia/lore/AssetHashing.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/AssetHashing.kt rename to src/main/kotlin/info/mechyrdia/lore/AssetHashing.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/FileData.kt b/src/main/kotlin/info/mechyrdia/lore/FileData.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/FileData.kt rename to src/main/kotlin/info/mechyrdia/lore/FileData.kt diff --git a/src/main/kotlin/info/mechyrdia/lore/FontAssets.kt b/src/main/kotlin/info/mechyrdia/lore/FontAssets.kt new file mode 100644 index 0000000..55b864c --- /dev/null +++ b/src/main/kotlin/info/mechyrdia/lore/FontAssets.kt @@ -0,0 +1,70 @@ +package info.mechyrdia.lore + +import info.mechyrdia.JsonFileCodec +import info.mechyrdia.concatenated +import info.mechyrdia.data.FileStorage +import info.mechyrdia.data.StoragePath +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.builtins.serializer + +@Serializable +data class FontAssetSrc( + val fileName: String, + val format: String = "woff" +) { + fun Appendable.renderSrcToCss() { + append("url(/assets/fonts/").append(fileName).append(") format('").append(format).append("')") + } +} + +@Serializable +data class FontAssetInfo( + val srcList: List, + val cssClassName: String, + val fontSize: String = "1.25em", + val textAreaFontSize: String = "2.5em", + val lineHeight: String = "1.0", + val textAreaLineHeight: String = "1.0", + val extraRules: List = listOf("font-variant: normal !important"), + val textAreaExtraRules: List = emptyList(), +) { + private fun Appendable.renderCssClass(declaration: String, key: String, fontSizeValue: String, lineHeightValue: String, extraRuleList: List) { + append(declaration).append('.').append(cssClassName).appendLine(" {") + append("\tfont-family: '").append(key).appendLine("', monospace;") + append("\tfont-size: ").append(fontSizeValue).appendLine(';') + append("\tline-height: ").append(lineHeightValue).appendLine(';') + for (extraRule in extraRuleList) + append('\t').append(extraRule).appendLine(';') + appendLine('}') + } + + fun Appendable.renderFontToCss(key: String) { + appendLine("@font-face {") + append("\tfont-family: '").append(key).appendLine("';") + append("\tsrc: ") + concatenated(srcList, { append(", ") }) { src -> with(src) { renderSrcToCss() } } + appendLine(';') + appendLine('}') + appendLine() + renderCssClass("", key, fontSize, lineHeight, extraRules) + appendLine() + renderCssClass("textarea", key, textAreaFontSize, textAreaLineHeight, textAreaExtraRules) + } +} + +@JvmInline +value class FontAssetsManifest(val fontAssets: Map) { + fun Appendable.renderCss() { + concatenated(fontAssets.entries, Appendable::appendLine) { (key, info) -> with(info) { renderFontToCss(key) } } + } +} + +private val fontsJsonPath = StoragePath.Root / "customFonts.json" + +suspend fun loadFontsJson(): FontAssetsManifest { + val fontsFile = FileStorage.instance.readFile(fontsJsonPath) ?: return FontAssetsManifest(emptyMap()) + val fontsJson = String(fontsFile) + val fontsMap = JsonFileCodec.decodeFromString(MapSerializer(String.serializer(), FontAssetInfo.serializer()), fontsJson) + return FontAssetsManifest(fontsMap) +} diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/Fonts.kt b/src/main/kotlin/info/mechyrdia/lore/FontDrawing.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/Fonts.kt rename to src/main/kotlin/info/mechyrdia/lore/FontDrawing.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/HttpUtils.kt b/src/main/kotlin/info/mechyrdia/lore/HttpUtils.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/HttpUtils.kt rename to src/main/kotlin/info/mechyrdia/lore/HttpUtils.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ParserBuilder.kt b/src/main/kotlin/info/mechyrdia/lore/ParserBuilder.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/ParserBuilder.kt rename to src/main/kotlin/info/mechyrdia/lore/ParserBuilder.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ParserHtml.kt b/src/main/kotlin/info/mechyrdia/lore/ParserHtml.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/ParserHtml.kt rename to src/main/kotlin/info/mechyrdia/lore/ParserHtml.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ParserLexer.kt b/src/main/kotlin/info/mechyrdia/lore/ParserLexer.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/ParserLexer.kt rename to src/main/kotlin/info/mechyrdia/lore/ParserLexer.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ParserLexerAsync.kt b/src/main/kotlin/info/mechyrdia/lore/ParserLexerAsync.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/ParserLexerAsync.kt rename to src/main/kotlin/info/mechyrdia/lore/ParserLexerAsync.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ParserPlain.kt b/src/main/kotlin/info/mechyrdia/lore/ParserPlain.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/ParserPlain.kt rename to src/main/kotlin/info/mechyrdia/lore/ParserPlain.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocess.kt b/src/main/kotlin/info/mechyrdia/lore/ParserPreprocess.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocess.kt rename to src/main/kotlin/info/mechyrdia/lore/ParserPreprocess.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocessInclude.kt b/src/main/kotlin/info/mechyrdia/lore/ParserPreprocessInclude.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocessInclude.kt rename to src/main/kotlin/info/mechyrdia/lore/ParserPreprocessInclude.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocessJson.kt b/src/main/kotlin/info/mechyrdia/lore/ParserPreprocessJson.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocessJson.kt rename to src/main/kotlin/info/mechyrdia/lore/ParserPreprocessJson.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocessMath.kt b/src/main/kotlin/info/mechyrdia/lore/ParserPreprocessMath.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocessMath.kt rename to src/main/kotlin/info/mechyrdia/lore/ParserPreprocessMath.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ParserRaw.kt b/src/main/kotlin/info/mechyrdia/lore/ParserRaw.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/ParserRaw.kt rename to src/main/kotlin/info/mechyrdia/lore/ParserRaw.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ParserRobot.kt b/src/main/kotlin/info/mechyrdia/lore/ParserRobot.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/ParserRobot.kt rename to src/main/kotlin/info/mechyrdia/lore/ParserRobot.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ParserTree.kt b/src/main/kotlin/info/mechyrdia/lore/ParserTree.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/ParserTree.kt rename to src/main/kotlin/info/mechyrdia/lore/ParserTree.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ParserUtils.kt b/src/main/kotlin/info/mechyrdia/lore/ParserUtils.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/ParserUtils.kt rename to src/main/kotlin/info/mechyrdia/lore/ParserUtils.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewBar.kt b/src/main/kotlin/info/mechyrdia/lore/ViewBar.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/ViewBar.kt rename to src/main/kotlin/info/mechyrdia/lore/ViewBar.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewMap.kt b/src/main/kotlin/info/mechyrdia/lore/ViewMap.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/ViewMap.kt rename to src/main/kotlin/info/mechyrdia/lore/ViewMap.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewNav.kt b/src/main/kotlin/info/mechyrdia/lore/ViewNav.kt similarity index 95% rename from src/jvmMain/kotlin/info/mechyrdia/lore/ViewNav.kt rename to src/main/kotlin/info/mechyrdia/lore/ViewNav.kt index 865de71..a567568 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewNav.kt +++ b/src/main/kotlin/info/mechyrdia/lore/ViewNav.kt @@ -24,9 +24,11 @@ private data class ExternalLink( val text: String, ) +private val extraLinksPath = StoragePath.Root / "externalLinks.json" + suspend fun loadExternalLinks(): List { - val extraLinksFile = StoragePath.Root / "externalLinks.json" - val extraLinksJson = String(FileStorage.instance.readFile(extraLinksFile)!!) + val extraLinksFile = FileStorage.instance.readFile(extraLinksPath) ?: return emptyList() + val extraLinksJson = String(extraLinksFile) val extraLinks = JsonFileCodec.decodeFromString(ListSerializer(ExternalLink.serializer()), extraLinksJson) return if (extraLinks.isEmpty()) emptyList() diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewOg.kt b/src/main/kotlin/info/mechyrdia/lore/ViewOg.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/ViewOg.kt rename to src/main/kotlin/info/mechyrdia/lore/ViewOg.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewTpl.kt b/src/main/kotlin/info/mechyrdia/lore/ViewTpl.kt similarity index 96% rename from src/jvmMain/kotlin/info/mechyrdia/lore/ViewTpl.kt rename to src/main/kotlin/info/mechyrdia/lore/ViewTpl.kt index fd5dc68..7e0c4cc 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewTpl.kt +++ b/src/main/kotlin/info/mechyrdia/lore/ViewTpl.kt @@ -20,10 +20,6 @@ private val preloadFonts = listOf( "Oxanium-ExtraBold.woff", "Oxanium-Regular.woff", "Oxanium-SemiBold.woff", - "tylan-language-alphabet-3.woff", - "thedish-language-alphabet.woff", - "pokhval-language-alphabet.woff", - "kishari-language-alphabet.woff", ) private val preloadImages = listOf( @@ -77,6 +73,7 @@ fun ApplicationCall.page(pageTitle: String, navBar: List? = null, sideb } link(rel = "stylesheet", type = "text/css", href = "/static/style.css") + link(rel = "stylesheet", type = "text/css", href = "/fonts.css") script(src = "/static/init.js") {} } diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsError.kt b/src/main/kotlin/info/mechyrdia/lore/ViewsError.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/ViewsError.kt rename to src/main/kotlin/info/mechyrdia/lore/ViewsError.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsLore.kt b/src/main/kotlin/info/mechyrdia/lore/ViewsLore.kt similarity index 93% rename from src/jvmMain/kotlin/info/mechyrdia/lore/ViewsLore.kt rename to src/main/kotlin/info/mechyrdia/lore/ViewsLore.kt index 2d936ef..d68b3ba 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsLore.kt +++ b/src/main/kotlin/info/mechyrdia/lore/ViewsLore.kt @@ -33,13 +33,17 @@ data class IntroMetaData( get() = OpenGraphData(desc, image) } +private val introMetaPath = StoragePath.Root / "introMeta.json" +private val introHtmlPath = StoragePath.Root / "intro.html" + suspend fun ApplicationCall.loreIntroPage(): HTML.() -> Unit { - val metaJson = String(FileStorage.instance.readFile(StoragePath.Root / "introMeta.json")!!) - val metaData = JsonFileCodec.decodeFromString(IntroMetaData.serializer(), metaJson) + val metaFile = FileStorage.instance.readFile(introMetaPath) + val metaJson = metaFile?.let(::String) + val metaData = metaJson?.let { JsonFileCodec.decodeFromString(IntroMetaData.serializer(), it) } - val html = String(FileStorage.instance.readFile(StoragePath.Root / "intro.html")!!) + val html = FileStorage.instance.readFile(introHtmlPath)?.let(::String).orEmpty() - return page(metaData.title, standardNavBar(), null, metaData.ogData) { + return page(metaData?.title.orEmpty(), standardNavBar(), null, metaData?.ogData) { section { a { id = "page-top" } unsafe { raw(html) } diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsPrefs.kt b/src/main/kotlin/info/mechyrdia/lore/ViewsPrefs.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/ViewsPrefs.kt rename to src/main/kotlin/info/mechyrdia/lore/ViewsPrefs.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsQuote.kt b/src/main/kotlin/info/mechyrdia/lore/ViewsQuote.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/ViewsQuote.kt rename to src/main/kotlin/info/mechyrdia/lore/ViewsQuote.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsRobots.kt b/src/main/kotlin/info/mechyrdia/lore/ViewsRobots.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/ViewsRobots.kt rename to src/main/kotlin/info/mechyrdia/lore/ViewsRobots.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsRss.kt b/src/main/kotlin/info/mechyrdia/lore/ViewsRss.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/lore/ViewsRss.kt rename to src/main/kotlin/info/mechyrdia/lore/ViewsRss.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/robot/RobotApi.kt b/src/main/kotlin/info/mechyrdia/robot/RobotApi.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/robot/RobotApi.kt rename to src/main/kotlin/info/mechyrdia/robot/RobotApi.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/robot/RobotCodec.kt b/src/main/kotlin/info/mechyrdia/robot/RobotCodec.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/robot/RobotCodec.kt rename to src/main/kotlin/info/mechyrdia/robot/RobotCodec.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/robot/RobotFiles.kt b/src/main/kotlin/info/mechyrdia/robot/RobotFiles.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/robot/RobotFiles.kt rename to src/main/kotlin/info/mechyrdia/robot/RobotFiles.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/robot/RobotRateLimiter.kt b/src/main/kotlin/info/mechyrdia/robot/RobotRateLimiter.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/robot/RobotRateLimiter.kt rename to src/main/kotlin/info/mechyrdia/robot/RobotRateLimiter.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/robot/RobotSchema.kt b/src/main/kotlin/info/mechyrdia/robot/RobotSchema.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/robot/RobotSchema.kt rename to src/main/kotlin/info/mechyrdia/robot/RobotSchema.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/robot/RobotService.kt b/src/main/kotlin/info/mechyrdia/robot/RobotService.kt similarity index 96% rename from src/jvmMain/kotlin/info/mechyrdia/robot/RobotService.kt rename to src/main/kotlin/info/mechyrdia/robot/RobotService.kt index ee8882c..383ebc9 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/robot/RobotService.kt +++ b/src/main/kotlin/info/mechyrdia/robot/RobotService.kt @@ -1,471 +1,471 @@ -package info.mechyrdia.robot - -import info.mechyrdia.Configuration -import info.mechyrdia.MainDomainName -import info.mechyrdia.OpenAiConfig -import info.mechyrdia.concat -import info.mechyrdia.data.DataDocument -import info.mechyrdia.data.DocumentTable -import info.mechyrdia.data.Id -import info.mechyrdia.data.InstantNullableSerializer -import info.mechyrdia.data.MONGODB_ID_KEY -import info.mechyrdia.data.NationData -import info.mechyrdia.data.TableHolder -import info.mechyrdia.lore.RobotFactbookLoader -import io.ktor.client.HttpClient -import io.ktor.client.engine.java.Java -import io.ktor.client.plugins.ClientRequestException -import io.ktor.client.plugins.HttpRequestRetry -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.client.plugins.defaultRequest -import io.ktor.client.plugins.logging.LogLevel -import io.ktor.client.plugins.logging.Logging -import io.ktor.client.request.header -import io.ktor.http.ContentType -import io.ktor.http.HttpHeaders -import io.ktor.http.withCharset -import io.ktor.serialization.kotlinx.json.json -import kotlinx.coroutines.CoroutineName -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.job -import kotlinx.coroutines.launch -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import java.time.Instant -import kotlin.collections.List -import kotlin.collections.Map -import kotlin.collections.Set -import kotlin.collections.buildMap -import kotlin.collections.component1 -import kotlin.collections.component2 -import kotlin.collections.emptyMap -import kotlin.collections.emptySet -import kotlin.collections.flatMap -import kotlin.collections.fold -import kotlin.collections.forEach -import kotlin.collections.iterator -import kotlin.collections.listOf -import kotlin.collections.map -import kotlin.collections.minus -import kotlin.collections.mutableListOf -import kotlin.collections.plus -import kotlin.collections.set -import kotlin.collections.toList -import kotlin.random.Random -import kotlin.time.Duration.Companion.minutes - -private val RobotServiceLogger: Logger = LoggerFactory.getLogger("info.mechyrdia.robot.RobotServiceKt") - -val RobotGlobalsId = Id("RobotGlobalsInstance") - -@Serializable -data class RobotGlobals( - @SerialName(MONGODB_ID_KEY) - override val id: Id = RobotGlobalsId, - - val lastFileUpload: @Serializable(with = InstantNullableSerializer::class) Instant? = null, - val fileIdMap: Map = emptyMap(), - val vectorStoreId: RobotVectorStoreId? = null, - val assistantId: RobotAssistantId? = null, - val ongoingThreadIds: Set = emptySet(), -) : DataDocument { - suspend fun save(): RobotGlobals { - set(this) - return this - } - - companion object : TableHolder { - override val Table = DocumentTable() - - suspend fun get() = Table.get(RobotGlobalsId) - suspend fun set(instance: RobotGlobals) = Table.put(instance) - suspend fun delete() = Table.del(RobotGlobalsId) - - override suspend fun initialize() = Unit - } -} - -private fun RobotGlobals.plusThread(threadId: RobotThreadId) = copy( - ongoingThreadIds = ongoingThreadIds + threadId -) - -private fun RobotGlobals.minusThread(threadId: RobotThreadId) = copy( - ongoingThreadIds = ongoingThreadIds - threadId -) - -enum class RobotServiceStatus { - NOT_CONFIGURED, - LOADING, - FAILED, - READY, -} - -class RobotService( - private val config: OpenAiConfig, -) { - private val robotClient = RobotClient( - HttpClient(Java) { - defaultRequest { - header(HttpHeaders.Authorization, "Bearer ${config.token}") - header("OpenAI-Organization", config.orgId) - config.project?.let { header("OpenAI-Project", it) } - header("OpenAI-Beta", "assistants=v2") - } - - install(ContentNegotiation) { - json(JsonRobotCodec) - } - - Logging { - level = LogLevel.INFO - sanitizeHeader("") { it == HttpHeaders.Authorization } - } - - install(HttpRequestRetry) { - retryOnExceptionOrServerErrors(5) - delayMillis { retry -> - (1 shl (retry - 1)) * 1000L + Random.nextLong(250L, 750L) - } - } - - expectSuccess = true - - install(RobotRateLimiter) - } - ) - - private suspend fun createThread(): RobotThreadId { - return robotClient.createThread(RobotCreateThreadRequest()).id.also { threadId -> - (RobotGlobals.get() ?: RobotGlobals()).plusThread(threadId).save() - } - } - - private suspend fun deleteThread(threadId: RobotThreadId) { - try { - robotClient.deleteThread(threadId) - } catch (ex: ClientRequestException) { - RobotServiceLogger.warn("Unable to delete thread at ID $threadId", ex) - } - (RobotGlobals.get() ?: RobotGlobals()).minusThread(threadId).save() - } - - private suspend fun RobotGlobals.gcOldThreads(): RobotGlobals { - for (threadId in ongoingThreadIds) - try { - robotClient.deleteThread(threadId) - } catch (ex: ClientRequestException) { - RobotServiceLogger.warn("Unable to delete thread at ID $threadId", ex) - } - return copy(ongoingThreadIds = emptySet()) - } - - private suspend fun updateFiles(prevGlobals: RobotGlobals?, onNewFileId: (suspend (RobotFileId) -> Unit)? = null): RobotGlobals { - val robotGlobals = prevGlobals ?: RobotGlobals() - - val fileIdMap = buildMap { - putAll(robotGlobals.fileIdMap) - - val factbooks = robotGlobals.lastFileUpload?.let { - RobotFactbookLoader.loadAllFactbooksSince(it) - } ?: RobotFactbookLoader.loadAllFactbooks() - - for ((name, text) in factbooks) { - remove(name)?.let { oldId -> - try { - robotClient.deleteFile(oldId) - } catch (ex: ClientRequestException) { - RobotServiceLogger.warn("Unable to delete file $name at ID $oldId", ex) - } - } - - val newId = robotClient.uploadFile( - "assistants", - FileUpload( - text.toByteArray(), - ContentType.Text.Plain.withCharset(Charsets.UTF_8), - name.toOpenAiName() - ) - ).id - - this[name] = newId - onNewFileId?.invoke(newId) - - RobotServiceLogger.info("Factbook $name has been uploaded") - } - } - - return robotGlobals.copy(lastFileUpload = Instant.now(), fileIdMap = fileIdMap).save() - } - - suspend fun initialize() { - var robotGlobals = updateFiles(RobotGlobals.get()?.gcOldThreads()) - - val vectorStoreId = robotGlobals.vectorStoreId ?: robotClient.createVectorStore( - RobotCreateVectorStoreRequest( - name = "lore_documents", - fileIds = robotGlobals.fileIdMap.values.toList(), - ) - ).id.also { vsId -> - robotGlobals = robotGlobals.copy(vectorStoreId = vsId).save() - } - - RobotServiceLogger.info("Vector store has been created") - - poll { - robotClient.getVectorStore(vectorStoreId).status == "completed" - } - - RobotServiceLogger.info("Vector store creation is complete") - - if (robotGlobals.assistantId == null) - robotGlobals = robotGlobals.copy( - assistantId = robotClient.createAssistant( - RobotCreateAssistantRequest( - model = config.assistantModel, - name = config.assistantName, - instructions = config.assistantInstructions, - tools = listOf( - RobotCreateAssistantRequestTool("file_search") - ), - toolResources = RobotCreateAssistantRequestToolResources( - fileSearch = RobotCreateAssistantRequestFileSearchResources( - vectorStoreIds = listOf(vectorStoreId) - ) - ), - temperature = config.assistantTemperature - ) - ).id - ).save() - - RobotServiceLogger.info("Assistant creation is complete") - - maintenanceScope.launch { - while (true) { - delay(30.minutes) - - launch(SupervisorJob(currentCoroutineContext().job)) { - performMaintenance() - } - } - } - } - - suspend fun performMaintenance() { - var robotGlobals = RobotGlobals.get() ?: RobotGlobals() - - val vectorStoreId = robotGlobals.vectorStoreId ?: robotClient.createVectorStore( - RobotCreateVectorStoreRequest( - name = "lore_documents", - fileIds = robotGlobals.fileIdMap.values.toList(), - ) - ).id.also { vsId -> - robotGlobals = robotGlobals.copy(vectorStoreId = vsId).save() - } - - updateFiles(robotGlobals) { fileId -> - robotClient.addFileToVectorStore(vectorStoreId, fileId) - } - - RobotServiceLogger.info("Vector store has been updated") - - poll { - robotClient.getVectorStore(vectorStoreId).fileCounts.inProgress == 0 - } - - RobotServiceLogger.info("Vector store update is complete") - } - - suspend fun reset() { - RobotGlobals.get()?.gcOldThreads()?.copy( - lastFileUpload = null, - fileIdMap = emptyMap(), - vectorStoreId = null, - assistantId = null, - )?.save() - - while (true) { - val assistants = robotClient.listAssistants().data - if (assistants.isEmpty()) break - - assistants.map { it.id }.forEach { - try { - robotClient.deleteAssistant(it) - } catch (ex: ClientRequestException) { - RobotServiceLogger.warn("Unable to delete assistant at ID $it", ex) - } - } - } - - while (true) { - val vectorStores = robotClient.listVectorStores().data - if (vectorStores.isEmpty()) break - - vectorStores.map { it.id }.forEach { - try { - robotClient.deleteVectorStore(it) - } catch (ex: ClientRequestException) { - RobotServiceLogger.warn("Unable to delete vector-store at ID $it", ex) - } - } - } - - while (true) { - val files = robotClient.listFiles().data - if (files.isEmpty()) break - - files.map { it.id }.forEach { - try { - robotClient.deleteFile(it) - } catch (ex: ClientRequestException) { - RobotServiceLogger.warn("Unable to delete file at ID $it", ex) - } - } - } - - initialize() - } - - inner class Conversation(private val nationId: Id) { - private var assistantId: RobotAssistantId? = null - private var threadId: RobotThreadId? = null - - suspend fun send(userMessage: String): Flow { - val assistant = assistantId ?: pollValue { RobotGlobals.get()?.assistantId } - .also { assistantId = it } - - val thread = threadId ?: createThread().also { threadId = it } - - val messages = listOf( - RobotCreateThreadRequestMessage( - role = "user", - content = userMessage - ) - ) - - val tokenTracker = ConversationMessageTokenTracker() - - return flow { - emit(RobotConversationMessage.User(userMessage)) - - val annotationTargets = mutableListOf>() - val collectionScope = CoroutineScope(currentCoroutineContext()) - - robotClient.createRun(thread, assistant, messages) - .filter { it.event == "thread.message.delta" } - .mapNotNull { it.data } - .map { JsonRobotCodec.decodeFromString(RobotMessageDelta.serializer(), it) } - .collect { eventData -> - val annotationTexts = eventData.delta.content.flatMap { it.text.annotations }.map { annotation -> - val annotationIndex = annotationTargets.size - annotationTargets.add(collectionScope.async { - val fileName = robotClient.getFile(annotation.fileCitation.fileId).filename.fromOpenAiName() - val fileText = annotation.fileCitation.quote.let { if (it.isNotBlank()) ": $it" else it } - "$MainDomainName/lore/$fileName$fileText" - }) - annotation.text to " [${annotationIndex + 1}]" - } - - val contents = eventData.delta.content.concat { textContent -> - textContent.text.value - } - - val replacedContents = annotationTexts.fold(contents) { text, (replace, replaceWith) -> - text.replace(replace, replaceWith) - } - - emit(RobotConversationMessage.Robot(replacedContents)) - } - - emit(RobotConversationMessage.Cite(annotationTargets.awaitAll())) - - emit(RobotConversationMessage.Ready) - }.onEach { message -> - tokenTracker.addMessage(message) - }.onCompletion { _ -> - RobotUser.addTokens(nationId, tokenTracker.calculateTokens()) - } - } - - suspend fun isExhausted(): Boolean { - val usedTokens = RobotUser.getTokens(nationId) - val tokenLimit = RobotUser.getMaxTokens(nationId) - return usedTokens >= tokenLimit - } - - suspend fun close() { - threadId?.let { deleteThread(it) } - } - } - - companion object { - private val maintenanceScope = CoroutineScope(SupervisorJob() + CoroutineName("robot-service-maintenance")) - - private val startInitializing = Job() - - private val instanceHolder = CoroutineScope(CoroutineName("robot-service-initialization")).async { - startInitializing.join() - Configuration.Current.openAi?.let { config -> - status = RobotServiceStatus.LOADING - RobotService(config).apply { initialize() } - } - }.also { deferred -> - deferred.invokeOnCompletion { ex -> - status = if (ex != null) { - RobotServiceLogger.error("RobotService failed to initialize", ex) - RobotServiceStatus.FAILED - } else { - RobotServiceLogger.info("RobotService successfully initialized") - RobotServiceStatus.READY - } - } - } - - var status: RobotServiceStatus = RobotServiceStatus.NOT_CONFIGURED - private set - - suspend fun getInstance() = try { - instanceHolder.await() - } catch (_: Exception) { - null - } - - fun start() { - startInitializing.complete() - } - } -} - -@Serializable -sealed class RobotConversationMessage { - @Serializable - @SerialName("ready") - data object Ready : RobotConversationMessage() - - @Serializable - @SerialName("user") - data class User(val text: String) : RobotConversationMessage() - - @Serializable - @SerialName("robot") - data class Robot(val text: String) : RobotConversationMessage() - - @Serializable - @SerialName("cite") - data class Cite(val urls: List) : RobotConversationMessage() -} +package info.mechyrdia.robot + +import info.mechyrdia.Configuration +import info.mechyrdia.MainDomainName +import info.mechyrdia.OpenAiConfig +import info.mechyrdia.concat +import info.mechyrdia.data.DataDocument +import info.mechyrdia.data.DocumentTable +import info.mechyrdia.data.Id +import info.mechyrdia.data.InstantNullableSerializer +import info.mechyrdia.data.MONGODB_ID_KEY +import info.mechyrdia.data.NationData +import info.mechyrdia.data.TableHolder +import info.mechyrdia.lore.RobotFactbookLoader +import io.ktor.client.HttpClient +import io.ktor.client.engine.java.Java +import io.ktor.client.plugins.ClientRequestException +import io.ktor.client.plugins.HttpRequestRetry +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.defaultRequest +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logging +import io.ktor.client.request.header +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.withCharset +import io.ktor.serialization.kotlinx.json.json +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.job +import kotlinx.coroutines.launch +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.time.Instant +import kotlin.collections.List +import kotlin.collections.Map +import kotlin.collections.Set +import kotlin.collections.buildMap +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.emptyMap +import kotlin.collections.emptySet +import kotlin.collections.flatMap +import kotlin.collections.fold +import kotlin.collections.forEach +import kotlin.collections.iterator +import kotlin.collections.listOf +import kotlin.collections.map +import kotlin.collections.minus +import kotlin.collections.mutableListOf +import kotlin.collections.plus +import kotlin.collections.set +import kotlin.collections.toList +import kotlin.random.Random +import kotlin.time.Duration.Companion.minutes + +private val RobotServiceLogger: Logger = LoggerFactory.getLogger("info.mechyrdia.robot.RobotServiceKt") + +val RobotGlobalsId = Id("RobotGlobalsInstance") + +@Serializable +data class RobotGlobals( + @SerialName(MONGODB_ID_KEY) + override val id: Id = RobotGlobalsId, + + val lastFileUpload: @Serializable(with = InstantNullableSerializer::class) Instant? = null, + val fileIdMap: Map = emptyMap(), + val vectorStoreId: RobotVectorStoreId? = null, + val assistantId: RobotAssistantId? = null, + val ongoingThreadIds: Set = emptySet(), +) : DataDocument { + suspend fun save(): RobotGlobals { + set(this) + return this + } + + companion object : TableHolder { + override val Table = DocumentTable() + + suspend fun get() = Table.get(RobotGlobalsId) + suspend fun set(instance: RobotGlobals) = Table.put(instance) + suspend fun delete() = Table.del(RobotGlobalsId) + + override suspend fun initialize() = Unit + } +} + +private fun RobotGlobals.plusThread(threadId: RobotThreadId) = copy( + ongoingThreadIds = ongoingThreadIds + threadId +) + +private fun RobotGlobals.minusThread(threadId: RobotThreadId) = copy( + ongoingThreadIds = ongoingThreadIds - threadId +) + +enum class RobotServiceStatus { + NOT_CONFIGURED, + LOADING, + FAILED, + READY, +} + +class RobotService( + private val config: OpenAiConfig, +) { + private val robotClient = RobotClient( + HttpClient(Java) { + defaultRequest { + header(HttpHeaders.Authorization, "Bearer ${config.token}") + header("OpenAI-Organization", config.orgId) + config.project?.let { header("OpenAI-Project", it) } + header("OpenAI-Beta", "assistants=v2") + } + + install(ContentNegotiation) { + json(JsonRobotCodec) + } + + Logging { + level = LogLevel.INFO + sanitizeHeader("") { it == HttpHeaders.Authorization } + } + + install(HttpRequestRetry) { + retryOnExceptionOrServerErrors(5) + delayMillis { retry -> + (1 shl (retry - 1)) * 1000L + Random.nextLong(250L, 750L) + } + } + + expectSuccess = true + + install(RobotRateLimiter) + } + ) + + private suspend fun createThread(): RobotThreadId { + return robotClient.createThread(RobotCreateThreadRequest()).id.also { threadId -> + (RobotGlobals.get() ?: RobotGlobals()).plusThread(threadId).save() + } + } + + private suspend fun deleteThread(threadId: RobotThreadId) { + try { + robotClient.deleteThread(threadId) + } catch (ex: ClientRequestException) { + RobotServiceLogger.warn("Unable to delete thread at ID $threadId", ex) + } + (RobotGlobals.get() ?: RobotGlobals()).minusThread(threadId).save() + } + + private suspend fun RobotGlobals.gcOldThreads(): RobotGlobals { + for (threadId in ongoingThreadIds) + try { + robotClient.deleteThread(threadId) + } catch (ex: ClientRequestException) { + RobotServiceLogger.warn("Unable to delete thread at ID $threadId", ex) + } + return copy(ongoingThreadIds = emptySet()) + } + + private suspend fun updateFiles(prevGlobals: RobotGlobals?, onNewFileId: (suspend (RobotFileId) -> Unit)? = null): RobotGlobals { + val robotGlobals = prevGlobals ?: RobotGlobals() + + val fileIdMap = buildMap { + putAll(robotGlobals.fileIdMap) + + val factbooks = robotGlobals.lastFileUpload?.let { + RobotFactbookLoader.loadAllFactbooksSince(it) + } ?: RobotFactbookLoader.loadAllFactbooks() + + for ((name, text) in factbooks) { + remove(name)?.let { oldId -> + try { + robotClient.deleteFile(oldId) + } catch (ex: ClientRequestException) { + RobotServiceLogger.warn("Unable to delete file $name at ID $oldId", ex) + } + } + + val newId = robotClient.uploadFile( + "assistants", + FileUpload( + text.toByteArray(), + ContentType.Text.Plain.withCharset(Charsets.UTF_8), + name.toOpenAiName() + ) + ).id + + this[name] = newId + onNewFileId?.invoke(newId) + + RobotServiceLogger.info("Factbook $name has been uploaded") + } + } + + return robotGlobals.copy(lastFileUpload = Instant.now(), fileIdMap = fileIdMap).save() + } + + suspend fun initialize() { + var robotGlobals = updateFiles(RobotGlobals.get()?.gcOldThreads()) + + val vectorStoreId = robotGlobals.vectorStoreId ?: robotClient.createVectorStore( + RobotCreateVectorStoreRequest( + name = "lore_documents", + fileIds = robotGlobals.fileIdMap.values.toList(), + ) + ).id.also { vsId -> + robotGlobals = robotGlobals.copy(vectorStoreId = vsId).save() + } + + RobotServiceLogger.info("Vector store has been created") + + poll { + robotClient.getVectorStore(vectorStoreId).status == "completed" + } + + RobotServiceLogger.info("Vector store creation is complete") + + if (robotGlobals.assistantId == null) + robotGlobals = robotGlobals.copy( + assistantId = robotClient.createAssistant( + RobotCreateAssistantRequest( + model = config.assistantModel, + name = config.assistantName, + instructions = config.assistantInstructions, + tools = listOf( + RobotCreateAssistantRequestTool("file_search") + ), + toolResources = RobotCreateAssistantRequestToolResources( + fileSearch = RobotCreateAssistantRequestFileSearchResources( + vectorStoreIds = listOf(vectorStoreId) + ) + ), + temperature = config.assistantTemperature + ) + ).id + ).save() + + RobotServiceLogger.info("Assistant creation is complete") + + maintenanceScope.launch { + while (true) { + delay(30.minutes) + + launch(SupervisorJob(currentCoroutineContext().job)) { + performMaintenance() + } + } + } + } + + suspend fun performMaintenance() { + var robotGlobals = RobotGlobals.get() ?: RobotGlobals() + + val vectorStoreId = robotGlobals.vectorStoreId ?: robotClient.createVectorStore( + RobotCreateVectorStoreRequest( + name = "lore_documents", + fileIds = robotGlobals.fileIdMap.values.toList(), + ) + ).id.also { vsId -> + robotGlobals = robotGlobals.copy(vectorStoreId = vsId).save() + } + + updateFiles(robotGlobals) { fileId -> + robotClient.addFileToVectorStore(vectorStoreId, fileId) + } + + RobotServiceLogger.info("Vector store has been updated") + + poll { + robotClient.getVectorStore(vectorStoreId).fileCounts.inProgress == 0 + } + + RobotServiceLogger.info("Vector store update is complete") + } + + suspend fun reset() { + RobotGlobals.get()?.gcOldThreads()?.copy( + lastFileUpload = null, + fileIdMap = emptyMap(), + vectorStoreId = null, + assistantId = null, + )?.save() + + while (true) { + val assistants = robotClient.listAssistants().data + if (assistants.isEmpty()) break + + assistants.map { it.id }.forEach { + try { + robotClient.deleteAssistant(it) + } catch (ex: ClientRequestException) { + RobotServiceLogger.warn("Unable to delete assistant at ID $it", ex) + } + } + } + + while (true) { + val vectorStores = robotClient.listVectorStores().data + if (vectorStores.isEmpty()) break + + vectorStores.map { it.id }.forEach { + try { + robotClient.deleteVectorStore(it) + } catch (ex: ClientRequestException) { + RobotServiceLogger.warn("Unable to delete vector-store at ID $it", ex) + } + } + } + + while (true) { + val files = robotClient.listFiles().data + if (files.isEmpty()) break + + files.map { it.id }.forEach { + try { + robotClient.deleteFile(it) + } catch (ex: ClientRequestException) { + RobotServiceLogger.warn("Unable to delete file at ID $it", ex) + } + } + } + + initialize() + } + + inner class Conversation(private val nationId: Id) { + private var assistantId: RobotAssistantId? = null + private var threadId: RobotThreadId? = null + + suspend fun send(userMessage: String): Flow { + val assistant = assistantId ?: pollValue { RobotGlobals.get()?.assistantId } + .also { assistantId = it } + + val thread = threadId ?: createThread().also { threadId = it } + + val messages = listOf( + RobotCreateThreadRequestMessage( + role = "user", + content = userMessage + ) + ) + + val tokenTracker = ConversationMessageTokenTracker() + + return flow { + emit(RobotConversationMessage.User(userMessage)) + + val annotationTargets = mutableListOf>() + val collectionScope = CoroutineScope(currentCoroutineContext()) + + robotClient.createRun(thread, assistant, messages) + .filter { it.event == "thread.message.delta" } + .mapNotNull { it.data } + .map { JsonRobotCodec.decodeFromString(RobotMessageDelta.serializer(), it) } + .collect { eventData -> + val annotationTexts = eventData.delta.content.flatMap { it.text.annotations }.map { annotation -> + val annotationIndex = annotationTargets.size + annotationTargets.add(collectionScope.async { + val fileName = robotClient.getFile(annotation.fileCitation.fileId).filename.fromOpenAiName() + val fileText = annotation.fileCitation.quote.let { if (it.isNotBlank()) ": $it" else it } + "$MainDomainName/lore/$fileName$fileText" + }) + annotation.text to " [${annotationIndex + 1}]" + } + + val contents = eventData.delta.content.concat { textContent -> + textContent.text.value + } + + val replacedContents = annotationTexts.fold(contents) { text, (replace, replaceWith) -> + text.replace(replace, replaceWith) + } + + emit(RobotConversationMessage.Robot(replacedContents)) + } + + emit(RobotConversationMessage.Cite(annotationTargets.awaitAll())) + + emit(RobotConversationMessage.Ready) + }.onEach { message -> + tokenTracker.addMessage(message) + }.onCompletion { _ -> + RobotUser.addTokens(nationId, tokenTracker.calculateTokens()) + } + } + + suspend fun isExhausted(): Boolean { + val usedTokens = RobotUser.getTokens(nationId) + val tokenLimit = RobotUser.getMaxTokens(nationId) + return usedTokens >= tokenLimit + } + + suspend fun close() { + threadId?.let { deleteThread(it) } + } + } + + companion object { + private val maintenanceScope = CoroutineScope(SupervisorJob() + CoroutineName("robot-service-maintenance")) + + private val startInitializing = Job() + + private val instanceHolder = CoroutineScope(CoroutineName("robot-service-initialization")).async { + startInitializing.join() + Configuration.Current.openAi?.let { config -> + status = RobotServiceStatus.LOADING + RobotService(config).apply { initialize() } + } + }.also { deferred -> + deferred.invokeOnCompletion { ex -> + status = if (ex != null) { + RobotServiceLogger.error("RobotService failed to initialize", ex) + RobotServiceStatus.FAILED + } else { + RobotServiceLogger.info("RobotService successfully initialized") + RobotServiceStatus.READY + } + } + } + + var status: RobotServiceStatus = RobotServiceStatus.NOT_CONFIGURED + private set + + suspend fun getInstance() = try { + instanceHolder.await() + } catch (_: Exception) { + null + } + + fun start() { + startInitializing.complete() + } + } +} + +@Serializable +sealed class RobotConversationMessage { + @Serializable + @SerialName("ready") + data object Ready : RobotConversationMessage() + + @Serializable + @SerialName("user") + data class User(val text: String) : RobotConversationMessage() + + @Serializable + @SerialName("robot") + data class Robot(val text: String) : RobotConversationMessage() + + @Serializable + @SerialName("cite") + data class Cite(val urls: List) : RobotConversationMessage() +} diff --git a/src/jvmMain/kotlin/info/mechyrdia/robot/RobotSse.kt b/src/main/kotlin/info/mechyrdia/robot/RobotSse.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/robot/RobotSse.kt rename to src/main/kotlin/info/mechyrdia/robot/RobotSse.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/robot/RobotUserLimiter.kt b/src/main/kotlin/info/mechyrdia/robot/RobotUserLimiter.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/robot/RobotUserLimiter.kt rename to src/main/kotlin/info/mechyrdia/robot/RobotUserLimiter.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/robot/ViewsRobot.kt b/src/main/kotlin/info/mechyrdia/robot/ViewsRobot.kt similarity index 96% rename from src/jvmMain/kotlin/info/mechyrdia/robot/ViewsRobot.kt rename to src/main/kotlin/info/mechyrdia/robot/ViewsRobot.kt index 4541e68..1e36cae 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/robot/ViewsRobot.kt +++ b/src/main/kotlin/info/mechyrdia/robot/ViewsRobot.kt @@ -54,8 +54,10 @@ suspend fun ApplicationCall.robotPage(): HTML.() -> Unit { suspend fun WebSocketSession.closeReasonably(reason: String) = close(CloseReason(Codes.NORMAL, reason)) -suspend fun DefaultWebSocketServerSession.robotConversation(csrfToken: String? = null) { +suspend fun DefaultWebSocketServerSession.robotConversation() { val nation = call.currentNation()?.id ?: return closeReasonably("Anonymous usage of NUKE is not allowed") + + val csrfToken = (incoming.receive() as? Frame.Text)?.readText() if (!call.checkCsrfToken(csrfToken, call.href(Root.Nuke.WS()))) return closeReasonably("CSRF token failed verification") diff --git a/src/jvmMain/kotlin/info/mechyrdia/route/ResourceBodies.kt b/src/main/kotlin/info/mechyrdia/route/ResourceBodies.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/route/ResourceBodies.kt rename to src/main/kotlin/info/mechyrdia/route/ResourceBodies.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/route/ResourceCsrf.kt b/src/main/kotlin/info/mechyrdia/route/ResourceCsrf.kt similarity index 100% rename from src/jvmMain/kotlin/info/mechyrdia/route/ResourceCsrf.kt rename to src/main/kotlin/info/mechyrdia/route/ResourceCsrf.kt diff --git a/src/jvmMain/kotlin/info/mechyrdia/route/ResourceHandler.kt b/src/main/kotlin/info/mechyrdia/route/ResourceHandler.kt similarity index 81% rename from src/jvmMain/kotlin/info/mechyrdia/route/ResourceHandler.kt rename to src/main/kotlin/info/mechyrdia/route/ResourceHandler.kt index d49326c..f8ed9af 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/route/ResourceHandler.kt +++ b/src/main/kotlin/info/mechyrdia/route/ResourceHandler.kt @@ -10,15 +10,21 @@ import io.ktor.server.application.ApplicationCall import io.ktor.server.application.ApplicationCallPipeline import io.ktor.server.application.application import io.ktor.server.application.call +import io.ktor.server.application.createRouteScopedPlugin +import io.ktor.server.application.hooks.CallSetup import io.ktor.server.application.plugin import io.ktor.server.plugins.BadRequestException import io.ktor.server.request.receiveMultipart +import io.ktor.server.request.uri import io.ktor.server.resources.Resources import io.ktor.server.resources.get import io.ktor.server.resources.href import io.ktor.server.resources.post import io.ktor.server.resources.resource import io.ktor.server.routing.Route +import io.ktor.server.routing.RoutingContext +import io.ktor.server.routing.application +import io.ktor.server.routing.intercept import io.ktor.server.websocket.DefaultWebSocketServerSession import io.ktor.server.websocket.WebSocketServerSession import io.ktor.server.websocket.application @@ -41,7 +47,7 @@ import kotlinx.serialization.serializer import kotlin.enums.EnumEntries interface ResourceHandler { - suspend fun PipelineContext.handleCall() + suspend fun RoutingContext.handleCall() } interface ResourceListener { @@ -49,7 +55,7 @@ interface ResourceListener { } interface ResourceReceiver

{ - suspend fun PipelineContext.handleCall(payload: P) + suspend fun RoutingContext.handleCall(payload: P) } interface ResourceFilter { @@ -76,18 +82,22 @@ inline fun , reified P : MultiPartPayload> Route val WebSocketResourceInstanceKey: AttributeKey = AttributeKey("WebSocketResourceInstance") +inline fun WebSocketResourcePlugin() = createRouteScopedPlugin("WebSocketResourcePlugin") { + val serializer = serializer() + on(CallSetup) { call -> + val resources = call.application.plugin(Resources) + try { + val resource = resources.resourcesFormat.decodeFromParameters(serializer, call.parameters) + call.attributes.put(WebSocketResourceInstanceKey, resource) + } catch (cause: Throwable) { + throw BadRequestException("Can't transform call into resource", cause) + } + } +} + inline fun Route.ws() { resource { - val serializer = serializer() - intercept(ApplicationCallPipeline.Plugins) { - val resources = application.plugin(Resources) - try { - val resource = resources.resourcesFormat.decodeFromParameters(serializer, call.parameters) - call.attributes.put(WebSocketResourceInstanceKey, resource) - } catch (cause: Throwable) { - throw BadRequestException("Can't transform call to resource", cause) - } - } + install(WebSocketResourcePlugin()) webSocket { val resource = call.attributes[WebSocketResourceInstanceKey] as T @@ -131,4 +141,4 @@ class FormUrlEncodedFormat(private val resourcesFormat: ResourcesFormat) : Strin inline fun Application.href(resource: T, hash: String? = null): String = URLBuilder().also { href(resource, it) }.build().fullPath + hash?.let { "#$it" }.orEmpty() inline fun ApplicationCall.href(resource: T, hash: String? = null) = application.href(resource, hash) inline fun WebSocketServerSession.href(resource: T, hash: String? = null) = application.href(resource, hash) -inline fun PipelineContext.href(resource: T, hash: String? = null) = application.href(resource, hash) +inline fun RoutingContext.href(resource: T, hash: String? = null) = call.href(resource, hash) diff --git a/src/jvmMain/kotlin/info/mechyrdia/route/ResourceMultipart.kt b/src/main/kotlin/info/mechyrdia/route/ResourceMultipart.kt similarity index 92% rename from src/jvmMain/kotlin/info/mechyrdia/route/ResourceMultipart.kt rename to src/main/kotlin/info/mechyrdia/route/ResourceMultipart.kt index 1e459b9..6675550 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/route/ResourceMultipart.kt +++ b/src/main/kotlin/info/mechyrdia/route/ResourceMultipart.kt @@ -3,7 +3,6 @@ package info.mechyrdia.route import io.ktor.http.content.MultiPartData import io.ktor.http.content.PartData import io.ktor.http.content.forEachPart -import io.ktor.http.content.readAllParts import kotlin.reflect.full.companionObjectInstance interface MultiPartPayload : AutoCloseable { @@ -49,7 +48,9 @@ data class PlainMultiPartPayload( ) : MultiPartPayload { companion object : MultiPartPayloadProcessor { override suspend fun process(data: MultiPartData): PlainMultiPartPayload { - return PlainMultiPartPayload(data.readAllParts()) + val payload = mutableListOf() + data.forEachPart { part -> payload.add(part) } + return PlainMultiPartPayload(payload) } } } diff --git a/src/jvmMain/kotlin/info/mechyrdia/route/ResourceTypes.kt b/src/main/kotlin/info/mechyrdia/route/ResourceTypes.kt similarity index 79% rename from src/jvmMain/kotlin/info/mechyrdia/route/ResourceTypes.kt rename to src/main/kotlin/info/mechyrdia/route/ResourceTypes.kt index e7831bd..58ad73b 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/route/ResourceTypes.kt +++ b/src/main/kotlin/info/mechyrdia/route/ResourceTypes.kt @@ -45,6 +45,7 @@ import info.mechyrdia.lore.buildSitemap import info.mechyrdia.lore.clientSettingsPage import info.mechyrdia.lore.galaxyMapPage import info.mechyrdia.lore.generateRecentPageEdits +import info.mechyrdia.lore.loadFontsJson import info.mechyrdia.lore.loreArticlePage import info.mechyrdia.lore.loreIntroPage import info.mechyrdia.lore.parseAs @@ -67,14 +68,14 @@ import io.ktor.http.HttpStatusCode import io.ktor.http.content.PartData import io.ktor.resources.Resource import io.ktor.server.application.ApplicationCall -import io.ktor.server.application.call import io.ktor.server.html.respondHtml import io.ktor.server.plugins.MissingRequestParameterException import io.ktor.server.response.header import io.ktor.server.response.respondText +import io.ktor.server.response.respondTextWriter +import io.ktor.server.routing.RoutingContext import io.ktor.server.websocket.DefaultWebSocketServerSession import io.ktor.util.AttributeKey -import io.ktor.util.pipeline.PipelineContext import kotlinx.coroutines.delay const val ErrorMessageCookieName = "ERROR_MSG" @@ -87,23 +88,35 @@ class Root : ResourceHandler, ResourceFilter { request.cookies[ErrorMessageCookieName]?.let { attributes.put(ErrorMessageAttributeKey, it) } } - override suspend fun PipelineContext.handleCall() { + override suspend fun RoutingContext.handleCall() { call.filterCall() call.respondHtml(HttpStatusCode.OK, call.loreIntroPage()) } @Resource("assets/{path...}") class AssetFile(val path: List, val root: Root = Root()) : ResourceHandler { - override suspend fun PipelineContext.handleCall() { + override suspend fun RoutingContext.handleCall() { with(root) { call.filterCall() } call.respondAsset(StoragePath.assetDir / path) } } + @Resource("fonts.css") + class CustomFontsStyle(val root: Root = Root()) : ResourceHandler { + override suspend fun RoutingContext.handleCall() { + with(root) { call.filterCall() } + + val fontsManifest = loadFontsJson() + call.respondTextWriter(ContentType.Text.CSS) { + with(fontsManifest) { renderCss() } + } + } + } + @Resource("lore/{path...}") class LorePage(val path: List, val format: LoreArticleFormat = LoreArticleFormat.HTML, val root: Root = Root()) : ResourceHandler { - override suspend fun PipelineContext.handleCall() { + override suspend fun RoutingContext.handleCall() { with(root) { call.filterCall() } call.respondHtml(HttpStatusCode.OK, call.loreArticlePage(path, format)) @@ -112,7 +125,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("map") class GalaxyMap(val root: Root = Root()) : ResourceHandler { - override suspend fun PipelineContext.handleCall() { + override suspend fun RoutingContext.handleCall() { with(root) { call.filterCall() } call.respondStoredFile(call.galaxyMapPage()) @@ -121,7 +134,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("quote") class RandomQuote(val format: QuoteFormat = QuoteFormat.HTML, val root: Root = Root()) : ResourceHandler { - override suspend fun PipelineContext.handleCall() { + override suspend fun RoutingContext.handleCall() { with(root) { call.filterCall() } with(format) { call.respondQuote(randomQuote()) } @@ -130,7 +143,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("robots.txt") class RobotsTxt(val root: Root = Root()) : ResourceHandler { - override suspend fun PipelineContext.handleCall() { + override suspend fun RoutingContext.handleCall() { with(root) { call.filterCall() } call.respondStoredFile(StoragePath.Root / "robots.txt") @@ -139,7 +152,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("sitemap.xml") class SitemapXml(val root: Root = Root()) : ResourceHandler { - override suspend fun PipelineContext.handleCall() { + override suspend fun RoutingContext.handleCall() { with(root) { call.filterCall() } val sitemap = buildSitemap(call) @@ -151,7 +164,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("edits.rss") class RecentEditsRss(val root: Root = Root()) : ResourceHandler { - override suspend fun PipelineContext.handleCall() { + override suspend fun RoutingContext.handleCall() { with(root) { call.filterCall() } call.respondRss(generateRecentPageEdits(call)) @@ -160,7 +173,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("comments.rss") class RecentCommentsRss(val limit: Int = 10, val root: Root = Root()) : ResourceHandler { - override suspend fun PipelineContext.handleCall() { + override suspend fun RoutingContext.handleCall() { with(root) { call.filterCall() } call.respondRss(call.recentCommentsRssFeedGenerator(limit)) @@ -169,7 +182,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("preferences") class ClientPreferences(val root: Root = Root()) : ResourceHandler { - override suspend fun PipelineContext.handleCall() { + override suspend fun RoutingContext.handleCall() { with(root) { call.filterCall() } call.respondHtml(HttpStatusCode.OK, call.clientSettingsPage()) @@ -184,7 +197,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("login") class LoginPage(val auth: Auth = Auth()) : ResourceHandler { - override suspend fun PipelineContext.handleCall() { + override suspend fun RoutingContext.handleCall() { with(auth) { call.filterCall() } call.respondHtml(HttpStatusCode.OK, call.loginPage()) @@ -193,7 +206,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("login") class LoginPost(val auth: Auth = Auth()) : ResourceReceiver { - override suspend fun PipelineContext.handleCall(payload: LoginPayload) { + override suspend fun RoutingContext.handleCall(payload: LoginPayload) { with(auth) { call.filterCall() } with(payload) { call.verifyCsrfToken() } @@ -203,7 +216,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("logout") class LogoutPost(val auth: Auth = Auth()) : ResourceReceiver { - override suspend fun PipelineContext.handleCall(payload: LogoutPayload) { + override suspend fun RoutingContext.handleCall(payload: LogoutPayload) { with(auth) { call.filterCall() } with(payload) { call.verifyCsrfToken() } @@ -218,17 +231,17 @@ class Root : ResourceHandler, ResourceFilter { with(root) { filterCall() } } - override suspend fun PipelineContext.handleCall() { + override suspend fun RoutingContext.handleCall() { call.filterCall() call.respondHtml(HttpStatusCode.OK, call.robotPage()) } @Resource("ws") - class WS(val csrfToken: String? = null, val nuke: Nuke = Nuke()) : ResourceListener { + class WS(val nuke: Nuke = Nuke()) : ResourceListener { override suspend fun DefaultWebSocketServerSession.handleCall() { with(nuke) { call.filterCall() } - robotConversation(csrfToken) + robotConversation() } } } @@ -241,7 +254,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("help") class HelpPage(val comments: Comments = Comments()) : ResourceHandler { - override suspend fun PipelineContext.handleCall() { + override suspend fun RoutingContext.handleCall() { with(comments) { call.filterCall() } call.respondHtml(HttpStatusCode.OK, call.commentHelpPage()) @@ -250,7 +263,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("recent") class RecentPage(val limit: Int? = null, val comments: Comments = Comments()) : ResourceHandler { - override suspend fun PipelineContext.handleCall() { + override suspend fun RoutingContext.handleCall() { with(comments) { call.filterCall() } call.respondHtml(HttpStatusCode.OK, call.recentCommentsPage(limit)) @@ -259,7 +272,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("new/{path...}") class NewPost(val path: List, val comments: Comments = Comments()) : ResourceReceiver { - override suspend fun PipelineContext.handleCall(payload: NewCommentPayload) { + override suspend fun RoutingContext.handleCall(payload: NewCommentPayload) { with(comments) { call.filterCall() } with(payload) { call.verifyCsrfToken() } @@ -269,7 +282,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("view/{id}") class ViewPage(val id: Id, val comments: Comments = Comments()) : ResourceHandler { - override suspend fun PipelineContext.handleCall() { + override suspend fun RoutingContext.handleCall() { with(comments) { call.filterCall() } call.viewCommentRoute(id) @@ -278,7 +291,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("edit/{id}") class EditPost(val id: Id, val comments: Comments = Comments()) : ResourceReceiver { - override suspend fun PipelineContext.handleCall(payload: EditCommentPayload) { + override suspend fun RoutingContext.handleCall(payload: EditCommentPayload) { with(comments) { call.filterCall() } with(payload) { call.verifyCsrfToken() } @@ -288,7 +301,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("delete/{id}") class DeleteConfirmPage(val id: Id, val comments: Comments = Comments()) : ResourceHandler { - override suspend fun PipelineContext.handleCall() { + override suspend fun RoutingContext.handleCall() { with(comments) { call.filterCall() } call.respondHtml(HttpStatusCode.OK, call.deleteCommentPage(id)) @@ -297,7 +310,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("delete/{id}") class DeleteConfirmPost(val id: Id, val comments: Comments = Comments()) : ResourceReceiver { - override suspend fun PipelineContext.handleCall(payload: DeleteCommentPayload) { + override suspend fun RoutingContext.handleCall(payload: DeleteCommentPayload) { with(comments) { call.filterCall() } with(payload) { call.verifyCsrfToken() } @@ -312,14 +325,14 @@ class Root : ResourceHandler, ResourceFilter { with(root) { filterCall() } } - override suspend fun PipelineContext.handleCall() { + override suspend fun RoutingContext.handleCall() { call.filterCall() call.currentUserPage() } @Resource("{id}") class ById(val id: Id, val user: User = User()) : ResourceHandler { - override suspend fun PipelineContext.handleCall() { + override suspend fun RoutingContext.handleCall() { with(user) { call.filterCall() } call.respondHtml(HttpStatusCode.OK, call.userPage(id)) @@ -336,7 +349,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("ban/{id}") class Ban(val id: Id, val admin: Admin = Admin()) : ResourceReceiver { - override suspend fun PipelineContext.handleCall(payload: AdminBanUserPayload) { + override suspend fun RoutingContext.handleCall(payload: AdminBanUserPayload) { with(admin) { call.filterCall() } with(payload) { call.verifyCsrfToken() } @@ -346,7 +359,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("unban/{id}") class Unban(val id: Id, val admin: Admin = Admin()) : ResourceReceiver { - override suspend fun PipelineContext.handleCall(payload: AdminUnbanUserPayload) { + override suspend fun RoutingContext.handleCall(payload: AdminUnbanUserPayload) { with(admin) { call.filterCall() } with(payload) { call.verifyCsrfToken() } @@ -360,14 +373,14 @@ class Root : ResourceHandler, ResourceFilter { with(admin) { filterCall() } } - override suspend fun PipelineContext.handleCall() { + override suspend fun RoutingContext.handleCall() { call.filterCall() call.respondHtml(HttpStatusCode.OK, call.robotManagementPage()) } @Resource("update") class Update(val nukeManagement: NukeManagement = NukeManagement()) : ResourceReceiver { - override suspend fun PipelineContext.handleCall(payload: AdminNukeUpdatePayload) { + override suspend fun RoutingContext.handleCall(payload: AdminNukeUpdatePayload) { with(nukeManagement) { call.filterCall() } with(payload) { call.verifyCsrfToken() } @@ -379,7 +392,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("reset") class Reset(val nukeManagement: NukeManagement = NukeManagement()) : ResourceReceiver { - override suspend fun PipelineContext.handleCall(payload: AdminNukeResetPayload) { + override suspend fun RoutingContext.handleCall(payload: AdminNukeResetPayload) { with(nukeManagement) { call.filterCall() } with(payload) { call.verifyCsrfToken() } @@ -398,7 +411,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("inline/{path...}") class Inline(val path: List, val vfs: Vfs = Vfs()) : ResourceHandler { - override suspend fun PipelineContext.handleCall() { + override suspend fun RoutingContext.handleCall() { with(vfs) { call.filterCall() } call.response.header(HttpHeaders.ContentDisposition, "inline") @@ -408,7 +421,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("download/{path...}") class Download(val path: List, val vfs: Vfs = Vfs()) : ResourceHandler { - override suspend fun PipelineContext.handleCall() { + override suspend fun RoutingContext.handleCall() { with(vfs) { call.filterCall() } call.response.header(HttpHeaders.ContentDisposition, "attachment; filename=\"${path.last()}\"") @@ -418,7 +431,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("view/{path...}") class View(val path: List, val vfs: Vfs = Vfs()) : ResourceHandler { - override suspend fun PipelineContext.handleCall() { + override suspend fun RoutingContext.handleCall() { with(vfs) { call.filterCall() } call.respondHtml(HttpStatusCode.OK, call.adminViewVfs(StoragePath(path))) @@ -427,7 +440,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("webdav-token") class WebDavTokenPage(val vfs: Vfs = Vfs()) : ResourceHandler { - override suspend fun PipelineContext.handleCall() { + override suspend fun RoutingContext.handleCall() { with(vfs) { call.filterCall() } call.respondHtml(HttpStatusCode.OK, call.adminRequestWebDavToken()) @@ -436,7 +449,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("webdav-token") class WebDavTokenPost(val vfs: Vfs = Vfs()) : ResourceReceiver { - override suspend fun PipelineContext.handleCall(payload: AdminVfsRequestWebDavTokenPayload) { + override suspend fun RoutingContext.handleCall(payload: AdminVfsRequestWebDavTokenPayload) { with(vfs) { call.filterCall() } with(payload) { call.verifyCsrfToken() } @@ -446,7 +459,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("copy/{path...}") class CopyPage(val path: List, val vfs: Vfs = Vfs()) : ResourceHandler { - override suspend fun PipelineContext.handleCall() { + override suspend fun RoutingContext.handleCall() { with(vfs) { call.filterCall() } call.respondHtml(HttpStatusCode.OK, call.adminShowCopyFile(StoragePath(path))) @@ -455,7 +468,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("copy/{path...}") class CopyPost(val path: List, val vfs: Vfs = Vfs()) : ResourceReceiver { - override suspend fun PipelineContext.handleCall(payload: AdminVfsCopyFilePayload) { + override suspend fun RoutingContext.handleCall(payload: AdminVfsCopyFilePayload) { with(vfs) { call.filterCall() } with(payload) { call.verifyCsrfToken() } @@ -465,7 +478,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("upload/{path...}") class Upload(val path: List, val vfs: Vfs = Vfs()) : ResourceReceiver { - override suspend fun PipelineContext.handleCall(payload: CsrfProtectedMultiPartPayload) { + override suspend fun RoutingContext.handleCall(payload: CsrfProtectedMultiPartPayload) { with(vfs) { call.filterCall() } with(payload) { call.verifyCsrfToken() } @@ -478,7 +491,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("overwrite/{path...}") class Overwrite(val path: List, val vfs: Vfs = Vfs()) : ResourceReceiver { - override suspend fun PipelineContext.handleCall(payload: CsrfProtectedMultiPartPayload) { + override suspend fun RoutingContext.handleCall(payload: CsrfProtectedMultiPartPayload) { with(vfs) { call.filterCall() } with(payload) { call.verifyCsrfToken() } @@ -491,7 +504,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("delete/{path...}") class DeleteConfirmPage(val path: List, val vfs: Vfs = Vfs()) : ResourceHandler { - override suspend fun PipelineContext.handleCall() { + override suspend fun RoutingContext.handleCall() { with(vfs) { call.filterCall() } call.adminConfirmDeleteFile(StoragePath(path)) @@ -500,7 +513,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("delete/{path...}") class DeleteConfirmPost(val path: List, val vfs: Vfs = Vfs()) : ResourceReceiver { - override suspend fun PipelineContext.handleCall(payload: AdminVfsDeleteFilePayload) { + override suspend fun RoutingContext.handleCall(payload: AdminVfsDeleteFilePayload) { with(vfs) { call.filterCall() } with(payload) { call.verifyCsrfToken() } @@ -510,7 +523,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("mkdir/{path...}") class MkDir(val path: List, val vfs: Vfs = Vfs()) : ResourceReceiver { - override suspend fun PipelineContext.handleCall(payload: AdminVfsMkDirPayload) { + override suspend fun RoutingContext.handleCall(payload: AdminVfsMkDirPayload) { with(vfs) { call.filterCall() } with(payload) { call.verifyCsrfToken() } @@ -520,7 +533,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("rmdir/{path...}") class RmDirConfirmPage(val path: List, val vfs: Vfs = Vfs()) : ResourceHandler { - override suspend fun PipelineContext.handleCall() { + override suspend fun RoutingContext.handleCall() { with(vfs) { call.filterCall() } call.adminConfirmRemoveDirectory(StoragePath(path)) @@ -529,7 +542,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("rmdir/{path...}") class RmDirConfirmPost(val path: List, val vfs: Vfs = Vfs()) : ResourceReceiver { - override suspend fun PipelineContext.handleCall(payload: AdminVfsRmDirPayload) { + override suspend fun RoutingContext.handleCall(payload: AdminVfsRmDirPayload) { with(vfs) { call.filterCall() } with(payload) { call.verifyCsrfToken() } @@ -549,7 +562,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("mechyrdia-sans") class MechyrdiaSans(val utils: Utils = Utils()) : ResourceReceiver { - override suspend fun PipelineContext.handleCall(payload: MechyrdiaSansPayload) { + override suspend fun RoutingContext.handleCall(payload: MechyrdiaSansPayload) { with(utils) { call.filterCall() } val svgDoc = MechyrdiaSansFont.renderTextToSvg(payload.lines.concat("\n") { it.trim() }, payload.bold, payload.italic, payload.align) @@ -561,7 +574,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("tylan-lang") class TylanLanguage(val utils: Utils = Utils()) : ResourceReceiver { - override suspend fun PipelineContext.handleCall(payload: TylanLanguagePayload) { + override suspend fun RoutingContext.handleCall(payload: TylanLanguagePayload) { with(utils) { call.filterCall() } call.respondText(TylanAlphabetFont.tylanToFontAlphabet(payload.lines.concat("\n"))) @@ -570,7 +583,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("pokhwal-lang") class PokhwalishLanguage(val utils: Utils = Utils()) : ResourceReceiver { - override suspend fun PipelineContext.handleCall(payload: PokhwalishLanguagePayload) { + override suspend fun RoutingContext.handleCall(payload: PokhwalishLanguagePayload) { with(utils) { call.filterCall() } call.respondText(PokhwalishAlphabetFont.pokhwalToFontAlphabet(payload.lines.concat("\n"))) @@ -579,7 +592,7 @@ class Root : ResourceHandler, ResourceFilter { @Resource("preview-comment") class PreviewComment(val utils: Utils = Utils()) : ResourceReceiver { - override suspend fun PipelineContext.handleCall(payload: PreviewCommentPayload) { + override suspend fun RoutingContext.handleCall(payload: PreviewCommentPayload) { with(utils) { call.filterCall() } call.respondText( diff --git a/src/jvmMain/kotlin/info/mechyrdia/route/ResourceWebDav.kt b/src/main/kotlin/info/mechyrdia/route/ResourceWebDav.kt similarity index 99% rename from src/jvmMain/kotlin/info/mechyrdia/route/ResourceWebDav.kt rename to src/main/kotlin/info/mechyrdia/route/ResourceWebDav.kt index 0b63763..d9fd484 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/route/ResourceWebDav.kt +++ b/src/main/kotlin/info/mechyrdia/route/ResourceWebDav.kt @@ -21,7 +21,6 @@ import io.ktor.http.HttpHeaders import io.ktor.http.HttpMethod import io.ktor.http.HttpStatusCode import io.ktor.server.application.ApplicationCall -import io.ktor.server.application.call import io.ktor.server.html.respondHtml import io.ktor.server.request.ApplicationRequest import io.ktor.server.request.authorization diff --git a/src/jvmMain/resources/logback.xml b/src/main/resources/logback.xml similarity index 100% rename from src/jvmMain/resources/logback.xml rename to src/main/resources/logback.xml diff --git a/src/jvmMain/resources/static/admin.css b/src/main/resources/static/admin.css similarity index 100% rename from src/jvmMain/resources/static/admin.css rename to src/main/resources/static/admin.css diff --git a/src/jvmMain/resources/static/admin.js b/src/main/resources/static/admin.js similarity index 100% rename from src/jvmMain/resources/static/admin.js rename to src/main/resources/static/admin.js diff --git a/src/jvmMain/resources/static/font/DejaVuSans-Bold.woff b/src/main/resources/static/font/DejaVuSans-Bold.woff similarity index 100% rename from src/jvmMain/resources/static/font/DejaVuSans-Bold.woff rename to src/main/resources/static/font/DejaVuSans-Bold.woff diff --git a/src/jvmMain/resources/static/font/DejaVuSans-BoldOblique.woff b/src/main/resources/static/font/DejaVuSans-BoldOblique.woff similarity index 100% rename from src/jvmMain/resources/static/font/DejaVuSans-BoldOblique.woff rename to src/main/resources/static/font/DejaVuSans-BoldOblique.woff diff --git a/src/jvmMain/resources/static/font/DejaVuSans-Oblique.woff b/src/main/resources/static/font/DejaVuSans-Oblique.woff similarity index 100% rename from src/jvmMain/resources/static/font/DejaVuSans-Oblique.woff rename to src/main/resources/static/font/DejaVuSans-Oblique.woff diff --git a/src/jvmMain/resources/static/font/DejaVuSans.woff b/src/main/resources/static/font/DejaVuSans.woff similarity index 100% rename from src/jvmMain/resources/static/font/DejaVuSans.woff rename to src/main/resources/static/font/DejaVuSans.woff diff --git a/src/jvmMain/resources/static/font/JetBrainsMono-ExtraBold.woff b/src/main/resources/static/font/JetBrainsMono-ExtraBold.woff similarity index 100% rename from src/jvmMain/resources/static/font/JetBrainsMono-ExtraBold.woff rename to src/main/resources/static/font/JetBrainsMono-ExtraBold.woff diff --git a/src/jvmMain/resources/static/font/JetBrainsMono-ExtraBoldItalic.woff b/src/main/resources/static/font/JetBrainsMono-ExtraBoldItalic.woff similarity index 100% rename from src/jvmMain/resources/static/font/JetBrainsMono-ExtraBoldItalic.woff rename to src/main/resources/static/font/JetBrainsMono-ExtraBoldItalic.woff diff --git a/src/jvmMain/resources/static/font/JetBrainsMono-Medium.woff b/src/main/resources/static/font/JetBrainsMono-Medium.woff similarity index 100% rename from src/jvmMain/resources/static/font/JetBrainsMono-Medium.woff rename to src/main/resources/static/font/JetBrainsMono-Medium.woff diff --git a/src/jvmMain/resources/static/font/JetBrainsMono-MediumItalic.woff b/src/main/resources/static/font/JetBrainsMono-MediumItalic.woff similarity index 100% rename from src/jvmMain/resources/static/font/JetBrainsMono-MediumItalic.woff rename to src/main/resources/static/font/JetBrainsMono-MediumItalic.woff diff --git a/src/jvmMain/resources/static/font/Oxanium-Bold.woff b/src/main/resources/static/font/Oxanium-Bold.woff similarity index 100% rename from src/jvmMain/resources/static/font/Oxanium-Bold.woff rename to src/main/resources/static/font/Oxanium-Bold.woff diff --git a/src/jvmMain/resources/static/font/Oxanium-ExtraBold.woff b/src/main/resources/static/font/Oxanium-ExtraBold.woff similarity index 100% rename from src/jvmMain/resources/static/font/Oxanium-ExtraBold.woff rename to src/main/resources/static/font/Oxanium-ExtraBold.woff diff --git a/src/jvmMain/resources/static/font/Oxanium-Regular.woff b/src/main/resources/static/font/Oxanium-Regular.woff similarity index 100% rename from src/jvmMain/resources/static/font/Oxanium-Regular.woff rename to src/main/resources/static/font/Oxanium-Regular.woff diff --git a/src/jvmMain/resources/static/font/Oxanium-SemiBold.woff b/src/main/resources/static/font/Oxanium-SemiBold.woff similarity index 100% rename from src/jvmMain/resources/static/font/Oxanium-SemiBold.woff rename to src/main/resources/static/font/Oxanium-SemiBold.woff diff --git a/src/jvmMain/resources/static/images/external-link-dark.png b/src/main/resources/static/images/external-link-dark.png similarity index 100% rename from src/jvmMain/resources/static/images/external-link-dark.png rename to src/main/resources/static/images/external-link-dark.png diff --git a/src/jvmMain/resources/static/images/external-link.png b/src/main/resources/static/images/external-link.png similarity index 100% rename from src/jvmMain/resources/static/images/external-link.png rename to src/main/resources/static/images/external-link.png diff --git a/src/jvmMain/resources/static/images/icon.png b/src/main/resources/static/images/icon.png similarity index 100% rename from src/jvmMain/resources/static/images/icon.png rename to src/main/resources/static/images/icon.png diff --git a/src/jvmMain/resources/static/images/icon.svg b/src/main/resources/static/images/icon.svg similarity index 100% rename from src/jvmMain/resources/static/images/icon.svg rename to src/main/resources/static/images/icon.svg diff --git a/src/jvmMain/resources/static/init.js b/src/main/resources/static/init.js similarity index 99% rename from src/jvmMain/resources/static/init.js rename to src/main/resources/static/init.js index ef3b1c0..764a90a 100644 --- a/src/jvmMain/resources/static/init.js +++ b/src/main/resources/static/init.js @@ -284,11 +284,10 @@ /** * @param {ParentNode} element - * @param {string} text + * @param {Array.} lines * @return {void} */ - function appendWithLineBreaks(element, text) { - const lines = text.split("\n"); + function appendWithLineBreaks(element, lines) { let isFirst = true; for (const line of lines) { if (isFirst) @@ -1273,7 +1272,7 @@ nukeBox.after(container); nukeBox.remove(); - const targetUrl = "ws" + window.location.href.substring(4) + "/ws?csrfToken=" + nukeBox.getAttribute("data-ws-csrf-token"); + const targetUrl = "ws" + window.location.href.substring(4) + "/ws"; const webSock = new WebSocket(targetUrl); inputForm.addEventListener("submit", e => { @@ -1293,7 +1292,7 @@ const userP = document.createElement("p"); userP.style.textAlign = "right"; userP.style.paddingLeft = "50%"; - appendWithLineBreaks(userP, data.text); + appendWithLineBreaks(userP, data.text.split("\n")); chatHistory.appendChild(userP); const robotP = document.createElement("p"); @@ -1302,7 +1301,7 @@ chatHistory.appendChild(robotP); } else if (data.type === "robot") { const robotP = chatHistory.lastElementChild; - appendWithLineBreaks(robotP, data.text); + appendWithLineBreaks(robotP, data.text.split("\n")); } else if (data.type === "cite") { const robotP = chatHistory.lastElementChild; const robotCiteList = robotP.appendChild(document.createElement("ol")); @@ -1319,11 +1318,15 @@ statusP.style.textAlign = "center"; statusP.style.paddingLeft = "25%"; statusP.style.paddingRight = "25%"; - appendWithLineBreaks(statusP, "The connection has been closed\n" + e.reason); + appendWithLineBreaks(statusP, ["The connection has been closed", e.reason]); chatHistory.appendChild(statusP); enterBtn.disabled = true; }); + + webSock.addEventListener("open", _ => { + webSock.send(nukeBox.getAttribute("data-ws-csrf-token")); + }) } })(); } diff --git a/src/jvmMain/resources/static/obj-viewer/hammer.min.js b/src/main/resources/static/obj-viewer/hammer.min.js similarity index 100% rename from src/jvmMain/resources/static/obj-viewer/hammer.min.js rename to src/main/resources/static/obj-viewer/hammer.min.js diff --git a/src/jvmMain/resources/static/obj-viewer/three-examples.js b/src/main/resources/static/obj-viewer/three-examples.js similarity index 100% rename from src/jvmMain/resources/static/obj-viewer/three-examples.js rename to src/main/resources/static/obj-viewer/three-examples.js diff --git a/src/jvmMain/resources/static/obj-viewer/three.js b/src/main/resources/static/obj-viewer/three.js similarity index 100% rename from src/jvmMain/resources/static/obj-viewer/three.js rename to src/main/resources/static/obj-viewer/three.js diff --git a/src/jvmMain/resources/static/raw.css b/src/main/resources/static/raw.css similarity index 100% rename from src/jvmMain/resources/static/raw.css rename to src/main/resources/static/raw.css diff --git a/src/jvmMain/resources/static/style.css b/src/main/resources/static/style.css similarity index 90% rename from src/jvmMain/resources/static/style.css rename to src/main/resources/static/style.css index 3f47a84..a9a77a8 100644 --- a/src/jvmMain/resources/static/style.css +++ b/src/main/resources/static/style.css @@ -703,78 +703,6 @@ iframe { text-align: center; } -@font-face { - font-family: 'Tulasra'; - src: url(/static/font/tylan-language-alphabet-3.woff) format('woff'); -} - -.lang-tylan { - font-family: Tulasra, monospace; - font-size: 1.25em; - line-height: 1.0; - font-variant: normal !important; -} - -textarea.lang-tylan { - font-family: Tulasra, monospace; - font-size: 2.5em; - line-height: 1.0; -} - -@font-face { - font-family: 'Theodisc'; - src: url(/static/font/thedish-language-alphabet.woff) format('woff'); -} - -.lang-thedish { - font-family: Theodisc, monospace; - font-size: 1.25em; - line-height: 1.0; - font-variant: normal !important; -} - -textarea.lang-thedish { - font-family: Theodisc, monospace; - font-size: 2.5em; - line-height: 1.0; -} - -@font-face { - font-family: 'Kishari'; - src: url(/static/font/kishari-language-alphabet.woff) format('woff'); -} - -.lang-kishari { - font-family: Kishari, monospace; - font-size: 1.25em; - line-height: 1.0; - font-variant: normal !important; -} - -textarea.lang-kishari { - font-family: Kishari, monospace; - font-size: 2.5em; - line-height: 1.0; -} - -@font-face { - font-family: 'Pochvalsk'; - src: url(/static/font/pokhval-language-alphabet.woff) format('woff'); -} - -.lang-pokhwal { - font-family: Pochvalsk, monospace; - font-size: 1em; - line-height: 1.0; - font-variant: normal !important; -} - -textarea.lang-pokhwal { - font-family: Pochvalsk, monospace; - font-size: 2em; - line-height: 1.0; -} - #thumb-view { z-index: 998; diff --git a/stuff/mintah_table.txt b/stuff/mintah_table.txt new file mode 100644 index 0000000..b134ae8 --- /dev/null +++ b/stuff/mintah_table.txt @@ -0,0 +1,2027 @@ +[table][tr] +[th=2x2]Part 1[/th] +[th=4x1]-∅[/th] +[th=4x1]-ng[/th] +[th=4x1]-n[/th] +[/tr][tr] +[th]-i[/th] +[th]-e[/th] +[th]-a[/th] +[th]-u[/th] +[th]-ing[/th] +[th]-eng[/th] +[th]-ang[/th] +[th]-ung[/th] +[th]-in[/th] +[th]-en[/th] +[th]-an[/th] +[th]-un[/th] +[/tr][tr] +[th=1x4]∅-[/th] +[th]-[/th] +[td]i[/td] +[td]e[/td] +[td]a[/td] +[td]u[/td] +[td]ing[/td] +[td]eng[/td] +[td]ang[/td] +[td]ung[/td] +[td]in[/td] +[td]en[/td] +[td]an[/td] +[td]un[/td] +[/tr][tr] +[th]y-[/th] +[td]❌[/td] +[td]❌[/td] +[td]ya[/td] +[td]yu[/td] +[td]❌[/td] +[td]❌[/td] +[td]yang[/td] +[td]yung[/td] +[td]❌[/td] +[td]❌[/td] +[td]yan[/td] +[td]yun[/td] +[/tr][tr] +[th]l-[/th] +[td]li[/td] +[td]le[/td] +[td]la[/td] +[td]lu[/td] +[td]ling[/td] +[td]leng[/td] +[td]lang[/td] +[td]lung[/td] +[td]lin[/td] +[td]len[/td] +[td]lan[/td] +[td]lun[/td] +[/tr][tr] +[th]w-[/th] +[td]wi[/td] +[td]we[/td] +[td]wa[/td] +[td]❌[/td] +[td]wing[/td] +[td]weng[/td] +[td]wang[/td] +[td]❌[/td] +[td]win[/td] +[td]wen[/td] +[td]wan[/td] +[td]❌[/td] +[/tr][tr] +[th=1x4]g-[/th] +[th]g-[/th] +[td]gi[/td] +[td]ge[/td] +[td]ga[/td] +[td]gu[/td] +[td]ging[/td] +[td]geng[/td] +[td]gang[/td] +[td]gung[/td] +[td]gin[/td] +[td]gen[/td] +[td]gan[/td] +[td]gun[/td] +[/tr][tr] +[th]gy-[/th] +[td]❌[/td] +[td]❌[/td] +[td]gia[/td] +[td]giu[/td] +[td]❌[/td] +[td]❌[/td] +[td]giang[/td] +[td]giung[/td] +[td]❌[/td] +[td]❌[/td] +[td]gian[/td] +[td]giun[/td] +[/tr][tr] +[th]gl-[/th] +[td]gli[/td] +[td]gle[/td] +[td]gla[/td] +[td]glu[/td] +[td]gling[/td] +[td]gleng[/td] +[td]glang[/td] +[td]glung[/td] +[td]glin[/td] +[td]glen[/td] +[td]glan[/td] +[td]glun[/td] +[/tr][tr] +[th]gw-[/th] +[td]gui[/td] +[td]gue[/td] +[td]gua[/td] +[td]❌[/td] +[td]guing[/td] +[td]gueng[/td] +[td]guang[/td] +[td]❌[/td] +[td]guin[/td] +[td]guen[/td] +[td]guan[/td] +[td]❌[/td] +[/tr][tr] +[th=1x3]d-[/th] +[th]d-[/th] +[td]di[/td] +[td]de[/td] +[td]da[/td] +[td]du[/td] +[td]ding[/td] +[td]deng[/td] +[td]dang[/td] +[td]dung[/td] +[td]din[/td] +[td]den[/td] +[td]dan[/td] +[td]dun[/td] +[/tr][tr] +[th]dy-[/th] +[td]❌[/td] +[td]❌[/td] +[td]ja[/td] +[td]ju[/td] +[td]❌[/td] +[td]❌[/td] +[td]jang[/td] +[td]jung[/td] +[td]❌[/td] +[td]❌[/td] +[td]jan[/td] +[td]jun[/td] +[/tr][tr] +[th]dw-[/th] +[td]dui[/td] +[td]due[/td] +[td]dua[/td] +[td]❌[/td] +[td]duing[/td] +[td]dueng[/td] +[td]duang[/td] +[td]❌[/td] +[td]duin[/td] +[td]duen[/td] +[td]duan[/td] +[td]❌[/td] +[/tr][tr] +[th=1x3]b-[/th] +[th]b-[/th] +[td]bi[/td] +[td]be[/td] +[td]ba[/td] +[td]bu[/td] +[td]bing[/td] +[td]beng[/td] +[td]bang[/td] +[td]bung[/td] +[td]bin[/td] +[td]ben[/td] +[td]ban[/td] +[td]bun[/td] +[/tr][tr] +[th]by-[/th] +[td]❌[/td] +[td]❌[/td] +[td]bia[/td] +[td]biu[/td] +[td]❌[/td] +[td]❌[/td] +[td]biang[/td] +[td]biung[/td] +[td]❌[/td] +[td]❌[/td] +[td]bian[/td] +[td]biun[/td] +[/tr][tr] +[th]bl-[/th] +[td]bli[/td] +[td]ble[/td] +[td]bla[/td] +[td]blu[/td] +[td]bling[/td] +[td]bleng[/td] +[td]blang[/td] +[td]blung[/td] +[td]blin[/td] +[td]blen[/td] +[td]blan[/td] +[td]blun[/td] +[/tr][tr] +[th=1x4]k-[/th] +[th]k-[/th] +[td]ki[/td] +[td]ke[/td] +[td]ka[/td] +[td]ku[/td] +[td]king[/td] +[td]keng[/td] +[td]kang[/td] +[td]kung[/td] +[td]kin[/td] +[td]ken[/td] +[td]kan[/td] +[td]kun[/td] +[/tr][tr] +[th]ky-[/th] +[td]❌[/td] +[td]❌[/td] +[td]kia[/td] +[td]kiu[/td] +[td]❌[/td] +[td]❌[/td] +[td]kiang[/td] +[td]kiung[/td] +[td]❌[/td] +[td]❌[/td] +[td]kian[/td] +[td]kiun[/td] +[/tr][tr] +[th]kl-[/th] +[td]kli[/td] +[td]kle[/td] +[td]kla[/td] +[td]klu[/td] +[td]kling[/td] +[td]kleng[/td] +[td]klang[/td] +[td]klung[/td] +[td]klin[/td] +[td]klen[/td] +[td]klan[/td] +[td]klun[/td] +[/tr][tr] +[th]kw-[/th] +[td]qui[/td] +[td]que[/td] +[td]qua[/td] +[td]❌[/td] +[td]quing[/td] +[td]queng[/td] +[td]quang[/td] +[td]❌[/td] +[td]quin[/td] +[td]quen[/td] +[td]quan[/td] +[td]❌[/td] +[/tr][tr] +[th=1x3]t-[/th] +[th]t-[/th] +[td]ti[/td] +[td]te[/td] +[td]ta[/td] +[td]tu[/td] +[td]ting[/td] +[td]teng[/td] +[td]tang[/td] +[td]tung[/td] +[td]tin[/td] +[td]ten[/td] +[td]tan[/td] +[td]tun[/td] +[/tr][tr] +[th]ty-[/th] +[td]❌[/td] +[td]❌[/td] +[td]cha[/td] +[td]chu[/td] +[td]❌[/td] +[td]❌[/td] +[td]chang[/td] +[td]chung[/td] +[td]❌[/td] +[td]❌[/td] +[td]chan[/td] +[td]chun[/td] +[/tr][tr] +[th]tw-[/th] +[td]tui[/td] +[td]tue[/td] +[td]tua[/td] +[td]❌[/td] +[td]tuing[/td] +[td]tueng[/td] +[td]tuang[/td] +[td]❌[/td] +[td]tuin[/td] +[td]tuen[/td] +[td]tuan[/td] +[td]❌[/td] +[/tr][tr] +[th=1x3]p-[/th] +[th]p-[/th] +[td]pi[/td] +[td]pe[/td] +[td]pa[/td] +[td]pu[/td] +[td]ping[/td] +[td]peng[/td] +[td]pang[/td] +[td]pung[/td] +[td]pin[/td] +[td]pen[/td] +[td]pan[/td] +[td]pun[/td] +[/tr][tr] +[th]py-[/th] +[td]❌[/td] +[td]❌[/td] +[td]pia[/td] +[td]piu[/td] +[td]❌[/td] +[td]❌[/td] +[td]piang[/td] +[td]piung[/td] +[td]❌[/td] +[td]❌[/td] +[td]pian[/td] +[td]piun[/td] +[/tr][tr] +[th]pl-[/th] +[td]pli[/td] +[td]ple[/td] +[td]pla[/td] +[td]plu[/td] +[td]pling[/td] +[td]pleng[/td] +[td]plang[/td] +[td]plung[/td] +[td]plin[/td] +[td]plen[/td] +[td]plan[/td] +[td]plun[/td] +[/tr][tr] +[th=2x1]gn-[/th] +[td]gni[/td] +[td]gne[/td] +[td]gna[/td] +[td]gnu[/td] +[td]gning[/td] +[td]gneng[/td] +[td]gnang[/td] +[td]gnung[/td] +[td]gnin[/td] +[td]gnen[/td] +[td]gnan[/td] +[td]gnun[/td] +[/tr][tr] +[th=2x1]n-[/th] +[td]ni[/td] +[td]ne[/td] +[td]na[/td] +[td]nu[/td] +[td]ning[/td] +[td]neng[/td] +[td]nang[/td] +[td]nung[/td] +[td]nin[/td] +[td]nen[/td] +[td]nan[/td] +[td]nun[/td] +[/tr][tr] +[th=2x1]m-[/th] +[td]mi[/td] +[td]me[/td] +[td]ma[/td] +[td]mu[/td] +[td]ming[/td] +[td]meng[/td] +[td]mang[/td] +[td]mung[/td] +[td]min[/td] +[td]men[/td] +[td]man[/td] +[td]mun[/td] +[/tr][tr] +[th=2x1]r-[/th] +[td]ri[/td] +[td]re[/td] +[td]ra[/td] +[td]ru[/td] +[td]ring[/td] +[td]reng[/td] +[td]rang[/td] +[td]rung[/td] +[td]rin[/td] +[td]ren[/td] +[td]ran[/td] +[td]run[/td] +[/tr][tr] +[th=1x4]z-[/th] +[th]z-[/th] +[td]zi[/td] +[td]ze[/td] +[td]za[/td] +[td]zu[/td] +[td]zing[/td] +[td]zeng[/td] +[td]zang[/td] +[td]zung[/td] +[td]zin[/td] +[td]zen[/td] +[td]zan[/td] +[td]zun[/td] +[/tr][tr] +[th]zy-[/th] +[td]❌[/td] +[td]❌[/td] +[td]zha[/td] +[td]zhu[/td] +[td]❌[/td] +[td]❌[/td] +[td]zhang[/td] +[td]zhung[/td] +[td]❌[/td] +[td]❌[/td] +[td]zhan[/td] +[td]zhun[/td] +[/tr][tr] +[th]zl-[/th] +[td]zli[/td] +[td]zle[/td] +[td]zla[/td] +[td]zlu[/td] +[td]zling[/td] +[td]zleng[/td] +[td]zlang[/td] +[td]zlung[/td] +[td]zlin[/td] +[td]zlen[/td] +[td]zlan[/td] +[td]zlun[/td] +[/tr][tr] +[th]zw-[/th] +[td]zui[/td] +[td]zue[/td] +[td]zua[/td] +[td]❌[/td] +[td]zuing[/td] +[td]zueng[/td] +[td]zuang[/td] +[td]❌[/td] +[td]zuin[/td] +[td]zuen[/td] +[td]zuan[/td] +[td]❌[/td] +[/tr][tr] +[th=1x3]v-[/th] +[th]v-[/th] +[td]vi[/td] +[td]ve[/td] +[td]va[/td] +[td]vu[/td] +[td]ving[/td] +[td]veng[/td] +[td]vang[/td] +[td]vung[/td] +[td]vin[/td] +[td]ven[/td] +[td]van[/td] +[td]vun[/td] +[/tr][tr] +[th]vy-[/th] +[td]❌[/td] +[td]❌[/td] +[td]via[/td] +[td]viu[/td] +[td]❌[/td] +[td]❌[/td] +[td]viang[/td] +[td]viung[/td] +[td]❌[/td] +[td]❌[/td] +[td]vian[/td] +[td]viun[/td] +[/tr][tr] +[th]vl-[/th] +[td]vli[/td] +[td]vle[/td] +[td]vla[/td] +[td]vlu[/td] +[td]vling[/td] +[td]vleng[/td] +[td]vlang[/td] +[td]vlung[/td] +[td]vlin[/td] +[td]vlen[/td] +[td]vlan[/td] +[td]vlun[/td] +[/tr][tr] +[th=1x4]h-[/th] +[th]h-[/th] +[td]hi[/td] +[td]he[/td] +[td]ha[/td] +[td]hu[/td] +[td]hing[/td] +[td]heng[/td] +[td]hang[/td] +[td]hung[/td] +[td]hin[/td] +[td]hen[/td] +[td]han[/td] +[td]hun[/td] +[/tr][tr] +[th]hy-[/th] +[td]❌[/td] +[td]❌[/td] +[td]hia[/td] +[td]hiu[/td] +[td]❌[/td] +[td]❌[/td] +[td]hiang[/td] +[td]hiung[/td] +[td]❌[/td] +[td]❌[/td] +[td]hian[/td] +[td]hiun[/td] +[/tr][tr] +[th]hl-[/th] +[td]hli[/td] +[td]hle[/td] +[td]hla[/td] +[td]hlu[/td] +[td]hling[/td] +[td]hleng[/td] +[td]hlang[/td] +[td]hlung[/td] +[td]hlin[/td] +[td]hlen[/td] +[td]hlan[/td] +[td]hlun[/td] +[/tr][tr] +[th]hw-[/th] +[td]hui[/td] +[td]hue[/td] +[td]hua[/td] +[td]❌[/td] +[td]huing[/td] +[td]hueng[/td] +[td]huang[/td] +[td]❌[/td] +[td]huin[/td] +[td]huen[/td] +[td]huan[/td] +[td]❌[/td] +[/tr][tr] +[th=1x4]s-[/th] +[th]s-[/th] +[td]si[/td] +[td]se[/td] +[td]sa[/td] +[td]su[/td] +[td]sing[/td] +[td]seng[/td] +[td]sang[/td] +[td]sung[/td] +[td]sin[/td] +[td]sen[/td] +[td]san[/td] +[td]sun[/td] +[/tr][tr] +[th]sy-[/th] +[td]❌[/td] +[td]❌[/td] +[td]sha[/td] +[td]shu[/td] +[td]❌[/td] +[td]❌[/td] +[td]shang[/td] +[td]shung[/td] +[td]❌[/td] +[td]❌[/td] +[td]shan[/td] +[td]shun[/td] +[/tr][tr] +[th]sl-[/th] +[td]sli[/td] +[td]sle[/td] +[td]sla[/td] +[td]slu[/td] +[td]sling[/td] +[td]sleng[/td] +[td]slang[/td] +[td]slung[/td] +[td]slin[/td] +[td]slen[/td] +[td]slan[/td] +[td]slun[/td] +[/tr][tr] +[th]sw-[/th] +[td]sui[/td] +[td]sue[/td] +[td]sua[/td] +[td]❌[/td] +[td]suing[/td] +[td]sueng[/td] +[td]suang[/td] +[td]❌[/td] +[td]suin[/td] +[td]suen[/td] +[td]suan[/td] +[td]❌[/td] +[/tr][tr] +[th=1x3]f-[/th] +[th]f-[/th] +[td]fi[/td] +[td]fe[/td] +[td]fa[/td] +[td]fu[/td] +[td]fing[/td] +[td]feng[/td] +[td]fang[/td] +[td]fung[/td] +[td]fin[/td] +[td]fen[/td] +[td]fan[/td] +[td]fun[/td] +[/tr][tr] +[th]fy-[/th] +[td]❌[/td] +[td]❌[/td] +[td]fia[/td] +[td]fiu[/td] +[td]❌[/td] +[td]❌[/td] +[td]fiang[/td] +[td]fiung[/td] +[td]❌[/td] +[td]❌[/td] +[td]fian[/td] +[td]fiun[/td] +[/tr][tr] +[th]fl-[/th] +[td]fli[/td] +[td]fle[/td] +[td]fla[/td] +[td]flu[/td] +[td]fling[/td] +[td]fleng[/td] +[td]flang[/td] +[td]flung[/td] +[td]flin[/td] +[td]flen[/td] +[td]flan[/td] +[td]flun[/td] +[/tr][tr] +[th=2x2]Part 2[/th] +[th=4x1]-m[/th] +[th=4x1]-h[/th] +[th=4x1]-r[/th] +[/tr][tr] +[th]-im[/th] +[th]-em[/th] +[th]-am[/th] +[th]-um[/th] +[th]-ih[/th] +[th]-eh[/th] +[th]-ah[/th] +[th]-uh[/th] +[th]-ir[/th] +[th]-er[/th] +[th]-ar[/th] +[th]-ur[/th] +[/tr][tr] +[th=1x4]∅-[/th] +[th]-[/th] +[td]im[/td] +[td]em[/td] +[td]am[/td] +[td]um[/td] +[td]ih[/td] +[td]eh[/td] +[td]ah[/td] +[td]uh[/td] +[td]ir[/td] +[td]er[/td] +[td]ar[/td] +[td]ur[/td] +[/tr][tr] +[th]y-[/th] +[td]❌[/td] +[td]❌[/td] +[td]yam[/td] +[td]yum[/td] +[td]❌[/td] +[td]❌[/td] +[td]yah[/td] +[td]yuh[/td] +[td]❌[/td] +[td]❌[/td] +[td]yar[/td] +[td]yur[/td] +[/tr][tr] +[th]l-[/th] +[td]lim[/td] +[td]lem[/td] +[td]lam[/td] +[td]lum[/td] +[td]lih[/td] +[td]leh[/td] +[td]lah[/td] +[td]luh[/td] +[td]lir[/td] +[td]ler[/td] +[td]lar[/td] +[td]lur[/td] +[/tr][tr] +[th]w-[/th] +[td]wim[/td] +[td]wem[/td] +[td]wam[/td] +[td]❌[/td] +[td]wih[/td] +[td]weh[/td] +[td]wah[/td] +[td]❌[/td] +[td]wir[/td] +[td]wer[/td] +[td]war[/td] +[td]❌[/td] +[/tr][tr] +[th=1x4]g-[/th] +[th]g-[/th] +[td]gim[/td] +[td]gem[/td] +[td]gam[/td] +[td]gum[/td] +[td]gih[/td] +[td]geh[/td] +[td]gah[/td] +[td]guh[/td] +[td]gir[/td] +[td]ger[/td] +[td]gar[/td] +[td]gur[/td] +[/tr][tr] +[th]gy-[/th] +[td]❌[/td] +[td]❌[/td] +[td]giam[/td] +[td]gium[/td] +[td]❌[/td] +[td]❌[/td] +[td]giah[/td] +[td]giuh[/td] +[td]❌[/td] +[td]❌[/td] +[td]giar[/td] +[td]giur[/td] +[/tr][tr] +[th]gl-[/th] +[td]glim[/td] +[td]glem[/td] +[td]glam[/td] +[td]glum[/td] +[td]glih[/td] +[td]gleh[/td] +[td]glah[/td] +[td]gluh[/td] +[td]glir[/td] +[td]gler[/td] +[td]glar[/td] +[td]glur[/td] +[/tr][tr] +[th]gw-[/th] +[td]guim[/td] +[td]guem[/td] +[td]guam[/td] +[td]❌[/td] +[td]guih[/td] +[td]gueh[/td] +[td]guah[/td] +[td]❌[/td] +[td]guir[/td] +[td]guer[/td] +[td]guar[/td] +[td]❌[/td] +[/tr][tr] +[th=1x3]d-[/th] +[th]d-[/th] +[td]dim[/td] +[td]dem[/td] +[td]dam[/td] +[td]dum[/td] +[td]dih[/td] +[td]deh[/td] +[td]dah[/td] +[td]duh[/td] +[td]dir[/td] +[td]der[/td] +[td]dar[/td] +[td]dur[/td] +[/tr][tr] +[th]dy-[/th] +[td]❌[/td] +[td]❌[/td] +[td]jam[/td] +[td]jum[/td] +[td]❌[/td] +[td]❌[/td] +[td]jah[/td] +[td]juh[/td] +[td]❌[/td] +[td]❌[/td] +[td]jar[/td] +[td]jur[/td] +[/tr][tr] +[th]dw-[/th] +[td]duim[/td] +[td]duem[/td] +[td]duam[/td] +[td]❌[/td] +[td]duih[/td] +[td]dueh[/td] +[td]duah[/td] +[td]❌[/td] +[td]duir[/td] +[td]duer[/td] +[td]duar[/td] +[td]❌[/td] +[/tr][tr] +[th=1x3]b-[/th] +[th]b-[/th] +[td]bim[/td] +[td]bem[/td] +[td]bam[/td] +[td]bum[/td] +[td]bih[/td] +[td]beh[/td] +[td]bah[/td] +[td]buh[/td] +[td]bir[/td] +[td]ber[/td] +[td]bar[/td] +[td]bur[/td] +[/tr][tr] +[th]by-[/th] +[td]❌[/td] +[td]❌[/td] +[td]biam[/td] +[td]bium[/td] +[td]❌[/td] +[td]❌[/td] +[td]biah[/td] +[td]biuh[/td] +[td]❌[/td] +[td]❌[/td] +[td]biar[/td] +[td]biur[/td] +[/tr][tr] +[th]bl-[/th] +[td]blim[/td] +[td]blem[/td] +[td]blam[/td] +[td]blum[/td] +[td]blih[/td] +[td]bleh[/td] +[td]blah[/td] +[td]bluh[/td] +[td]blir[/td] +[td]bler[/td] +[td]blar[/td] +[td]blur[/td] +[/tr][tr] +[th=1x4]k-[/th] +[th]k-[/th] +[td]kim[/td] +[td]kem[/td] +[td]kam[/td] +[td]kum[/td] +[td]kih[/td] +[td]keh[/td] +[td]kah[/td] +[td]kuh[/td] +[td]kir[/td] +[td]ker[/td] +[td]kar[/td] +[td]kur[/td] +[/tr][tr] +[th]ky-[/th] +[td]❌[/td] +[td]❌[/td] +[td]kiam[/td] +[td]kium[/td] +[td]❌[/td] +[td]❌[/td] +[td]kiah[/td] +[td]kiuh[/td] +[td]❌[/td] +[td]❌[/td] +[td]kiar[/td] +[td]kiur[/td] +[/tr][tr] +[th]kl-[/th] +[td]klim[/td] +[td]klem[/td] +[td]klam[/td] +[td]klum[/td] +[td]klih[/td] +[td]kleh[/td] +[td]klah[/td] +[td]kluh[/td] +[td]klir[/td] +[td]kler[/td] +[td]klar[/td] +[td]klur[/td] +[/tr][tr] +[th]kw-[/th] +[td]quim[/td] +[td]quem[/td] +[td]quam[/td] +[td]❌[/td] +[td]quih[/td] +[td]queh[/td] +[td]quah[/td] +[td]❌[/td] +[td]quir[/td] +[td]quer[/td] +[td]quar[/td] +[td]❌[/td] +[/tr][tr] +[th=1x3]t-[/th] +[th]t-[/th] +[td]tim[/td] +[td]tem[/td] +[td]tam[/td] +[td]tum[/td] +[td]tih[/td] +[td]teh[/td] +[td]tah[/td] +[td]tuh[/td] +[td]tir[/td] +[td]ter[/td] +[td]tar[/td] +[td]tur[/td] +[/tr][tr] +[th]ty-[/th] +[td]❌[/td] +[td]❌[/td] +[td]cham[/td] +[td]chum[/td] +[td]❌[/td] +[td]❌[/td] +[td]chah[/td] +[td]chuh[/td] +[td]❌[/td] +[td]❌[/td] +[td]char[/td] +[td]chur[/td] +[/tr][tr] +[th]tw-[/th] +[td]tuim[/td] +[td]tuem[/td] +[td]tuam[/td] +[td]❌[/td] +[td]tuih[/td] +[td]tueh[/td] +[td]tuah[/td] +[td]❌[/td] +[td]tuir[/td] +[td]tuer[/td] +[td]tuar[/td] +[td]❌[/td] +[/tr][tr] +[th=1x3]p-[/th] +[th]p-[/th] +[td]pim[/td] +[td]pem[/td] +[td]pam[/td] +[td]pum[/td] +[td]pih[/td] +[td]peh[/td] +[td]pah[/td] +[td]puh[/td] +[td]pir[/td] +[td]per[/td] +[td]par[/td] +[td]pur[/td] +[/tr][tr] +[th]py-[/th] +[td]❌[/td] +[td]❌[/td] +[td]piam[/td] +[td]pium[/td] +[td]❌[/td] +[td]❌[/td] +[td]piah[/td] +[td]piuh[/td] +[td]❌[/td] +[td]❌[/td] +[td]piar[/td] +[td]piur[/td] +[/tr][tr] +[th]pl-[/th] +[td]plim[/td] +[td]plem[/td] +[td]plam[/td] +[td]plum[/td] +[td]plih[/td] +[td]pleh[/td] +[td]plah[/td] +[td]pluh[/td] +[td]plir[/td] +[td]pler[/td] +[td]plar[/td] +[td]plur[/td] +[/tr][tr] +[th=2x1]gn-[/th] +[td]gnim[/td] +[td]gnem[/td] +[td]gnam[/td] +[td]gnum[/td] +[td]gnih[/td] +[td]gneh[/td] +[td]gnah[/td] +[td]gnuh[/td] +[td]gnir[/td] +[td]gner[/td] +[td]gnar[/td] +[td]gnur[/td] +[/tr][tr] +[th=2x1]n-[/th] +[td]nim[/td] +[td]nem[/td] +[td]nam[/td] +[td]num[/td] +[td]nih[/td] +[td]neh[/td] +[td]nah[/td] +[td]nuh[/td] +[td]nir[/td] +[td]ner[/td] +[td]nar[/td] +[td]nur[/td] +[/tr][tr] +[th=2x1]m-[/th] +[td]mim[/td] +[td]mem[/td] +[td]mam[/td] +[td]mum[/td] +[td]mih[/td] +[td]meh[/td] +[td]mah[/td] +[td]muh[/td] +[td]mir[/td] +[td]mer[/td] +[td]mar[/td] +[td]mur[/td] +[/tr][tr] +[th=2x1]r-[/th] +[td]rim[/td] +[td]rem[/td] +[td]ram[/td] +[td]rum[/td] +[td]rih[/td] +[td]reh[/td] +[td]rah[/td] +[td]ruh[/td] +[td]rir[/td] +[td]rer[/td] +[td]rar[/td] +[td]rur[/td] +[/tr][tr] +[th=1x4]z-[/th] +[th]z-[/th] +[td]zim[/td] +[td]zem[/td] +[td]zam[/td] +[td]zum[/td] +[td]zih[/td] +[td]zeh[/td] +[td]zah[/td] +[td]zuh[/td] +[td]zir[/td] +[td]zer[/td] +[td]zar[/td] +[td]zur[/td] +[/tr][tr] +[th]zy-[/th] +[td]❌[/td] +[td]❌[/td] +[td]zham[/td] +[td]zhum[/td] +[td]❌[/td] +[td]❌[/td] +[td]zhah[/td] +[td]zhuh[/td] +[td]❌[/td] +[td]❌[/td] +[td]zhar[/td] +[td]zhur[/td] +[/tr][tr] +[th]zl-[/th] +[td]zlim[/td] +[td]zlem[/td] +[td]zlam[/td] +[td]zlum[/td] +[td]zlih[/td] +[td]zleh[/td] +[td]zlah[/td] +[td]zluh[/td] +[td]zlir[/td] +[td]zler[/td] +[td]zlar[/td] +[td]zlur[/td] +[/tr][tr] +[th]zw-[/th] +[td]zuim[/td] +[td]zuem[/td] +[td]zuam[/td] +[td]❌[/td] +[td]zuih[/td] +[td]zueh[/td] +[td]zuah[/td] +[td]❌[/td] +[td]zuir[/td] +[td]zuer[/td] +[td]zuar[/td] +[td]❌[/td] +[/tr][tr] +[th=1x3]v-[/th] +[th]v-[/th] +[td]vim[/td] +[td]vem[/td] +[td]vam[/td] +[td]vum[/td] +[td]vih[/td] +[td]veh[/td] +[td]vah[/td] +[td]vuh[/td] +[td]vir[/td] +[td]ver[/td] +[td]var[/td] +[td]vur[/td] +[/tr][tr] +[th]vy-[/th] +[td]❌[/td] +[td]❌[/td] +[td]viam[/td] +[td]vium[/td] +[td]❌[/td] +[td]❌[/td] +[td]viah[/td] +[td]viuh[/td] +[td]❌[/td] +[td]❌[/td] +[td]viar[/td] +[td]viur[/td] +[/tr][tr] +[th]vl-[/th] +[td]vlim[/td] +[td]vlem[/td] +[td]vlam[/td] +[td]vlum[/td] +[td]vlih[/td] +[td]vleh[/td] +[td]vlah[/td] +[td]vluh[/td] +[td]vlir[/td] +[td]vler[/td] +[td]vlar[/td] +[td]vlur[/td] +[/tr][tr] +[th=1x4]h-[/th] +[th]h-[/th] +[td]him[/td] +[td]hem[/td] +[td]ham[/td] +[td]hum[/td] +[td]hih[/td] +[td]heh[/td] +[td]hah[/td] +[td]huh[/td] +[td]hir[/td] +[td]her[/td] +[td]har[/td] +[td]hur[/td] +[/tr][tr] +[th]hy-[/th] +[td]❌[/td] +[td]❌[/td] +[td]hiam[/td] +[td]hium[/td] +[td]❌[/td] +[td]❌[/td] +[td]hiah[/td] +[td]hiuh[/td] +[td]❌[/td] +[td]❌[/td] +[td]hiar[/td] +[td]hiur[/td] +[/tr][tr] +[th]hl-[/th] +[td]hlim[/td] +[td]hlem[/td] +[td]hlam[/td] +[td]hlum[/td] +[td]hlih[/td] +[td]hleh[/td] +[td]hlah[/td] +[td]hluh[/td] +[td]hlir[/td] +[td]hler[/td] +[td]hlar[/td] +[td]hlur[/td] +[/tr][tr] +[th]hw-[/th] +[td]huim[/td] +[td]huem[/td] +[td]huam[/td] +[td]❌[/td] +[td]huih[/td] +[td]hueh[/td] +[td]huah[/td] +[td]❌[/td] +[td]huir[/td] +[td]huer[/td] +[td]huar[/td] +[td]❌[/td] +[/tr][tr] +[th=1x4]s-[/th] +[th]s-[/th] +[td]sim[/td] +[td]sem[/td] +[td]sam[/td] +[td]sum[/td] +[td]sih[/td] +[td]seh[/td] +[td]sah[/td] +[td]suh[/td] +[td]sir[/td] +[td]ser[/td] +[td]sar[/td] +[td]sur[/td] +[/tr][tr] +[th]sy-[/th] +[td]❌[/td] +[td]❌[/td] +[td]sham[/td] +[td]shum[/td] +[td]❌[/td] +[td]❌[/td] +[td]shah[/td] +[td]shuh[/td] +[td]❌[/td] +[td]❌[/td] +[td]shar[/td] +[td]shur[/td] +[/tr][tr] +[th]sl-[/th] +[td]slim[/td] +[td]slem[/td] +[td]slam[/td] +[td]slum[/td] +[td]slih[/td] +[td]sleh[/td] +[td]slah[/td] +[td]sluh[/td] +[td]slir[/td] +[td]sler[/td] +[td]slar[/td] +[td]slur[/td] +[/tr][tr] +[th]sw-[/th] +[td]suim[/td] +[td]suem[/td] +[td]suam[/td] +[td]❌[/td] +[td]suih[/td] +[td]sueh[/td] +[td]suah[/td] +[td]❌[/td] +[td]suir[/td] +[td]suer[/td] +[td]suar[/td] +[td]❌[/td] +[/tr][tr] +[th=1x3]f-[/th] +[th]f-[/th] +[td]fim[/td] +[td]fem[/td] +[td]fam[/td] +[td]fum[/td] +[td]fih[/td] +[td]feh[/td] +[td]fah[/td] +[td]fuh[/td] +[td]fir[/td] +[td]fer[/td] +[td]far[/td] +[td]fur[/td] +[/tr][tr] +[th]fy-[/th] +[td]❌[/td] +[td]❌[/td] +[td]fiam[/td] +[td]fium[/td] +[td]❌[/td] +[td]❌[/td] +[td]fiah[/td] +[td]fiuh[/td] +[td]❌[/td] +[td]❌[/td] +[td]fiar[/td] +[td]fiur[/td] +[/tr][tr] +[th]fl-[/th] +[td]flim[/td] +[td]flem[/td] +[td]flam[/td] +[td]flum[/td] +[td]flih[/td] +[td]fleh[/td] +[td]flah[/td] +[td]fluh[/td] +[td]flir[/td] +[td]fler[/td] +[td]flar[/td] +[td]flur[/td] +[/tr][tr] +[th=2x2]Part 3[/th] +[th=4x1]-y[/th] +[th=4x1]-l[/th] +[th=4x1]-w[/th] +[/tr][tr] +[th]-iy[/th] +[th]-ey[/th] +[th]-ay[/th] +[th]-uy[/th] +[th]-il[/th] +[th]-el[/th] +[th]-al[/th] +[th]-ul[/th] +[th]-iw[/th] +[th]-ew[/th] +[th]-aw[/th] +[th]-uw[/th] +[/tr][tr] +[th=1x4]∅-[/th] +[th]-[/th] +[td]ie[/td] +[td]ei[/td] +[td]ai[/td] +[td]ui[/td] +[td]il[/td] +[td]el[/td] +[td]al[/td] +[td]ul[/td] +[td]iu[/td] +[td]eu[/td] +[td]au[/td] +[td]ou[/td] +[/tr][tr] +[th]y-[/th] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]yal[/td] +[td]yul[/td] +[td]❌[/td] +[td]❌[/td] +[td]yau[/td] +[td]❌[/td] +[/tr][tr] +[th]l-[/th] +[td]lie[/td] +[td]lei[/td] +[td]lai[/td] +[td]lui[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]liu[/td] +[td]leu[/td] +[td]lau[/td] +[td]lou[/td] +[/tr][tr] +[th]w-[/th] +[td]❌[/td] +[td]wei[/td] +[td]wai[/td] +[td]❌[/td] +[td]wil[/td] +[td]wel[/td] +[td]wal[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[/tr][tr] +[th=1x4]g-[/th] +[th]g-[/th] +[td]gie[/td] +[td]gei[/td] +[td]gai[/td] +[td]gui[/td] +[td]gil[/td] +[td]gel[/td] +[td]gal[/td] +[td]gul[/td] +[td]giu[/td] +[td]geu[/td] +[td]gau[/td] +[td]gou[/td] +[/tr][tr] +[th]gy-[/th] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]gial[/td] +[td]giul[/td] +[td]❌[/td] +[td]❌[/td] +[td]giau[/td] +[td]❌[/td] +[/tr][tr] +[th]gl-[/th] +[td]glie[/td] +[td]glei[/td] +[td]glai[/td] +[td]glui[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]gliu[/td] +[td]gleu[/td] +[td]glau[/td] +[td]glou[/td] +[/tr][tr] +[th]gw-[/th] +[td]❌[/td] +[td]guei[/td] +[td]guai[/td] +[td]❌[/td] +[td]guil[/td] +[td]guel[/td] +[td]gual[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[/tr][tr] +[th=1x3]d-[/th] +[th]d-[/th] +[td]die[/td] +[td]dei[/td] +[td]dai[/td] +[td]dui[/td] +[td]dil[/td] +[td]del[/td] +[td]dal[/td] +[td]dul[/td] +[td]diu[/td] +[td]deu[/td] +[td]dau[/td] +[td]dou[/td] +[/tr][tr] +[th]dy-[/th] +[td]❌[/td] +[td]❌[/td] +[td]jai[/td] +[td]jui[/td] +[td]❌[/td] +[td]❌[/td] +[td]jal[/td] +[td]jul[/td] +[td]❌[/td] +[td]❌[/td] +[td]jau[/td] +[td]❌[/td] +[/tr][tr] +[th]dw-[/th] +[td]❌[/td] +[td]duei[/td] +[td]duai[/td] +[td]❌[/td] +[td]duil[/td] +[td]duel[/td] +[td]dual[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[/tr][tr] +[th=1x3]b-[/th] +[th]b-[/th] +[td]bie[/td] +[td]bei[/td] +[td]bai[/td] +[td]bui[/td] +[td]bil[/td] +[td]bel[/td] +[td]bal[/td] +[td]bul[/td] +[td]biu[/td] +[td]beu[/td] +[td]bau[/td] +[td]bou[/td] +[/tr][tr] +[th]by-[/th] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]bial[/td] +[td]biul[/td] +[td]❌[/td] +[td]❌[/td] +[td]biau[/td] +[td]❌[/td] +[/tr][tr] +[th]bl-[/th] +[td]blie[/td] +[td]blei[/td] +[td]blai[/td] +[td]blui[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]bliu[/td] +[td]bleu[/td] +[td]blau[/td] +[td]blou[/td] +[/tr][tr] +[th=1x4]k-[/th] +[th]k-[/th] +[td]kie[/td] +[td]kei[/td] +[td]kai[/td] +[td]kui[/td] +[td]kil[/td] +[td]kel[/td] +[td]kal[/td] +[td]kul[/td] +[td]kiu[/td] +[td]keu[/td] +[td]kau[/td] +[td]kou[/td] +[/tr][tr] +[th]ky-[/th] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]kial[/td] +[td]kiul[/td] +[td]❌[/td] +[td]❌[/td] +[td]kiau[/td] +[td]❌[/td] +[/tr][tr] +[th]kl-[/th] +[td]klie[/td] +[td]klei[/td] +[td]klai[/td] +[td]klui[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]kliu[/td] +[td]kleu[/td] +[td]klau[/td] +[td]klou[/td] +[/tr][tr] +[th]kw-[/th] +[td]❌[/td] +[td]quei[/td] +[td]quai[/td] +[td]❌[/td] +[td]quil[/td] +[td]quel[/td] +[td]qual[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[/tr][tr] +[th=1x3]t-[/th] +[th]t-[/th] +[td]tie[/td] +[td]tei[/td] +[td]tai[/td] +[td]tui[/td] +[td]til[/td] +[td]tel[/td] +[td]tal[/td] +[td]tul[/td] +[td]tiu[/td] +[td]teu[/td] +[td]tau[/td] +[td]tou[/td] +[/tr][tr] +[th]ty-[/th] +[td]❌[/td] +[td]❌[/td] +[td]chai[/td] +[td]chui[/td] +[td]❌[/td] +[td]❌[/td] +[td]chal[/td] +[td]chul[/td] +[td]❌[/td] +[td]❌[/td] +[td]chau[/td] +[td]❌[/td] +[/tr][tr] +[th]tw-[/th] +[td]❌[/td] +[td]tuei[/td] +[td]tuai[/td] +[td]❌[/td] +[td]tuil[/td] +[td]tuel[/td] +[td]tual[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[/tr][tr] +[th=1x3]p-[/th] +[th]p-[/th] +[td]pie[/td] +[td]pei[/td] +[td]pai[/td] +[td]pui[/td] +[td]pil[/td] +[td]pel[/td] +[td]pal[/td] +[td]pul[/td] +[td]piu[/td] +[td]peu[/td] +[td]pau[/td] +[td]pou[/td] +[/tr][tr] +[th]py-[/th] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]pial[/td] +[td]piul[/td] +[td]❌[/td] +[td]❌[/td] +[td]piau[/td] +[td]❌[/td] +[/tr][tr] +[th]pl-[/th] +[td]plie[/td] +[td]plei[/td] +[td]plai[/td] +[td]plui[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]pliu[/td] +[td]pleu[/td] +[td]plau[/td] +[td]plou[/td] +[/tr][tr] +[th=2x1]gn-[/th] +[td]gnie[/td] +[td]gnei[/td] +[td]gnai[/td] +[td]gnui[/td] +[td]gnil[/td] +[td]gnel[/td] +[td]gnal[/td] +[td]gnul[/td] +[td]gniu[/td] +[td]gneu[/td] +[td]gnau[/td] +[td]gnou[/td] +[/tr][tr] +[th=2x1]n-[/th] +[td]nie[/td] +[td]nei[/td] +[td]nai[/td] +[td]nui[/td] +[td]nil[/td] +[td]nel[/td] +[td]nal[/td] +[td]nul[/td] +[td]niu[/td] +[td]neu[/td] +[td]nau[/td] +[td]nou[/td] +[/tr][tr] +[th=2x1]m-[/th] +[td]mie[/td] +[td]mei[/td] +[td]mai[/td] +[td]mui[/td] +[td]mil[/td] +[td]mel[/td] +[td]mal[/td] +[td]mul[/td] +[td]miu[/td] +[td]meu[/td] +[td]mau[/td] +[td]mou[/td] +[/tr][tr] +[th=2x1]r-[/th] +[td]rie[/td] +[td]rei[/td] +[td]rai[/td] +[td]rui[/td] +[td]ril[/td] +[td]rel[/td] +[td]ral[/td] +[td]rul[/td] +[td]riu[/td] +[td]reu[/td] +[td]rau[/td] +[td]rou[/td] +[/tr][tr] +[th=1x4]z-[/th] +[th]z-[/th] +[td]zie[/td] +[td]zei[/td] +[td]zai[/td] +[td]zui[/td] +[td]zil[/td] +[td]zel[/td] +[td]zal[/td] +[td]zul[/td] +[td]ziu[/td] +[td]zeu[/td] +[td]zau[/td] +[td]zou[/td] +[/tr][tr] +[th]zy-[/th] +[td]❌[/td] +[td]❌[/td] +[td]zhai[/td] +[td]zhui[/td] +[td]❌[/td] +[td]❌[/td] +[td]zhal[/td] +[td]zhul[/td] +[td]❌[/td] +[td]❌[/td] +[td]zhau[/td] +[td]❌[/td] +[/tr][tr] +[th]zl-[/th] +[td]zlie[/td] +[td]zlei[/td] +[td]zlai[/td] +[td]zlui[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]zliu[/td] +[td]zleu[/td] +[td]zlau[/td] +[td]zlou[/td] +[/tr][tr] +[th]zw-[/th] +[td]❌[/td] +[td]zuei[/td] +[td]zuai[/td] +[td]❌[/td] +[td]zuil[/td] +[td]zuel[/td] +[td]zual[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[/tr][tr] +[th=1x3]v-[/th] +[th]v-[/th] +[td]vie[/td] +[td]vei[/td] +[td]vai[/td] +[td]vui[/td] +[td]vil[/td] +[td]vel[/td] +[td]val[/td] +[td]vul[/td] +[td]viu[/td] +[td]veu[/td] +[td]vau[/td] +[td]vou[/td] +[/tr][tr] +[th]vy-[/th] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]vial[/td] +[td]viul[/td] +[td]❌[/td] +[td]❌[/td] +[td]viau[/td] +[td]❌[/td] +[/tr][tr] +[th]vl-[/th] +[td]vlie[/td] +[td]vlei[/td] +[td]vlai[/td] +[td]vlui[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]vliu[/td] +[td]vleu[/td] +[td]vlau[/td] +[td]vlou[/td] +[/tr][tr] +[th=1x4]h-[/th] +[th]h-[/th] +[td]hie[/td] +[td]hei[/td] +[td]hai[/td] +[td]hui[/td] +[td]hil[/td] +[td]hel[/td] +[td]hal[/td] +[td]hul[/td] +[td]hiu[/td] +[td]heu[/td] +[td]hau[/td] +[td]hou[/td] +[/tr][tr] +[th]hy-[/th] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]hial[/td] +[td]hiul[/td] +[td]❌[/td] +[td]❌[/td] +[td]hiau[/td] +[td]❌[/td] +[/tr][tr] +[th]hl-[/th] +[td]hlie[/td] +[td]hlei[/td] +[td]hlai[/td] +[td]hlui[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]hliu[/td] +[td]hleu[/td] +[td]hlau[/td] +[td]hlou[/td] +[/tr][tr] +[th]hw-[/th] +[td]❌[/td] +[td]huei[/td] +[td]huai[/td] +[td]❌[/td] +[td]huil[/td] +[td]huel[/td] +[td]hual[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[/tr][tr] +[th=1x4]s-[/th] +[th]s-[/th] +[td]sie[/td] +[td]sei[/td] +[td]sai[/td] +[td]sui[/td] +[td]sil[/td] +[td]sel[/td] +[td]sal[/td] +[td]sul[/td] +[td]siu[/td] +[td]seu[/td] +[td]sau[/td] +[td]sou[/td] +[/tr][tr] +[th]sy-[/th] +[td]❌[/td] +[td]❌[/td] +[td]shai[/td] +[td]shui[/td] +[td]❌[/td] +[td]❌[/td] +[td]shal[/td] +[td]shul[/td] +[td]❌[/td] +[td]❌[/td] +[td]shau[/td] +[td]❌[/td] +[/tr][tr] +[th]sl-[/th] +[td]slie[/td] +[td]slei[/td] +[td]slai[/td] +[td]slui[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]sliu[/td] +[td]sleu[/td] +[td]slau[/td] +[td]slou[/td] +[/tr][tr] +[th]sw-[/th] +[td]❌[/td] +[td]suei[/td] +[td]suai[/td] +[td]❌[/td] +[td]suil[/td] +[td]suel[/td] +[td]sual[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[/tr][tr] +[th=1x3]f-[/th] +[th]f-[/th] +[td]fie[/td] +[td]fei[/td] +[td]fai[/td] +[td]fui[/td] +[td]fil[/td] +[td]fel[/td] +[td]fal[/td] +[td]ful[/td] +[td]fiu[/td] +[td]feu[/td] +[td]fau[/td] +[td]fou[/td] +[/tr][tr] +[th]fy-[/th] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]fial[/td] +[td]fiul[/td] +[td]❌[/td] +[td]❌[/td] +[td]fiau[/td] +[td]❌[/td] +[/tr][tr] +[th]fl-[/th] +[td]flie[/td] +[td]flei[/td] +[td]flai[/td] +[td]flui[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]❌[/td] +[td]fliu[/td] +[td]fleu[/td] +[td]flau[/td] +[td]flou[/td] +[/tr][tr] +[td=14] +❌ indicates a disallowed syllable +[/td] +[/tr][/table] diff --git a/stuff/mintah_table_script.py b/stuff/mintah_table_script.py new file mode 100644 index 0000000..e539c41 --- /dev/null +++ b/stuff/mintah_table_script.py @@ -0,0 +1,121 @@ +from collections.abc import Container, Iterable, Iterator +from functools import reduce +from itertools import batched, zip_longest +import re + + +def trace[T](formatter: str, value: T, **kwargs) -> T: + print(formatter.format(value, **kwargs)) + return value + + +def is_forbidden[T](result: tuple[T | None, ...], forbids: Iterable[tuple[Container[T] | None, ...]]) -> bool: + return any(all(result_part is not None and result_part in forbid_part for (result_part, forbid_part) in zip(result, forbid) if forbid_part is not None) for forbid in forbids) + + +def do_subst_singular(txt: str, substitution: tuple[re.Pattern, str]) -> str: + pattern, replace_with = substitution + return pattern.sub(replace_with, txt) + + +def do_subst(txt: str, substitutions: Iterable[tuple[re.Pattern, str]]) -> str: + return reduce(do_subst_singular, substitutions, txt) + + +def render_mintah_table(initials: list[str], pre_vocalics: list[str], vowels: list[str], post_vocalics: list[str], post_vocalics_batch_len: int, label_substitutions: list[tuple[re.Pattern, str]], result_forbids: list[tuple[set[str] | None, set[str] | None, set[str] | None, set[str] | None]], result_substitutions: list[tuple[re.Pattern, str]]) -> Iterator[str]: + yield '[table][tr]' + for part_n, post_vocalics_batch in enumerate(batched(post_vocalics, post_vocalics_batch_len), 1): + yield f'[th=2x2]Part {part_n}[/th]' + for post_vocalic in post_vocalics_batch: + yield f'[th={len(vowels)}x1]-{do_subst(post_vocalic, label_substitutions)}[/th]' + yield '[/tr][tr]' + for post_vocalic in post_vocalics_batch: + for vowel in vowels: + yield f'[th]-{do_subst(vowel + post_vocalic, label_substitutions)}[/th]' + yield '[/tr][tr]' + for initial in initials: + pre_vocalics_allowed = tuple(v for v in pre_vocalics if not is_forbidden((initial, v, None, None), result_forbids)) + if len(pre_vocalics_allowed) == 1: + pre_vocalic = pre_vocalics_allowed[0] + yield f'[th=2x1]{do_subst(initial + pre_vocalic, label_substitutions)}-[/th]' + for post_vocalic in post_vocalics_batch: + for vowel in vowels: + syllable_result = (initial, pre_vocalic, vowel, post_vocalic) + if is_forbidden(syllable_result, result_forbids): + yield '[td]\u274C[/td]' + else: + syllable = ''.join(syllable_result) + yield f'[td]{do_subst(syllable, result_substitutions)}[/td]' + yield f'[/tr][tr]' + else: + yield f'[th=1x{len(pre_vocalics_allowed)}]{do_subst(initial, label_substitutions)}-[/th]' + for pre_vocalic in pre_vocalics_allowed: + yield f'[th]{do_subst(initial + pre_vocalic, label_substitutions)}-[/th]' + for post_vocalic in post_vocalics_batch: + for vowel in vowels: + syllable_result = (initial, pre_vocalic, vowel, post_vocalic) + if is_forbidden(syllable_result, result_forbids): + yield '[td]\u274C[/td]' + else: + syllable = ''.join(syllable_result) + yield f'[td]{do_subst(syllable, result_substitutions)}[/td]' + yield f'[/tr][tr]' + yield f'[td={2 + len(vowels) * post_vocalics_batch_len}]' + yield '\u274C indicates a disallowed syllable' + yield '[/td]' + yield '[/tr][/table]' + + +# '1' = initial velar nasal, '2' = post-vocalic velar nasal +INITIALS = ['#', 'g', 'd', 'b', 'k', 't', 'p', '1', 'n', 'm', 'r', 'z', 'v', 'h', 's', 'f'] +PRE_VOCALICS = ['#', 'y', 'l', 'w'] +VOWELS = ['i', 'e', 'a', 'u'] +POST_VOCALICS = ['#', '2', 'n', 'm', 'h', 'r', 'y', 'l', 'w'] + +POST_VOCALICS_BATCH_LEN = 3 + +LABEL_SUBSTITUTIONS = [ + (re.compile(r'^1'), 'gn'), + (re.compile(r'2$'), 'ng'), + (re.compile(r'^#$'), '\u2205'), + (re.compile(r'#'), r''), +] + +RESULT_FORBIDS = [ + # Initial + pre-vocalic + ({'d', 't'}, {'l'}, None, None), + ({'b', 'p', 'v', 'f'}, {'w'}, None, None), + ({'1', 'n', 'm', 'r'}, {'y', 'l', 'w'}, None, None), + # Pre-vocalic + vowel + (None, {'y'}, {'i', 'e'}, None), + (None, {'w'}, {'u'}, None), + # Pre-vocalic + vowel + post-vocalic + (None, {'y'}, {'u'}, {'w'}), + (None, {'w'}, {'i'}, {'y'}), + # Pre-vocalic + post-vocalic + ({'#', 'g', 'b', 'k', 'p', '1', 'n', 'm', 'r', 'v', 'h', 'f'}, {'y'}, None, {'y'}), + (None, {'l'}, None, {'l'}), + (None, {'w'}, None, {'w'}), +] + +RESULT_SUBSTITUTIONS = [ + (re.compile(r'^1'), 'gn'), + (re.compile(r'2$'), 'ng'), + (re.compile(r'^gw'), 'gu'), + (re.compile(r'^kw'), 'qu'), + (re.compile(r'^hw'), 'hu'), + (re.compile(r'^dy'), 'j'), + (re.compile(r'^ty'), 'ch'), + (re.compile(r'^zy'), 'zh'), + (re.compile(r'^sy'), 'sh'), + (re.compile(r'(?<=^[^#])y'), 'i'), + (re.compile(r'(?<=^[^#])w'), 'u'), + (re.compile(r'iy$'), 'ie'), + (re.compile(r'y$'), 'i'), + (re.compile(r'uw$'), 'ou'), + (re.compile(r'w$'), 'u'), + (re.compile(r'#'), ''), +] + +for line in render_mintah_table(INITIALS, PRE_VOCALICS, VOWELS, POST_VOCALICS, POST_VOCALICS_BATCH_LEN, LABEL_SUBSTITUTIONS, RESULT_FORBIDS, RESULT_SUBSTITUTIONS): + print(line) -- 2.25.1