Italicize titles of factbooks that contain redirects when in page-index view - as...
authorLanius Trolling <lanius@laniustrolling.dev>
Sun, 11 Aug 2024 23:43:09 +0000 (19:43 -0400)
committerLanius Trolling <lanius@laniustrolling.dev>
Sun, 11 Aug 2024 23:43:09 +0000 (19:43 -0400)
Also, update dependencies and declare a great holy war against star imports (except when they're kotlinx.html THOUGH)

55 files changed:
.idea/.gitignore
build.gradle.kts
src/jvmMain/kotlin/info/mechyrdia/Factbooks.kt
src/jvmMain/kotlin/info/mechyrdia/auth/SessionStorage.kt
src/jvmMain/kotlin/info/mechyrdia/auth/Sessions.kt
src/jvmMain/kotlin/info/mechyrdia/auth/ViewsLogin.kt
src/jvmMain/kotlin/info/mechyrdia/auth/WebDav.kt
src/jvmMain/kotlin/info/mechyrdia/data/Comments.kt
src/jvmMain/kotlin/info/mechyrdia/data/Data.kt
src/jvmMain/kotlin/info/mechyrdia/data/DataFiles.kt
src/jvmMain/kotlin/info/mechyrdia/data/Nations.kt
src/jvmMain/kotlin/info/mechyrdia/data/ViewComments.kt
src/jvmMain/kotlin/info/mechyrdia/data/ViewsComment.kt
src/jvmMain/kotlin/info/mechyrdia/data/ViewsFiles.kt
src/jvmMain/kotlin/info/mechyrdia/data/ViewsUser.kt
src/jvmMain/kotlin/info/mechyrdia/data/Visits.kt
src/jvmMain/kotlin/info/mechyrdia/data/Xml.kt
src/jvmMain/kotlin/info/mechyrdia/lore/April1st.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ArticleListing.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ArticleTitles.kt
src/jvmMain/kotlin/info/mechyrdia/lore/AssetCaching.kt
src/jvmMain/kotlin/info/mechyrdia/lore/AssetCompression.kt
src/jvmMain/kotlin/info/mechyrdia/lore/AssetHashing.kt
src/jvmMain/kotlin/info/mechyrdia/lore/Fonts.kt
src/jvmMain/kotlin/info/mechyrdia/lore/HttpUtils.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ParserHtml.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocess.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocessInclude.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocessJson.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocessMath.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ParserRaw.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ViewNav.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ViewOg.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ViewTpl.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ViewsError.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ViewsLore.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ViewsPrefs.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ViewsQuote.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ViewsRobots.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ViewsRss.kt
src/jvmMain/kotlin/info/mechyrdia/robot/RobotApi.kt
src/jvmMain/kotlin/info/mechyrdia/robot/RobotCodec.kt
src/jvmMain/kotlin/info/mechyrdia/robot/RobotFiles.kt
src/jvmMain/kotlin/info/mechyrdia/robot/RobotRateLimiter.kt
src/jvmMain/kotlin/info/mechyrdia/robot/RobotService.kt
src/jvmMain/kotlin/info/mechyrdia/robot/RobotSse.kt
src/jvmMain/kotlin/info/mechyrdia/robot/RobotUserLimiter.kt
src/jvmMain/kotlin/info/mechyrdia/robot/ViewsRobot.kt
src/jvmMain/kotlin/info/mechyrdia/route/ResourceBodies.kt
src/jvmMain/kotlin/info/mechyrdia/route/ResourceCsrf.kt
src/jvmMain/kotlin/info/mechyrdia/route/ResourceHandler.kt
src/jvmMain/kotlin/info/mechyrdia/route/ResourceMultipart.kt
src/jvmMain/kotlin/info/mechyrdia/route/ResourceTypes.kt
src/jvmMain/kotlin/info/mechyrdia/route/ResourceWebDav.kt
src/jvmMain/resources/logback.xml

index 13566b81b018ad684f3a35fee301741b2734c8f4..3630c6491c1c53c69bb75ddd9ffeecf2bb01ced1 100644 (file)
@@ -6,3 +6,5 @@
 # Datasource local storage ignored files
 /dataSources/
 /dataSources.local.xml
+# Stuff that I want to ignore
+/codeStyles/
index d8f7bb7c71ca5622eaee7e737e42ee13cc2748ce..c75a7398234733c3c88a0d749a451e47627e23d1 100644 (file)
@@ -28,8 +28,8 @@ buildscript {
 
 plugins {
        java
-       kotlin("multiplatform") version "2.0.0"
-       kotlin("plugin.serialization") version "2.0.0"
+       kotlin("multiplatform") version "2.0.10"
+       kotlin("plugin.serialization") version "2.0.10"
        id("com.github.johnrengelman.shadow") version "7.1.2"
        application
 }
@@ -110,7 +110,7 @@ kotlin {
                val mapMain by getting {
                        dependencies {
                                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
-                               implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0")
+                               implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1")
                                implementation("org.jetbrains.kotlinx:kotlinx-html-js:0.11.0")
                                
                                implementation(project(":externals"))
@@ -127,8 +127,8 @@ kotlin {
                                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.0")
-                               implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.7.0")
+                               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")
@@ -151,7 +151,7 @@ kotlin {
                                
                                implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.11.0")
                                
-                               implementation("org.apache.groovy:groovy-jsr223:4.0.10")
+                               implementation("org.apache.groovy:groovy-jsr223:4.0.22")
                                
                                implementation(files("libs/nsapi4j.jar"))
                                
@@ -170,7 +170,6 @@ kotlin {
                                implementation("io.ktor:ktor-client-logging:2.3.12")
                                
                                implementation(project(":fontparser"))
-                               //implementation(project(":fightgame"))
                        }
                }
        }
index 8d95c019c4d4992dcf49c5befe78aecabe521ac0..dbbb971fb42331980458f990e76dbfc7b1733b14 100644 (file)
@@ -2,30 +2,62 @@
 
 package info.mechyrdia
 
-import info.mechyrdia.auth.*
-import info.mechyrdia.data.*
-import info.mechyrdia.lore.*
+import info.mechyrdia.auth.ForbiddenException
+import info.mechyrdia.auth.PageDoNotCacheAttributeKey
+import info.mechyrdia.auth.SessionStorageMongoDB
+import info.mechyrdia.auth.UserSession
+import info.mechyrdia.data.ConnectionHolder
+import info.mechyrdia.data.FileStorage
+import info.mechyrdia.data.Id
+import info.mechyrdia.lore.HttpRedirectException
+import info.mechyrdia.lore.error400
+import info.mechyrdia.lore.error403
+import info.mechyrdia.lore.error403PageExpired
+import info.mechyrdia.lore.error404
+import info.mechyrdia.lore.error409
+import info.mechyrdia.lore.error500
+import info.mechyrdia.lore.getVersionHeaders
 import info.mechyrdia.robot.JsonRobotCodec
 import info.mechyrdia.robot.RobotService
-import info.mechyrdia.route.*
-import io.ktor.http.*
-import io.ktor.http.content.*
-import io.ktor.serialization.kotlinx.*
-import io.ktor.server.application.*
-import io.ktor.server.cio.*
-import io.ktor.server.engine.*
-import io.ktor.server.html.*
-import io.ktor.server.http.content.*
-import io.ktor.server.plugins.*
-import io.ktor.server.plugins.autohead.*
-import io.ktor.server.plugins.cachingheaders.*
-import io.ktor.server.plugins.callid.*
-import io.ktor.server.plugins.callloging.*
-import io.ktor.server.plugins.conditionalheaders.*
-import io.ktor.server.plugins.contentnegotiation.*
-import io.ktor.server.plugins.defaultheaders.*
-import io.ktor.server.plugins.forwardedheaders.*
-import io.ktor.server.plugins.statuspages.*
+import info.mechyrdia.route.CsrfFailedException
+import info.mechyrdia.route.FormUrlEncodedFormat
+import info.mechyrdia.route.Root
+import info.mechyrdia.route.WebDavAuthRequired
+import info.mechyrdia.route.get
+import info.mechyrdia.route.installWebDav
+import info.mechyrdia.route.isWebDav
+import info.mechyrdia.route.post
+import info.mechyrdia.route.postMultipart
+import info.mechyrdia.route.ws
+import io.ktor.http.CacheControl
+import io.ktor.http.ContentType
+import io.ktor.http.HttpHeaders
+import io.ktor.http.HttpStatusCode
+import io.ktor.http.content.CachingOptions
+import io.ktor.serialization.kotlinx.KotlinxSerializationConverter
+import io.ktor.serialization.kotlinx.KotlinxWebsocketSerializationConverter
+import io.ktor.server.application.Application
+import io.ktor.server.application.install
+import io.ktor.server.application.log
+import io.ktor.server.cio.CIO
+import io.ktor.server.engine.embeddedServer
+import io.ktor.server.html.respondHtml
+import io.ktor.server.http.content.CompressedFileType
+import io.ktor.server.http.content.JarFileContent
+import io.ktor.server.http.content.staticResources
+import io.ktor.server.plugins.MissingRequestParameterException
+import io.ktor.server.plugins.autohead.AutoHeadResponse
+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.conditionalheaders.ConditionalHeaders
+import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
+import io.ktor.server.plugins.defaultheaders.DefaultHeaders
+import io.ktor.server.plugins.forwardedheaders.XForwardedHeaders
+import io.ktor.server.plugins.origin
+import io.ktor.server.plugins.statuspages.StatusPages
 import io.ktor.server.request.*
 import io.ktor.server.resources.*
 import io.ktor.server.response.*
@@ -48,7 +80,7 @@ fun main() {
        
        FileStorage.initialize()
        
-       RobotService.initialize()
+       RobotService.start()
        
        embeddedServer(CIO, port = Configuration.Current.port, host = Configuration.Current.host, module = Application::factbooks).start(wait = true)
 }
@@ -86,7 +118,7 @@ fun Application.factbooks() {
        install(CallId) {
                val counter = AtomicLong(Random.nextLong())
                generate {
-                       "call-${counter.incrementAndGet().toULong()}-${System.currentTimeMillis()}"
+                       "call_${counter.incrementAndGet().toULong()}_${System.currentTimeMillis()}"
                }
                reply { call, callId ->
                        call.response.header("X-Call-Id", callId)
@@ -96,10 +128,10 @@ fun Application.factbooks() {
        install(CallLogging) {
                level = Level.INFO
                
-               callIdMdc("ktor-call-id")
+               callIdMdc("ktor_call_id")
                
                format { call ->
-                       "Call #${call.callId} Client ${call.request.origin.remoteHost} `${call.request.userAgent()}` Request ${call.request.httpMethod.value} ${call.request.uri} Response ${call.response.status()}"
+                       "Client ${call.request.origin.remoteHost} `${call.request.userAgent()}` requested ${call.request.httpMethod.value} ${call.request.uri} for response ${call.response.status()}"
                }
        }
        
index 0fb6763e276785f652f5484e6621f1fb1984c7f1..96451424ae88be4e1d7b708cc7e51a3394391f01 100644 (file)
@@ -1,8 +1,13 @@
 package info.mechyrdia.auth
 
 import info.mechyrdia.JsonStorageCodec
-import info.mechyrdia.data.*
-import io.ktor.server.sessions.*
+import info.mechyrdia.data.DataDocument
+import info.mechyrdia.data.DocumentTable
+import info.mechyrdia.data.Id
+import info.mechyrdia.data.InstantSerializer
+import info.mechyrdia.data.MONGODB_ID_KEY
+import info.mechyrdia.data.TableHolder
+import io.ktor.server.sessions.SessionStorage
 import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
 import java.time.Instant
index 7beb75cb724c09231a9073d8091f7f4d289e7860..e52174587d7932861c55568d265c26f933ff5791 100644 (file)
@@ -3,12 +3,20 @@ package info.mechyrdia.auth
 import info.mechyrdia.data.Id
 import info.mechyrdia.data.InstantSerializer
 import info.mechyrdia.data.NationData
-import io.ktor.server.application.*
-import io.ktor.server.plugins.*
-import io.ktor.server.sessions.*
+import io.ktor.server.application.ApplicationCall
+import io.ktor.server.plugins.origin
+import io.ktor.server.sessions.TooLateSessionSetException
+import io.ktor.server.sessions.get
+import io.ktor.server.sessions.sessionId
+import io.ktor.server.sessions.sessions
+import io.ktor.server.sessions.set
 import kotlinx.serialization.Serializable
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
 import java.time.Instant
 
+private val SessionsLogger: Logger = LoggerFactory.getLogger("info.mechyrdia.auth.SessionsKt")
+
 @Serializable
 data class CsrfTokenEntry(
        val targetRoute: String,
@@ -40,8 +48,15 @@ fun ApplicationCall.createCsrfToken(targetRoute: String = request.origin.uri, ex
                expiresAt = Instant.now().plusSeconds(expireSeconds.toLong())
        )
        
-       currentUserSession = currentUserSession.let { sess ->
-               sess.copy(csrfTokens = sess.csrfTokens + (token to entry))
+       try {
+               currentUserSession = currentUserSession.let { sess ->
+                       sess.copy(csrfTokens = sess.csrfTokens + (token to entry))
+               }
+       } catch (ex: TooLateSessionSetException) {
+               // Yeah, this just happens on occasion. I don't want it to pollute the log files,
+               // so we just ignore the exception itself and log the CSRF token that couldn't be
+               // created, so we have some record in case this weirdness actually impacts a user.
+               SessionsLogger.warn("CSRF Token $token could not be created for target route $targetRoute")
        }
        
        return token
index 9fe6dec4ad07c4cc3f74de332a77e940b6c337da..815d3a486c1535a7550339cbc78276f6b265f408 100644 (file)
@@ -2,7 +2,13 @@ package info.mechyrdia.auth
 
 import com.github.agadar.nationstates.shard.NationShard
 import info.mechyrdia.Configuration
-import info.mechyrdia.data.*
+import info.mechyrdia.data.DataDocument
+import info.mechyrdia.data.DocumentTable
+import info.mechyrdia.data.Id
+import info.mechyrdia.data.InstantSerializer
+import info.mechyrdia.data.MONGODB_ID_KEY
+import info.mechyrdia.data.NationData
+import info.mechyrdia.data.TableHolder
 import info.mechyrdia.lore.page
 import info.mechyrdia.lore.redirectHref
 import info.mechyrdia.lore.redirectHrefWithError
@@ -10,11 +16,26 @@ import info.mechyrdia.lore.standardNavBar
 import info.mechyrdia.route.Root
 import info.mechyrdia.route.href
 import info.mechyrdia.route.installCsrfToken
-import io.ktor.server.application.*
-import io.ktor.server.plugins.*
-import io.ktor.server.sessions.*
-import io.ktor.util.*
-import kotlinx.html.*
+import io.ktor.server.application.ApplicationCall
+import io.ktor.server.plugins.MissingRequestParameterException
+import io.ktor.server.sessions.clear
+import io.ktor.server.sessions.sessionId
+import io.ktor.server.sessions.sessions
+import io.ktor.server.sessions.set
+import io.ktor.util.AttributeKey
+import kotlinx.html.FormMethod
+import kotlinx.html.HTML
+import kotlinx.html.br
+import kotlinx.html.button
+import kotlinx.html.form
+import kotlinx.html.h1
+import kotlinx.html.hiddenInput
+import kotlinx.html.label
+import kotlinx.html.p
+import kotlinx.html.section
+import kotlinx.html.style
+import kotlinx.html.submitInput
+import kotlinx.html.textInput
 import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
 import java.time.Instant
@@ -62,7 +83,7 @@ suspend fun ApplicationCall.loginPage(): HTML.() -> Unit {
                section {
                        h1 { +"Log In With NationStates" }
                        form(method = FormMethod.post, action = href(Root.Auth.LoginPost())) {
-                               installCsrfToken()
+                               installCsrfToken(call = this@loginPage)
                                
                                hiddenInput {
                                        name = "tokenId"
index fbbdbbf3b41194d0e95b326588cf366477bd7326..9f9175604c2d47dbeae1e4e38d871dceeaec7672 100644 (file)
@@ -1,17 +1,37 @@
 package info.mechyrdia.auth
 
 import com.mongodb.client.model.Filters
-import info.mechyrdia.data.*
+import info.mechyrdia.data.DataDocument
+import info.mechyrdia.data.DocumentTable
+import info.mechyrdia.data.Id
+import info.mechyrdia.data.InstantSerializer
+import info.mechyrdia.data.MONGODB_ID_KEY
+import info.mechyrdia.data.NationData
+import info.mechyrdia.data.TableHolder
+import info.mechyrdia.data.ascending
+import info.mechyrdia.data.currentNation
+import info.mechyrdia.data.serialName
 import info.mechyrdia.lore.adminPage
 import info.mechyrdia.lore.dateTime
-import info.mechyrdia.lore.redirectHref
 import info.mechyrdia.lore.redirectHrefWithError
 import info.mechyrdia.route.Root
 import info.mechyrdia.route.href
 import info.mechyrdia.route.installCsrfToken
-import io.ktor.server.application.*
+import io.ktor.server.application.ApplicationCall
 import kotlinx.coroutines.flow.toList
-import kotlinx.html.*
+import kotlinx.html.FormMethod
+import kotlinx.html.HTML
+import kotlinx.html.div
+import kotlinx.html.form
+import kotlinx.html.h1
+import kotlinx.html.p
+import kotlinx.html.style
+import kotlinx.html.submitInput
+import kotlinx.html.table
+import kotlinx.html.td
+import kotlinx.html.textInput
+import kotlinx.html.th
+import kotlinx.html.tr
 import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
 import java.time.Instant
@@ -47,7 +67,7 @@ suspend fun ApplicationCall.adminRequestWebDavToken(): HTML.() -> Unit {
                        div {
                                style = "text-align:center"
                                form(method = FormMethod.post, action = href(Root.Admin.Vfs.WebDavTokenPost())) {
-                                       installCsrfToken()
+                                       installCsrfToken(call = this@adminRequestWebDavToken)
                                        submitInput { value = "Request WebDAV Token" }
                                }
                                
index 0829658d74e5e4a5527cd9288d20d7abdfe3b448..f16fa05ee67eca6d5b018c481a29e9c8298ddfe6 100644 (file)
@@ -1,6 +1,10 @@
 package info.mechyrdia.data
 
-import com.mongodb.client.model.*
+import com.mongodb.client.model.Filters
+import com.mongodb.client.model.Sorts
+import com.mongodb.client.model.UpdateOneModel
+import com.mongodb.client.model.UpdateOptions
+import com.mongodb.client.model.Updates
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.toList
 import kotlinx.serialization.SerialName
index b1f1b405b8b7d0ed3ca8c56aa11784947db5de74..4c7f79f84cf59b04e548a680c8c0674ae8bff9ee 100644 (file)
@@ -4,7 +4,14 @@ import com.aventrix.jnanoid.jnanoid.NanoIdUtils
 import com.mongodb.ConnectionString
 import com.mongodb.MongoClientSettings
 import com.mongodb.MongoDriverInformation
-import com.mongodb.client.model.*
+import com.mongodb.client.model.BulkWriteOptions
+import com.mongodb.client.model.Filters
+import com.mongodb.client.model.IndexOptions
+import com.mongodb.client.model.Indexes
+import com.mongodb.client.model.ReplaceOneModel
+import com.mongodb.client.model.ReplaceOptions
+import com.mongodb.client.model.UpdateOptions
+import com.mongodb.client.model.WriteModel
 import com.mongodb.kotlin.client.coroutine.MongoDatabase
 import com.mongodb.kotlin.client.coroutine.expireAfter
 import com.mongodb.reactivestreams.client.MongoClients
index 6858d2c2e012249e21e2fd607ecdd1ed072bb0ff..8f2cf2255b280db9da38ae4d14028261d6c86879 100644 (file)
@@ -7,16 +7,33 @@ import info.mechyrdia.Configuration
 import info.mechyrdia.FileStorageConfig
 import info.mechyrdia.lore.StoragePathAttributeKey
 import info.mechyrdia.lore.forEachSuspend
-import io.ktor.http.*
-import io.ktor.server.application.*
-import io.ktor.server.response.*
-import io.ktor.util.*
-import kotlinx.coroutines.*
-import kotlinx.coroutines.flow.*
+import io.ktor.http.ContentType
+import io.ktor.http.HttpStatusCode
+import io.ktor.http.defaultForFileExtension
+import io.ktor.server.application.ApplicationCall
+import io.ktor.server.response.respond
+import io.ktor.server.response.respondBytes
+import io.ktor.util.combineSafe
+import io.ktor.util.moveToByteArray
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.flow.toSet
+import kotlinx.coroutines.job
+import kotlinx.coroutines.launch
 import kotlinx.coroutines.reactive.asFlow
 import kotlinx.coroutines.reactive.asPublisher
 import kotlinx.coroutines.reactive.awaitFirst
 import kotlinx.coroutines.reactive.awaitFirstOrNull
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.runInterruptible
+import kotlinx.coroutines.withContext
 import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
 import org.bson.types.ObjectId
index 6c0d73a6e0d1722f5877f89a3e239008de80cbde..800bd274ac13e73b0c8ee3b374eddcec3a8a176c 100644 (file)
@@ -5,15 +5,18 @@ import info.mechyrdia.OwnerNationId
 import info.mechyrdia.auth.NSAPI
 import info.mechyrdia.auth.UserSession
 import info.mechyrdia.auth.executeSuspend
-import io.ktor.server.application.*
-import io.ktor.server.sessions.*
-import io.ktor.util.*
+import io.ktor.server.application.ApplicationCall
+import io.ktor.server.sessions.get
+import io.ktor.server.sessions.sessions
+import io.ktor.util.AttributeKey
 import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
 import org.slf4j.Logger
 import org.slf4j.LoggerFactory
 import java.util.concurrent.ConcurrentHashMap
 
+private val NationsLogger: Logger = LoggerFactory.getLogger("info.mechyrdia.data.NationsKt")
+
 @Serializable
 data class NationData(
        @SerialName(MONGODB_ID_KEY)
@@ -24,8 +27,6 @@ data class NationData(
        val isBanned: Boolean = false
 ) : DataDocument<NationData> {
        companion object : TableHolder<NationData> {
-               private val logger: Logger = LoggerFactory.getLogger(NationData::class.java)
-               
                override val Table = DocumentTable<NationData>()
                
                override suspend fun initialize() {
@@ -33,7 +34,7 @@ data class NationData(
                }
                
                fun unknown(id: Id<NationData>): NationData {
-                       logger.warn("Unable to find nation with Id $id - did it CTE?")
+                       NationsLogger.warn("Unable to find nation with Id $id - did it CTE?")
                        return NationData(id, "Unknown Nation", "https://www.nationstates.net/images/flags/exnation.png")
                }
                
index 8916fbbdc8bbe62afb6d680c3b3595d74a3627dc..ad245648f4097d83e9f4e883ada6cfdb2a8b8529 100644 (file)
@@ -2,11 +2,17 @@ package info.mechyrdia.data
 
 import info.mechyrdia.MainDomainName
 import info.mechyrdia.OwnerNationId
-import info.mechyrdia.lore.*
+import info.mechyrdia.lore.ParserTree
+import info.mechyrdia.lore.append
+import info.mechyrdia.lore.dateTime
+import info.mechyrdia.lore.mapSuspend
+import info.mechyrdia.lore.parseAs
+import info.mechyrdia.lore.toCommentHtml
+import info.mechyrdia.lore.toFriendlyPathTitle
 import info.mechyrdia.route.Root
 import info.mechyrdia.route.href
 import info.mechyrdia.route.installCsrfToken
-import io.ktor.server.application.*
+import io.ktor.server.application.ApplicationCall
 import kotlinx.coroutines.async
 import kotlinx.coroutines.coroutineScope
 import kotlinx.html.*
@@ -61,8 +67,7 @@ data class CommentRenderData(
        }
 }
 
-context(ApplicationCall)
-fun FlowContent.commentBox(comment: CommentRenderData, loggedInAs: Id<NationData>?, viewingUserPage: Boolean = false) {
+fun FlowContent.commentBox(comment: CommentRenderData, loggedInAs: Id<NationData>?, viewingUserPage: Boolean = false, call: ApplicationCall) {
        if (comment.submittedBy.isBanned && !viewingUserPage && loggedInAs != comment.submittedBy.id && loggedInAs != OwnerNationId)
                return
        
@@ -70,7 +75,7 @@ fun FlowContent.commentBox(comment: CommentRenderData, loggedInAs: Id<NationData
                p {
                        style = "font-size:1.5em;margin-top:2.5em"
                        +"On factbook "
-                       a(href = href(Root.LorePage(comment.submittedIn))) {
+                       a(href = call.href(Root.LorePage(comment.submittedIn))) {
                                +comment.submittedInTitle
                        }
                }
@@ -81,7 +86,7 @@ fun FlowContent.commentBox(comment: CommentRenderData, loggedInAs: Id<NationData
                        img(src = comment.submittedBy.flag, alt = "Flag of ${comment.submittedBy.name}", classes = "flag-icon")
                        span(classes = "author-name") {
                                +Entities.nbsp
-                               a(href = href(Root.User.ById(comment.submittedBy.id))) {
+                               a(href = call.href(Root.User.ById(comment.submittedBy.id))) {
                                        +comment.submittedBy.name
                                }
                        }
@@ -91,7 +96,7 @@ fun FlowContent.commentBox(comment: CommentRenderData, loggedInAs: Id<NationData
                }
                
                div(classes = "comment") {
-                       +comment.contentsHtml
+                       append(comment.contentsHtml)
                        comment.lastEdit?.let { lastEdit ->
                                p {
                                        style = "font-size:0.8em"
@@ -101,12 +106,12 @@ fun FlowContent.commentBox(comment: CommentRenderData, loggedInAs: Id<NationData
                        }
                        p {
                                style = "font-size:0.8em"
-                               a(href = href(Root.Comments.ViewPage(comment.id))) {
+                               a(href = call.href(Root.Comments.ViewPage(comment.id))) {
                                        +"Permalink"
                                }
                                +Entities.nbsp
                                a(href = "#", classes = "copy-text") {
-                                       attributes["data-text"] = "$MainDomainName${href(Root.Comments.ViewPage(comment.id))}"
+                                       attributes["data-text"] = "$MainDomainName${call.href(Root.Comments.ViewPage(comment.id))}"
                                        +"(Copy)"
                                }
                                
@@ -142,7 +147,7 @@ fun FlowContent.commentBox(comment: CommentRenderData, loggedInAs: Id<NationData
                                        +Entities.nbsp
                                        +"\u2022"
                                        +Entities.nbsp
-                                       a(href = href(Root.Comments.DeleteConfirmPage(comment.id)), classes = "comment-delete-link") {
+                                       a(href = call.href(Root.Comments.DeleteConfirmPage(comment.id)), classes = "comment-delete-link") {
                                                +"Delete"
                                        }
                                }
@@ -153,7 +158,7 @@ fun FlowContent.commentBox(comment: CommentRenderData, loggedInAs: Id<NationData
                                        +"Replies:"
                                        for (reply in comment.replyLinks) {
                                                +" "
-                                               a(href = href(Root.Comments.ViewPage(reply))) {
+                                               a(href = call.href(Root.Comments.ViewPage(reply))) {
                                                        +">>$reply"
                                                }
                                        }
@@ -162,7 +167,7 @@ fun FlowContent.commentBox(comment: CommentRenderData, loggedInAs: Id<NationData
        }
        
        if (loggedInAs == comment.submittedBy.id) {
-               val formPath = href(Root.Comments.EditPost(comment.id))
+               val formPath = call.href(Root.Comments.EditPost(comment.id))
                form(action = formPath, method = FormMethod.post, classes = "comment-input comment-edit-box") {
                        id = "comment-edit-box-${comment.id}"
                        div(classes = "comment-preview")
@@ -170,7 +175,7 @@ fun FlowContent.commentBox(comment: CommentRenderData, loggedInAs: Id<NationData
                                name = "comment"
                                +comment.contentsRaw
                        }
-                       installCsrfToken()
+                       installCsrfToken(call = call)
                        submitInput { value = "Edit Comment" }
                        button(classes = "comment-cancel-edit evil") {
                                attributes["data-edit-id"] = "comment-edit-box-${comment.id}"
@@ -180,22 +185,21 @@ fun FlowContent.commentBox(comment: CommentRenderData, loggedInAs: Id<NationData
        }
 }
 
-context(ApplicationCall)
-fun FlowContent.commentInput(pagePathParts: List<String>, commentingAs: NationData?) {
+fun FlowContent.commentInput(pagePathParts: List<String>, commentingAs: NationData?, call: ApplicationCall) {
        if (commentingAs == null) {
                p {
-                       a(href = href(Root.Auth.LoginPage())) { +"Log in" }
+                       a(href = call.href(Root.Auth.LoginPage())) { +"Log in" }
                        +" to comment"
                }
                return
        }
        
-       form(action = href(Root.Comments.NewPost(path = pagePathParts)), method = FormMethod.post, classes = "comment-input") {
+       form(action = call.href(Root.Comments.NewPost(path = pagePathParts)), method = FormMethod.post, classes = "comment-input") {
                div(classes = "comment-preview")
                textArea(classes = "comment-markup") {
                        name = "comment"
                }
-               installCsrfToken()
+               installCsrfToken(call = call)
                submitInput { value = "Submit Comment" }
        }
 }
index d9cd3f037e577e406c81cc88067f2e87f83e6a4e..c5a42a581e103de113174e8d360f55530e3b7774 100644 (file)
@@ -3,11 +3,22 @@ package info.mechyrdia.data
 import com.mongodb.client.model.Sorts
 import info.mechyrdia.OwnerNationId
 import info.mechyrdia.auth.ForbiddenException
-import info.mechyrdia.lore.*
+import info.mechyrdia.lore.ParserTree
+import info.mechyrdia.lore.PokhwalishAlphabetFont
+import info.mechyrdia.lore.TylanAlphabetFont
+import info.mechyrdia.lore.append
+import info.mechyrdia.lore.getImageSizeStyleValue
+import info.mechyrdia.lore.getReplies
+import info.mechyrdia.lore.page
+import info.mechyrdia.lore.parseAs
+import info.mechyrdia.lore.redirectHref
+import info.mechyrdia.lore.redirectHrefWithError
+import info.mechyrdia.lore.standardNavBar
+import info.mechyrdia.lore.toCommentHtml
 import info.mechyrdia.route.Root
 import info.mechyrdia.route.href
 import info.mechyrdia.route.installCsrfToken
-import io.ktor.server.application.*
+import io.ktor.server.application.ApplicationCall
 import kotlinx.coroutines.flow.filterNot
 import kotlinx.coroutines.flow.take
 import kotlinx.coroutines.flow.toList
@@ -57,7 +68,7 @@ suspend fun ApplicationCall.recentCommentsPage(limit: Int?): HTML.() -> Unit {
                        }
                        
                        for (comment in comments)
-                               commentBox(comment, currNation?.id, viewingUserPage = true)
+                               commentBox(comment, currNation?.id, viewingUserPage = true, call = this@recentCommentsPage)
                }
        }
 }
@@ -149,13 +160,14 @@ suspend fun ApplicationCall.deleteCommentPage(commentId: Id<Comment>): HTML.() -
                                strong { +"It will be gone forever!" }
                        }
                        
-                       commentBox(commentDisplay, currNation.id)
+                       commentBox(commentDisplay, currNation.id, call = this@deleteCommentPage)
                        
                        form(method = FormMethod.get, action = href(Root.Comments.ViewPage(comment.id))) {
                                submitInput { value = "No, take me back" }
                        }
+                       
                        form(method = FormMethod.post, action = href(Root.Comments.DeleteConfirmPost(comment.id))) {
-                               installCsrfToken()
+                               installCsrfToken(call = this@deleteCommentPage)
                                submitInput(classes = "evil") { value = "Yes, delete it" }
                        }
                }
@@ -428,7 +440,7 @@ suspend fun ApplicationCall.commentHelpPage(): HTML.() -> Unit = page("Commentin
                        +"Table cells in this custom BBCode markup also support row-spans and column-spans, even at the same time:"
                }
                pre { +tableDemoMarkup }
-               +tableDemoHtml
+               append(tableDemoHtml)
                p {
                        +"The format goes as [td=(width)x(height)] or [th=(width)x(height)]. If one parameter is omitted (assumed to be 1), then the format can be [td=(width)] or [td=x(height)]"
                }
index 2d5fd496799b8daf40802e280c6275e5b9fbae3a..f6e1ee6edf7dc706f685a07c42c6a56ac403993a 100644 (file)
@@ -8,12 +8,16 @@ import info.mechyrdia.lore.redirectHref
 import info.mechyrdia.route.Root
 import info.mechyrdia.route.href
 import info.mechyrdia.route.installCsrfToken
-import io.ktor.http.*
-import io.ktor.http.content.*
-import io.ktor.server.application.*
-import io.ktor.server.html.*
-import io.ktor.server.plugins.*
-import io.ktor.server.response.*
+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 kotlinx.html.*
@@ -45,19 +49,18 @@ private suspend fun fileTree(path: StoragePath): TreeNode? {
        }?.filterNotNull()?.toMap()?.let { TreeNode.DirNode(it) }
 }
 
-context(ApplicationCall)
-private fun UL.render(path: StoragePath, childNodes: Map<String, TreeNode>) {
+private fun UL.render(path: StoragePath, childNodes: Map<String, TreeNode>, call: ApplicationCall) {
        val sortedChildren = childNodes.sortedAsNodes()
        
        for ((name, child) in sortedChildren)
-               render(path / name, child)
+               render(path / name, child, call = call)
        
        li {
                style = "list-style:none"
                
                p {
-                       form(action = href(Root.Admin.Vfs.Upload(path.elements)), method = FormMethod.post, encType = FormEncType.multipartFormData) {
-                               installCsrfToken()
+                       form(action = call.href(Root.Admin.Vfs.Upload(path.elements)), method = FormMethod.post, encType = FormEncType.multipartFormData) {
+                               installCsrfToken(call = call)
                                label {
                                        fileInput(name = "uploaded")
                                        +"Upload File"
@@ -67,8 +70,8 @@ private fun UL.render(path: StoragePath, childNodes: Map<String, TreeNode>) {
                }
                
                p {
-                       form(action = href(Root.Admin.Vfs.MkDir(path.elements)), method = FormMethod.post) {
-                               installCsrfToken()
+                       form(action = call.href(Root.Admin.Vfs.MkDir(path.elements)), method = FormMethod.post) {
+                               installCsrfToken(call = call)
                                textInput {
                                        placeholder = "new-dir"
                                }
@@ -81,7 +84,7 @@ private fun UL.render(path: StoragePath, childNodes: Map<String, TreeNode>) {
                
                if (!path.isRoot)
                        p {
-                               form(action = href(Root.Admin.Vfs.RmDirConfirmPage(path.elements)), method = FormMethod.get) {
+                               form(action = call.href(Root.Admin.Vfs.RmDirConfirmPage(path.elements)), method = FormMethod.get) {
                                        submitInput(classes = "evil") {
                                                value = "Delete (Recursive)"
                                        }
@@ -90,21 +93,20 @@ private fun UL.render(path: StoragePath, childNodes: Map<String, TreeNode>) {
        }
 }
 
-context(ApplicationCall)
-private fun UL.render(path: StoragePath, node: TreeNode) {
+private fun UL.render(path: StoragePath, node: TreeNode, call: ApplicationCall) {
        when (node) {
                is TreeNode.FileNode -> li {
-                       a(href = href(Root.Admin.Vfs.View(path.elements))) {
+                       a(href = call.href(Root.Admin.Vfs.View(path.elements))) {
                                +path.name
                        }
                }
                
                is TreeNode.DirNode -> li {
-                       a(href = href(Root.Admin.Vfs.View(path.elements))) {
+                       a(href = call.href(Root.Admin.Vfs.View(path.elements))) {
                                +path.name
                        }
                        ul {
-                               render(path, node.children)
+                               render(path, node.children, call = call)
                        }
                }
        }
@@ -156,7 +158,7 @@ suspend fun ApplicationCall.adminViewVfs(path: StoragePath): HTML.() -> Unit {
                                                                }
                                                                li {
                                                                        form(action = href(Root.Admin.Vfs.Overwrite(path.elements)), method = FormMethod.post, encType = FormEncType.multipartFormData) {
-                                                                               installCsrfToken()
+                                                                               installCsrfToken(call = this@adminViewVfs)
                                                                                label {
                                                                                        fileInput(name = "uploaded")
                                                                                        +"Upload New Version"
@@ -202,7 +204,7 @@ suspend fun ApplicationCall.adminViewVfs(path: StoragePath): HTML.() -> Unit {
                                                        }
                                                }
                                        
-                                       render(path, tree.children)
+                                       render(path, tree.children, call = this@adminViewVfs)
                                }
                        }
                }
@@ -236,18 +238,17 @@ private suspend fun fileTreeForCopy(path: StoragePath): TreeNode.DirNode? {
        }?.filterNotNull()?.toMap()?.let { TreeNode.DirNode(it) }
 }
 
-context(ApplicationCall)
-private fun UL.renderForCopy(fromPath: StoragePath, intoPath: StoragePath, node: TreeNode.DirNode) {
+private fun UL.renderForCopy(fromPath: StoragePath, intoPath: StoragePath, node: TreeNode.DirNode, call: ApplicationCall) {
        li {
-               form(method = FormMethod.post, action = href(Root.Admin.Vfs.CopyPost(intoPath.elements))) {
-                       installCsrfToken()
+               form(method = FormMethod.post, action = call.href(Root.Admin.Vfs.CopyPost(intoPath.elements))) {
+                       installCsrfToken(call = call)
                        hiddenInput(name = "from") { value = fromPath.toString() }
                        submitInput { value = "Copy Into /$intoPath" }
                }
                ul {
                        for ((childName, childNode) in node.children)
                                if (childNode is TreeNode.DirNode)
-                                       renderForCopy(fromPath, intoPath / childName, childNode)
+                                       renderForCopy(fromPath, intoPath / childName, childNode, call = call)
                }
        }
 }
@@ -267,7 +268,7 @@ suspend fun ApplicationCall.adminShowCopyFile(from: StoragePath): HTML.() -> Uni
                                                submitInput { value = "Cancel Copy" }
                                        }
                                }
-                               renderForCopy(from, StoragePath.Root, tree)
+                               renderForCopy(from, StoragePath.Root, tree, call = this@adminShowCopyFile)
                        }
                }
        }
@@ -332,7 +333,7 @@ suspend fun ApplicationCall.adminConfirmDeleteFile(path: StoragePath) {
                                        }
                                        +Entities.nbsp
                                        form(method = FormMethod.post, action = href(Root.Admin.Vfs.DeleteConfirmPost(path.elements))) {
-                                               installCsrfToken()
+                                               installCsrfToken(call = this@adminConfirmDeleteFile)
                                                submitInput(classes = "evil") { value = "Yes, delete it" }
                                        }
                                }
@@ -385,7 +386,7 @@ suspend fun ApplicationCall.adminConfirmRemoveDirectory(path: StoragePath) {
                                        }
                                        +Entities.nbsp
                                        form(method = FormMethod.post, action = href(Root.Admin.Vfs.RmDirConfirmPost(path.elements))) {
-                                               installCsrfToken()
+                                               installCsrfToken(call = this@adminConfirmRemoveDirectory)
                                                submitInput(classes = "evil") { value = "Yes, delete it" }
                                        }
                                }
index 4b720232b336dd6d62d4840ea5c7fe78655c2e5e..456b0af809099721032701072c8a951f7a376884 100644 (file)
@@ -10,10 +10,16 @@ import info.mechyrdia.lore.standardNavBar
 import info.mechyrdia.route.Root
 import info.mechyrdia.route.href
 import info.mechyrdia.route.installCsrfToken
-import io.ktor.server.application.*
-import io.ktor.server.sessions.*
+import io.ktor.server.application.ApplicationCall
+import io.ktor.server.sessions.get
+import io.ktor.server.sessions.sessions
 import kotlinx.coroutines.flow.toList
-import kotlinx.html.*
+import kotlinx.html.HTML
+import kotlinx.html.a
+import kotlinx.html.h1
+import kotlinx.html.id
+import kotlinx.html.p
+import kotlinx.html.section
 
 fun ApplicationCall.currentUserPage(): Nothing {
        val currNationId = sessions.get<UserSession>()?.nationId
@@ -41,19 +47,19 @@ suspend fun ApplicationCall.userPage(userId: Id<NationData>): HTML.() -> Unit {
                                        p { +"This user is banned" }
                                        val unbanLink = href(Root.Admin.Unban(viewingNation.id))
                                        a(href = unbanLink) {
-                                               installCsrfToken(unbanLink)
+                                               installCsrfToken(unbanLink, call = this@userPage)
                                                +"Unban"
                                        }
                                } else {
                                        val banLink = href(Root.Admin.Ban(viewingNation.id))
                                        a(href = banLink) {
-                                               installCsrfToken(banLink)
+                                               installCsrfToken(banLink, call = this@userPage)
                                                +"Ban"
                                        }
                                }
                        }
                        for (comment in comments)
-                               commentBox(comment, currNation?.id, viewingUserPage = true)
+                               commentBox(comment, currNation?.id, viewingUserPage = true, call = this@userPage)
                }
        }
 }
index e678ed807c029daf2bd7e1f5bd054c0c4c130de9..42b70a26a49e8da6cc4f043085e0b021ab213e0b 100644 (file)
@@ -6,13 +6,12 @@ import com.mongodb.client.model.Filters
 import com.mongodb.client.model.Updates
 import info.mechyrdia.auth.UserSession
 import info.mechyrdia.lore.dateTime
-import io.ktor.server.application.*
-import io.ktor.server.request.*
-import io.ktor.server.sessions.*
+import io.ktor.server.application.ApplicationCall
+import io.ktor.server.request.path
+import io.ktor.server.request.userAgent
+import io.ktor.server.sessions.sessionId
 import kotlinx.coroutines.flow.firstOrNull
-import kotlinx.html.FlowContent
-import kotlinx.html.p
-import kotlinx.html.style
+import kotlinx.html.*
 import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
 import org.intellij.lang.annotations.Language
index 7dc5a91723b1b8395aac9b5cb1ce308dd5cfa234..441b2194e0f625d365f040bd80f53948ecd29068 100644 (file)
@@ -1,21 +1,17 @@
 package info.mechyrdia.data
 
-import io.ktor.http.*
-import io.ktor.server.application.*
-import io.ktor.server.response.*
-import kotlinx.html.Tag
-import kotlinx.html.TagConsumer
-import kotlinx.html.consumers.DelayedConsumer
-import kotlinx.html.consumers.FinalizeConsumer
-import kotlinx.html.consumers.TraceConsumer
-import kotlinx.html.dom.HTMLDOMBuilder
-import kotlinx.html.impl.DelegatingMap
-import kotlinx.html.org.w3c.dom.events.Event
-import kotlinx.html.stream.HTMLStreamBuilder
-import kotlinx.html.stream.appendHTML
-import kotlinx.html.stream.createHTML
-import kotlinx.html.visit
-import kotlinx.html.visitAndFinalize
+import info.mechyrdia.lore.RssCategory
+import io.ktor.http.ContentType
+import io.ktor.http.HttpStatusCode
+import io.ktor.http.withCharsetIfNeeded
+import io.ktor.server.application.ApplicationCall
+import io.ktor.server.response.respondText
+import kotlinx.html.*
+import kotlinx.html.consumers.*
+import kotlinx.html.dom.*
+import kotlinx.html.impl.*
+import kotlinx.html.org.w3c.dom.events.*
+import kotlinx.html.stream.*
 import org.w3c.dom.Document
 
 @DslMarker
@@ -120,6 +116,12 @@ class XmlTag(
                get() = attributes.immutableEntries
        
        operator fun String.invoke(attributes: Map<String, String> = emptyMap(), namespace: String? = null, isInline: Boolean = false, block: (XmlTag.() -> Unit)? = null) = XmlTag(this, consumer, attributes, namespace, isInline, block == null).visit(block ?: emptyBlock)
+       
+       operator fun XmlInsertable.unaryPlus() = intoXml()
+}
+
+interface XmlInsertable {
+       fun XmlTag.intoXml()
 }
 
 private val emptyBlock: XmlTag.() -> Unit = {}
index e7a064152f0b49aa6b74085519358ba1332e8e7b..3905766a0480ec863ee8762286e70ace5ffae9b1 100644 (file)
@@ -2,7 +2,7 @@ package info.mechyrdia.lore
 
 import info.mechyrdia.data.FileStorage
 import info.mechyrdia.data.StoragePath
-import io.ktor.server.application.*
+import io.ktor.server.application.ApplicationCall
 import java.time.Instant
 import java.time.Month
 import java.time.ZoneId
@@ -14,18 +14,16 @@ fun isApril1st(time: Instant = Instant.now()): Boolean {
        return zonedDateTime.month == Month.APRIL && zonedDateTime.dayOfMonth == 1
 }
 
-context(ApplicationCall)
-suspend fun redirectFileOnApril1st(requestedFile: StoragePath): StoragePath? {
-       if (!april1stMode.isEnabled) return null
+suspend fun redirectFileOnApril1st(requestedFile: StoragePath, call: ApplicationCall): StoragePath? {
+       if (!call.april1stMode.isEnabled) return null
        
        val path = StoragePath.april1Dir / requestedFile.elements
        if (FileStorage.instance.statFile(path) == null) return null
        return path
 }
 
-context(ApplicationCall)
-suspend fun getAssetFile(requestedFile: StoragePath): StoragePath {
-       return redirectFileOnApril1st(requestedFile) ?: requestedFile
+suspend fun ApplicationCall.getAssetFile(requestedFile: StoragePath): StoragePath {
+       return redirectFileOnApril1st(requestedFile, call = this) ?: requestedFile
 }
 
 suspend fun ApplicationCall.respondAsset(assetFile: StoragePath) {
index 83da218d82109e823554b9a89baaa3dc053fc310..f0670ef11a790f5883598eac059fa3e258346e82 100644 (file)
@@ -5,15 +5,12 @@ import info.mechyrdia.data.FileStorage
 import info.mechyrdia.data.StoragePath
 import info.mechyrdia.route.Root
 import info.mechyrdia.route.href
-import io.ktor.server.application.*
-import kotlinx.html.UL
-import kotlinx.html.a
-import kotlinx.html.li
-import kotlinx.html.ul
+import io.ktor.server.application.ApplicationCall
+import kotlinx.html.*
 import java.text.Collator
-import java.util.*
+import java.util.Locale
 
-data class ArticleNode(val name: String, val title: String, val subNodes: List<ArticleNode>?)
+data class ArticleNode(val name: String, val title: ArticleTitle, val subNodes: List<ArticleNode>?)
 
 suspend fun rootArticleNodeList(): List<ArticleNode> = StoragePath.articleDir.toArticleNode().subNodes.orEmpty()
 
@@ -34,7 +31,7 @@ fun <T> List<T>.sortedLexically(selector: (T) -> String?) = map { it to collator
        .sortedBy { it.second }
        .map { (it, _) -> it }
 
-private fun List<ArticleNode>.sortedAsArticles() = sortedLexically { it.title }.sortedBy { it.subNodes == null }
+private fun List<ArticleNode>.sortedAsArticles() = sortedLexically { it.title.title }.sortedBy { it.subNodes == null }
 
 private val String.isViewable: Boolean
        get() = Configuration.Current.isDevMode || !(endsWith(".wip") || endsWith(".old"))
@@ -45,33 +42,37 @@ val ArticleNode.isViewable: Boolean
 val StoragePath.isViewable: Boolean
        get() = name.isViewable
 
-context(ApplicationCall)
-fun List<ArticleNode>.renderInto(list: UL, base: List<String> = emptyList(), format: LoreArticleFormat = LoreArticleFormat.HTML) {
+fun List<ArticleNode>.renderInto(list: UL, base: List<String> = emptyList(), format: LoreArticleFormat = LoreArticleFormat.HTML, call: ApplicationCall) {
        for (node in this)
                if (node.isViewable)
                        list.li {
                                val nodePath = base + node.name
-                               a(href = href(Root.LorePage(nodePath, format))) { +node.title }
+                               a(href = call.href(Root.LorePage(nodePath, format))) {
+                                       style = node.title.css
+                                       +node.title.title
+                               }
                                node.subNodes?.let { subNodes ->
                                        ul {
-                                               subNodes.renderInto(this, nodePath, format)
+                                               subNodes.renderInto(this, nodePath, format, call = call)
                                        }
                                }
                        }
 }
 
 suspend fun StoragePath.toFriendlyPageTitle() = ArticleTitleCache.get(this)
-       ?: if (elements.size > 1)
-               elements.lastOrNull()?.split('-')?.joinToString(separator = " ") { word ->
-                       word.lowercase().replaceFirstChar { it.titlecase() }
-               }.orEmpty()
-       else TOC_TITLE
+       ?: ArticleTitle(
+               if (elements.size > 1)
+                       elements.lastOrNull()?.split('-')?.joinToString(separator = " ") { word ->
+                               word.lowercase().replaceFirstChar { it.titlecase() }
+                       }.orEmpty()
+               else TOC_TITLE
+       )
 
 suspend fun StoragePath.toFriendlyPathTitle(): String {
        val lorePath = elements.drop(1)
        if (lorePath.isEmpty()) return TOC_TITLE
        
        return lorePath.indices.mapSuspend { index ->
-               StoragePath(lorePath.take(index + 1)).toFriendlyPageTitle()
+               StoragePath(lorePath.take(index + 1)).toFriendlyPageTitle().title
        }.joinToString(separator = " - ")
 }
index 337126ff81e1e0bb96c5182862615d8035a60ae5..fdcaded447b89d4bd2a4416470a8e317c14031af 100644 (file)
@@ -1,20 +1,32 @@
 package info.mechyrdia.lore
 
-import info.mechyrdia.data.FileStorage
 import info.mechyrdia.data.StoragePath
 
-object ArticleTitleCache : FileDependentCache<String>() {
-       override suspend fun processFile(path: StoragePath): String? {
+data class ArticleTitle(val title: String, val css: String = "")
+
+object ArticleTitleCache : FileDependentCache<ArticleTitle>() {
+       override suspend fun processFile(path: StoragePath): ArticleTitle? {
                if (path !in StoragePath.articleDir)
                        return null
                
-               val bytes = FileStorage.instance.readFile(path) ?: return null
-               val text = String(bytes)
+               val factbookAst = FactbookLoader.loadFactbook(path.elements.drop(1)) ?: return null
+               
+               val title = factbookAst
+                       .firstNotNullOfOrNull { node ->
+                               (node as? ParserTreeNode.Tag)?.takeIf { tag -> tag.tag == "h1" }
+                       }
+                       ?.subNodes
+                       ?.treeToText()
+                       ?: return null
+               
+               val css = listOfNotNull(
+                       if (factbookAst.any { it is ParserTreeNode.Tag && it.tag == "redirect" }) "font-style:italic" else null,
+                       
+                       // Only used in dev-mode
+                       if (path.name.endsWith(".wip")) "opacity:0.675" else null,
+                       if (path.name.endsWith(".old")) "text-decoration:line-through" else null,
+               ).joinToString(separator = ";")
                
-               return text
-                       .lineSequence()
-                       .first()
-                       .removePrefix("[h1]")
-                       .removeSuffix("[/h1]")
+               return ArticleTitle(title, css)
        }
 }
index 4c9128f180129fc495d541dd060aa16e9ddbb631..708b607632d36d9ee45dd2cd525faa9c8dc24c41 100644 (file)
@@ -2,7 +2,7 @@ package info.mechyrdia.lore
 
 import info.mechyrdia.data.FileStorage
 import info.mechyrdia.data.StoragePath
-import io.ktor.util.*
+import io.ktor.util.AttributeKey
 import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
 import java.time.Instant
index 030ce603f819175a4f270583dd7c99af3f7485c7..1bb1702f38b307c7472a7678892d63ab81db7670 100644 (file)
@@ -3,10 +3,13 @@ package info.mechyrdia.lore
 import info.mechyrdia.data.FileStorage
 import info.mechyrdia.data.StoragePath
 import info.mechyrdia.data.respondStoredFile
-import io.ktor.http.*
-import io.ktor.server.application.*
-import io.ktor.server.request.*
-import io.ktor.server.response.*
+import io.ktor.http.HttpHeaders
+import io.ktor.http.HttpStatusCode
+import io.ktor.server.application.ApplicationCall
+import io.ktor.server.request.acceptEncodingItems
+import io.ktor.server.response.header
+import io.ktor.server.response.respond
+import io.ktor.server.response.respondBytes
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.withContext
 import java.io.ByteArrayOutputStream
index eab3a879897ff446e46e4acf4fad302840a095b3..f467a8f0ff0289f9aab2014297f490a11672fe60 100644 (file)
@@ -2,14 +2,19 @@ package info.mechyrdia.lore
 
 import info.mechyrdia.data.FileStorage
 import info.mechyrdia.data.StoragePath
-import io.ktor.http.content.*
-import io.ktor.server.application.*
-import io.ktor.server.http.content.*
-import kotlinx.coroutines.*
+import io.ktor.http.content.EntityTagVersion
+import io.ktor.http.content.Version
+import io.ktor.server.application.ApplicationCall
+import io.ktor.server.http.content.LastModifiedVersion
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.withContext
 import java.io.IOException
 import java.io.OutputStream
 import java.security.MessageDigest
-import java.util.*
+import java.util.Base64
 
 private class DigestingOutputStream(stomach: MessageDigest) : OutputStream() {
        private var stomachStore: MessageDigest? = stomach
index 72dc3bfe916cef447e80c7976fa4a6408f43310a..2a101b4b7d18f0dbf068f48fe3a45cf353fb7418 100644 (file)
@@ -3,7 +3,11 @@ package info.mechyrdia.lore
 import com.jaredrummler.fontreader.truetype.FontFileReader
 import com.jaredrummler.fontreader.truetype.TTFFile
 import com.jaredrummler.fontreader.util.GlyphSequence
-import info.mechyrdia.data.*
+import info.mechyrdia.data.FileStorage
+import info.mechyrdia.data.StoragePath
+import info.mechyrdia.data.XmlTagConsumer
+import info.mechyrdia.data.declaration
+import info.mechyrdia.data.root
 import info.mechyrdia.route.KeyedEnumSerializer
 import info.mechyrdia.yieldThread
 import kotlinx.coroutines.Dispatchers
@@ -23,6 +27,8 @@ import java.io.IOException
 import java.nio.IntBuffer
 import kotlin.properties.ReadOnlyProperty
 
+private val FontsLogger: Logger = LoggerFactory.getLogger("info.mechyrdia.lore.FontsKt")
+
 @Serializable(with = TextAlignmentSerializer::class)
 enum class TextAlignment {
        LEFT {
@@ -79,8 +85,6 @@ fun <T, C : XmlTagConsumer<T>> C.svg(svgDoc: SvgDoc) = declaration(standalone =
        ) { "path"(attributes = mapOf("d" to svgDoc.path.d, "fill-rule" to svgDoc.path.fillRule)) }
 
 object MechyrdiaSansFont {
-       private val logger: Logger = LoggerFactory.getLogger(MechyrdiaSansFont::class.java)
-       
        suspend fun renderTextToSvg(text: String, bold: Boolean, italic: Boolean, align: TextAlignment): SvgDoc {
                val (file, font) = getFont(bold, italic)
                
@@ -238,7 +242,7 @@ object MechyrdiaSansFont {
                        
                        return shape
                } catch (ex: Exception) {
-                       logger.error("Error converting text $text to font shape", ex)
+                       FontsLogger.error("Error converting text $text to font shape", ex)
                        return GeneralPath()
                } finally {
                        g2d.dispose()
index 1b46330d8b2d77a1ba57796bb66003eadb4e66bc..f6f028cc162c531d313e7ccc4f54f994737f8200 100644 (file)
@@ -2,7 +2,7 @@ package info.mechyrdia.lore
 
 import info.mechyrdia.route.ErrorMessageCookieName
 import info.mechyrdia.route.href
-import io.ktor.server.application.*
+import io.ktor.server.application.ApplicationCall
 
 data class HttpRedirectException(val url: String, val permanent: Boolean) : RuntimeException()
 
index 842efe9bbfd919bb2a96014f6cffe06be2ff0b83..ff4661c65dfaa3ad76aff7d60e090346e3a6f267 100644 (file)
@@ -1,21 +1,18 @@
 package info.mechyrdia.lore
 
 import info.mechyrdia.JsonStorageCodec
-import io.ktor.util.*
 import kotlinx.html.*
-import kotlinx.html.org.w3c.dom.events.Event
-import kotlinx.html.stream.createHTML
+import kotlinx.html.org.w3c.dom.events.*
+import kotlinx.html.stream.*
 import java.time.Instant
-import kotlin.text.toCharArray
 
 typealias HtmlBuilderContext = Unit
 typealias HtmlBuilderSubject = TagConsumer<*>.() -> Any?
 
-context(T)
-operator fun <T : Tag> (TagConsumer<*>.() -> Any?).unaryPlus() = with(HtmlLexerTagConsumer(consumer)) { this@unaryPlus() }
+fun <T : Tag> T.append(block: TagConsumer<*>.() -> Any?) = HtmlLexerTagConsumer(consumer).block()
 
-fun (TagConsumer<*>.() -> Any?).toFragment() = createHTML().also { builder ->
-       with(HtmlLexerTagConsumer(builder)) { this@toFragment() }
+fun (TagConsumer<*>.() -> Any?).toFragmentString() = createHTML().also { builder ->
+       with(HtmlLexerTagConsumer(builder)) { this@toFragmentString() }
 }.finalize()
 
 class HtmlLexerTagConsumer private constructor(private val downstream: TagConsumer<*>) : TagConsumer<Unit> {
@@ -64,11 +61,8 @@ class HtmlLexerTagConsumer private constructor(private val downstream: TagConsum
        }
 }
 
-context(C)
-operator fun <T, C : TagConsumer<T>> String.unaryPlus() = onTagContent(this)
-
-context(C)
-operator fun <T, C : TagConsumer<T>> Entities.unaryPlus() = onTagContentEntity(this)
+fun TagConsumer<*>.append(text: String) = onTagContent(text)
+fun TagConsumer<*>.append(entity: Entities) = onTagContentEntity(entity)
 
 fun <T, C : TagConsumer<T>> C.unsafe(block: Unsafe.() -> Unit) = onTagContentUnsafe(block)
 
@@ -102,7 +96,7 @@ fun ParserTree.toHtmlParagraph(env: LexerTagEnvironment<HtmlBuilderContext, Html
        null
 else if (isParagraph(HtmlLexerProcessor.inlineTags)) {
        val concat = HtmlLexerProcessor.combineInline(env, this)
-       ({ p { +concat } })
+       ({ p { append(concat) } })
 } else
        HtmlLexerProcessor.combineInline(env, this)
 
@@ -134,14 +128,14 @@ object HtmlLexerProcessor : LexerTagFallback<HtmlBuilderContext, HtmlBuilderSubj
                val content = env.processTree(subNodes)
                
                return {
-                       +if (param == null) "[$tag]" else "[$tag=$param]"
+                       append(if (param == null) "[$tag]" else "[$tag=$param]")
                        content()
-                       +"[/$tag]"
+                       append("[/$tag]")
                }
        }
        
        override fun processText(env: LexerTagEnvironment<HtmlBuilderContext, HtmlBuilderSubject>, text: String): HtmlBuilderSubject {
-               return { +text }
+               return { append(text) }
        }
        
        override fun processLineBreak(env: LexerTagEnvironment<HtmlBuilderContext, HtmlBuilderSubject>): HtmlBuilderSubject {
@@ -159,7 +153,7 @@ object HtmlLexerProcessor : LexerTagFallback<HtmlBuilderContext, HtmlBuilderSubj
                return if (nodes.shouldSplitSections()) {
                        val pageParts = nodes.splitSections().map { combineBlock(env, it) }
                        ({
-                               for (pagePart in pageParts) section { +pagePart }
+                               for (pagePart in pageParts) section { append(pagePart) }
                        })
                } else
                        combineBlock(env, nodes)
@@ -183,7 +177,7 @@ object HtmlLexerProcessor : LexerTagFallback<HtmlBuilderContext, HtmlBuilderSubj
                        })
                } else if (nodes.isParagraph(inlineTags)) {
                        val concat = combineInline(env, nodes)
-                       ({ p { +concat } })
+                       ({ p { append(concat) } })
                } else
                        combineInline(env, nodes)
        }
@@ -275,7 +269,7 @@ class HtmlTagLexerTag(
                        tagCreator {
                                for ((name, value) in calculatedAttributes)
                                        attributes[name] = value
-                               +body
+                               append(body)
                        }
                }
        }
@@ -497,7 +491,7 @@ enum class FactbookFormattingTag(val type: HtmlLexerTag) {
        MOMENT(HtmlTextBodyLexerTag { _, _, content ->
                val instant = content.toLongOrNull()?.let { Instant.ofEpochMilli(it) }
                if (instant == null)
-                       ({ +content })
+                       ({ append(content) })
                else
                        ({ dateTime(instant) })
        }),
@@ -551,7 +545,7 @@ enum class FactbookFormattingTag(val type: HtmlLexerTag) {
                        } else {
                                val foreign = content.treeToText()
                                ({
-                                       +foreign
+                                       append(foreign)
                                })
                        }
                }
@@ -719,7 +713,7 @@ enum class CommentFormattingTag(val type: HtmlLexerTag) {
                val id = sanitizeId(content)
                
                if (id == null)
-                       ({ +">>$content" })
+                       ({ append(">>$content") })
                else
                        ({
                                a(href = "/comment/view/$id") {
index ba8debd355cf3c652ee4211238b625f38c98f563..63f6d78078ddd92be31fda81ac0e2d25dab5f8b1 100644 (file)
@@ -2,9 +2,13 @@ package info.mechyrdia.lore
 
 import info.mechyrdia.JsonStorageCodec
 import info.mechyrdia.data.StoragePath
-import io.ktor.server.application.*
-import io.ktor.server.request.*
-import kotlinx.coroutines.*
+import io.ktor.server.application.ApplicationCall
+import io.ktor.server.request.path
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.joinAll
+import kotlinx.coroutines.launch
 import java.time.Instant
 import kotlin.math.roundToInt
 import java.util.function.Function as JFunction
@@ -43,9 +47,6 @@ class PreProcessorContext private constructor(
                private const val PAGE_PATH_KEY = "PAGE_PATH"
                private const val INSTANT_NOW_KEY = "INSTANT_NOW"
                
-               context(ApplicationCall)
-               fun defaults() = defaults(StoragePath(request.path()))
-               
                fun defaults(lorePath: StoragePath) = defaults(lorePath.elements.drop(1))
                
                fun defaults(lorePath: List<String>) = mapOf(
index a4309ab40cb880ae16d2135363ac0e297d1b3d34..e6b85d9a20b4d33203e6d222a7e113bf57b86a6e 100644 (file)
@@ -3,11 +3,18 @@ package info.mechyrdia.lore
 import info.mechyrdia.JsonStorageCodec
 import info.mechyrdia.data.FileStorage
 import info.mechyrdia.data.StoragePath
-import io.ktor.util.*
+import io.ktor.util.hex
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.runInterruptible
 import kotlinx.coroutines.suspendCancellableCoroutine
-import kotlinx.serialization.json.*
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonNull
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.booleanOrNull
+import kotlinx.serialization.json.double
+import kotlinx.serialization.json.intOrNull
 import java.security.MessageDigest
 import java.util.concurrent.ConcurrentHashMap
 import java.util.concurrent.locks.ReentrantLock
@@ -17,7 +24,11 @@ import javax.script.CompiledScript
 import javax.script.ScriptEngineManager
 import javax.script.SimpleBindings
 import kotlin.concurrent.withLock
-import kotlin.coroutines.*
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.startCoroutine
 
 object PreProcessorTemplateLoader {
        suspend fun loadTemplate(name: String): ParserTree {
index 2e6155534a9689595d1cc1d205e78c78d2535a65..a9e4d157675aea710c6b2aab0fabd27cb87ba975 100644 (file)
@@ -3,7 +3,11 @@ package info.mechyrdia.lore
 import info.mechyrdia.JsonStorageCodec
 import info.mechyrdia.data.FileStorage
 import info.mechyrdia.data.StoragePath
-import kotlinx.serialization.json.*
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonNull
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.JsonPrimitive
 
 fun JsonElement.toPreProcessTree(): ParserTree = when (this) {
        JsonNull -> emptyList()
index 5a7de206f7a12ccb9c23340fcb868ceb650eadef..72c2881c9da3dd24890a2bafe768882d0d971b90 100644 (file)
@@ -1,7 +1,24 @@
 package info.mechyrdia.lore
 
 import java.time.Instant
-import kotlin.math.*
+import kotlin.math.acos
+import kotlin.math.asin
+import kotlin.math.atan
+import kotlin.math.atan2
+import kotlin.math.cbrt
+import kotlin.math.ceil
+import kotlin.math.cos
+import kotlin.math.floor
+import kotlin.math.hypot
+import kotlin.math.log
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.math.pow
+import kotlin.math.round
+import kotlin.math.sin
+import kotlin.math.sqrt
+import kotlin.math.tan
+import kotlin.math.truncate
 
 fun <T : Number> ParserTree.treeToNumberOrNull(convert: String.() -> T?) = treeToText().convert()
 
index afdf692ffb54cc896dc39cd6bc4d07623f9ff303..28d9cde593d892582f4382798ed4faeb2b0e7de9 100644 (file)
@@ -1,6 +1,5 @@
 package info.mechyrdia.lore
 
-import io.ktor.util.*
 import kotlinx.html.*
 import java.time.Instant
 
@@ -20,7 +19,7 @@ private class HtmlDataFormatTag(val dataFormat: String) : HtmlLexerTag {
                return {
                        span {
                                attributes["data-format"] = dataFormat
-                               +content
+                               append(content)
                        }
                }
        }
@@ -44,7 +43,7 @@ enum class RawFactbookFormattingTag(val type: HtmlLexerTag) {
                ({
                        div {
                                attributes["data-format"] = "code"
-                               pre { +content }
+                               pre { append(content) }
                        }
                })
        }),
@@ -53,7 +52,7 @@ enum class RawFactbookFormattingTag(val type: HtmlLexerTag) {
                ({
                        div {
                                attributes["data-format"] = "error"
-                               +content
+                               append(content)
                        }
                })
        }),
@@ -71,7 +70,7 @@ enum class RawFactbookFormattingTag(val type: HtmlLexerTag) {
                ({
                        div {
                                alignment?.let { attributes["data-align"] = it }
-                               +content
+                               append(content)
                        }
                })
        }),
@@ -83,7 +82,7 @@ enum class RawFactbookFormattingTag(val type: HtmlLexerTag) {
                ({
                        div {
                                alignment?.let { attributes["data-aside"] = it }
-                               +content
+                               append(content)
                        }
                })
        }),
@@ -113,11 +112,8 @@ enum class RawFactbookFormattingTag(val type: HtmlLexerTag) {
        MODEL(HtmlNotSupportedInRawViewTag("Unfortunately, raw view does not support interactive 3D model views")),
        QUIZ(HtmlNotSupportedInRawViewTag("Unfortunately, raw view does not support interactive quizzes")),
        MOMENT(HtmlTextBodyLexerTag { _, _, content ->
-               val epochMilli = content.toLongOrNull()
-               if (epochMilli == null)
-                       ({ +content })
-               else
-                       ({ +Instant.ofEpochMilli(epochMilli).toString() })
+               val epochMilli = content.toLongOrNull()?.let { Instant.ofEpochMilli(it).toString() } ?: content
+               ({ append(epochMilli) })
        }),
        LINK(HtmlTagLexerTag(attributes = ::processRawInternalLink, tagMode = HtmlTagMode.INLINE, tagCreator = TagConsumer<*>::a.toTagCreator())),
        REDIRECT(HtmlTextBodyLexerTag { _, _, content ->
index 60a0b292ccf705d27cd3ddca29b2363d23e47780..73c9b57ed4ebc0aa87a760922138a262f9f2903c 100644 (file)
@@ -10,7 +10,7 @@ import info.mechyrdia.robot.RobotService
 import info.mechyrdia.robot.RobotServiceStatus
 import info.mechyrdia.route.Root
 import info.mechyrdia.route.href
-import io.ktor.server.application.*
+import io.ktor.server.application.ApplicationCall
 import kotlinx.html.*
 import kotlinx.serialization.Serializable
 import kotlinx.serialization.builtins.ListSerializer
@@ -21,7 +21,7 @@ import kotlin.collections.set
 @Serializable
 private data class ExternalLink(
        val url: String,
-       val text: String
+       val text: String,
 )
 
 suspend fun loadExternalLinks(): List<NavItem> {
@@ -39,7 +39,7 @@ suspend fun ApplicationCall.standardNavBar(path: List<String>? = null) = listOf(
 ) + path?.let { pathParts ->
        pathParts.indices.drop(1).map { i ->
                val subPath = pathParts.take(i)
-               NavLink(href(Root.LorePage(subPath)), (StoragePath.articleDir / subPath).toFriendlyPageTitle())
+               NavLink.ofArticleTitle(href(Root.LorePage(subPath)), (StoragePath.articleDir / subPath).toFriendlyPageTitle())
        }
 }.orEmpty() + (currentNation()?.let { data ->
        (if (RobotService.status == RobotServiceStatus.READY)
@@ -48,7 +48,7 @@ suspend fun ApplicationCall.standardNavBar(path: List<String>? = null) = listOf(
                NavHead(data.name),
                NavLink(href(Root.User()), "Your User Page"),
                NavLink("https://www.nationstates.net/nation=${data.id}", "Your NationStates Page"),
-               NavLink.withCsrf(href(Root.Auth.LogoutPost()), "Log Out"),
+               NavLink.withCsrf(href(Root.Auth.LogoutPost()), "Log Out", call = this),
        )
 } ?: listOf(
        NavHead("Log In"),
@@ -88,7 +88,7 @@ data class NavLink(
        val text: String,
        val textIsHtml: Boolean = false,
        val aClasses: String? = null,
-       val linkAttributes: Map<String, String> = emptyMap()
+       val linkAttributes: Map<String, String> = emptyMap(),
 ) : NavItem() {
        override fun DIV.display() {
                a(href = to, classes = aClasses) {
@@ -103,8 +103,7 @@ data class NavLink(
        }
        
        companion object {
-               context(ApplicationCall)
-               fun withCsrf(to: String, text: String, textIsHtml: Boolean = false, aClasses: String? = null, extraAttributes: Map<String, String> = emptyMap): NavLink {
+               fun withCsrf(to: String, text: String, textIsHtml: Boolean = false, aClasses: String? = null, extraAttributes: Map<String, String> = emptyMap, call: ApplicationCall): NavLink {
                        return NavLink(
                                to = to,
                                text = text,
@@ -112,9 +111,23 @@ data class NavLink(
                                aClasses = aClasses,
                                linkAttributes = extraAttributes + mapOf(
                                        "data-method" to "post",
-                                       "data-csrf-token" to createCsrfToken(to)
+                                       "data-csrf-token" to call.createCsrfToken(to)
                                )
                        )
                }
+               
+               fun ofArticleTitle(to: String, title: ArticleTitle, aClasses: String? = null, extraAttributes: Map<String, String> = emptyMap): NavLink {
+                       return NavLink(
+                               to = to,
+                               text = title.title,
+                               textIsHtml = false,
+                               aClasses = aClasses,
+                               linkAttributes = extraAttributes + if (title.css.isNotEmpty())
+                                       mapOf(
+                                               "style" to extraAttributes["style"]?.let { "$it;" }.orEmpty() + "font-style:italic"
+                                       )
+                               else emptyMap()
+                       )
+               }
        }
 }
index c0efafa222a686f6ca36a880f1ba11d7ced72c00..9d880da91148620f814a790b6b74a562cc1230d6 100644 (file)
@@ -1,8 +1,8 @@
 package info.mechyrdia.lore
 
 import info.mechyrdia.MainDomainName
-import io.ktor.server.application.*
-import io.ktor.server.request.*
+import io.ktor.server.application.ApplicationCall
+import io.ktor.server.request.path
 import kotlinx.html.HEAD
 import kotlinx.html.meta
 import kotlinx.serialization.Serializable
@@ -20,13 +20,12 @@ fun HEAD.ogProperty(property: String, content: String) {
        }
 }
 
-context(ApplicationCall)
-fun HEAD.renderOgData(title: String, data: OpenGraphData) {
+fun HEAD.renderOgData(title: String, data: OpenGraphData, call: ApplicationCall) {
        meta(name = "description", content = data.description)
        
        ogProperty("title", title)
        ogProperty("type", "website")
        ogProperty("description", data.description)
        ogProperty("image", data.image)
-       ogProperty("url", "$MainDomainName/${request.path().removePrefix("/")}")
+       ogProperty("url", "$MainDomainName/${call.request.path().removePrefix("/")}")
 }
index 60da713dd479cfb51b9ad7cf58e852d2ccffa006..fd5dc688851e85420effa40ebaab2cd1b42498a4 100644 (file)
@@ -1,13 +1,11 @@
 package info.mechyrdia.lore
 
-import info.mechyrdia.robot.toQueryString
-import info.mechyrdia.route.ErrorMessageAttributeKey
-import io.ktor.http.*
-import io.ktor.server.application.*
-import io.ktor.server.request.*
-import io.ktor.util.*
+import io.ktor.server.application.ApplicationCall
 import kotlinx.html.*
 import java.time.Instant
+import kotlin.collections.List
+import kotlin.collections.listOf
+import kotlin.collections.set
 
 private val preloadFonts = listOf(
        "DejaVuSans-Bold.woff",
@@ -33,15 +31,14 @@ private val preloadImages = listOf(
        "external-link.png",
 )
 
-context(ApplicationCall)
-private fun HEAD.initialHead(pageTitle: String, ogData: OpenGraphData?) {
+private fun HEAD.initialHead(pageTitle: String, ogData: OpenGraphData?, call: ApplicationCall) {
        meta(charset = "utf-8")
        meta(name = "viewport", content = "width=device-width, initial-scale=1.0")
        
        meta(name = "theme-color", content = "#FFCC33")
        
        ogData?.let { data ->
-               renderOgData(pageTitle, data)
+               renderOgData(pageTitle, data, call = call)
        }
        
        link(rel = "icon", type = "image/png", href = "/static/images/icon.png")
@@ -59,7 +56,7 @@ fun ApplicationCall.page(pageTitle: String, navBar: List<NavItem>? = null, sideb
                lang = "en"
                
                head {
-                       initialHead(pageTitle, ogData)
+                       initialHead(pageTitle, ogData, call = this@page)
                        
                        for (font in preloadFonts)
                                link(
@@ -134,7 +131,7 @@ fun ApplicationCall.rawPage(pageTitle: String, ogData: OpenGraphData? = null, co
                lang = "en"
                
                head {
-                       initialHead(pageTitle, ogData)
+                       initialHead(pageTitle, ogData, call = this@rawPage)
                        
                        link(rel = "stylesheet", type = "text/css", href = "/static/raw.css")
                }
@@ -156,7 +153,7 @@ fun ApplicationCall.adminPage(pageTitle: String, content: BODY.() -> Unit): HTML
                lang = "en"
                
                head {
-                       initialHead(pageTitle, null)
+                       initialHead(pageTitle, null, call = this@adminPage)
                        
                        for (font in adminPreloadFonts)
                                link(
index 7f9ebfbb9a1d57ea76bdc7c8da982c698a283483..f649fee0175d35835e8ac7200922458ecee936e8 100644 (file)
@@ -5,9 +5,10 @@ import info.mechyrdia.data.currentNation
 import info.mechyrdia.route.CsrfProtectedResourcePayload
 import info.mechyrdia.route.Root
 import info.mechyrdia.route.href
-import io.ktor.http.*
-import io.ktor.server.application.*
-import io.ktor.server.request.*
+import io.ktor.http.HttpHeaders
+import io.ktor.server.application.ApplicationCall
+import io.ktor.server.request.header
+import io.ktor.server.request.uri
 import kotlinx.html.*
 
 suspend fun ApplicationCall.errorPage(title: String, body: FlowContent.() -> Unit): HTML.() -> Unit {
index 699bc1122e24e0a706c4b35db0cf792cbc5cac11..0ac31c11fb671c326ace9b47bbe10eb6497c34d0 100644 (file)
@@ -1,11 +1,22 @@
 package info.mechyrdia.lore
 
 import info.mechyrdia.JsonFileCodec
-import info.mechyrdia.data.*
+import info.mechyrdia.data.Comment
+import info.mechyrdia.data.CommentRenderData
+import info.mechyrdia.data.FileStorage
+import info.mechyrdia.data.NationData
+import info.mechyrdia.data.PageVisitTotals
+import info.mechyrdia.data.StoragePath
+import info.mechyrdia.data.commentBox
+import info.mechyrdia.data.commentInput
+import info.mechyrdia.data.currentNation
+import info.mechyrdia.data.guestbook
+import info.mechyrdia.data.nationCache
+import info.mechyrdia.data.processGuestbook
 import info.mechyrdia.route.KeyedEnumSerializer
 import info.mechyrdia.route.Root
 import info.mechyrdia.route.href
-import io.ktor.server.application.*
+import io.ktor.server.application.ApplicationCall
 import kotlinx.coroutines.async
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.flow.toList
@@ -43,10 +54,12 @@ private val Tag.breadCrumbArrow: Unit
                +Entities.nbsp
        }
 
-context(ApplicationCall)
-private fun FlowContent.breadCrumbs(links: List<Pair<Root.LorePage, String>>) = p {
-       links.joinToHtml(Tag::breadCrumbArrow) { (url, text) ->
-               a(href = href(url)) { +text }
+private fun FlowContent.breadCrumbs(links: List<Pair<Root.LorePage, ArticleTitle>>, call: ApplicationCall) = p {
+       joined(links, Tag::breadCrumbArrow) { (url, articleTitle) ->
+               a(href = call.href(url)) {
+                       style = articleTitle.css
+                       +articleTitle.title
+               }
        }
 }
 
@@ -76,11 +89,14 @@ suspend fun ApplicationCall.loreRawArticlePage(pagePath: List<String>): HTML.()
        
        if (isValid) {
                if (pageNode.subNodes != null) {
-                       return rawPage(pageNode.title) {
-                               breadCrumbs(parentPaths)
-                               h1 { +pageNode.title }
+                       return rawPage(pageNode.title.title) {
+                               breadCrumbs(parentPaths, call = this@loreRawArticlePage)
+                               h1 {
+                                       style = pageNode.title.css
+                                       +pageNode.title.title
+                               }
                                ul {
-                                       pageNode.subNodes.renderInto(this, pagePath, LoreArticleFormat.RAW_HTML)
+                                       pageNode.subNodes.renderInto(this, pagePath, LoreArticleFormat.RAW_HTML, call = this@loreRawArticlePage)
                                }
                        }
                }
@@ -93,15 +109,18 @@ suspend fun ApplicationCall.loreRawArticlePage(pagePath: List<String>): HTML.()
                        pageMarkup.buildToC(pageToC)
                        
                        return rawPage(pageToC.toPageTitle(), pageToC.toOpenGraph()) {
-                               breadCrumbs(parentPaths)
-                               +pageHtml
+                               breadCrumbs(parentPaths, call = this@loreRawArticlePage)
+                               append(pageHtml)
                        }
                }
        }
        
-       return rawPage(pageNode.title) {
-               breadCrumbs(parentPaths)
-               h1 { +pageNode.title }
+       return rawPage(pageNode.title.title) {
+               breadCrumbs(parentPaths, call = this@loreRawArticlePage)
+               h1 {
+                       style = pageNode.title.css
+                       +pageNode.title.title
+               }
                p {
                        +"This factbook does not exist. Would you like to "
                        a(href = href(Root.LorePage(emptyList(), LoreArticleFormat.RAW_HTML))) { +"return to the table of contents" }
@@ -136,21 +155,24 @@ suspend fun ApplicationCall.loreArticlePage(pagePath: List<String>, format: Lore
                        
                        val sidebar = PageNavSidebar(
                                listOf(
-                                       NavLink("#page-top", pageNode.title, aClasses = "left"),
+                                       NavLink.ofArticleTitle("#page-top", pageNode.title, aClasses = "left"),
                                        NavLink("#comments", "Comments", aClasses = "left")
                                )
                        )
                        
-                       return page(pageNode.title, navbar, sidebar) {
+                       return page(pageNode.title.title, navbar, sidebar) {
                                section {
                                        a { id = "page-top" }
-                                       h1 { +pageNode.title }
+                                       h1 {
+                                               style = pageNode.title.css
+                                               +pageNode.title.title
+                                       }
                                        ul {
-                                               pageNode.subNodes.renderInto(this, pagePath, format = format)
+                                               pageNode.subNodes.renderInto(this, pagePath, format = format, call = this@loreArticlePage)
                                        }
                                }
                                
-                               finalSection(pagePath, canCommentAs, comments, totalsData)
+                               finalSection(pagePath, canCommentAs, comments, totalsData, call = this@loreArticlePage)
                        }
                }
                
@@ -167,9 +189,9 @@ suspend fun ApplicationCall.loreArticlePage(pagePath: List<String>, format: Lore
                        val sidebar = PageNavSidebar(pageNav)
                        
                        return page(pageToC.toPageTitle(), navbar, sidebar, pageToC.toOpenGraph()) {
-                               +pageHtml
+                               append(pageHtml)
                                
-                               finalSection(pagePath, canCommentAs, comments, totalsData)
+                               finalSection(pagePath, canCommentAs, comments, totalsData, call = this@loreArticlePage)
                        }
                }
        }
@@ -178,15 +200,18 @@ suspend fun ApplicationCall.loreArticlePage(pagePath: List<String>, format: Lore
        val navbar = standardNavBar(pagePath)
        val sidebar = PageNavSidebar(
                listOf(
-                       NavLink("#page-top", title, aClasses = "left"),
+                       NavLink.ofArticleTitle("#page-top", title, aClasses = "left"),
                        NavLink("#comments", "Comments", aClasses = "left")
                )
        )
        
-       return page(title, navbar, sidebar) {
+       return page(title.title, navbar, sidebar) {
                section {
                        a { id = "page-top" }
-                       h1 { +pageNode.title }
+                       h1 {
+                               style = pageNode.title.css
+                               +pageNode.title.title
+                       }
                        p {
                                +"This factbook does not exist. Would you like to "
                                a(href = href(Root())) { +"return to the index page" }
@@ -194,20 +219,19 @@ suspend fun ApplicationCall.loreArticlePage(pagePath: List<String>, format: Lore
                        }
                }
                
-               finalSection(pagePath, canCommentAs, comments, totalsData)
+               finalSection(pagePath, canCommentAs, comments, totalsData, call = this@loreArticlePage)
        }
 }
 
-context(ApplicationCall)
-private fun SectioningOrFlowContent.finalSection(pagePathParts: List<String>, canCommentAs: NationData?, comments: List<CommentRenderData>, totalsData: PageVisitTotals) {
+private fun SectioningOrFlowContent.finalSection(pagePathParts: List<String>, canCommentAs: NationData?, comments: List<CommentRenderData>, totalsData: PageVisitTotals, call: ApplicationCall) {
        section {
                h2 {
                        a { id = "comments" }
                        +"Comments"
                }
-               commentInput(pagePathParts, canCommentAs)
+               commentInput(pagePathParts, canCommentAs, call = call)
                for (comment in comments)
-                       commentBox(comment, canCommentAs?.id)
+                       commentBox(comment, canCommentAs?.id, call = call)
                
                guestbook(totalsData)
        }
index f2d6a61954bc5866510395a037ca37681ae34354..22acc69db33356e4bac6cdf7d562f9e28c97a290 100644 (file)
@@ -2,7 +2,7 @@ package info.mechyrdia.lore
 
 import info.mechyrdia.auth.PageDoNotCacheAttributeKey
 import info.mechyrdia.route.KeyedEnumSerializer
-import io.ktor.server.application.*
+import io.ktor.server.application.ApplicationCall
 import kotlinx.html.*
 import kotlinx.serialization.Serializable
 import kotlinx.serialization.serializer
@@ -50,32 +50,22 @@ val ApplicationCall.april1stMode: April1stMode
                April1stMode.entries.firstOrNull { mode -> mode.name.equals(modeCookie, ignoreCase = true) }
        } ?: April1stMode.DEFAULT
 
-class JoinToHtmlConsumer<E>(val iterator: Iterator<E>) {
-       inline fun <T : Tag> T.invokeReceiver(separator: T.() -> Unit, body: T.(E) -> Unit) {
-               var isFirst = true
-               for (item in iterator) {
-                       if (isFirst)
-                               isFirst = false
-                       else
-                               separator()
-                       body(item)
-               }
-       }
-       
-       context(T)
-       inline operator fun <T : Tag> invoke(separator: T.() -> Unit, body: T.(E) -> Unit) {
-               invokeReceiver(separator, body)
+fun <T : Tag, E> T.joined(iterable: Iterable<E>, separator: T.() -> Unit, body: T.(E) -> Unit) {
+       var isFirst = true
+       for (item in iterable) {
+               if (isFirst)
+                       isFirst = false
+               else
+                       separator()
+               body(item)
        }
 }
 
-val <E> Iterable<E>.joinToHtml: JoinToHtmlConsumer<E>
-       get() = JoinToHtmlConsumer(iterator())
-
 inline fun <reified E : Enum<E>> FlowOrInteractiveOrPhrasingContent.preference(inputName: String, current: E, crossinline localize: (E) -> String) {
        val serializer = serializer<E>() as? KeyedEnumSerializer<E> ?: throw UnsupportedOperationException("Serializer for ${E::class.simpleName} has not been declared as KeyedEnumSerializer")
        val entries = serializer.entries
        
-       entries.joinToHtml(Tag::br) { option ->
+       joined(entries, Tag::br) { option ->
                label {
                        radioInput(name = inputName, classes = "pref-$inputName") {
                                value = serializer.getKey(option) ?: "null"
index 52aacacadcc12df1dd3c3cb6d41347ed4538e527..ce0c53bb12c5a17b55b698afa08a7fca0d32b4bb 100644 (file)
@@ -2,12 +2,18 @@ package info.mechyrdia.lore
 
 import info.mechyrdia.JsonFileCodec
 import info.mechyrdia.MainDomainName
-import info.mechyrdia.data.*
+import info.mechyrdia.data.FileStorage
+import info.mechyrdia.data.StoragePath
+import info.mechyrdia.data.XmlTagConsumer
+import info.mechyrdia.data.declaration
+import info.mechyrdia.data.respondXml
+import info.mechyrdia.data.root
 import info.mechyrdia.route.KeyedEnumSerializer
-import io.ktor.http.*
-import io.ktor.server.application.*
-import io.ktor.server.html.*
-import io.ktor.server.response.*
+import io.ktor.http.ContentType
+import io.ktor.http.HttpStatusCode
+import io.ktor.server.application.ApplicationCall
+import io.ktor.server.html.respondHtml
+import io.ktor.server.response.respondText
 import kotlinx.html.*
 import kotlinx.serialization.Serializable
 import kotlinx.serialization.builtins.ListSerializer
@@ -48,12 +54,12 @@ suspend fun randomQuote(): Quote = getQuotesList().random()
 enum class QuoteFormat(val format: String?) {
        HTML(null) {
                override suspend fun ApplicationCall.respondQuote(quote: Quote) {
-                       respondHtml(HttpStatusCode.OK, block = quote.toHtml(RANDOM_QUOTE_HTML_TITLE))
+                       respondHtml(HttpStatusCode.OK, block = quote.toHtml(RANDOM_QUOTE_HTML_TITLE, this))
                }
        },
        RAW_HTML("raw") {
                override suspend fun ApplicationCall.respondQuote(quote: Quote) {
-                       respondHtml(HttpStatusCode.OK, block = quote.toRawHtml(RANDOM_QUOTE_HTML_TITLE))
+                       respondHtml(HttpStatusCode.OK, block = quote.toRawHtml(RANDOM_QUOTE_HTML_TITLE, this))
                }
        },
        JSON("json") {
@@ -92,34 +98,31 @@ fun Quote.toJson(): String {
        }.toString()
 }
 
-context(Quote)
-private fun FlowContent.quoteWithAttribution(pageTitle: String) {
+private fun FlowContent.quoteWithAttribution(quote: Quote, pageTitle: String) {
        h1 { +pageTitle }
        blockQuote {
-               +quote
+               +quote.quote
        }
        p {
                style = "align:right"
                unsafe { raw("&#x2015;") }
                +Entities.nbsp
-               a(href = fullLink) { +author }
+               a(href = quote.fullLink) { +quote.author }
        }
 }
 
-context(ApplicationCall)
-suspend fun Quote.toHtml(title: String): HTML.() -> Unit {
-       return page(title, standardNavBar(), QuoteOriginSidebar(author, fullPortrait, fullLink)) {
+suspend fun Quote.toHtml(title: String, call: ApplicationCall): HTML.() -> Unit {
+       return call.page(title, call.standardNavBar(), QuoteOriginSidebar(author, fullPortrait, fullLink)) {
                section {
                        a { id = "page-top" }
-                       quoteWithAttribution(title)
+                       quoteWithAttribution(this@toHtml, title)
                }
        }
 }
 
-context(ApplicationCall)
-fun Quote.toRawHtml(title: String): HTML.() -> Unit {
-       return rawPage(title) {
-               quoteWithAttribution(title)
+fun Quote.toRawHtml(title: String, call: ApplicationCall): HTML.() -> Unit {
+       return call.rawPage(title) {
+               quoteWithAttribution(this@toRawHtml, title)
                p {
                        style = "align:center"
                        a(href = fullLink) {
index 587513e23dedb913445344bf95355808697a7b04..8d9f322ee21791ad7a9e177a735947b2a0f7d945 100644 (file)
@@ -1,7 +1,13 @@
 package info.mechyrdia.lore
 
 import info.mechyrdia.MainDomainName
-import info.mechyrdia.data.*
+import info.mechyrdia.data.FileStorage
+import info.mechyrdia.data.StoragePath
+import info.mechyrdia.data.XmlInsertable
+import info.mechyrdia.data.XmlTag
+import info.mechyrdia.data.XmlTagConsumer
+import info.mechyrdia.data.declaration
+import info.mechyrdia.data.root
 import java.time.Instant
 import java.time.ZoneId
 import java.time.format.DateTimeFormatter
@@ -36,14 +42,15 @@ data class SitemapEntry(
        val lastModified: Instant,
        val changeFrequency: SitemapChangeFrequency,
        val priority: Double,
-)
-
-context(XmlTag)
-operator fun SitemapEntry.unaryPlus() = "url" {
-       "loc" { +loc }
-       "lastmod" { +lastModified.xmlValue }
-       "changefreq" { +changeFrequency.xmlValue }
-       "priority" { +priority.xmlValue }
+) : XmlInsertable {
+       override fun XmlTag.intoXml() {
+               "url" {
+                       "loc" { +loc }
+                       "lastmod" { +lastModified.xmlValue }
+                       "changefreq" { +changeFrequency.xmlValue }
+                       "priority" { +priority.xmlValue }
+               }
+       }
 }
 
 private suspend fun buildIntroSitemap(): SitemapEntry? {
index 0e5184b7666bc2c4bb9709e85e233db94e27c865..503c61c808809cc9d4f1c5dc5693334302db015b 100644 (file)
@@ -3,9 +3,25 @@ package info.mechyrdia.lore
 import com.mongodb.client.model.Sorts
 import info.mechyrdia.MainDomainName
 import info.mechyrdia.OwnerNationId
-import info.mechyrdia.data.*
-import io.ktor.http.*
-import io.ktor.server.application.*
+import info.mechyrdia.data.Comment
+import info.mechyrdia.data.CommentRenderData
+import info.mechyrdia.data.FileStorage
+import info.mechyrdia.data.StoragePath
+import info.mechyrdia.data.StoredFileStats
+import info.mechyrdia.data.XmlInsertable
+import info.mechyrdia.data.XmlTag
+import info.mechyrdia.data.XmlTagConsumer
+import info.mechyrdia.data.currentNation
+import info.mechyrdia.data.declaration
+import info.mechyrdia.data.getNation
+import info.mechyrdia.data.nationCache
+import info.mechyrdia.data.respondXml
+import info.mechyrdia.data.root
+import info.mechyrdia.data.serialName
+import io.ktor.http.ContentType
+import io.ktor.http.HttpStatusCode
+import io.ktor.http.defaultForFileExtension
+import io.ktor.server.application.ApplicationCall
 import kotlinx.coroutines.flow.filterNot
 import kotlinx.coroutines.flow.take
 import kotlinx.coroutines.flow.toList
@@ -167,25 +183,27 @@ suspend fun ApplicationCall.recentCommentsRssFeedGenerator(limit: Int): RssChann
 data class RssCategory(
        val category: String,
        val domain: String? = null
-)
-
-context(XmlTag)
-operator fun RssCategory.unaryPlus() = "category" {
-       if (domain != null) attributes["domain"] = domain
-       +category
+) : XmlInsertable {
+       override fun XmlTag.intoXml() {
+               "category" {
+                       if (domain != null) attributes["domain"] = domain
+                       +category
+               }
+       }
 }
 
 data class RssChannelImage(
        val url: String,
        val title: String,
        val link: String,
-)
-
-context(XmlTag)
-operator fun RssChannelImage.unaryPlus() = "image" {
-       "url" { +url }
-       "title" { +title }
-       "link" { +link }
+) : XmlInsertable {
+       override fun XmlTag.intoXml() {
+               "image" {
+                       "url" { +url }
+                       "title" { +title }
+                       "link" { +link }
+               }
+       }
 }
 
 const val DEFAULT_RSS_COPYRIGHT = "Copyright 2022 Lanius Trolling"
@@ -209,47 +227,52 @@ data class RssChannel(
        val image: RssChannelImage? = null,
        val categories: List<RssCategory> = emptyList(),
        val items: List<RssItem> = emptyList(),
-)
-
-fun <T, C : XmlTagConsumer<T>> C.rss(rssChannel: RssChannel) = declaration()
-       .root("rss") {
-               attributes["version"] = "2.0"
+): XmlInsertable {
+       override fun XmlTag.intoXml() {
                "channel" {
-                       "title" { +rssChannel.title }
-                       "link" { +rssChannel.link }
-                       "description" { +rssChannel.description }
+                       "title" { +title }
+                       "link" { +link }
+                       "description" { +description }
                        
-                       if (rssChannel.language != null) "language" { +rssChannel.language }
-                       if (rssChannel.copyright != null) "copyright" { +rssChannel.copyright }
-                       if (rssChannel.managingEditor != null) "managingEditor" { +rssChannel.managingEditor }
-                       if (rssChannel.webMaster != null) "webMaster" { +rssChannel.webMaster }
-                       if (rssChannel.pubDate != null) "pubDate" { +rssChannel.pubDate.rssValue }
-                       if (rssChannel.lastBuildDate != null) "lastBuildDate" { +rssChannel.lastBuildDate.rssValue }
-                       if (rssChannel.ttl != null) "ttl" { +rssChannel.ttl.toString() }
+                       if (language != null) "language" { +language }
+                       if (copyright != null) "copyright" { +copyright }
+                       if (managingEditor != null) "managingEditor" { +managingEditor }
+                       if (webMaster != null) "webMaster" { +webMaster }
+                       if (pubDate != null) "pubDate" { +pubDate.rssValue }
+                       if (lastBuildDate != null) "lastBuildDate" { +lastBuildDate.rssValue }
+                       if (ttl != null) "ttl" { +ttl.toString() }
                        
-                       if (rssChannel.image != null) +rssChannel.image
+                       if (image != null) +image
                        
-                       for (category in rssChannel.categories)
+                       for (category in categories)
                                +category
-                       for (item in rssChannel.items)
+                       for (item in items)
                                +item
                }
        }
+}
+
+fun <T, C : XmlTagConsumer<T>> C.rss(rssChannel: RssChannel) = declaration()
+       .root("rss") {
+               attributes["version"] = "2.0"
+               +rssChannel
+       }
 
 data class RssItemEnclosure(
        val url: String,
        val length: Long,
        val type: String,
-)
-
-context(XmlTag)
-operator fun RssItemEnclosure.unaryPlus() = "enclosure"(
-       attributes = mapOf(
-               "url" to url,
-               "length" to length.toString(),
-               "type" to type,
-       )
-)
+) : XmlInsertable {
+       override fun XmlTag.intoXml() {
+               "enclosure"(
+                       attributes = mapOf(
+                               "url" to url,
+                               "length" to length.toString(),
+                               "type" to type,
+                       )
+               )
+       }
+}
 
 data class RssItem(
        val title: String? = null,
@@ -260,22 +283,23 @@ data class RssItem(
        val enclosure: RssItemEnclosure? = null,
        val pubDate: Instant? = null,
        val categories: List<RssCategory> = emptyList(),
-) {
+) : XmlInsertable {
        init {
                require(title != null || description != null) { "Either title or description must be provided, got null for both" }
        }
-}
-
-context(XmlTag)
-operator fun RssItem.unaryPlus() = "item" {
-       if (title != null) "title" { +title }
-       if (description != null) "description" { +description }
-       if (link != null) "link" { +link }
-       if (author != null) "author" { +author }
-       if (comments != null) "comments" { +comments }
-       if (enclosure != null) +enclosure
-       if (pubDate != null) "pubDate" { +pubDate.rssValue }
        
-       for (category in categories)
-               +category
+       override fun XmlTag.intoXml() {
+               "item" {
+                       if (title != null) "title" { +title }
+                       if (description != null) "description" { +description }
+                       if (link != null) "link" { +link }
+                       if (author != null) "author" { +author }
+                       if (comments != null) "comments" { +comments }
+                       if (enclosure != null) +enclosure
+                       if (pubDate != null) "pubDate" { +pubDate.rssValue }
+                       
+                       for (category in categories)
+                               +category
+               }
+       }
 }
index 5ca4e0b1ae34fd45361f1216f4511bb133aa0abe..d753e4d6141f8868cae918898023235d373caf7d 100644 (file)
@@ -1,10 +1,17 @@
 package info.mechyrdia.robot
 
-import io.ktor.client.*
-import io.ktor.client.call.*
-import io.ktor.client.request.*
-import io.ktor.client.request.forms.*
-import io.ktor.http.*
+import io.ktor.client.HttpClient
+import io.ktor.client.call.body
+import io.ktor.client.request.HttpRequestBuilder
+import io.ktor.client.request.delete
+import io.ktor.client.request.forms.formData
+import io.ktor.client.request.forms.submitFormWithBinaryData
+import io.ktor.client.request.get
+import io.ktor.client.request.post
+import io.ktor.client.request.setBody
+import io.ktor.http.ContentType
+import io.ktor.http.contentType
+import io.ktor.http.parameters
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
 
index b67af685c7b7d9d580aa92063e9ffbb4ecbebbf8..a9895116ef3dcb29a0db5ddc646d49fa63de8a50 100644 (file)
@@ -1,6 +1,7 @@
 package info.mechyrdia.robot
 
-import io.ktor.http.*
+import io.ktor.http.Parameters
+import io.ktor.http.formUrlEncode
 import kotlinx.serialization.ExperimentalSerializationApi
 import kotlinx.serialization.json.Json
 
index f274ae6465450ed07302060f77f332632253db83..6b3ad8afa86b48b50e9b5adaa64418a19c5f1079 100644 (file)
@@ -1,7 +1,10 @@
 package info.mechyrdia.robot
 
-import io.ktor.client.request.forms.*
-import io.ktor.http.*
+import io.ktor.client.request.forms.FormBuilder
+import io.ktor.http.ContentType
+import io.ktor.http.Headers
+import io.ktor.http.HttpHeaders
+import io.ktor.http.append
 
 class FileUpload(
        val content: ByteArray,
index e5f581229ead225b19f607e9ae3aa5398997b434..91d705f3c5303153e8dda3e21341b76527b58cf1 100644 (file)
@@ -2,8 +2,9 @@ package info.mechyrdia.robot
 
 import com.aallam.ktoken.Encoding
 import com.aallam.ktoken.Tokenizer
-import io.ktor.client.plugins.api.*
-import io.ktor.util.*
+import io.ktor.client.plugins.api.createClientPlugin
+import io.ktor.util.AttributeKey
+import io.ktor.util.Attributes
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
index 830bf15df1f4d7624123a01b5ae9aa8ee44890c3..bcbb5255803c9a718418848cf3d8d583a14e7917 100644 (file)
@@ -3,26 +3,75 @@ package info.mechyrdia.robot
 import info.mechyrdia.Configuration\r
 import info.mechyrdia.MainDomainName\r
 import info.mechyrdia.OpenAiConfig\r
-import info.mechyrdia.data.*\r
+import info.mechyrdia.data.DataDocument\r
+import info.mechyrdia.data.DocumentTable\r
+import info.mechyrdia.data.Id\r
+import info.mechyrdia.data.InstantNullableSerializer\r
+import info.mechyrdia.data.MONGODB_ID_KEY\r
+import info.mechyrdia.data.NationData\r
+import info.mechyrdia.data.TableHolder\r
 import info.mechyrdia.lore.RobotFactbookLoader\r
-import io.ktor.client.*\r
-import io.ktor.client.engine.java.*\r
-import io.ktor.client.plugins.*\r
-import io.ktor.client.plugins.contentnegotiation.*\r
-import io.ktor.client.plugins.logging.*\r
-import io.ktor.client.request.*\r
-import io.ktor.http.*\r
-import io.ktor.serialization.kotlinx.json.*\r
-import kotlinx.coroutines.*\r
-import kotlinx.coroutines.flow.*\r
+import io.ktor.client.HttpClient\r
+import io.ktor.client.engine.java.Java\r
+import io.ktor.client.plugins.ClientRequestException\r
+import io.ktor.client.plugins.HttpRequestRetry\r
+import io.ktor.client.plugins.contentnegotiation.ContentNegotiation\r
+import io.ktor.client.plugins.defaultRequest\r
+import io.ktor.client.plugins.logging.LogLevel\r
+import io.ktor.client.plugins.logging.Logging\r
+import io.ktor.client.request.header\r
+import io.ktor.http.ContentType\r
+import io.ktor.http.HttpHeaders\r
+import io.ktor.http.withCharset\r
+import io.ktor.serialization.kotlinx.json.json\r
+import kotlinx.coroutines.CoroutineName\r
+import kotlinx.coroutines.CoroutineScope\r
+import kotlinx.coroutines.Deferred\r
+import kotlinx.coroutines.Job\r
+import kotlinx.coroutines.SupervisorJob\r
+import kotlinx.coroutines.async\r
+import kotlinx.coroutines.awaitAll\r
+import kotlinx.coroutines.currentCoroutineContext\r
+import kotlinx.coroutines.delay\r
+import kotlinx.coroutines.flow.Flow\r
+import kotlinx.coroutines.flow.filter\r
+import kotlinx.coroutines.flow.flow\r
+import kotlinx.coroutines.flow.map\r
+import kotlinx.coroutines.flow.mapNotNull\r
+import kotlinx.coroutines.flow.onCompletion\r
+import kotlinx.coroutines.flow.onEach\r
+import kotlinx.coroutines.job\r
+import kotlinx.coroutines.launch\r
 import kotlinx.serialization.SerialName\r
 import kotlinx.serialization.Serializable\r
 import org.slf4j.Logger\r
 import org.slf4j.LoggerFactory\r
 import java.time.Instant\r
+import kotlin.collections.List\r
+import kotlin.collections.Map\r
+import kotlin.collections.Set\r
+import kotlin.collections.buildMap\r
+import kotlin.collections.component1\r
+import kotlin.collections.component2\r
+import kotlin.collections.emptyMap\r
+import kotlin.collections.emptySet\r
+import kotlin.collections.flatMap\r
+import kotlin.collections.fold\r
+import kotlin.collections.forEach\r
+import kotlin.collections.iterator\r
+import kotlin.collections.joinToString\r
+import kotlin.collections.listOf\r
+import kotlin.collections.map\r
+import kotlin.collections.minus\r
+import kotlin.collections.mutableListOf\r
+import kotlin.collections.plus\r
+import kotlin.collections.set\r
+import kotlin.collections.toList\r
 import kotlin.random.Random\r
 import kotlin.time.Duration.Companion.minutes\r
 \r
+private val RobotServiceLogger: Logger = LoggerFactory.getLogger("info.mechyrdia.robot.RobotServiceKt")\r
+\r
 val RobotGlobalsId = Id<RobotGlobals>("RobotGlobalsInstance")\r
 \r
 @Serializable\r
@@ -111,7 +160,7 @@ class RobotService(
                try {\r
                        robotClient.deleteThread(threadId)\r
                } catch (ex: ClientRequestException) {\r
-                       logger.warn("Unable to delete thread at ID $threadId", ex)\r
+                       RobotServiceLogger.warn("Unable to delete thread at ID $threadId", ex)\r
                }\r
                (RobotGlobals.get() ?: RobotGlobals()).minusThread(threadId).save()\r
        }\r
@@ -121,7 +170,7 @@ class RobotService(
                        try {\r
                                robotClient.deleteThread(threadId)\r
                        } catch (ex: ClientRequestException) {\r
-                               logger.warn("Unable to delete thread at ID $threadId", ex)\r
+                               RobotServiceLogger.warn("Unable to delete thread at ID $threadId", ex)\r
                        }\r
                return copy(ongoingThreadIds = emptySet())\r
        }\r
@@ -141,7 +190,7 @@ class RobotService(
                                        try {\r
                                                robotClient.deleteFile(oldId)\r
                                        } catch (ex: ClientRequestException) {\r
-                                               logger.warn("Unable to delete file $name at ID $oldId", ex)\r
+                                               RobotServiceLogger.warn("Unable to delete file $name at ID $oldId", ex)\r
                                        }\r
                                }\r
                                \r
@@ -157,7 +206,7 @@ class RobotService(
                                this[name] = newId\r
                                onNewFileId?.invoke(newId)\r
                                \r
-                               logger.info("Factbook $name has been uploaded")\r
+                               RobotServiceLogger.info("Factbook $name has been uploaded")\r
                        }\r
                }\r
                \r
@@ -176,13 +225,13 @@ class RobotService(
                        robotGlobals = robotGlobals.copy(vectorStoreId = vsId).save()\r
                }\r
                \r
-               logger.info("Vector store has been created")\r
+               RobotServiceLogger.info("Vector store has been created")\r
                \r
                poll {\r
                        robotClient.getVectorStore(vectorStoreId).status == "completed"\r
                }\r
                \r
-               logger.info("Vector store creation is complete")\r
+               RobotServiceLogger.info("Vector store creation is complete")\r
                \r
                if (robotGlobals.assistantId == null)\r
                        robotGlobals = robotGlobals.copy(\r
@@ -204,7 +253,17 @@ class RobotService(
                                ).id\r
                        ).save()\r
                \r
-               logger.info("Assistant has been created")\r
+               RobotServiceLogger.info("Assistant creation is complete")\r
+               \r
+               maintenanceScope.launch {\r
+                       while (true) {\r
+                               delay(30.minutes)\r
+                               \r
+                               launch(SupervisorJob(currentCoroutineContext().job)) {\r
+                                       performMaintenance()\r
+                               }\r
+                       }\r
+               }\r
        }\r
        \r
        suspend fun performMaintenance() {\r
@@ -223,13 +282,13 @@ class RobotService(
                        robotClient.addFileToVectorStore(vectorStoreId, fileId)\r
                }\r
                \r
-               logger.info("Vector store has been updated")\r
+               RobotServiceLogger.info("Vector store has been updated")\r
                \r
                poll {\r
                        robotClient.getVectorStore(vectorStoreId).fileCounts.inProgress == 0\r
                }\r
                \r
-               logger.info("Vector store update is complete")\r
+               RobotServiceLogger.info("Vector store update is complete")\r
        }\r
        \r
        suspend fun reset() {\r
@@ -248,7 +307,7 @@ class RobotService(
                                try {\r
                                        robotClient.deleteAssistant(it)\r
                                } catch (ex: ClientRequestException) {\r
-                                       logger.warn("Unable to delete assistant at ID $it", ex)\r
+                                       RobotServiceLogger.warn("Unable to delete assistant at ID $it", ex)\r
                                }\r
                        }\r
                }\r
@@ -261,7 +320,7 @@ class RobotService(
                                try {\r
                                        robotClient.deleteVectorStore(it)\r
                                } catch (ex: ClientRequestException) {\r
-                                       logger.warn("Unable to delete vector-store at ID $it", ex)\r
+                                       RobotServiceLogger.warn("Unable to delete vector-store at ID $it", ex)\r
                                }\r
                        }\r
                }\r
@@ -274,7 +333,7 @@ class RobotService(
                                try {\r
                                        robotClient.deleteFile(it)\r
                                } catch (ex: ClientRequestException) {\r
-                                       logger.warn("Unable to delete file at ID $it", ex)\r
+                                       RobotServiceLogger.warn("Unable to delete file at ID $it", ex)\r
                                }\r
                        }\r
                }\r
@@ -355,19 +414,29 @@ class RobotService(
        }\r
        \r
        companion object {\r
-               private val logger: Logger = LoggerFactory.getLogger(RobotService::class.java)\r
-               \r
                private val maintenanceScope = CoroutineScope(SupervisorJob() + CoroutineName("robot-service-maintenance"))\r
                \r
-               private val instanceHolder by lazy {\r
-                       CoroutineScope(CoroutineName("robot-service-initialization")).async {\r
-                               Configuration.Current.openAi?.let(::RobotService)?.apply {\r
-                                       initialize()\r
+               private val startInitializing = Job()\r
+               \r
+               private val instanceHolder = CoroutineScope(CoroutineName("robot-service-initialization")).async {\r
+                       startInitializing.join()\r
+                       Configuration.Current.openAi?.let { config ->\r
+                               status = RobotServiceStatus.LOADING\r
+                               RobotService(config).apply { initialize() }\r
+                       }\r
+               }.also { deferred ->\r
+                       deferred.invokeOnCompletion { ex ->\r
+                               status = if (ex != null) {\r
+                                       RobotServiceLogger.error("RobotService failed to initialize", ex)\r
+                                       RobotServiceStatus.FAILED\r
+                               } else {\r
+                                       RobotServiceLogger.info("RobotService successfully initialized")\r
+                                       RobotServiceStatus.READY\r
                                }\r
                        }\r
                }\r
                \r
-               var status: RobotServiceStatus = if (Configuration.Current.openAi != null) RobotServiceStatus.LOADING else RobotServiceStatus.NOT_CONFIGURED\r
+               var status: RobotServiceStatus = RobotServiceStatus.NOT_CONFIGURED\r
                        private set\r
                \r
                suspend fun getInstance() = try {\r
@@ -376,28 +445,8 @@ class RobotService(
                        null\r
                }\r
                \r
-               fun initialize() {\r
-                       instanceHolder.invokeOnCompletion { ex ->\r
-                               status = if (ex != null) {\r
-                                       logger.error("RobotService failed to initialize", ex)\r
-                                       RobotServiceStatus.FAILED\r
-                               } else {\r
-                                       logger.info("RobotService successfully initialized")\r
-                                       RobotServiceStatus.READY\r
-                               }\r
-                       }\r
-                       \r
-                       maintenanceScope.launch {\r
-                               getInstance()?.let { instance ->\r
-                                       while (true) {\r
-                                               delay(30.minutes)\r
-                                               \r
-                                               launch(SupervisorJob(currentCoroutineContext().job)) {\r
-                                                       instance.performMaintenance()\r
-                                               }\r
-                                       }\r
-                               }\r
-                       }\r
+               fun start() {\r
+                       startInitializing.complete()\r
                }\r
        }\r
 }\r
index 44f2290aed9ccc08fe35a0807ca9bf67dcf8f1e7..f6738040cba875efde5ff243430a85b680baaf17 100644 (file)
@@ -1,9 +1,12 @@
 package info.mechyrdia.robot
 
-import io.ktor.client.*
-import io.ktor.client.request.*
-import io.ktor.client.statement.*
-import io.ktor.utils.io.*
+import io.ktor.client.HttpClient
+import io.ktor.client.request.HttpRequestBuilder
+import io.ktor.client.request.prepareGet
+import io.ktor.client.request.preparePost
+import io.ktor.client.statement.HttpResponse
+import io.ktor.client.statement.bodyAsChannel
+import io.ktor.utils.io.readUTF8Line
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.FlowCollector
 import kotlinx.coroutines.flow.flow
index 75e3e83ee0bb71df2dca6cb86470bf3cbddac943..965b3663bfac0faa8d312b9bc12caabe28dfeeb5 100644 (file)
@@ -3,7 +3,14 @@ package info.mechyrdia.robot
 import com.mongodb.client.model.Filters
 import com.mongodb.client.model.Updates
 import info.mechyrdia.OwnerNationId
-import info.mechyrdia.data.*
+import info.mechyrdia.data.DataDocument
+import info.mechyrdia.data.DocumentTable
+import info.mechyrdia.data.Id
+import info.mechyrdia.data.MONGODB_ID_KEY
+import info.mechyrdia.data.NationData
+import info.mechyrdia.data.TableHolder
+import info.mechyrdia.data.ascending
+import info.mechyrdia.data.serialName
 import info.mechyrdia.lore.MyTimeZone
 import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
index fa93817b6ae03ad26b0043f86937a15f552eb905..b6a2c089baf58446001012ce37356f514d0ca8e4 100644 (file)
@@ -10,11 +10,27 @@ import info.mechyrdia.route.Root
 import info.mechyrdia.route.checkCsrfToken
 import info.mechyrdia.route.href
 import info.mechyrdia.route.installCsrfToken
-import io.ktor.server.application.*
-import io.ktor.server.websocket.*
-import io.ktor.websocket.*
-import io.ktor.websocket.CloseReason.*
-import kotlinx.html.*
+import io.ktor.server.application.ApplicationCall
+import io.ktor.server.websocket.DefaultWebSocketServerSession
+import io.ktor.server.websocket.sendSerialized
+import io.ktor.websocket.CloseReason
+import io.ktor.websocket.CloseReason.Codes
+import io.ktor.websocket.Frame
+import io.ktor.websocket.WebSocketSession
+import io.ktor.websocket.close
+import io.ktor.websocket.readText
+import kotlinx.html.FormMethod
+import kotlinx.html.HTML
+import kotlinx.html.b
+import kotlinx.html.form
+import kotlinx.html.h1
+import kotlinx.html.li
+import kotlinx.html.main
+import kotlinx.html.p
+import kotlinx.html.section
+import kotlinx.html.span
+import kotlinx.html.submitInput
+import kotlinx.html.ul
 
 suspend fun ApplicationCall.robotPage(): HTML.() -> Unit {
        val nation = currentNation()?.id ?: redirectHrefWithError(Root.Auth.LoginPage(), error = "You must be logged in to use the NUKE")
@@ -95,7 +111,7 @@ fun ApplicationCall.robotManagementPage(): HTML.() -> Unit {
                                RobotServiceStatus.READY -> ul {
                                        li {
                                                form(action = href(Root.Admin.NukeManagement.Update()), method = FormMethod.post) {
-                                                       installCsrfToken()
+                                                       installCsrfToken(call = this@robotManagementPage)
                                                        submitInput {
                                                                value = "Manually Trigger File Update"
                                                        }
@@ -103,7 +119,7 @@ fun ApplicationCall.robotManagementPage(): HTML.() -> Unit {
                                        }
                                        li {
                                                form(action = href(Root.Admin.NukeManagement.Reset()), method = FormMethod.post) {
-                                                       installCsrfToken()
+                                                       installCsrfToken(call = this@robotManagementPage)
                                                        submitInput(classes = "evil") {
                                                                value = "Reset All Data And Start Over"
                                                        }
index 44cf57a8d2a3a14a577cdab66c9ad511ac59013a..bc5507d4b58b531722248e6f7920b5556fa846c4 100644 (file)
@@ -1,9 +1,7 @@
 package info.mechyrdia.route
 
 import info.mechyrdia.lore.TextAlignment
-import kotlinx.html.FlowContent
-import kotlinx.html.p
-import kotlinx.html.textArea
+import kotlinx.html.*
 import kotlinx.serialization.Serializable
 
 @Serializable
index 52db438a3dae01a70f36c206d12f533775c33d8b..9e5a554f85e2007a2c59d2bdd944462bb2976a40 100644 (file)
@@ -2,12 +2,9 @@ package info.mechyrdia.route
 
 import info.mechyrdia.auth.createCsrfToken
 import info.mechyrdia.auth.retrieveCsrfToken
-import io.ktor.server.application.*
-import io.ktor.server.request.*
-import kotlinx.html.A
-import kotlinx.html.FORM
-import kotlinx.html.FlowContent
-import kotlinx.html.hiddenInput
+import io.ktor.server.application.ApplicationCall
+import io.ktor.server.request.uri
+import kotlinx.html.*
 import java.time.Instant
 import kotlin.collections.set
 
@@ -34,16 +31,14 @@ suspend fun ApplicationCall.checkCsrfToken(csrfToken: String?, route: String = r
        return entry.targetRoute == route && entry.expiresAt >= Instant.now()
 }
 
-context(ApplicationCall)
-fun A.installCsrfToken(route: String = href) {
+fun A.installCsrfToken(route: String = href, call: ApplicationCall) {
        attributes["data-method"] = "post"
-       attributes["data-csrf-token"] = createCsrfToken(route)
+       attributes["data-csrf-token"] = call.createCsrfToken(route)
 }
 
-context(ApplicationCall)
-fun FORM.installCsrfToken(route: String = action) {
+fun FORM.installCsrfToken(route: String = action, call: ApplicationCall) {
        hiddenInput {
                name = "csrfToken"
-               value = createCsrfToken(route)
+               value = call.createCsrfToken(route)
        }
 }
index 7af94c217f53bdea6bf85066c485a83c04ccb792..a075fb7b5f58f22d3fe7b8ac2aa0639f4ece4403 100644 (file)
@@ -1,18 +1,34 @@
 package info.mechyrdia.route
 
-import io.ktor.http.*
-import io.ktor.resources.serialization.*
-import io.ktor.server.application.*
-import io.ktor.server.plugins.*
-import io.ktor.server.request.*
-import io.ktor.server.resources.*
+import io.ktor.http.URLBuilder
+import io.ktor.http.formUrlEncode
+import io.ktor.http.fullPath
+import io.ktor.http.parseUrlEncodedParameters
+import io.ktor.resources.serialization.ResourcesFormat
+import io.ktor.server.application.Application
+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.plugin
+import io.ktor.server.plugins.BadRequestException
+import io.ktor.server.request.receiveMultipart
+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.routing.*
-import io.ktor.server.websocket.*
-import io.ktor.util.*
-import io.ktor.util.pipeline.*
-import kotlinx.html.P
-import kotlinx.serialization.*
+import io.ktor.server.resources.resource
+import io.ktor.server.routing.Route
+import io.ktor.server.websocket.DefaultWebSocketServerSession
+import io.ktor.server.websocket.WebSocketServerSession
+import io.ktor.server.websocket.application
+import io.ktor.server.websocket.webSocket
+import io.ktor.util.AttributeKey
+import io.ktor.util.pipeline.PipelineContext
+import kotlinx.serialization.DeserializationStrategy
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.SerializationStrategy
+import kotlinx.serialization.StringFormat
 import kotlinx.serialization.builtins.nullable
 import kotlinx.serialization.builtins.serializer
 import kotlinx.serialization.descriptors.PrimitiveKind
@@ -21,6 +37,7 @@ import kotlinx.serialization.descriptors.SerialDescriptor
 import kotlinx.serialization.encoding.Decoder
 import kotlinx.serialization.encoding.Encoder
 import kotlinx.serialization.modules.SerializersModule
+import kotlinx.serialization.serializer
 import kotlin.enums.EnumEntries
 
 interface ResourceHandler {
index 51ac7b66a5aaa0767c8a01ff6cf635816a730f96..1e459b97b7925ce864746ef91bb4cd013947dc9e 100644 (file)
@@ -1,6 +1,9 @@
 package info.mechyrdia.route
 
-import io.ktor.http.content.*
+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 {
index e3fb1bc66647577c60b1bf5ffb4f203349b31b38..abb19440b9d078d6f357b0f9a4cc2657647db269 100644 (file)
@@ -1,22 +1,79 @@
 package info.mechyrdia.route
 
-import info.mechyrdia.auth.*
-import info.mechyrdia.data.*
-import info.mechyrdia.lore.*
+import info.mechyrdia.auth.adminObtainWebDavToken
+import info.mechyrdia.auth.adminRequestWebDavToken
+import info.mechyrdia.auth.loginPage
+import info.mechyrdia.auth.loginRoute
+import info.mechyrdia.auth.logoutRoute
+import info.mechyrdia.data.Comment
+import info.mechyrdia.data.Id
+import info.mechyrdia.data.NationData
+import info.mechyrdia.data.StoragePath
+import info.mechyrdia.data.adminBanUserRoute
+import info.mechyrdia.data.adminConfirmDeleteFile
+import info.mechyrdia.data.adminConfirmRemoveDirectory
+import info.mechyrdia.data.adminDeleteFile
+import info.mechyrdia.data.adminDoCopyFile
+import info.mechyrdia.data.adminMakeDirectory
+import info.mechyrdia.data.adminOverwriteFile
+import info.mechyrdia.data.adminPreviewFile
+import info.mechyrdia.data.adminRemoveDirectory
+import info.mechyrdia.data.adminShowCopyFile
+import info.mechyrdia.data.adminUnbanUserRoute
+import info.mechyrdia.data.adminUploadFile
+import info.mechyrdia.data.adminViewVfs
+import info.mechyrdia.data.commentHelpPage
+import info.mechyrdia.data.currentUserPage
+import info.mechyrdia.data.deleteCommentPage
+import info.mechyrdia.data.deleteCommentRoute
+import info.mechyrdia.data.editCommentRoute
+import info.mechyrdia.data.newCommentRoute
+import info.mechyrdia.data.ownerNationOnly
+import info.mechyrdia.data.recentCommentsPage
+import info.mechyrdia.data.respondStoredFile
+import info.mechyrdia.data.respondXml
+import info.mechyrdia.data.userPage
+import info.mechyrdia.data.viewCommentRoute
+import info.mechyrdia.lore.LoreArticleFormat
+import info.mechyrdia.lore.MechyrdiaSansFont
+import info.mechyrdia.lore.ParserTree
+import info.mechyrdia.lore.PokhwalishAlphabetFont
+import info.mechyrdia.lore.QuoteFormat
+import info.mechyrdia.lore.TylanAlphabetFont
+import info.mechyrdia.lore.buildSitemap
+import info.mechyrdia.lore.clientSettingsPage
+import info.mechyrdia.lore.galaxyMapPage
+import info.mechyrdia.lore.generateRecentPageEdits
+import info.mechyrdia.lore.loreArticlePage
+import info.mechyrdia.lore.loreIntroPage
+import info.mechyrdia.lore.parseAs
+import info.mechyrdia.lore.randomQuote
+import info.mechyrdia.lore.recentCommentsRssFeedGenerator
+import info.mechyrdia.lore.redirectHref
+import info.mechyrdia.lore.respondAsset
+import info.mechyrdia.lore.respondRss
+import info.mechyrdia.lore.sitemap
+import info.mechyrdia.lore.svg
+import info.mechyrdia.lore.toCommentHtml
+import info.mechyrdia.lore.toFragmentString
 import info.mechyrdia.robot.RobotService
 import info.mechyrdia.robot.robotConversation
 import info.mechyrdia.robot.robotManagementPage
 import info.mechyrdia.robot.robotPage
-import io.ktor.http.*
-import io.ktor.http.content.*
-import io.ktor.resources.*
-import io.ktor.server.application.*
-import io.ktor.server.html.*
-import io.ktor.server.plugins.*
-import io.ktor.server.response.*
-import io.ktor.server.websocket.*
-import io.ktor.util.*
-import io.ktor.util.pipeline.*
+import io.ktor.http.ContentType
+import io.ktor.http.HttpHeaders
+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.websocket.DefaultWebSocketServerSession
+import io.ktor.util.AttributeKey
+import io.ktor.util.pipeline.PipelineContext
 import kotlinx.coroutines.delay
 
 const val ErrorMessageCookieName = "ERROR_MSG"
@@ -520,7 +577,7 @@ class Root : ResourceHandler, ResourceFilter {
                                with(utils) { filterCall() }
                                
                                call.respondText(
-                                       text = payload.lines.joinToString(separator = "\n").parseAs(ParserTree::toCommentHtml).toFragment(),
+                                       text = payload.lines.joinToString(separator = "\n").parseAs(ParserTree::toCommentHtml).toFragmentString(),
                                        contentType = ContentType.Text.Html
                                )
                        }
index 36de95ba13f9a57c1c2f2b7b0a1522e1a158e8c7..f2a310d050318c2d7225ed7b242f2164e286d039 100644 (file)
@@ -2,22 +2,46 @@ package info.mechyrdia.route
 
 import info.mechyrdia.auth.WebDavToken
 import info.mechyrdia.auth.toNationId
-import info.mechyrdia.data.*
+import info.mechyrdia.data.FileStorage
+import info.mechyrdia.data.Id
+import info.mechyrdia.data.StoragePath
+import info.mechyrdia.data.StoredFileType
+import info.mechyrdia.data.XmlInsertable
+import info.mechyrdia.data.XmlTag
+import info.mechyrdia.data.XmlTagConsumer
+import info.mechyrdia.data.contentType
+import info.mechyrdia.data.declaration
+import info.mechyrdia.data.respondXml
+import info.mechyrdia.data.root
+import info.mechyrdia.data.sortedAsFiles
 import info.mechyrdia.lore.mapSuspend
-import io.ktor.http.*
-import io.ktor.server.application.*
-import io.ktor.server.html.*
-import io.ktor.server.request.*
-import io.ktor.server.response.*
-import io.ktor.server.routing.*
-import io.ktor.util.*
+import io.ktor.http.ContentType
+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
+import io.ktor.server.request.header
+import io.ktor.server.request.receive
+import io.ktor.server.request.receiveText
+import io.ktor.server.response.header
+import io.ktor.server.response.respond
+import io.ktor.server.response.respondBytes
+import io.ktor.server.routing.Route
+import io.ktor.server.routing.method
+import io.ktor.server.routing.route
+import io.ktor.util.AttributeKey
 import kotlinx.html.*
 import java.net.URI
 import java.time.Instant
 import java.time.ZoneOffset
 import java.time.ZonedDateTime
 import java.time.format.DateTimeFormatter
-import java.util.*
+import java.util.Base64
+import java.util.UUID
 
 const val WebDavDomainName = "https://dav.mechyrdia.info"
 
@@ -26,12 +50,35 @@ private val dateTimeFormatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME
 private val Instant.webDavValue: String
        get() = dateTimeFormatter.format(ZonedDateTime.ofInstant(this, ZoneOffset.UTC))
 
-sealed class WebDavProperties {
+sealed class WebDavProperties : XmlInsertable {
        abstract val creationDate: Instant?
        abstract val lastModified: Instant?
        abstract val displayName: String
        abstract val displayHref: String
        
+       protected abstract fun XmlTag.resourceProps()
+       
+       override fun XmlTag.intoXml() {
+               "response" {
+                       "href" { +displayHref }
+                       "propstat" {
+                               "props" {
+                                       creationDate?.webDavValue?.let { value -> "creationdate" { +value } }
+                                       lastModified?.webDavValue?.let { value -> "getlastmodified" { +value } }
+                                       "displayname" { +displayName }
+                                       resourceProps()
+                                       "supportedlock" {
+                                               "lockentry" {
+                                                       "lockscope" { "shared"() }
+                                                       "locktype" { "write"() }
+                                               }
+                                       }
+                               }
+                               "status" { +"HTTP/1.1 200 OK" }
+                       }
+               }
+       }
+       
        data class Leaf(
                override val creationDate: Instant,
                override val lastModified: Instant,
@@ -39,39 +86,23 @@ sealed class WebDavProperties {
                override val displayHref: String,
                val contentLength: Long,
                val contentType: ContentType,
-       ) : WebDavProperties()
+       ) : WebDavProperties() {
+               override fun XmlTag.resourceProps() {
+                       "getcontentlength" { +"$contentLength" }
+                       "getcontenttype" { +"${contentType.withoutParameters()}" }
+                       "resourcetype"()
+               }
+       }
        
        data class Collection(
                override val creationDate: Instant?,
                override val lastModified: Instant?,
                override val displayName: String,
                override val displayHref: String,
-       ) : WebDavProperties()
-}
-
-context(XmlTag)
-operator fun WebDavProperties.unaryPlus() = "response" {
-       "href" { +displayHref }
-       "propstat" {
-               "props" {
-                       creationDate?.webDavValue?.let { value -> "creationdate" { +value } }
-                       lastModified?.webDavValue?.let { value -> "getlastmodified" { +value } }
-                       "displayname" { +displayName }
-                       if (this@unaryPlus is WebDavProperties.Leaf) {
-                               "getcontentlength" { +"$contentLength" }
-                               "getcontenttype" { +"${contentType.withoutParameters()}" }
-                               "resourcetype"()
-                       } else {
-                               "resourcetype" { "collection"() }
-                       }
-                       "supportedlock" {
-                               "lockentry" {
-                                       "lockscope" { "shared"() }
-                                       "locktype" { "write"() }
-                               }
-                       }
+       ) : WebDavProperties() {
+               override fun XmlTag.resourceProps() {
+                       "resourcetype" { "collection"() }
                }
-               "status" { +"HTTP/1.1 200 OK" }
        }
 }
 
index a9f975ae694d4d3ae134b6e300a2b02a333c3bd5..56903ae59335930bbd63312a0516ac541bb8d3bb 100644 (file)
@@ -8,13 +8,13 @@
 
                <encoder>
                        <charset>UTF-8</charset>
-                       <pattern>%d{yyyy-MM-dd/HH:mm:ss.SSS} [%thread] %.-1level \(%logger\) - %msg%n</pattern>
+                       <pattern>%d{yyyy-MM-dd/HH:mm:ss.SSS} [%thread] %.-1level \(%logger\) @%X{ktor_call_id:-no_call} - %msg%n</pattern>
                </encoder>
        </appender>
 
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
                <encoder>
-                       <pattern>%d{yyyy-MM-dd/HH:mm:ss.SSS} [%thread] %.-1level \(%logger\) - %msg%n</pattern>
+                       <pattern>%d{yyyy-MM-dd/HH:mm:ss.SSS} [%thread] %.-1level \(%logger\) @%X{ktor_call_id:-no_call} - %msg%n</pattern>
                </encoder>
        </appender>