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()
}
}
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<Boolean>("Mechyrdia.PageDoNotCache")
+
private val nsTokenMap = ConcurrentHashMap<String, String>()
-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" }
}
}
}
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 {
td {
+"Writes text in the Pokhwalish alphabet: "
span(classes = "lang-pokhwal") {
- +"poKvalsqo jArgo"
+ +PokhwalishAlphabet.pokhwalToFontAlphabet("pokhvalsqo jargo")
}
}
}
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)
import kotlinx.html.*
import kotlinx.serialization.Serializable
import java.io.File
+import java.time.Instant
@Serializable
data class IntroMetaData(
get() = OpenGraphData(desc, image)
}
+val FactbookLastModifiedAttributeKey = AttributeKey<Instant>("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())
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" }
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() })
package info.mechyrdia.lore
+import info.mechyrdia.auth.PageDoNotCacheAttributeKey
import info.mechyrdia.auth.createCsrfToken
import info.mechyrdia.auth.installCsrfToken
import info.mechyrdia.auth.verifyCsrfToken
import kotlinx.html.*
suspend fun ApplicationCall.changeThemePage(): HTML.() -> Unit {
+ attributes.put(PageDoNotCacheAttributeKey, true)
+
val theme = when (request.cookies["FACTBOOK_THEME"]) {
"light" -> "light"
"dark" -> "dark"
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);
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);