val isDevMode: Boolean = false,
+ val rootDir: String = "..",
val articleDir: String = "../lore",
val assetDir: String = "../assets",
val templateDir: String = "../tpl",
val dbConn: String = "mongodb://localhost:27017",
) {
companion object {
- private val DEFAULT_CONFIG = Configuration()
-
- private var currentConfig: Configuration? = null
-
- val CurrentConfiguration: Configuration
- get() {
- currentConfig?.let { return it }
+ val CurrentConfiguration: Configuration by lazy {
+ val file = File(System.getProperty("factbooks.configpath", "./config.json"))
+ if (!file.isFile) {
+ if (file.exists())
+ file.deleteRecursively()
- val file = File(System.getProperty("factbooks.configpath", "./config.json"))
- if (!file.isFile) {
- if (file.exists())
- file.deleteRecursively()
-
- val json = JsonFileCodec.encodeToString(serializer(), DEFAULT_CONFIG)
- file.writeText(json, Charsets.UTF_8)
- return DEFAULT_CONFIG
- }
-
- val json = file.readText()
- return JsonFileCodec.decodeFromString(serializer(), json).also { currentConfig = it }
+ file.writeText(JsonFileCodec.encodeToString(serializer(), Configuration()), Charsets.UTF_8)
}
+
+ JsonFileCodec.decodeFromString(serializer(), file.readText(Charsets.UTF_8))
+ }
}
}
import io.ktor.server.routing.*
import io.ktor.server.sessions.*
import io.ktor.server.sessions.serialization.*
+import io.ktor.util.*
import org.slf4j.event.Level
import java.io.File
import java.io.IOException
call.respondHtml(HttpStatusCode.OK, call.galaxyMapPage())
}
+ // Routes for robots
+
+ get("/robots.txt") {
+ call.respondFile(File(Configuration.CurrentConfiguration.rootDir).combineSafe("robots.txt"))
+ }
+
+ get("/sitemap.xml") {
+ call.respondText(buildString { generateSitemap() }, ContentType.Text.Xml)
+ }
+
// Client settings
get("/change-theme") {
@OptIn(ExperimentalSerializationApi::class)
prettyPrintIndent = "\t"
+ encodeDefaults = true
+ ignoreUnknownKeys = true
useAlternativeNames = false
}
}
}
-private sealed interface NationSession {
- object Anonymous : NationSession
+private sealed class NationSession {
+ data object Anonymous : NationSession()
- @JvmInline
- value class LoggedIn(val nation: NationData) : NationSession
+ data class LoggedIn(val nation: NationData) : NationSession()
}
data class ArticleNode(val name: String, val subNodes: List<ArticleNode>)
+fun rootArticleNodeList(): List<ArticleNode> = File(Configuration.CurrentConfiguration.articleDir)
+ .toArticleNode()
+ .subNodes
+
fun File.toArticleNode(): ArticleNode = ArticleNode(
name,
listFiles()
)
fun List<ArticleNode>.renderInto(list: UL, base: String? = null) {
- val prefix = base?.let { "$it/" } ?: ""
- forEach { node ->
+ val prefix = lazy(LazyThreadSafetyMode.NONE) { base?.let { "$it/" } ?: "" }
+ for (node in this) {
if (Configuration.CurrentConfiguration.isDevMode || !(node.name.endsWith(".wip") || node.name.endsWith(".old")))
list.li {
a(href = "/lore/$prefix${node.name}") { +node.name }
import info.mechyrdia.Configuration
import info.mechyrdia.JsonStorageCodec
+import io.ktor.util.*
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.JsonPrimitive
import java.io.File
val (width, height) = getSizeParam(tagParam)
if (imageUrl.endsWith(".svg"))
- File(Configuration.CurrentConfiguration.assetDir, "images/$imageUrl")
+ File(Configuration.CurrentConfiguration.assetDir)
+ .combineSafe("images/$imageUrl")
.readText()
.replace("<svg", "<svg${getImageSizeAttributes(width, height)}")
else
),
QUIZ(
TextParserTagType.Indirect { _, content, _ ->
- val quizText = File(Configuration.CurrentConfiguration.quizDir, "$content.json").readText()
+ val quizText = File(Configuration.CurrentConfiguration.quizDir).combineSafe("$content.json").readText()
val quizJson = JsonStorageCodec.encodeToString(String.serializer(), quizText)
"<script>window.renderQuiz(JSON.parse($quizJson));</script>"
private val compiler = Mustache.compiler()
.withEscaper(Escapers.NONE)
.defaultValue("{{ MISSING VALUE \"{{name}}\" }}")
- .withLoader { File(Configuration.CurrentConfiguration.templateDir, "$it.tpl").bufferedReader() }
+ .withLoader { File(Configuration.CurrentConfiguration.templateDir).combineSafe("$it.tpl").bufferedReader() }
private fun convertJson(json: JsonElement, currentFile: File): Any? = when (json) {
JsonNull -> null
}
private fun loadJsonContext(name: String): Map<*, *> =
- File(Configuration.CurrentConfiguration.jsonDocDir).combineSafe("$name.json")
+ File(Configuration.CurrentConfiguration.jsonDocDir)
+ .combineSafe("$name.json")
.takeIf { it.isFile }
?.let { file ->
val text = file.readText()
)
fun loadExternalLinks(): List<NavItem> {
- val extraLinksFile = File(Configuration.CurrentConfiguration.articleDir).parentFile.combineSafe("externalLinks.json")
+ val extraLinksFile = File(Configuration.CurrentConfiguration.rootDir).combineSafe("externalLinks.json")
val extraLinks = JsonFileCodec.decodeFromString(ListSerializer(ExternalLink.serializer()), extraLinksFile.readText())
return if (extraLinks.isEmpty())
emptyList()
import kotlinx.html.*
import kotlinx.serialization.Serializable
import java.io.File
-import java.time.Instant
@Serializable
data class IntroMetaData(
}
suspend fun ApplicationCall.loreIntroPage(): HTML.() -> Unit {
- val metaJsonFile = File(Configuration.CurrentConfiguration.articleDir).parentFile.combineSafe("introMeta.json")
+ val rootDirFile = File(Configuration.CurrentConfiguration.rootDir)
+
+ val metaJsonFile = rootDirFile.combineSafe("introMeta.json")
val metaData = JsonFileCodec.decodeFromString(IntroMetaData.serializer(), metaJsonFile.readText())
- val htmlFile = File(Configuration.CurrentConfiguration.articleDir).parentFile.combineSafe("intro.html")
+ val htmlFile = rootDirFile.combineSafe("intro.html")
val fileHtml = htmlFile.readText()
return page(metaData.title, standardNavBar(), null, metaData.ogData) {
--- /dev/null
+package info.mechyrdia.lore
+
+import info.mechyrdia.Configuration
+import io.ktor.util.*
+import java.io.File
+import java.time.Instant
+
+private const val AVERAGE_FACTBOOK_PAGE_CHANGEFREQ = "hourly"
+private const val AVERAGE_FACTBOOK_INDEX_CHANGEFREQ = "daily"
+private const val FACTBOOK_ROOT_PRIORITY = "0.6"
+private const val FACTBOOK_INDEX_PRIORITY = "0.4"
+private const val FACTBOOK_PAGE_PRIORITY = "0.8"
+
+private val File.lastSubFilesModified: Instant?
+ get() = if (isDirectory)
+ (listFiles().orEmpty().mapNotNull {
+ it.lastSubFilesModified
+ } + Instant.ofEpochMilli(lastModified())).max()
+ else null
+
+private val File.lastContentModified: Instant
+ get() = lastSubFilesModified ?: Instant.ofEpochMilli(lastModified())
+
+private fun List<ArticleNode>.renderIntoSitemap(sitemap: Appendable, base: String? = null) {
+ val prefix by lazy(LazyThreadSafetyMode.NONE) { base?.let { "$it/" } ?: "" }
+ for (node in this) {
+ if (Configuration.CurrentConfiguration.isDevMode || !(node.name.endsWith(".wip") || node.name.endsWith(".old"))) {
+ val path = "$prefix${node.name}"
+
+ val file = File(Configuration.CurrentConfiguration.articleDir).combineSafe(path)
+ val lastModified = file.lastContentModified
+ val changeFreq = if (node.subNodes.isNotEmpty()) AVERAGE_FACTBOOK_INDEX_CHANGEFREQ else AVERAGE_FACTBOOK_PAGE_CHANGEFREQ
+ val priority = if (node.subNodes.isNotEmpty()) FACTBOOK_INDEX_PRIORITY else FACTBOOK_PAGE_PRIORITY
+
+ sitemap.appendLine("\t<url>")
+ sitemap.appendLine("\t\t<loc>https://mechyrdia.info/lore/$path</loc>")
+ sitemap.appendLine("\t\t<lastmod>$lastModified</lastmod>")
+ sitemap.appendLine("\t\t<changefreq>$changeFreq</changefreq>")
+ sitemap.appendLine("\t\t<priority>$priority</priority>")
+ sitemap.appendLine("\t</url>")
+ node.subNodes.renderIntoSitemap(sitemap, path)
+ }
+ }
+}
+
+private fun renderLoreSitemap(sitemap: Appendable) {
+ val rootFile = File(Configuration.CurrentConfiguration.articleDir)
+ val rootLastModified = rootFile.lastContentModified
+
+ sitemap.appendLine("\t<url>")
+ sitemap.appendLine("\t\t<loc>https://mechyrdia.info/lore</loc>")
+ sitemap.appendLine("\t\t<lastmod>$rootLastModified</lastmod>")
+ sitemap.appendLine("\t\t<changefreq>$AVERAGE_FACTBOOK_INDEX_CHANGEFREQ</changefreq>")
+ sitemap.appendLine("\t\t<priority>$FACTBOOK_ROOT_PRIORITY</priority>")
+ sitemap.appendLine("\t</url>")
+
+ rootArticleNodeList().renderIntoSitemap(sitemap)
+}
+
+fun Appendable.generateSitemap() {
+ appendLine("<?xml version='1.0' encoding='UTF-8'?>")
+ appendLine("<urlset xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd\" xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">")
+ renderLoreSitemap(this)
+ appendLine("</urlset>")
+}