Also, update dependencies and declare a great holy war against star imports (except when they're kotlinx.html THOUGH)
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
+# Stuff that I want to ignore
+/codeStyles/
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
}
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"))
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")
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"))
implementation("io.ktor:ktor-client-logging:2.3.12")
implementation(project(":fontparser"))
- //implementation(project(":fightgame"))
}
}
}
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.*
FileStorage.initialize()
- RobotService.initialize()
+ RobotService.start()
embeddedServer(CIO, port = Configuration.Current.port, host = Configuration.Current.host, module = Application::factbooks).start(wait = true)
}
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)
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()}"
}
}
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
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,
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
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
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
section {
h1 { +"Log In With NationStates" }
form(method = FormMethod.post, action = href(Root.Auth.LoginPost())) {
- installCsrfToken()
+ installCsrfToken(call = this@loginPage)
hiddenInput {
name = "tokenId"
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
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" }
}
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
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
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
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)
val isBanned: Boolean = false
) : DataDocument<NationData> {
companion object : TableHolder<NationData> {
- private val logger: Logger = LoggerFactory.getLogger(NationData::class.java)
-
override val Table = DocumentTable<NationData>()
override suspend fun initialize() {
}
fun unknown(id: Id<NationData>): NationData {
- logger.warn("Unable to find nation with Id $id - did it CTE?")
+ NationsLogger.warn("Unable to find nation with Id $id - did it CTE?")
return NationData(id, "Unknown Nation", "https://www.nationstates.net/images/flags/exnation.png")
}
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.*
}
}
-context(ApplicationCall)
-fun FlowContent.commentBox(comment: CommentRenderData, loggedInAs: Id<NationData>?, viewingUserPage: Boolean = false) {
+fun FlowContent.commentBox(comment: CommentRenderData, loggedInAs: Id<NationData>?, viewingUserPage: Boolean = false, call: ApplicationCall) {
if (comment.submittedBy.isBanned && !viewingUserPage && loggedInAs != comment.submittedBy.id && loggedInAs != OwnerNationId)
return
p {
style = "font-size:1.5em;margin-top:2.5em"
+"On factbook "
- a(href = href(Root.LorePage(comment.submittedIn))) {
+ a(href = call.href(Root.LorePage(comment.submittedIn))) {
+comment.submittedInTitle
}
}
img(src = comment.submittedBy.flag, alt = "Flag of ${comment.submittedBy.name}", classes = "flag-icon")
span(classes = "author-name") {
+Entities.nbsp
- a(href = href(Root.User.ById(comment.submittedBy.id))) {
+ a(href = call.href(Root.User.ById(comment.submittedBy.id))) {
+comment.submittedBy.name
}
}
}
div(classes = "comment") {
- +comment.contentsHtml
+ append(comment.contentsHtml)
comment.lastEdit?.let { lastEdit ->
p {
style = "font-size:0.8em"
}
p {
style = "font-size:0.8em"
- a(href = href(Root.Comments.ViewPage(comment.id))) {
+ a(href = call.href(Root.Comments.ViewPage(comment.id))) {
+"Permalink"
}
+Entities.nbsp
a(href = "#", classes = "copy-text") {
- attributes["data-text"] = "$MainDomainName${href(Root.Comments.ViewPage(comment.id))}"
+ attributes["data-text"] = "$MainDomainName${call.href(Root.Comments.ViewPage(comment.id))}"
+"(Copy)"
}
+Entities.nbsp
+"\u2022"
+Entities.nbsp
- a(href = href(Root.Comments.DeleteConfirmPage(comment.id)), classes = "comment-delete-link") {
+ a(href = call.href(Root.Comments.DeleteConfirmPage(comment.id)), classes = "comment-delete-link") {
+"Delete"
}
}
+"Replies:"
for (reply in comment.replyLinks) {
+" "
- a(href = href(Root.Comments.ViewPage(reply))) {
+ a(href = call.href(Root.Comments.ViewPage(reply))) {
+">>$reply"
}
}
}
if (loggedInAs == comment.submittedBy.id) {
- val formPath = href(Root.Comments.EditPost(comment.id))
+ val formPath = call.href(Root.Comments.EditPost(comment.id))
form(action = formPath, method = FormMethod.post, classes = "comment-input comment-edit-box") {
id = "comment-edit-box-${comment.id}"
div(classes = "comment-preview")
name = "comment"
+comment.contentsRaw
}
- installCsrfToken()
+ installCsrfToken(call = call)
submitInput { value = "Edit Comment" }
button(classes = "comment-cancel-edit evil") {
attributes["data-edit-id"] = "comment-edit-box-${comment.id}"
}
}
-context(ApplicationCall)
-fun FlowContent.commentInput(pagePathParts: List<String>, commentingAs: NationData?) {
+fun FlowContent.commentInput(pagePathParts: List<String>, commentingAs: NationData?, call: ApplicationCall) {
if (commentingAs == null) {
p {
- a(href = href(Root.Auth.LoginPage())) { +"Log in" }
+ a(href = call.href(Root.Auth.LoginPage())) { +"Log in" }
+" to comment"
}
return
}
- form(action = href(Root.Comments.NewPost(path = pagePathParts)), method = FormMethod.post, classes = "comment-input") {
+ form(action = call.href(Root.Comments.NewPost(path = pagePathParts)), method = FormMethod.post, classes = "comment-input") {
div(classes = "comment-preview")
textArea(classes = "comment-markup") {
name = "comment"
}
- installCsrfToken()
+ installCsrfToken(call = call)
submitInput { value = "Submit Comment" }
}
}
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
}
for (comment in comments)
- commentBox(comment, currNation?.id, viewingUserPage = true)
+ commentBox(comment, currNation?.id, viewingUserPage = true, call = this@recentCommentsPage)
}
}
}
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" }
}
}
+"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)]"
}
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.*
}?.filterNotNull()?.toMap()?.let { TreeNode.DirNode(it) }
}
-context(ApplicationCall)
-private fun UL.render(path: StoragePath, childNodes: Map<String, TreeNode>) {
+private fun UL.render(path: StoragePath, childNodes: Map<String, TreeNode>, call: ApplicationCall) {
val sortedChildren = childNodes.sortedAsNodes()
for ((name, child) in sortedChildren)
- render(path / name, child)
+ render(path / name, child, call = call)
li {
style = "list-style:none"
p {
- form(action = href(Root.Admin.Vfs.Upload(path.elements)), method = FormMethod.post, encType = FormEncType.multipartFormData) {
- installCsrfToken()
+ form(action = call.href(Root.Admin.Vfs.Upload(path.elements)), method = FormMethod.post, encType = FormEncType.multipartFormData) {
+ installCsrfToken(call = call)
label {
fileInput(name = "uploaded")
+"Upload File"
}
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"
}
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)"
}
}
}
-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)
}
}
}
}
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"
}
}
- render(path, tree.children)
+ render(path, tree.children, call = this@adminViewVfs)
}
}
}
}?.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)
}
}
}
submitInput { value = "Cancel Copy" }
}
}
- renderForCopy(from, StoragePath.Root, tree)
+ renderForCopy(from, StoragePath.Root, tree, call = this@adminShowCopyFile)
}
}
}
}
+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" }
}
}
}
+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" }
}
}
import info.mechyrdia.route.Root
import info.mechyrdia.route.href
import info.mechyrdia.route.installCsrfToken
-import io.ktor.server.application.*
-import io.ktor.server.sessions.*
+import io.ktor.server.application.ApplicationCall
+import io.ktor.server.sessions.get
+import io.ktor.server.sessions.sessions
import kotlinx.coroutines.flow.toList
-import kotlinx.html.*
+import kotlinx.html.HTML
+import kotlinx.html.a
+import kotlinx.html.h1
+import kotlinx.html.id
+import kotlinx.html.p
+import kotlinx.html.section
fun ApplicationCall.currentUserPage(): Nothing {
val currNationId = sessions.get<UserSession>()?.nationId
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)
}
}
}
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
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
get() = attributes.immutableEntries
operator fun String.invoke(attributes: Map<String, String> = emptyMap(), namespace: String? = null, isInline: Boolean = false, block: (XmlTag.() -> Unit)? = null) = XmlTag(this, consumer, attributes, namespace, isInline, block == null).visit(block ?: emptyBlock)
+
+ operator fun XmlInsertable.unaryPlus() = intoXml()
+}
+
+interface XmlInsertable {
+ fun XmlTag.intoXml()
}
private val emptyBlock: XmlTag.() -> Unit = {}
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
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) {
import info.mechyrdia.data.StoragePath
import info.mechyrdia.route.Root
import info.mechyrdia.route.href
-import io.ktor.server.application.*
-import kotlinx.html.UL
-import kotlinx.html.a
-import kotlinx.html.li
-import kotlinx.html.ul
+import io.ktor.server.application.ApplicationCall
+import kotlinx.html.*
import java.text.Collator
-import java.util.*
+import java.util.Locale
-data class ArticleNode(val name: String, val title: String, val subNodes: List<ArticleNode>?)
+data class ArticleNode(val name: String, val title: ArticleTitle, val subNodes: List<ArticleNode>?)
suspend fun rootArticleNodeList(): List<ArticleNode> = StoragePath.articleDir.toArticleNode().subNodes.orEmpty()
.sortedBy { it.second }
.map { (it, _) -> it }
-private fun List<ArticleNode>.sortedAsArticles() = sortedLexically { it.title }.sortedBy { it.subNodes == null }
+private fun List<ArticleNode>.sortedAsArticles() = sortedLexically { it.title.title }.sortedBy { it.subNodes == null }
private val String.isViewable: Boolean
get() = Configuration.Current.isDevMode || !(endsWith(".wip") || endsWith(".old"))
val StoragePath.isViewable: Boolean
get() = name.isViewable
-context(ApplicationCall)
-fun List<ArticleNode>.renderInto(list: UL, base: List<String> = emptyList(), format: LoreArticleFormat = LoreArticleFormat.HTML) {
+fun List<ArticleNode>.renderInto(list: UL, base: List<String> = emptyList(), format: LoreArticleFormat = LoreArticleFormat.HTML, call: ApplicationCall) {
for (node in this)
if (node.isViewable)
list.li {
val nodePath = base + node.name
- a(href = href(Root.LorePage(nodePath, format))) { +node.title }
+ a(href = call.href(Root.LorePage(nodePath, format))) {
+ style = node.title.css
+ +node.title.title
+ }
node.subNodes?.let { subNodes ->
ul {
- subNodes.renderInto(this, nodePath, format)
+ subNodes.renderInto(this, nodePath, format, call = call)
}
}
}
}
suspend fun StoragePath.toFriendlyPageTitle() = ArticleTitleCache.get(this)
- ?: if (elements.size > 1)
- elements.lastOrNull()?.split('-')?.joinToString(separator = " ") { word ->
- word.lowercase().replaceFirstChar { it.titlecase() }
- }.orEmpty()
- else TOC_TITLE
+ ?: ArticleTitle(
+ if (elements.size > 1)
+ elements.lastOrNull()?.split('-')?.joinToString(separator = " ") { word ->
+ word.lowercase().replaceFirstChar { it.titlecase() }
+ }.orEmpty()
+ else TOC_TITLE
+ )
suspend fun StoragePath.toFriendlyPathTitle(): String {
val lorePath = elements.drop(1)
if (lorePath.isEmpty()) return TOC_TITLE
return lorePath.indices.mapSuspend { index ->
- StoragePath(lorePath.take(index + 1)).toFriendlyPageTitle()
+ StoragePath(lorePath.take(index + 1)).toFriendlyPageTitle().title
}.joinToString(separator = " - ")
}
package info.mechyrdia.lore
-import info.mechyrdia.data.FileStorage
import info.mechyrdia.data.StoragePath
-object ArticleTitleCache : FileDependentCache<String>() {
- override suspend fun processFile(path: StoragePath): String? {
+data class ArticleTitle(val title: String, val css: String = "")
+
+object ArticleTitleCache : FileDependentCache<ArticleTitle>() {
+ override suspend fun processFile(path: StoragePath): ArticleTitle? {
if (path !in StoragePath.articleDir)
return null
- val bytes = FileStorage.instance.readFile(path) ?: return null
- val text = String(bytes)
+ val factbookAst = FactbookLoader.loadFactbook(path.elements.drop(1)) ?: return null
+
+ val title = factbookAst
+ .firstNotNullOfOrNull { node ->
+ (node as? ParserTreeNode.Tag)?.takeIf { tag -> tag.tag == "h1" }
+ }
+ ?.subNodes
+ ?.treeToText()
+ ?: return null
+
+ val css = listOfNotNull(
+ if (factbookAst.any { it is ParserTreeNode.Tag && it.tag == "redirect" }) "font-style:italic" else null,
+
+ // Only used in dev-mode
+ if (path.name.endsWith(".wip")) "opacity:0.675" else null,
+ if (path.name.endsWith(".old")) "text-decoration:line-through" else null,
+ ).joinToString(separator = ";")
- return text
- .lineSequence()
- .first()
- .removePrefix("[h1]")
- .removeSuffix("[/h1]")
+ return ArticleTitle(title, css)
}
}
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
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
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
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
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 {
) { "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)
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()
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()
package info.mechyrdia.lore
import info.mechyrdia.JsonStorageCodec
-import io.ktor.util.*
import kotlinx.html.*
-import kotlinx.html.org.w3c.dom.events.Event
-import kotlinx.html.stream.createHTML
+import kotlinx.html.org.w3c.dom.events.*
+import kotlinx.html.stream.*
import java.time.Instant
-import kotlin.text.toCharArray
typealias HtmlBuilderContext = Unit
typealias HtmlBuilderSubject = TagConsumer<*>.() -> Any?
-context(T)
-operator fun <T : Tag> (TagConsumer<*>.() -> Any?).unaryPlus() = with(HtmlLexerTagConsumer(consumer)) { this@unaryPlus() }
+fun <T : Tag> T.append(block: TagConsumer<*>.() -> Any?) = HtmlLexerTagConsumer(consumer).block()
-fun (TagConsumer<*>.() -> Any?).toFragment() = createHTML().also { builder ->
- with(HtmlLexerTagConsumer(builder)) { this@toFragment() }
+fun (TagConsumer<*>.() -> Any?).toFragmentString() = createHTML().also { builder ->
+ with(HtmlLexerTagConsumer(builder)) { this@toFragmentString() }
}.finalize()
class HtmlLexerTagConsumer private constructor(private val downstream: TagConsumer<*>) : TagConsumer<Unit> {
}
}
-context(C)
-operator fun <T, C : TagConsumer<T>> String.unaryPlus() = onTagContent(this)
-
-context(C)
-operator fun <T, C : TagConsumer<T>> Entities.unaryPlus() = onTagContentEntity(this)
+fun TagConsumer<*>.append(text: String) = onTagContent(text)
+fun TagConsumer<*>.append(entity: Entities) = onTagContentEntity(entity)
fun <T, C : TagConsumer<T>> C.unsafe(block: Unsafe.() -> Unit) = onTagContentUnsafe(block)
null
else if (isParagraph(HtmlLexerProcessor.inlineTags)) {
val concat = HtmlLexerProcessor.combineInline(env, this)
- ({ p { +concat } })
+ ({ p { append(concat) } })
} else
HtmlLexerProcessor.combineInline(env, this)
val content = env.processTree(subNodes)
return {
- +if (param == null) "[$tag]" else "[$tag=$param]"
+ append(if (param == null) "[$tag]" else "[$tag=$param]")
content()
- +"[/$tag]"
+ append("[/$tag]")
}
}
override fun processText(env: LexerTagEnvironment<HtmlBuilderContext, HtmlBuilderSubject>, text: String): HtmlBuilderSubject {
- return { +text }
+ return { append(text) }
}
override fun processLineBreak(env: LexerTagEnvironment<HtmlBuilderContext, HtmlBuilderSubject>): HtmlBuilderSubject {
return if (nodes.shouldSplitSections()) {
val pageParts = nodes.splitSections().map { combineBlock(env, it) }
({
- for (pagePart in pageParts) section { +pagePart }
+ for (pagePart in pageParts) section { append(pagePart) }
})
} else
combineBlock(env, nodes)
})
} else if (nodes.isParagraph(inlineTags)) {
val concat = combineInline(env, nodes)
- ({ p { +concat } })
+ ({ p { append(concat) } })
} else
combineInline(env, nodes)
}
tagCreator {
for ((name, value) in calculatedAttributes)
attributes[name] = value
- +body
+ append(body)
}
}
}
MOMENT(HtmlTextBodyLexerTag { _, _, content ->
val instant = content.toLongOrNull()?.let { Instant.ofEpochMilli(it) }
if (instant == null)
- ({ +content })
+ ({ append(content) })
else
({ dateTime(instant) })
}),
} else {
val foreign = content.treeToText()
({
- +foreign
+ append(foreign)
})
}
}
val id = sanitizeId(content)
if (id == null)
- ({ +">>$content" })
+ ({ append(">>$content") })
else
({
a(href = "/comment/view/$id") {
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
private const val PAGE_PATH_KEY = "PAGE_PATH"
private const val INSTANT_NOW_KEY = "INSTANT_NOW"
- context(ApplicationCall)
- fun defaults() = defaults(StoragePath(request.path()))
-
fun defaults(lorePath: StoragePath) = defaults(lorePath.elements.drop(1))
fun defaults(lorePath: List<String>) = mapOf(
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
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 {
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()
package info.mechyrdia.lore
import java.time.Instant
-import kotlin.math.*
+import kotlin.math.acos
+import kotlin.math.asin
+import kotlin.math.atan
+import kotlin.math.atan2
+import kotlin.math.cbrt
+import kotlin.math.ceil
+import kotlin.math.cos
+import kotlin.math.floor
+import kotlin.math.hypot
+import kotlin.math.log
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.math.pow
+import kotlin.math.round
+import kotlin.math.sin
+import kotlin.math.sqrt
+import kotlin.math.tan
+import kotlin.math.truncate
fun <T : Number> ParserTree.treeToNumberOrNull(convert: String.() -> T?) = treeToText().convert()
package info.mechyrdia.lore
-import io.ktor.util.*
import kotlinx.html.*
import java.time.Instant
return {
span {
attributes["data-format"] = dataFormat
- +content
+ append(content)
}
}
}
({
div {
attributes["data-format"] = "code"
- pre { +content }
+ pre { append(content) }
}
})
}),
({
div {
attributes["data-format"] = "error"
- +content
+ append(content)
}
})
}),
({
div {
alignment?.let { attributes["data-align"] = it }
- +content
+ append(content)
}
})
}),
({
div {
alignment?.let { attributes["data-aside"] = it }
- +content
+ append(content)
}
})
}),
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 ->
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
@Serializable
private data class ExternalLink(
val url: String,
- val text: String
+ val text: String,
)
suspend fun loadExternalLinks(): List<NavItem> {
) + 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)
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"),
val text: String,
val textIsHtml: Boolean = false,
val aClasses: String? = null,
- val linkAttributes: Map<String, String> = emptyMap()
+ val linkAttributes: Map<String, String> = emptyMap(),
) : NavItem() {
override fun DIV.display() {
a(href = to, classes = aClasses) {
}
companion object {
- context(ApplicationCall)
- fun withCsrf(to: String, text: String, textIsHtml: Boolean = false, aClasses: String? = null, extraAttributes: Map<String, String> = emptyMap): NavLink {
+ fun withCsrf(to: String, text: String, textIsHtml: Boolean = false, aClasses: String? = null, extraAttributes: Map<String, String> = emptyMap, call: ApplicationCall): NavLink {
return NavLink(
to = to,
text = text,
aClasses = aClasses,
linkAttributes = extraAttributes + mapOf(
"data-method" to "post",
- "data-csrf-token" to createCsrfToken(to)
+ "data-csrf-token" to call.createCsrfToken(to)
)
)
}
+
+ fun ofArticleTitle(to: String, title: ArticleTitle, aClasses: String? = null, extraAttributes: Map<String, String> = emptyMap): NavLink {
+ return NavLink(
+ to = to,
+ text = title.title,
+ textIsHtml = false,
+ aClasses = aClasses,
+ linkAttributes = extraAttributes + if (title.css.isNotEmpty())
+ mapOf(
+ "style" to extraAttributes["style"]?.let { "$it;" }.orEmpty() + "font-style:italic"
+ )
+ else emptyMap()
+ )
+ }
}
}
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
}
}
-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("/")}")
}
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",
"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")
lang = "en"
head {
- initialHead(pageTitle, ogData)
+ initialHead(pageTitle, ogData, call = this@page)
for (font in preloadFonts)
link(
lang = "en"
head {
- initialHead(pageTitle, ogData)
+ initialHead(pageTitle, ogData, call = this@rawPage)
link(rel = "stylesheet", type = "text/css", href = "/static/raw.css")
}
lang = "en"
head {
- initialHead(pageTitle, null)
+ initialHead(pageTitle, null, call = this@adminPage)
for (font in adminPreloadFonts)
link(
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 {
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
+Entities.nbsp
}
-context(ApplicationCall)
-private fun FlowContent.breadCrumbs(links: List<Pair<Root.LorePage, String>>) = p {
- links.joinToHtml(Tag::breadCrumbArrow) { (url, text) ->
- a(href = href(url)) { +text }
+private fun FlowContent.breadCrumbs(links: List<Pair<Root.LorePage, ArticleTitle>>, call: ApplicationCall) = p {
+ joined(links, Tag::breadCrumbArrow) { (url, articleTitle) ->
+ a(href = call.href(url)) {
+ style = articleTitle.css
+ +articleTitle.title
+ }
}
}
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)
}
}
}
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" }
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)
}
}
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)
}
}
}
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" }
}
}
- finalSection(pagePath, canCommentAs, comments, totalsData)
+ finalSection(pagePath, canCommentAs, comments, totalsData, call = this@loreArticlePage)
}
}
-context(ApplicationCall)
-private fun SectioningOrFlowContent.finalSection(pagePathParts: List<String>, canCommentAs: NationData?, comments: List<CommentRenderData>, totalsData: PageVisitTotals) {
+private fun SectioningOrFlowContent.finalSection(pagePathParts: List<String>, canCommentAs: NationData?, comments: List<CommentRenderData>, totalsData: PageVisitTotals, call: ApplicationCall) {
section {
h2 {
a { id = "comments" }
+"Comments"
}
- commentInput(pagePathParts, canCommentAs)
+ commentInput(pagePathParts, canCommentAs, call = call)
for (comment in comments)
- commentBox(comment, canCommentAs?.id)
+ commentBox(comment, canCommentAs?.id, call = call)
guestbook(totalsData)
}
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
April1stMode.entries.firstOrNull { mode -> mode.name.equals(modeCookie, ignoreCase = true) }
} ?: April1stMode.DEFAULT
-class JoinToHtmlConsumer<E>(val iterator: Iterator<E>) {
- inline fun <T : Tag> T.invokeReceiver(separator: T.() -> Unit, body: T.(E) -> Unit) {
- var isFirst = true
- for (item in iterator) {
- if (isFirst)
- isFirst = false
- else
- separator()
- body(item)
- }
- }
-
- context(T)
- inline operator fun <T : Tag> invoke(separator: T.() -> Unit, body: T.(E) -> Unit) {
- invokeReceiver(separator, body)
+fun <T : Tag, E> T.joined(iterable: Iterable<E>, separator: T.() -> Unit, body: T.(E) -> Unit) {
+ var isFirst = true
+ for (item in iterable) {
+ if (isFirst)
+ isFirst = false
+ else
+ separator()
+ body(item)
}
}
-val <E> Iterable<E>.joinToHtml: JoinToHtmlConsumer<E>
- get() = JoinToHtmlConsumer(iterator())
-
inline fun <reified E : Enum<E>> FlowOrInteractiveOrPhrasingContent.preference(inputName: String, current: E, crossinline localize: (E) -> String) {
val serializer = serializer<E>() as? KeyedEnumSerializer<E> ?: throw UnsupportedOperationException("Serializer for ${E::class.simpleName} has not been declared as KeyedEnumSerializer")
val entries = serializer.entries
- entries.joinToHtml(Tag::br) { option ->
+ joined(entries, Tag::br) { option ->
label {
radioInput(name = inputName, classes = "pref-$inputName") {
value = serializer.getKey(option) ?: "null"
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
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") {
}.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) {
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
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? {
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
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"
val image: RssChannelImage? = null,
val categories: List<RssCategory> = emptyList(),
val items: List<RssItem> = emptyList(),
-)
-
-fun <T, C : XmlTagConsumer<T>> C.rss(rssChannel: RssChannel) = declaration()
- .root("rss") {
- attributes["version"] = "2.0"
+): XmlInsertable {
+ override fun XmlTag.intoXml() {
"channel" {
- "title" { +rssChannel.title }
- "link" { +rssChannel.link }
- "description" { +rssChannel.description }
+ "title" { +title }
+ "link" { +link }
+ "description" { +description }
- if (rssChannel.language != null) "language" { +rssChannel.language }
- if (rssChannel.copyright != null) "copyright" { +rssChannel.copyright }
- if (rssChannel.managingEditor != null) "managingEditor" { +rssChannel.managingEditor }
- if (rssChannel.webMaster != null) "webMaster" { +rssChannel.webMaster }
- if (rssChannel.pubDate != null) "pubDate" { +rssChannel.pubDate.rssValue }
- if (rssChannel.lastBuildDate != null) "lastBuildDate" { +rssChannel.lastBuildDate.rssValue }
- if (rssChannel.ttl != null) "ttl" { +rssChannel.ttl.toString() }
+ if (language != null) "language" { +language }
+ if (copyright != null) "copyright" { +copyright }
+ if (managingEditor != null) "managingEditor" { +managingEditor }
+ if (webMaster != null) "webMaster" { +webMaster }
+ if (pubDate != null) "pubDate" { +pubDate.rssValue }
+ if (lastBuildDate != null) "lastBuildDate" { +lastBuildDate.rssValue }
+ if (ttl != null) "ttl" { +ttl.toString() }
- if (rssChannel.image != null) +rssChannel.image
+ if (image != null) +image
- for (category in rssChannel.categories)
+ for (category in categories)
+category
- for (item in rssChannel.items)
+ for (item in items)
+item
}
}
+}
+
+fun <T, C : XmlTagConsumer<T>> C.rss(rssChannel: RssChannel) = declaration()
+ .root("rss") {
+ attributes["version"] = "2.0"
+ +rssChannel
+ }
data class RssItemEnclosure(
val url: String,
val length: Long,
val type: String,
-)
-
-context(XmlTag)
-operator fun RssItemEnclosure.unaryPlus() = "enclosure"(
- attributes = mapOf(
- "url" to url,
- "length" to length.toString(),
- "type" to type,
- )
-)
+) : XmlInsertable {
+ override fun XmlTag.intoXml() {
+ "enclosure"(
+ attributes = mapOf(
+ "url" to url,
+ "length" to length.toString(),
+ "type" to type,
+ )
+ )
+ }
+}
data class RssItem(
val title: String? = null,
val enclosure: RssItemEnclosure? = null,
val pubDate: Instant? = null,
val categories: List<RssCategory> = emptyList(),
-) {
+) : XmlInsertable {
init {
require(title != null || description != null) { "Either title or description must be provided, got null for both" }
}
-}
-
-context(XmlTag)
-operator fun RssItem.unaryPlus() = "item" {
- if (title != null) "title" { +title }
- if (description != null) "description" { +description }
- if (link != null) "link" { +link }
- if (author != null) "author" { +author }
- if (comments != null) "comments" { +comments }
- if (enclosure != null) +enclosure
- if (pubDate != null) "pubDate" { +pubDate.rssValue }
- for (category in categories)
- +category
+ override fun XmlTag.intoXml() {
+ "item" {
+ if (title != null) "title" { +title }
+ if (description != null) "description" { +description }
+ if (link != null) "link" { +link }
+ if (author != null) "author" { +author }
+ if (comments != null) "comments" { +comments }
+ if (enclosure != null) +enclosure
+ if (pubDate != null) "pubDate" { +pubDate.rssValue }
+
+ for (category in categories)
+ +category
+ }
+ }
}
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
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
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,
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
import info.mechyrdia.Configuration\r
import info.mechyrdia.MainDomainName\r
import info.mechyrdia.OpenAiConfig\r
-import info.mechyrdia.data.*\r
+import info.mechyrdia.data.DataDocument\r
+import info.mechyrdia.data.DocumentTable\r
+import info.mechyrdia.data.Id\r
+import info.mechyrdia.data.InstantNullableSerializer\r
+import info.mechyrdia.data.MONGODB_ID_KEY\r
+import info.mechyrdia.data.NationData\r
+import info.mechyrdia.data.TableHolder\r
import info.mechyrdia.lore.RobotFactbookLoader\r
-import io.ktor.client.*\r
-import io.ktor.client.engine.java.*\r
-import io.ktor.client.plugins.*\r
-import io.ktor.client.plugins.contentnegotiation.*\r
-import io.ktor.client.plugins.logging.*\r
-import io.ktor.client.request.*\r
-import io.ktor.http.*\r
-import io.ktor.serialization.kotlinx.json.*\r
-import kotlinx.coroutines.*\r
-import kotlinx.coroutines.flow.*\r
+import io.ktor.client.HttpClient\r
+import io.ktor.client.engine.java.Java\r
+import io.ktor.client.plugins.ClientRequestException\r
+import io.ktor.client.plugins.HttpRequestRetry\r
+import io.ktor.client.plugins.contentnegotiation.ContentNegotiation\r
+import io.ktor.client.plugins.defaultRequest\r
+import io.ktor.client.plugins.logging.LogLevel\r
+import io.ktor.client.plugins.logging.Logging\r
+import io.ktor.client.request.header\r
+import io.ktor.http.ContentType\r
+import io.ktor.http.HttpHeaders\r
+import io.ktor.http.withCharset\r
+import io.ktor.serialization.kotlinx.json.json\r
+import kotlinx.coroutines.CoroutineName\r
+import kotlinx.coroutines.CoroutineScope\r
+import kotlinx.coroutines.Deferred\r
+import kotlinx.coroutines.Job\r
+import kotlinx.coroutines.SupervisorJob\r
+import kotlinx.coroutines.async\r
+import kotlinx.coroutines.awaitAll\r
+import kotlinx.coroutines.currentCoroutineContext\r
+import kotlinx.coroutines.delay\r
+import kotlinx.coroutines.flow.Flow\r
+import kotlinx.coroutines.flow.filter\r
+import kotlinx.coroutines.flow.flow\r
+import kotlinx.coroutines.flow.map\r
+import kotlinx.coroutines.flow.mapNotNull\r
+import kotlinx.coroutines.flow.onCompletion\r
+import kotlinx.coroutines.flow.onEach\r
+import kotlinx.coroutines.job\r
+import kotlinx.coroutines.launch\r
import kotlinx.serialization.SerialName\r
import kotlinx.serialization.Serializable\r
import org.slf4j.Logger\r
import org.slf4j.LoggerFactory\r
import java.time.Instant\r
+import kotlin.collections.List\r
+import kotlin.collections.Map\r
+import kotlin.collections.Set\r
+import kotlin.collections.buildMap\r
+import kotlin.collections.component1\r
+import kotlin.collections.component2\r
+import kotlin.collections.emptyMap\r
+import kotlin.collections.emptySet\r
+import kotlin.collections.flatMap\r
+import kotlin.collections.fold\r
+import kotlin.collections.forEach\r
+import kotlin.collections.iterator\r
+import kotlin.collections.joinToString\r
+import kotlin.collections.listOf\r
+import kotlin.collections.map\r
+import kotlin.collections.minus\r
+import kotlin.collections.mutableListOf\r
+import kotlin.collections.plus\r
+import kotlin.collections.set\r
+import kotlin.collections.toList\r
import kotlin.random.Random\r
import kotlin.time.Duration.Companion.minutes\r
\r
+private val RobotServiceLogger: Logger = LoggerFactory.getLogger("info.mechyrdia.robot.RobotServiceKt")\r
+\r
val RobotGlobalsId = Id<RobotGlobals>("RobotGlobalsInstance")\r
\r
@Serializable\r
try {\r
robotClient.deleteThread(threadId)\r
} catch (ex: ClientRequestException) {\r
- logger.warn("Unable to delete thread at ID $threadId", ex)\r
+ RobotServiceLogger.warn("Unable to delete thread at ID $threadId", ex)\r
}\r
(RobotGlobals.get() ?: RobotGlobals()).minusThread(threadId).save()\r
}\r
try {\r
robotClient.deleteThread(threadId)\r
} catch (ex: ClientRequestException) {\r
- logger.warn("Unable to delete thread at ID $threadId", ex)\r
+ RobotServiceLogger.warn("Unable to delete thread at ID $threadId", ex)\r
}\r
return copy(ongoingThreadIds = emptySet())\r
}\r
try {\r
robotClient.deleteFile(oldId)\r
} catch (ex: ClientRequestException) {\r
- logger.warn("Unable to delete file $name at ID $oldId", ex)\r
+ RobotServiceLogger.warn("Unable to delete file $name at ID $oldId", ex)\r
}\r
}\r
\r
this[name] = newId\r
onNewFileId?.invoke(newId)\r
\r
- logger.info("Factbook $name has been uploaded")\r
+ RobotServiceLogger.info("Factbook $name has been uploaded")\r
}\r
}\r
\r
robotGlobals = robotGlobals.copy(vectorStoreId = vsId).save()\r
}\r
\r
- logger.info("Vector store has been created")\r
+ RobotServiceLogger.info("Vector store has been created")\r
\r
poll {\r
robotClient.getVectorStore(vectorStoreId).status == "completed"\r
}\r
\r
- logger.info("Vector store creation is complete")\r
+ RobotServiceLogger.info("Vector store creation is complete")\r
\r
if (robotGlobals.assistantId == null)\r
robotGlobals = robotGlobals.copy(\r
).id\r
).save()\r
\r
- logger.info("Assistant has been created")\r
+ RobotServiceLogger.info("Assistant creation is complete")\r
+ \r
+ maintenanceScope.launch {\r
+ while (true) {\r
+ delay(30.minutes)\r
+ \r
+ launch(SupervisorJob(currentCoroutineContext().job)) {\r
+ performMaintenance()\r
+ }\r
+ }\r
+ }\r
}\r
\r
suspend fun performMaintenance() {\r
robotClient.addFileToVectorStore(vectorStoreId, fileId)\r
}\r
\r
- logger.info("Vector store has been updated")\r
+ RobotServiceLogger.info("Vector store has been updated")\r
\r
poll {\r
robotClient.getVectorStore(vectorStoreId).fileCounts.inProgress == 0\r
}\r
\r
- logger.info("Vector store update is complete")\r
+ RobotServiceLogger.info("Vector store update is complete")\r
}\r
\r
suspend fun reset() {\r
try {\r
robotClient.deleteAssistant(it)\r
} catch (ex: ClientRequestException) {\r
- logger.warn("Unable to delete assistant at ID $it", ex)\r
+ RobotServiceLogger.warn("Unable to delete assistant at ID $it", ex)\r
}\r
}\r
}\r
try {\r
robotClient.deleteVectorStore(it)\r
} catch (ex: ClientRequestException) {\r
- logger.warn("Unable to delete vector-store at ID $it", ex)\r
+ RobotServiceLogger.warn("Unable to delete vector-store at ID $it", ex)\r
}\r
}\r
}\r
try {\r
robotClient.deleteFile(it)\r
} catch (ex: ClientRequestException) {\r
- logger.warn("Unable to delete file at ID $it", ex)\r
+ RobotServiceLogger.warn("Unable to delete file at ID $it", ex)\r
}\r
}\r
}\r
}\r
\r
companion object {\r
- private val logger: Logger = LoggerFactory.getLogger(RobotService::class.java)\r
- \r
private val maintenanceScope = CoroutineScope(SupervisorJob() + CoroutineName("robot-service-maintenance"))\r
\r
- private val instanceHolder by lazy {\r
- CoroutineScope(CoroutineName("robot-service-initialization")).async {\r
- Configuration.Current.openAi?.let(::RobotService)?.apply {\r
- initialize()\r
+ private val startInitializing = Job()\r
+ \r
+ private val instanceHolder = CoroutineScope(CoroutineName("robot-service-initialization")).async {\r
+ startInitializing.join()\r
+ Configuration.Current.openAi?.let { config ->\r
+ status = RobotServiceStatus.LOADING\r
+ RobotService(config).apply { initialize() }\r
+ }\r
+ }.also { deferred ->\r
+ deferred.invokeOnCompletion { ex ->\r
+ status = if (ex != null) {\r
+ RobotServiceLogger.error("RobotService failed to initialize", ex)\r
+ RobotServiceStatus.FAILED\r
+ } else {\r
+ RobotServiceLogger.info("RobotService successfully initialized")\r
+ RobotServiceStatus.READY\r
}\r
}\r
}\r
\r
- var status: RobotServiceStatus = if (Configuration.Current.openAi != null) RobotServiceStatus.LOADING else RobotServiceStatus.NOT_CONFIGURED\r
+ var status: RobotServiceStatus = RobotServiceStatus.NOT_CONFIGURED\r
private set\r
\r
suspend fun getInstance() = try {\r
null\r
}\r
\r
- fun initialize() {\r
- instanceHolder.invokeOnCompletion { ex ->\r
- status = if (ex != null) {\r
- logger.error("RobotService failed to initialize", ex)\r
- RobotServiceStatus.FAILED\r
- } else {\r
- logger.info("RobotService successfully initialized")\r
- RobotServiceStatus.READY\r
- }\r
- }\r
- \r
- maintenanceScope.launch {\r
- getInstance()?.let { instance ->\r
- while (true) {\r
- delay(30.minutes)\r
- \r
- launch(SupervisorJob(currentCoroutineContext().job)) {\r
- instance.performMaintenance()\r
- }\r
- }\r
- }\r
- }\r
+ fun start() {\r
+ startInitializing.complete()\r
}\r
}\r
}\r
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
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
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")
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"
}
}
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"
}
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
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
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)
}
}
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
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 {
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 {
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"
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
)
}
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"
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,
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" }
}
}
<encoder>
<charset>UTF-8</charset>
- <pattern>%d{yyyy-MM-dd/HH:mm:ss.SSS} [%thread] %.-1level \(%logger\) - %msg%n</pattern>
+ <pattern>%d{yyyy-MM-dd/HH:mm:ss.SSS} [%thread] %.-1level \(%logger\) @%X{ktor_call_id:-no_call} - %msg%n</pattern>
</encoder>
</appender>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
- <pattern>%d{yyyy-MM-dd/HH:mm:ss.SSS} [%thread] %.-1level \(%logger\) - %msg%n</pattern>
+ <pattern>%d{yyyy-MM-dd/HH:mm:ss.SSS} [%thread] %.-1level \(%logger\) @%X{ktor_call_id:-no_call} - %msg%n</pattern>
</encoder>
</appender>