From 270092529486c72505c3ea8807b9f125d98e88ff Mon Sep 17 00:00:00 2001 From: Lanius Trolling Date: Thu, 19 Oct 2023 11:18:08 -0400 Subject: [PATCH] Correctly pluralize page-view numbers AND implement factbook page caching --- src/main/kotlin/info/mechyrdia/Factbooks.kt | 14 ++-- .../kotlin/info/mechyrdia/auth/views_login.kt | 77 ++++++++++--------- .../info/mechyrdia/data/views_comment.kt | 4 +- src/main/kotlin/info/mechyrdia/data/visits.kt | 4 +- .../kotlin/info/mechyrdia/lore/views_lore.kt | 28 +++++++ .../kotlin/info/mechyrdia/lore/views_prefs.kt | 3 + src/main/resources/static/init.js | 4 +- 7 files changed, 89 insertions(+), 45 deletions(-) diff --git a/src/main/kotlin/info/mechyrdia/Factbooks.kt b/src/main/kotlin/info/mechyrdia/Factbooks.kt index 93e5626..d98cbf3 100644 --- a/src/main/kotlin/info/mechyrdia/Factbooks.kt +++ b/src/main/kotlin/info/mechyrdia/Factbooks.kt @@ -49,20 +49,22 @@ fun Application.factbooks() { install(XForwardedHeaders) install(CachingHeaders) { - options { _, outgoingContent -> + options { call, outgoingContent -> if (outgoingContent is JarFileContent) CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 3600)) - else - null + else if (call.attributes.getOrNull(PageDoNotCacheAttributeKey) == true) + CachingOptions(CacheControl.NoStore(null)) + else null } } install(ConditionalHeaders) { - version { _, outgoingContent -> + version { call, outgoingContent -> if (outgoingContent is LocalFileContent) listOf(LastModifiedVersion(outgoingContent.file.lastModified())) - else - emptyList() + else call.attributes.getOrNull(FactbookLastModifiedAttributeKey)?.let { lastModified -> + listOf(LastModifiedVersion(lastModified.toEpochMilli())) + } ?: emptyList() } } diff --git a/src/main/kotlin/info/mechyrdia/auth/views_login.kt b/src/main/kotlin/info/mechyrdia/auth/views_login.kt index cce96bf..3cd4df1 100644 --- a/src/main/kotlin/info/mechyrdia/auth/views_login.kt +++ b/src/main/kotlin/info/mechyrdia/auth/views_login.kt @@ -11,51 +11,58 @@ import io.ktor.server.application.* import io.ktor.server.plugins.* import io.ktor.server.sessions.* import io.ktor.server.util.* +import io.ktor.util.* import kotlinx.html.* import java.util.concurrent.ConcurrentHashMap +val PageDoNotCacheAttributeKey = AttributeKey("Mechyrdia.PageDoNotCache") + private val nsTokenMap = ConcurrentHashMap() -suspend fun ApplicationCall.loginPage(): HTML.() -> Unit = page("Log In With NationStates", standardNavBar()) { - val tokenKey = token() - val nsToken = token() - - nsTokenMap[tokenKey] = nsToken +suspend fun ApplicationCall.loginPage(): HTML.() -> Unit { + attributes.put(PageDoNotCacheAttributeKey, true) - section { - h1 { +"Log In With NationStates" } - form(method = FormMethod.post, action = "/auth/login") { - installCsrfToken(createCsrfToken()) - - hiddenInput { - name = "token" - value = tokenKey - } - - label { - +"Nation Name" - br - textInput { - name = "nation" - placeholder = "Name of your nation without pretitle, e.g. Mechyrdia, Valentine Z, Reinkalistan, etc." + return page("Log In With NationStates", standardNavBar()) { + val tokenKey = token() + val nsToken = token() + + nsTokenMap[tokenKey] = nsToken + + section { + h1 { +"Log In With NationStates" } + form(method = FormMethod.post, action = "/auth/login") { + installCsrfToken(createCsrfToken()) + + hiddenInput { + name = "token" + value = tokenKey } - } - p { - style = "text-align:center" - button(classes = "view-checksum") { - attributes["data-token"] = "mechyrdia_$nsToken" - +"View Your Checksum" + + label { + +"Nation Name" + br + textInput { + name = "nation" + placeholder = "Name of your nation without pretitle, e.g. Mechyrdia, Valentine Z, Reinkalistan, etc." + } } - } - label { - +"Verification Checksum" - br - textInput { - name = "checksum" - placeholder = "The random text checksum generated by NationStates for verification" + p { + style = "text-align:center" + button(classes = "view-checksum") { + attributes["data-token"] = "mechyrdia_$nsToken" + +"View Your Checksum" + } + } + label { + +"Verification Checksum" + br + textInput { + name = "checksum" + placeholder = "The random text checksum generated by NationStates for verification" + } } + submitInput { value = "Log In" } } - submitInput { value = "Log In" } } } } diff --git a/src/main/kotlin/info/mechyrdia/data/views_comment.kt b/src/main/kotlin/info/mechyrdia/data/views_comment.kt index 6c3710e..89bb037 100644 --- a/src/main/kotlin/info/mechyrdia/data/views_comment.kt +++ b/src/main/kotlin/info/mechyrdia/data/views_comment.kt @@ -442,7 +442,7 @@ suspend fun ApplicationCall.commentHelpPage(): HTML.() -> Unit = page("Commentin pre { +tableDemoMarkup } unsafe { raw(tableDemoHtml) } p { - +"The format goes as [td=(width)x(height)] or [th=(width)x(height)]. If one is omitted, then the format can be [td=(width)] or [td=x(height)]" + +"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)]" } table { thead { @@ -518,7 +518,7 @@ suspend fun ApplicationCall.commentHelpPage(): HTML.() -> Unit = page("Commentin td { +"Writes text in the Pokhwalish alphabet: " span(classes = "lang-pokhwal") { - +"poKvalsqo jArgo" + +PokhwalishAlphabet.pokhwalToFontAlphabet("pokhvalsqo jargo") } } } diff --git a/src/main/kotlin/info/mechyrdia/data/visits.kt b/src/main/kotlin/info/mechyrdia/data/visits.kt index 021cae8..1b7620f 100644 --- a/src/main/kotlin/info/mechyrdia/data/visits.kt +++ b/src/main/kotlin/info/mechyrdia/data/visits.kt @@ -107,11 +107,13 @@ suspend fun ApplicationCall.processGuestbook(): PageVisitTotals { return totals } +fun Long.pluralize(singular: String, plural: String = singular + "s") = if (this == 1L) singular else plural + fun FlowContent.guestbook(totalsData: PageVisitTotals) { p { style = "font-size:0.8em" - +"This page has been visited ${totalsData.total} times by ${totalsData.totalUnique} unique visitors, most recently " + +"This page has been visited ${totalsData.total} ${totalsData.total.pluralize("time")} by ${totalsData.totalUnique} unique ${totalsData.totalUnique.pluralize("visitor")}, most recently " val mostRecent = totalsData.mostRecent if (mostRecent == null) diff --git a/src/main/kotlin/info/mechyrdia/lore/views_lore.kt b/src/main/kotlin/info/mechyrdia/lore/views_lore.kt index 6b132f3..2c3ecad 100644 --- a/src/main/kotlin/info/mechyrdia/lore/views_lore.kt +++ b/src/main/kotlin/info/mechyrdia/lore/views_lore.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.toList import kotlinx.html.* import kotlinx.serialization.Serializable import java.io.File +import java.time.Instant @Serializable data class IntroMetaData( @@ -22,6 +23,18 @@ data class IntroMetaData( get() = OpenGraphData(desc, image) } +val FactbookLastModifiedAttributeKey = AttributeKey("Mechyrdia.FactbookLastModified") + +private val File.lastSubFilesModified: Instant? + get() = if (isDirectory) + (listFiles()!!.mapNotNull { + it.lastSubFilesModified + } + Instant.ofEpochMilli(lastModified())!!).max() + else null + +private val File.lastContentModified: Instant + get() = lastSubFilesModified ?: Instant.ofEpochMilli(lastModified())!! + suspend fun ApplicationCall.loreIntroPage(): HTML.() -> Unit { val metaJsonFile = File(Configuration.CurrentConfiguration.articleDir).parentFile.combineSafe("introMeta.json") val metaData = JsonFileCodec.decodeFromString(IntroMetaData.serializer(), metaJsonFile.readText()) @@ -29,6 +42,14 @@ suspend fun ApplicationCall.loreIntroPage(): HTML.() -> Unit { val htmlFile = File(Configuration.CurrentConfiguration.articleDir).parentFile.combineSafe("intro.html") val fileHtml = htmlFile.readText() + attributes.put( + FactbookLastModifiedAttributeKey, + maxOf( + Instant.ofEpochMilli(htmlFile.lastModified()), + Instant.ofEpochMilli(metaJsonFile.lastModified()) + ) + ) + return page(metaData.title, standardNavBar(), null, metaData.ogData) { section { a { id = "page-top" } @@ -54,6 +75,13 @@ suspend fun ApplicationCall.loreArticlePage(): HTML.() -> Unit { canCommentAs.await() to comments.await() } + if (pageFile.exists()) + attributes.put(FactbookLastModifiedAttributeKey, + (comments.map { comment -> + comment.lastEdit ?: comment.submittedAt + } + pageFile.lastContentModified).max() + ) + if (pageFile.isDirectory) { val navbar = standardNavBar(pagePathParts.takeIf { it.isNotEmpty() }) diff --git a/src/main/kotlin/info/mechyrdia/lore/views_prefs.kt b/src/main/kotlin/info/mechyrdia/lore/views_prefs.kt index 44bcefd..6e8894f 100644 --- a/src/main/kotlin/info/mechyrdia/lore/views_prefs.kt +++ b/src/main/kotlin/info/mechyrdia/lore/views_prefs.kt @@ -1,5 +1,6 @@ package info.mechyrdia.lore +import info.mechyrdia.auth.PageDoNotCacheAttributeKey import info.mechyrdia.auth.createCsrfToken import info.mechyrdia.auth.installCsrfToken import info.mechyrdia.auth.verifyCsrfToken @@ -7,6 +8,8 @@ import io.ktor.server.application.* import kotlinx.html.* suspend fun ApplicationCall.changeThemePage(): HTML.() -> Unit { + attributes.put(PageDoNotCacheAttributeKey, true) + val theme = when (request.cookies["FACTBOOK_THEME"]) { "light" -> "light" "dark" -> "dark" diff --git a/src/main/resources/static/init.js b/src/main/resources/static/init.js index 4acac51..4e0a82b 100644 --- a/src/main/resources/static/init.js +++ b/src/main/resources/static/init.js @@ -267,7 +267,7 @@ thirdCell.style.textAlign = "center"; const beginLink = thirdCell.appendChild(document.createElement("a")); beginLink.href = "#"; - beginLink.append("Begin Quiz"); + beginLink.append("Begin Quiz (" + quiz.questions.length + " questions)"); beginLink.onclick = e => { e.preventDefault(); quizFunctions.renderQuestion(0); @@ -342,6 +342,8 @@ firstCell.style.textAlign = "center"; firstCell.style.fontSize = "1.5em"; firstCell.style.fontWeight = "bold"; + firstCell.append("Question " + (index + 1) + "/" + quiz.questions.length); + firstCell.append(document.createElement("br")); firstCell.append(question.asks.toString()); firstRow.appendChild(firstCell); quizRoot.appendChild(firstRow); -- 2.25.1