val OwnerNationId: Id<NationData>
get() = Id(Configuration.Current.ownerNation)
+
+const val MainDomainName = "https://mechyrdia.info"
import kotlin.String
import kotlin.time.Duration.Companion.hours
-fun StoragePath.getContentType(): ContentType {
- val extension = elements.last().substringAfter('.', "")
- return if (extension.isEmpty()) ContentType.Text.Plain else ContentType.defaultForFileExtension(extension)
-}
+val StoragePath.contentType: ContentType
+ get() {
+ val extension = elements.last().substringAfter('.', "")
+ return if (extension.isEmpty()) ContentType.Text.Plain else ContentType.defaultForFileExtension(extension)
+ }
suspend fun ApplicationCall.respondStoredFile(path: StoragePath) {
val content = FileStorage.instance.readFile(path) ?: return respond(HttpStatusCode.NotFound)
- val type = path.getContentType()
-
attributes.put(StoragePathAttributeKey, path)
- respondBytes(content, type)
+ respondBytes(content, path.contentType)
}
@JvmInline
}
}
-val CallNationCacheAttribute = AttributeKey<MutableMap<Id<NationData>, NationData>>("NationCache")
+val CallNationCacheAttribute = AttributeKey<MutableMap<Id<NationData>, NationData>>("Mechyrdia.NationCache")
val ApplicationCall.nationCache: MutableMap<Id<NationData>, NationData>
get() = attributes.getOrNull(CallNationCacheAttribute)
}
}
-private val callCurrentNationAttribute = AttributeKey<NationSession>("CurrentNation")
+private val CallCurrentNationAttribute = AttributeKey<NationSession>("Mechyrdia.CurrentNation")
fun ApplicationCall.ownerNationOnly() {
if (sessions.get<UserSession>()?.nationId != OwnerNationId)
}
suspend fun ApplicationCall.currentNation(): NationData? {
- attributes.getOrNull(callCurrentNationAttribute)?.let { sess ->
+ attributes.getOrNull(CallCurrentNationAttribute)?.let { sess ->
return when (sess) {
NationSession.Anonymous -> null
is NationSession.LoggedIn -> sess.nation
val nationId = sessions.get<UserSession>()?.nationId
return if (nationId == null) {
- attributes.put(callCurrentNationAttribute, NationSession.Anonymous)
+ attributes.put(CallCurrentNationAttribute, NationSession.Anonymous)
null
} else nationCache.getNation(nationId).also { data ->
- attributes.put(callCurrentNationAttribute, NationSession.LoggedIn(data))
+ attributes.put(CallCurrentNationAttribute, NationSession.LoggedIn(data))
}
}
package info.mechyrdia.data
+import info.mechyrdia.MainDomainName
import info.mechyrdia.OwnerNationId
import info.mechyrdia.lore.*
import info.mechyrdia.route.Root
}
+Entities.nbsp
a(href = "#", classes = "copy-text") {
- attributes["data-text"] = "https://mechyrdia.info${href(Root.Comments.ViewPage(comment.id))}"
+ attributes["data-text"] = "$MainDomainName${href(Root.Comments.ViewPage(comment.id))}"
+"(Copy)"
}
import kotlinx.coroutines.*
import kotlinx.html.*
-private fun Map<String, StoredFileType>.sortedAsFiles() = toList()
+fun Map<String, StoredFileType>.sortedAsFiles() = toList()
.sortedBy { (name, _) -> name }
.sortedBy { (_, type) -> type }
import kotlinx.html.visit
import kotlinx.html.visitAndFinalize
import org.w3c.dom.Document
-import kotlin.collections.component1
-import kotlin.collections.component2
-import kotlin.collections.contains
-import kotlin.collections.set
@DslMarker
annotation class XmlTagMarker
fun onTagDeclaration(version: String, standalone: Boolean?)
fun onTagDeclaration(standalone: Boolean?) = onTagDeclaration("1.0", standalone)
- fun onTagNamespace(prefix: String?, namespace: String)
-
override fun onTagEvent(tag: Tag, event: String, value: (Event) -> Unit) {
tagEventsNotSupported()
}
else -> throw IllegalArgumentException("Unsupported TagConsumer subtype ${this::class.qualifiedName}")
}
-private data class XmlTagImpl(val tag: Tag, val prefix: String?) : Tag by tag {
- override val namespace: String?
- get() = null
-
- override val tagName: String
- get() = prefix?.let { "$it:" }.orEmpty() + tag.tagName
-}
-
private class XmlTagConsumerImpl<out R>(val downstream: TagConsumer<R>) : XmlTagConsumer<R>, TagConsumer<R> by downstream {
private var isDeclared = false
isDeclared = true
}
- private var defaultNamespace: String? = null
- private var namespaces: MutableMap<String, String>? = mutableMapOf()
- private val namespaceLookup = mutableMapOf<String, String>()
-
- override fun onTagNamespace(prefix: String?, namespace: String) {
- if (prefix == null) {
- if (namespaces == null)
- error("Unable to add xmlns attribute after document has already started")
- defaultNamespace = namespace
- } else
- (namespaces
- ?: error("Unable to add xmlns attribute after document has already started"))[prefix] = namespace
- }
-
- override fun onTagStart(tag: Tag) {
- namespaces?.let { namespaceList ->
- defaultNamespace?.let { namespaceDefault ->
- tag.attributes["xmlns"] = namespaceDefault
- }
-
- for ((prefix, namespace) in namespaceList) {
- tag.attributes["xmlns:$prefix"] = namespace
- namespaceLookup[namespace] = prefix
- }
-
- namespaces = null
- }
-
- val prefix = when (tag.namespace) {
- null, defaultNamespace -> null
- in namespaceLookup -> namespaceLookup[tag.namespace]
- else -> throw IllegalArgumentException("Unrecognized namespace ${tag.namespace}")
- }
-
- downstream.onTagStart(XmlTagImpl(tag, prefix))
- }
-
- override fun onTagEnd(tag: Tag) {
- val prefix = when (tag.namespace) {
- null, defaultNamespace -> null
- in namespaceLookup -> namespaceLookup[tag.namespace]
- else -> throw IllegalArgumentException("Unrecognized namespace ${tag.namespace}")
- }
-
- downstream.onTagEnd(XmlTagImpl(tag, prefix))
- }
-
override fun onTagEvent(tag: Tag, event: String, value: (Event) -> Unit) {
tagEventsNotSupported()
}
onTagDeclaration(version, standalone)
}
-@XmlTagMarker
-fun <T, C : XmlTagConsumer<T>> C.defaultXmlns(namespace: String) = apply {
- onTagNamespace(null, namespace)
-}
-
-@XmlTagMarker
-fun <T, C : XmlTagConsumer<T>> C.prefixedXmlns(prefix: String, namespace: String) = apply {
- onTagNamespace(prefix, namespace)
-}
-
-@XmlTagMarker
-fun <T, C : XmlTagConsumer<T>> C.prefixedXmlns(map: Map<String, String>) = apply {
- for ((prefix, namespace) in map)
- onTagNamespace(prefix, namespace)
-}
-
-@XmlTagMarker
-fun <T, C : XmlTagConsumer<T>> C.prefixedXmlns(map: Iterable<Pair<String, String>>) = apply {
- for ((prefix, namespace) in map)
- onTagNamespace(prefix, namespace)
-}
-
@XmlTagMarker
class XmlTag(
override val tagName: String,
)
fun <T, C : XmlTagConsumer<T>> C.svg(svgDoc: SvgDoc) = declaration(standalone = false)
- .defaultXmlns("http://www.w3.org/2000/svg")
.root(
"svg",
+ namespace = "http://www.w3.org/2000/svg",
attributes = mapOf(
"width" to svgDoc.width.xmlValue,
"height" to svgDoc.height.xmlValue,
package info.mechyrdia.lore
+import info.mechyrdia.MainDomainName
import info.mechyrdia.data.Comment
import info.mechyrdia.data.Id
}
}
-fun String.imagePathToOpenGraphValue() = "https://mechyrdia.info/assets/images/${sanitizeLink()}"
+fun String.imagePathToOpenGraphValue() = "$MainDomainName/assets/images/${sanitizeLink()}"
enum class ToCBuilderTag(val type: BuilderTag<TableOfContentsBuilder>) {
H1(ToCHeaderBuilderTag(0)),
package info.mechyrdia.lore
+import info.mechyrdia.MainDomainName
import io.ktor.server.application.*
import io.ktor.server.request.*
import kotlinx.html.HEAD
ogProperty("type", "website")
ogProperty("description", data.desc)
ogProperty("image", data.image)
- ogProperty("url", "https://mechyrdia.info/${request.path().removePrefix("/")}")
+ ogProperty("url", "$MainDomainName/${request.path().removePrefix("/")}")
}
package info.mechyrdia.lore
import info.mechyrdia.JsonFileCodec
+import info.mechyrdia.MainDomainName
import info.mechyrdia.data.*
import info.mechyrdia.route.KeyedEnumSerializer
import io.ktor.http.*
get() = if (portrait.startsWith("http://") || portrait.startsWith("https://"))
portrait
else
- "https://mechyrdia.info/assets/images/$portrait"
+ "$MainDomainName/assets/images/$portrait"
val fullLink: String
get() = if (link.startsWith("http://") || link.startsWith("https://"))
link
else
- "https://mechyrdia.info/lore/$link"
+ "$MainDomainName/lore/$link"
}
private val quotesListGetter by storedData(StoragePath("quotes.json")) { jsonPath ->
package info.mechyrdia.lore
+import info.mechyrdia.MainDomainName
import info.mechyrdia.data.*
import java.time.Instant
import java.time.format.DateTimeFormatter
val introLastModified = maxOf(introFile.updated, introMetaFile.updated)
return SitemapEntry(
- loc = "https://mechyrdia.info/",
+ loc = "$MainDomainName/",
lastModified = introLastModified,
changeFrequency = AVERAGE_FACTBOOK_INTRO_CHANGEFREQ,
priority = FACTBOOK_INTRO_PRIORITY
return allPages().mapNotNull { page ->
if (!page.path.isViewable) null
else SitemapEntry(
- loc = "https://mechyrdia.info/lore/${page.path}",
+ loc = "$MainDomainName/lore/${page.path}",
lastModified = page.stat.updated,
changeFrequency = AVERAGE_FACTBOOK_PAGE_CHANGEFREQ,
priority = FACTBOOK_PAGE_PRIORITY
suspend fun buildSitemap() = listOfNotNull(buildIntroSitemap()) + buildLoreSitemap()
fun <T, C : XmlTagConsumer<T>> C.sitemap(entries: List<SitemapEntry>) = declaration()
- .defaultXmlns("http://www.sitemaps.org/schemas/sitemap/0.9")
- .root("urlset") {
+ .root("urlset", namespace = "http://www.sitemaps.org/schemas/sitemap/0.9") {
for (entry in entries)
+entry
}
package info.mechyrdia.lore
import com.mongodb.client.model.Sorts
+import info.mechyrdia.MainDomainName
import info.mechyrdia.OwnerNationId
import info.mechyrdia.data.*
import io.ktor.http.*
return RssChannel(
title = "Recently Edited Factbooks | The Hour of Decision",
- link = "https://mechyrdia.info",
+ link = MainDomainName,
description = "An RSS feed containing all factbooks in The Hour of Decision, in order of most recently edited.",
pubDate = mostRecentChange,
lastBuildDate = mostRecentChange,
val pageOg = pageToC.toOpenGraph()
val imageEnclosure = pageOg?.image?.let { url ->
- val assetPath = url.removePrefix("https://mechyrdia.info/assets/")
+ val assetPath = url.removePrefix("$MainDomainName/assets/")
val file = StoragePath.assetDir / assetPath
RssItemEnclosure(
url = url,
RssItem(
title = pageToC.toPageTitle(),
description = pageOg?.desc,
- link = "https://mechyrdia.info/lore${pageLink.joinToString { "/$it" }}",
+ link = "$MainDomainName/lore${pageLink.joinToString { "/$it" }}",
author = null,
- comments = "https://mechyrdia.info/lore${pageLink.joinToString { "/$it" }}#comments",
+ comments = "$MainDomainName/lore${pageLink.joinToString { "/$it" }}#comments",
enclosure = imageEnclosure,
pubDate = page.stat.updated
)
return RssChannel(
title = "Recent Comments - Error | The Hour of Decision",
- link = "https://mechyrdia.info/comment/recent",
+ link = "$MainDomainName/comment/recent",
description = "Comment limit must be between ${validLimits.first} and ${validLimits.last}, got $limit",
pubDate = null,
lastBuildDate = Instant.now(),
return RssChannel(
title = "Recent Comments | The Hour of Decision",
- link = "https://mechyrdia.info/comment/recent",
+ link = "$MainDomainName/comment/recent",
description = "An RSS feed containing the $limit most recently-submitted comments",
pubDate = mostRecentComment,
lastBuildDate = mostRecentComment,
),
items = comments.map { comment ->
RssItem(
- title = "Comment by ${comment.submittedBy.name} on https://mechyrdia.info/lore/${comment.submittedIn}",
+ title = "Comment by ${comment.submittedBy.name} on $MainDomainName/${comment.submittedIn}",
description = comment.contentsRaw.parseAs(ParserTree::toCommentPlainText),
- link = "https://mechyrdia.info/comment/view/${comment.id}",
+ link = "$MainDomainName/comment/view/${comment.id}",
author = null,
- comments = "https://mechyrdia.info/lore/${comment.submittedIn}#comment-${comment.id}",
+ comments = "$MainDomainName/lore/${comment.submittedIn}#comment-${comment.id}",
pubDate = comment.lastEdit ?: comment.submittedAt,
categories = listOf(
RssCategory(domain = "https://nationstates.net", category = comment.submittedBy.name)
package info.mechyrdia.route
-import info.mechyrdia.auth.loginPage
-import info.mechyrdia.auth.loginRoute
-import info.mechyrdia.auth.logoutRoute
+import info.mechyrdia.auth.*
import info.mechyrdia.data.*
import info.mechyrdia.lore.*
import io.ktor.http.*
import io.ktor.util.pipeline.*
import kotlinx.coroutines.delay
-val ErrorMessageAttributeKey = AttributeKey<String>("ErrorMessage")
+val ErrorMessageAttributeKey = AttributeKey<String>("Mechyrdia.ErrorMessage")
@Resource("/")
class Root(val error: String? = null) : ResourceHandler, ResourceFilter {
class CopyPost(val path: List<String>, val vfs: Vfs = Vfs()) : ResourceReceiver<AdminVfsCopyFilePayload> {
override suspend fun PipelineContext<Unit, ApplicationCall>.handleCall(payload: AdminVfsCopyFilePayload) {
with(vfs) { filterCall() }
+ with(payload) { call.verifyCsrfToken() }
call.adminDoCopyFile(StoragePath(payload.from), StoragePath(path))
}