From a645555c13c62dc39dd2e7574de247859d8ab64f Mon Sep 17 00:00:00 2001 From: Lanius Trolling Date: Sun, 11 Aug 2024 19:43:09 -0400 Subject: [PATCH] Italicize titles of factbooks that contain redirects when in page-index view - as well as fading WIP factbooks and striking-out old factbooks Also, update dependencies and declare a great holy war against star imports (except when they're kotlinx.html THOUGH) --- .idea/.gitignore | 2 + build.gradle.kts | 13 +- .../kotlin/info/mechyrdia/Factbooks.kt | 84 +++++++--- .../info/mechyrdia/auth/SessionStorage.kt | 9 +- .../kotlin/info/mechyrdia/auth/Sessions.kt | 25 ++- .../kotlin/info/mechyrdia/auth/ViewsLogin.kt | 35 +++- .../kotlin/info/mechyrdia/auth/WebDav.kt | 30 +++- .../kotlin/info/mechyrdia/data/Comments.kt | 6 +- .../kotlin/info/mechyrdia/data/Data.kt | 9 +- .../kotlin/info/mechyrdia/data/DataFiles.kt | 29 +++- .../kotlin/info/mechyrdia/data/Nations.kt | 13 +- .../info/mechyrdia/data/ViewComments.kt | 40 ++--- .../info/mechyrdia/data/ViewsComment.kt | 24 ++- .../kotlin/info/mechyrdia/data/ViewsFiles.kt | 59 +++---- .../kotlin/info/mechyrdia/data/ViewsUser.kt | 18 ++- .../kotlin/info/mechyrdia/data/Visits.kt | 11 +- src/jvmMain/kotlin/info/mechyrdia/data/Xml.kt | 34 ++-- .../kotlin/info/mechyrdia/lore/April1st.kt | 12 +- .../info/mechyrdia/lore/ArticleListing.kt | 37 ++--- .../info/mechyrdia/lore/ArticleTitles.kt | 32 ++-- .../info/mechyrdia/lore/AssetCaching.kt | 2 +- .../info/mechyrdia/lore/AssetCompression.kt | 11 +- .../info/mechyrdia/lore/AssetHashing.kt | 15 +- .../kotlin/info/mechyrdia/lore/Fonts.kt | 12 +- .../kotlin/info/mechyrdia/lore/HttpUtils.kt | 2 +- .../kotlin/info/mechyrdia/lore/ParserHtml.kt | 40 ++--- .../info/mechyrdia/lore/ParserPreprocess.kt | 13 +- .../mechyrdia/lore/ParserPreprocessInclude.kt | 17 +- .../mechyrdia/lore/ParserPreprocessJson.kt | 6 +- .../mechyrdia/lore/ParserPreprocessMath.kt | 19 ++- .../kotlin/info/mechyrdia/lore/ParserRaw.kt | 18 +-- .../kotlin/info/mechyrdia/lore/ViewNav.kt | 29 +++- .../kotlin/info/mechyrdia/lore/ViewOg.kt | 9 +- .../kotlin/info/mechyrdia/lore/ViewTpl.kt | 21 ++- .../kotlin/info/mechyrdia/lore/ViewsError.kt | 7 +- .../kotlin/info/mechyrdia/lore/ViewsLore.kt | 84 ++++++---- .../kotlin/info/mechyrdia/lore/ViewsPrefs.kt | 30 ++-- .../kotlin/info/mechyrdia/lore/ViewsQuote.kt | 41 ++--- .../kotlin/info/mechyrdia/lore/ViewsRobots.kt | 25 +-- .../kotlin/info/mechyrdia/lore/ViewsRss.kt | 140 +++++++++------- .../kotlin/info/mechyrdia/robot/RobotApi.kt | 17 +- .../kotlin/info/mechyrdia/robot/RobotCodec.kt | 3 +- .../kotlin/info/mechyrdia/robot/RobotFiles.kt | 7 +- .../info/mechyrdia/robot/RobotRateLimiter.kt | 5 +- .../info/mechyrdia/robot/RobotService.kt | 153 ++++++++++++------ .../kotlin/info/mechyrdia/robot/RobotSse.kt | 11 +- .../info/mechyrdia/robot/RobotUserLimiter.kt | 9 +- .../kotlin/info/mechyrdia/robot/ViewsRobot.kt | 30 +++- .../info/mechyrdia/route/ResourceBodies.kt | 4 +- .../info/mechyrdia/route/ResourceCsrf.kt | 19 +-- .../info/mechyrdia/route/ResourceHandler.kt | 41 +++-- .../info/mechyrdia/route/ResourceMultipart.kt | 5 +- .../info/mechyrdia/route/ResourceTypes.kt | 85 ++++++++-- .../info/mechyrdia/route/ResourceWebDav.kt | 103 +++++++----- src/jvmMain/resources/logback.xml | 4 +- 55 files changed, 999 insertions(+), 560 deletions(-) diff --git a/.idea/.gitignore b/.idea/.gitignore index 13566b8..3630c64 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -6,3 +6,5 @@ # Datasource local storage ignored files /dataSources/ /dataSources.local.xml +# Stuff that I want to ignore +/codeStyles/ diff --git a/build.gradle.kts b/build.gradle.kts index d8f7bb7..c75a739 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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")) } } } diff --git a/src/jvmMain/kotlin/info/mechyrdia/Factbooks.kt b/src/jvmMain/kotlin/info/mechyrdia/Factbooks.kt index 8d95c01..dbbb971 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/Factbooks.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/Factbooks.kt @@ -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()}" } } diff --git a/src/jvmMain/kotlin/info/mechyrdia/auth/SessionStorage.kt b/src/jvmMain/kotlin/info/mechyrdia/auth/SessionStorage.kt index 0fb6763..9645142 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/auth/SessionStorage.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/auth/SessionStorage.kt @@ -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 diff --git a/src/jvmMain/kotlin/info/mechyrdia/auth/Sessions.kt b/src/jvmMain/kotlin/info/mechyrdia/auth/Sessions.kt index 7beb75c..e521745 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/auth/Sessions.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/auth/Sessions.kt @@ -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 diff --git a/src/jvmMain/kotlin/info/mechyrdia/auth/ViewsLogin.kt b/src/jvmMain/kotlin/info/mechyrdia/auth/ViewsLogin.kt index 9fe6dec..815d3a4 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/auth/ViewsLogin.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/auth/ViewsLogin.kt @@ -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" diff --git a/src/jvmMain/kotlin/info/mechyrdia/auth/WebDav.kt b/src/jvmMain/kotlin/info/mechyrdia/auth/WebDav.kt index fbbdbbf..9f91756 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/auth/WebDav.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/auth/WebDav.kt @@ -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" } } diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/Comments.kt b/src/jvmMain/kotlin/info/mechyrdia/data/Comments.kt index 0829658..f16fa05 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/data/Comments.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/data/Comments.kt @@ -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 diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/Data.kt b/src/jvmMain/kotlin/info/mechyrdia/data/Data.kt index b1f1b40..4c7f79f 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/data/Data.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/data/Data.kt @@ -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 diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/DataFiles.kt b/src/jvmMain/kotlin/info/mechyrdia/data/DataFiles.kt index 6858d2c..8f2cf22 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/data/DataFiles.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/data/DataFiles.kt @@ -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 diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/Nations.kt b/src/jvmMain/kotlin/info/mechyrdia/data/Nations.kt index 6c0d73a..800bd27 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/data/Nations.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/data/Nations.kt @@ -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 { companion object : TableHolder { - private val logger: Logger = LoggerFactory.getLogger(NationData::class.java) - override val Table = DocumentTable() override suspend fun initialize() { @@ -33,7 +34,7 @@ data class NationData( } fun unknown(id: Id): 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") } diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/ViewComments.kt b/src/jvmMain/kotlin/info/mechyrdia/data/ViewComments.kt index 8916fbb..ad24564 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/data/ViewComments.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/data/ViewComments.kt @@ -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?, viewingUserPage: Boolean = false) { +fun FlowContent.commentBox(comment: CommentRenderData, loggedInAs: Id?, 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 p { style = "font-size:0.8em" @@ -101,12 +106,12 @@ fun FlowContent.commentBox(comment: CommentRenderData, loggedInAs: Id>$reply" } } @@ -162,7 +167,7 @@ fun FlowContent.commentBox(comment: CommentRenderData, loggedInAs: Id, commentingAs: NationData?) { +fun FlowContent.commentInput(pagePathParts: List, 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" } } } diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/ViewsComment.kt b/src/jvmMain/kotlin/info/mechyrdia/data/ViewsComment.kt index d9cd3f0..c5a42a5 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/data/ViewsComment.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/data/ViewsComment.kt @@ -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): 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)]" } diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/ViewsFiles.kt b/src/jvmMain/kotlin/info/mechyrdia/data/ViewsFiles.kt index 2d5fd49..f6e1ee6 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/data/ViewsFiles.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/data/ViewsFiles.kt @@ -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) { +private fun UL.render(path: StoragePath, childNodes: Map, 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) { } 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) { 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) { } } -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" } } } diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/ViewsUser.kt b/src/jvmMain/kotlin/info/mechyrdia/data/ViewsUser.kt index 4b72023..456b0af 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/data/ViewsUser.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/data/ViewsUser.kt @@ -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()?.nationId @@ -41,19 +47,19 @@ suspend fun ApplicationCall.userPage(userId: Id): 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) } } } diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/Visits.kt b/src/jvmMain/kotlin/info/mechyrdia/data/Visits.kt index e678ed8..42b70a2 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/data/Visits.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/data/Visits.kt @@ -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 diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/Xml.kt b/src/jvmMain/kotlin/info/mechyrdia/data/Xml.kt index 7dc5a91..441b219 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/data/Xml.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/data/Xml.kt @@ -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 = 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 = {} diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/April1st.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/April1st.kt index e7a0641..3905766 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/April1st.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/April1st.kt @@ -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) { diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ArticleListing.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ArticleListing.kt index 83da218..f0670ef 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/ArticleListing.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/ArticleListing.kt @@ -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?) +data class ArticleNode(val name: String, val title: ArticleTitle, val subNodes: List?) suspend fun rootArticleNodeList(): List = StoragePath.articleDir.toArticleNode().subNodes.orEmpty() @@ -34,7 +31,7 @@ fun List.sortedLexically(selector: (T) -> String?) = map { it to collator .sortedBy { it.second } .map { (it, _) -> it } -private fun List.sortedAsArticles() = sortedLexically { it.title }.sortedBy { it.subNodes == null } +private fun List.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.renderInto(list: UL, base: List = emptyList(), format: LoreArticleFormat = LoreArticleFormat.HTML) { +fun List.renderInto(list: UL, base: List = 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 = " - ") } diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ArticleTitles.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ArticleTitles.kt index 337126f..fdcaded 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/ArticleTitles.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/ArticleTitles.kt @@ -1,20 +1,32 @@ package info.mechyrdia.lore -import info.mechyrdia.data.FileStorage import info.mechyrdia.data.StoragePath -object ArticleTitleCache : FileDependentCache() { - override suspend fun processFile(path: StoragePath): String? { +data class ArticleTitle(val title: String, val css: String = "") + +object ArticleTitleCache : FileDependentCache() { + 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) } } diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/AssetCaching.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/AssetCaching.kt index 4c9128f..708b607 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/AssetCaching.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/AssetCaching.kt @@ -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 diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/AssetCompression.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/AssetCompression.kt index 030ce60..1bb1702 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/AssetCompression.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/AssetCompression.kt @@ -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 diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/AssetHashing.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/AssetHashing.kt index eab3a87..f467a8f 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/AssetHashing.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/AssetHashing.kt @@ -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 diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/Fonts.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/Fonts.kt index 72dc3bf..2a101b4 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/Fonts.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/Fonts.kt @@ -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 > 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() diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/HttpUtils.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/HttpUtils.kt index 1b46330..f6f028c 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/HttpUtils.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/HttpUtils.kt @@ -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() diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ParserHtml.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ParserHtml.kt index 842efe9..ff4661c 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/ParserHtml.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/ParserHtml.kt @@ -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 (TagConsumer<*>.() -> Any?).unaryPlus() = with(HtmlLexerTagConsumer(consumer)) { this@unaryPlus() } +fun 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 { @@ -64,11 +61,8 @@ class HtmlLexerTagConsumer private constructor(private val downstream: TagConsum } } -context(C) -operator fun > String.unaryPlus() = onTagContent(this) - -context(C) -operator fun > Entities.unaryPlus() = onTagContentEntity(this) +fun TagConsumer<*>.append(text: String) = onTagContent(text) +fun TagConsumer<*>.append(entity: Entities) = onTagContentEntity(entity) fun > C.unsafe(block: Unsafe.() -> Unit) = onTagContentUnsafe(block) @@ -102,7 +96,7 @@ fun ParserTree.toHtmlParagraph(env: LexerTagEnvironment, text: String): HtmlBuilderSubject { - return { +text } + return { append(text) } } override fun processLineBreak(env: LexerTagEnvironment): HtmlBuilderSubject { @@ -159,7 +153,7 @@ object HtmlLexerProcessor : LexerTagFallback 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") { diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocess.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocess.kt index ba8debd..63f6d78 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocess.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocess.kt @@ -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) = mapOf( diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocessInclude.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocessInclude.kt index a4309ab..e6b85d9 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocessInclude.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocessInclude.kt @@ -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 { diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocessJson.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocessJson.kt index 2e61555..a9e4d15 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocessJson.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocessJson.kt @@ -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() diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocessMath.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocessMath.kt index 5a7de20..72c2881 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocessMath.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocessMath.kt @@ -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 ParserTree.treeToNumberOrNull(convert: String.() -> T?) = treeToText().convert() diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ParserRaw.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ParserRaw.kt index afdf692..28d9cde 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/ParserRaw.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/ParserRaw.kt @@ -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 -> diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewNav.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewNav.kt index 60a0b29..73c9b57 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewNav.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewNav.kt @@ -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 { @@ -39,7 +39,7 @@ suspend fun ApplicationCall.standardNavBar(path: List? = 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? = 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 = emptyMap() + val linkAttributes: Map = 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 = emptyMap): NavLink { + fun withCsrf(to: String, text: String, textIsHtml: Boolean = false, aClasses: String? = null, extraAttributes: Map = 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 = 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() + ) + } } } diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewOg.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewOg.kt index c0efafa..9d880da 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewOg.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewOg.kt @@ -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("/")}") } diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewTpl.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewTpl.kt index 60da713..fd5dc68 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewTpl.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewTpl.kt @@ -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? = 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( diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsError.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsError.kt index 7f9ebfb..f649fee 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsError.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsError.kt @@ -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 { diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsLore.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsLore.kt index 699bc11..0ac31c1 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsLore.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsLore.kt @@ -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>) = p { - links.joinToHtml(Tag::breadCrumbArrow) { (url, text) -> - a(href = href(url)) { +text } +private fun FlowContent.breadCrumbs(links: List>, 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): 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): 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, 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, 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, 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, format: Lore } } - finalSection(pagePath, canCommentAs, comments, totalsData) + finalSection(pagePath, canCommentAs, comments, totalsData, call = this@loreArticlePage) } } -context(ApplicationCall) -private fun SectioningOrFlowContent.finalSection(pagePathParts: List, canCommentAs: NationData?, comments: List, totalsData: PageVisitTotals) { +private fun SectioningOrFlowContent.finalSection(pagePathParts: List, canCommentAs: NationData?, comments: List, 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) } diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsPrefs.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsPrefs.kt index f2d6a61..22acc69 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsPrefs.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsPrefs.kt @@ -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(val iterator: Iterator) { - inline fun 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 invoke(separator: T.() -> Unit, body: T.(E) -> Unit) { - invokeReceiver(separator, body) +fun T.joined(iterable: Iterable, separator: T.() -> Unit, body: T.(E) -> Unit) { + var isFirst = true + for (item in iterable) { + if (isFirst) + isFirst = false + else + separator() + body(item) } } -val Iterable.joinToHtml: JoinToHtmlConsumer - get() = JoinToHtmlConsumer(iterator()) - inline fun > FlowOrInteractiveOrPhrasingContent.preference(inputName: String, current: E, crossinline localize: (E) -> String) { val serializer = serializer() as? KeyedEnumSerializer ?: 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" diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsQuote.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsQuote.kt index 52aacac..ce0c53b 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsQuote.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsQuote.kt @@ -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("―") } +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) { diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsRobots.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsRobots.kt index 587513e..8d9f322 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsRobots.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsRobots.kt @@ -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? { diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsRss.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsRss.kt index 0e5184b..503c61c 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsRss.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsRss.kt @@ -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 = emptyList(), val items: List = emptyList(), -) - -fun > 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 > 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 = 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 + } + } } diff --git a/src/jvmMain/kotlin/info/mechyrdia/robot/RobotApi.kt b/src/jvmMain/kotlin/info/mechyrdia/robot/RobotApi.kt index 5ca4e0b..d753e4d 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/robot/RobotApi.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/robot/RobotApi.kt @@ -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 diff --git a/src/jvmMain/kotlin/info/mechyrdia/robot/RobotCodec.kt b/src/jvmMain/kotlin/info/mechyrdia/robot/RobotCodec.kt index b67af68..a989511 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/robot/RobotCodec.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/robot/RobotCodec.kt @@ -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 diff --git a/src/jvmMain/kotlin/info/mechyrdia/robot/RobotFiles.kt b/src/jvmMain/kotlin/info/mechyrdia/robot/RobotFiles.kt index f274ae6..6b3ad8a 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/robot/RobotFiles.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/robot/RobotFiles.kt @@ -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, diff --git a/src/jvmMain/kotlin/info/mechyrdia/robot/RobotRateLimiter.kt b/src/jvmMain/kotlin/info/mechyrdia/robot/RobotRateLimiter.kt index e5f5812..91d705f 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/robot/RobotRateLimiter.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/robot/RobotRateLimiter.kt @@ -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 diff --git a/src/jvmMain/kotlin/info/mechyrdia/robot/RobotService.kt b/src/jvmMain/kotlin/info/mechyrdia/robot/RobotService.kt index 830bf15..bcbb525 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/robot/RobotService.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/robot/RobotService.kt @@ -3,26 +3,75 @@ package info.mechyrdia.robot import info.mechyrdia.Configuration import info.mechyrdia.MainDomainName import info.mechyrdia.OpenAiConfig -import info.mechyrdia.data.* +import info.mechyrdia.data.DataDocument +import info.mechyrdia.data.DocumentTable +import info.mechyrdia.data.Id +import info.mechyrdia.data.InstantNullableSerializer +import info.mechyrdia.data.MONGODB_ID_KEY +import info.mechyrdia.data.NationData +import info.mechyrdia.data.TableHolder import info.mechyrdia.lore.RobotFactbookLoader -import io.ktor.client.* -import io.ktor.client.engine.java.* -import io.ktor.client.plugins.* -import io.ktor.client.plugins.contentnegotiation.* -import io.ktor.client.plugins.logging.* -import io.ktor.client.request.* -import io.ktor.http.* -import io.ktor.serialization.kotlinx.json.* -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* +import io.ktor.client.HttpClient +import io.ktor.client.engine.java.Java +import io.ktor.client.plugins.ClientRequestException +import io.ktor.client.plugins.HttpRequestRetry +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.defaultRequest +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logging +import io.ktor.client.request.header +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.withCharset +import io.ktor.serialization.kotlinx.json.json +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.job +import kotlinx.coroutines.launch import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import org.slf4j.Logger import org.slf4j.LoggerFactory import java.time.Instant +import kotlin.collections.List +import kotlin.collections.Map +import kotlin.collections.Set +import kotlin.collections.buildMap +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.emptyMap +import kotlin.collections.emptySet +import kotlin.collections.flatMap +import kotlin.collections.fold +import kotlin.collections.forEach +import kotlin.collections.iterator +import kotlin.collections.joinToString +import kotlin.collections.listOf +import kotlin.collections.map +import kotlin.collections.minus +import kotlin.collections.mutableListOf +import kotlin.collections.plus +import kotlin.collections.set +import kotlin.collections.toList import kotlin.random.Random import kotlin.time.Duration.Companion.minutes +private val RobotServiceLogger: Logger = LoggerFactory.getLogger("info.mechyrdia.robot.RobotServiceKt") + val RobotGlobalsId = Id("RobotGlobalsInstance") @Serializable @@ -111,7 +160,7 @@ class RobotService( try { robotClient.deleteThread(threadId) } catch (ex: ClientRequestException) { - logger.warn("Unable to delete thread at ID $threadId", ex) + RobotServiceLogger.warn("Unable to delete thread at ID $threadId", ex) } (RobotGlobals.get() ?: RobotGlobals()).minusThread(threadId).save() } @@ -121,7 +170,7 @@ class RobotService( try { robotClient.deleteThread(threadId) } catch (ex: ClientRequestException) { - logger.warn("Unable to delete thread at ID $threadId", ex) + RobotServiceLogger.warn("Unable to delete thread at ID $threadId", ex) } return copy(ongoingThreadIds = emptySet()) } @@ -141,7 +190,7 @@ class RobotService( try { robotClient.deleteFile(oldId) } catch (ex: ClientRequestException) { - logger.warn("Unable to delete file $name at ID $oldId", ex) + RobotServiceLogger.warn("Unable to delete file $name at ID $oldId", ex) } } @@ -157,7 +206,7 @@ class RobotService( this[name] = newId onNewFileId?.invoke(newId) - logger.info("Factbook $name has been uploaded") + RobotServiceLogger.info("Factbook $name has been uploaded") } } @@ -176,13 +225,13 @@ class RobotService( robotGlobals = robotGlobals.copy(vectorStoreId = vsId).save() } - logger.info("Vector store has been created") + RobotServiceLogger.info("Vector store has been created") poll { robotClient.getVectorStore(vectorStoreId).status == "completed" } - logger.info("Vector store creation is complete") + RobotServiceLogger.info("Vector store creation is complete") if (robotGlobals.assistantId == null) robotGlobals = robotGlobals.copy( @@ -204,7 +253,17 @@ class RobotService( ).id ).save() - logger.info("Assistant has been created") + RobotServiceLogger.info("Assistant creation is complete") + + maintenanceScope.launch { + while (true) { + delay(30.minutes) + + launch(SupervisorJob(currentCoroutineContext().job)) { + performMaintenance() + } + } + } } suspend fun performMaintenance() { @@ -223,13 +282,13 @@ class RobotService( robotClient.addFileToVectorStore(vectorStoreId, fileId) } - logger.info("Vector store has been updated") + RobotServiceLogger.info("Vector store has been updated") poll { robotClient.getVectorStore(vectorStoreId).fileCounts.inProgress == 0 } - logger.info("Vector store update is complete") + RobotServiceLogger.info("Vector store update is complete") } suspend fun reset() { @@ -248,7 +307,7 @@ class RobotService( try { robotClient.deleteAssistant(it) } catch (ex: ClientRequestException) { - logger.warn("Unable to delete assistant at ID $it", ex) + RobotServiceLogger.warn("Unable to delete assistant at ID $it", ex) } } } @@ -261,7 +320,7 @@ class RobotService( try { robotClient.deleteVectorStore(it) } catch (ex: ClientRequestException) { - logger.warn("Unable to delete vector-store at ID $it", ex) + RobotServiceLogger.warn("Unable to delete vector-store at ID $it", ex) } } } @@ -274,7 +333,7 @@ class RobotService( try { robotClient.deleteFile(it) } catch (ex: ClientRequestException) { - logger.warn("Unable to delete file at ID $it", ex) + RobotServiceLogger.warn("Unable to delete file at ID $it", ex) } } } @@ -355,19 +414,29 @@ class RobotService( } companion object { - private val logger: Logger = LoggerFactory.getLogger(RobotService::class.java) - private val maintenanceScope = CoroutineScope(SupervisorJob() + CoroutineName("robot-service-maintenance")) - private val instanceHolder by lazy { - CoroutineScope(CoroutineName("robot-service-initialization")).async { - Configuration.Current.openAi?.let(::RobotService)?.apply { - initialize() + private val startInitializing = Job() + + private val instanceHolder = CoroutineScope(CoroutineName("robot-service-initialization")).async { + startInitializing.join() + Configuration.Current.openAi?.let { config -> + status = RobotServiceStatus.LOADING + RobotService(config).apply { initialize() } + } + }.also { deferred -> + deferred.invokeOnCompletion { ex -> + status = if (ex != null) { + RobotServiceLogger.error("RobotService failed to initialize", ex) + RobotServiceStatus.FAILED + } else { + RobotServiceLogger.info("RobotService successfully initialized") + RobotServiceStatus.READY } } } - var status: RobotServiceStatus = if (Configuration.Current.openAi != null) RobotServiceStatus.LOADING else RobotServiceStatus.NOT_CONFIGURED + var status: RobotServiceStatus = RobotServiceStatus.NOT_CONFIGURED private set suspend fun getInstance() = try { @@ -376,28 +445,8 @@ class RobotService( null } - fun initialize() { - instanceHolder.invokeOnCompletion { ex -> - status = if (ex != null) { - logger.error("RobotService failed to initialize", ex) - RobotServiceStatus.FAILED - } else { - logger.info("RobotService successfully initialized") - RobotServiceStatus.READY - } - } - - maintenanceScope.launch { - getInstance()?.let { instance -> - while (true) { - delay(30.minutes) - - launch(SupervisorJob(currentCoroutineContext().job)) { - instance.performMaintenance() - } - } - } - } + fun start() { + startInitializing.complete() } } } diff --git a/src/jvmMain/kotlin/info/mechyrdia/robot/RobotSse.kt b/src/jvmMain/kotlin/info/mechyrdia/robot/RobotSse.kt index 44f2290..f673804 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/robot/RobotSse.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/robot/RobotSse.kt @@ -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 diff --git a/src/jvmMain/kotlin/info/mechyrdia/robot/RobotUserLimiter.kt b/src/jvmMain/kotlin/info/mechyrdia/robot/RobotUserLimiter.kt index 75e3e83..965b366 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/robot/RobotUserLimiter.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/robot/RobotUserLimiter.kt @@ -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 diff --git a/src/jvmMain/kotlin/info/mechyrdia/robot/ViewsRobot.kt b/src/jvmMain/kotlin/info/mechyrdia/robot/ViewsRobot.kt index fa93817..b6a2c08 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/robot/ViewsRobot.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/robot/ViewsRobot.kt @@ -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" } diff --git a/src/jvmMain/kotlin/info/mechyrdia/route/ResourceBodies.kt b/src/jvmMain/kotlin/info/mechyrdia/route/ResourceBodies.kt index 44cf57a..bc5507d 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/route/ResourceBodies.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/route/ResourceBodies.kt @@ -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 diff --git a/src/jvmMain/kotlin/info/mechyrdia/route/ResourceCsrf.kt b/src/jvmMain/kotlin/info/mechyrdia/route/ResourceCsrf.kt index 52db438..9e5a554 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/route/ResourceCsrf.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/route/ResourceCsrf.kt @@ -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) } } diff --git a/src/jvmMain/kotlin/info/mechyrdia/route/ResourceHandler.kt b/src/jvmMain/kotlin/info/mechyrdia/route/ResourceHandler.kt index 7af94c2..a075fb7 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/route/ResourceHandler.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/route/ResourceHandler.kt @@ -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 { diff --git a/src/jvmMain/kotlin/info/mechyrdia/route/ResourceMultipart.kt b/src/jvmMain/kotlin/info/mechyrdia/route/ResourceMultipart.kt index 51ac7b6..1e459b9 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/route/ResourceMultipart.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/route/ResourceMultipart.kt @@ -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 { diff --git a/src/jvmMain/kotlin/info/mechyrdia/route/ResourceTypes.kt b/src/jvmMain/kotlin/info/mechyrdia/route/ResourceTypes.kt index e3fb1bc..abb1944 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/route/ResourceTypes.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/route/ResourceTypes.kt @@ -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 ) } diff --git a/src/jvmMain/kotlin/info/mechyrdia/route/ResourceWebDav.kt b/src/jvmMain/kotlin/info/mechyrdia/route/ResourceWebDav.kt index 36de95b..f2a310d 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/route/ResourceWebDav.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/route/ResourceWebDav.kt @@ -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" } } } diff --git a/src/jvmMain/resources/logback.xml b/src/jvmMain/resources/logback.xml index a9f975a..56903ae 100644 --- a/src/jvmMain/resources/logback.xml +++ b/src/jvmMain/resources/logback.xml @@ -8,13 +8,13 @@ UTF-8 - %d{yyyy-MM-dd/HH:mm:ss.SSS} [%thread] %.-1level \(%logger\) - %msg%n + %d{yyyy-MM-dd/HH:mm:ss.SSS} [%thread] %.-1level \(%logger\) @%X{ktor_call_id:-no_call} - %msg%n - %d{yyyy-MM-dd/HH:mm:ss.SSS} [%thread] %.-1level \(%logger\) - %msg%n + %d{yyyy-MM-dd/HH:mm:ss.SSS} [%thread] %.-1level \(%logger\) @%X{ktor_call_id:-no_call} - %msg%n -- 2.25.1