basic types and identifier implementation

This commit is contained in:
mae
2026-03-21 00:41:36 -05:00
parent 8972a90b6c
commit 9d60bdb915
13 changed files with 261 additions and 28 deletions

View File

@@ -6,6 +6,7 @@ kotlinx-html = "0.12.0"
kotlinx-browser = "0.5.0"
kotlinx-serialization = "1.8.0"
kotlin-css-jvm = "2025.6.4"
kotlinx-datetime = "0.7.1"
ktor = "3.4.1"
ktor-server-rate-limiting = "2.2.1"
logback = "1.5.13"
@@ -25,6 +26,7 @@ kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotl
kotlinx-html = { module = "org.jetbrains.kotlinx:kotlinx-html", version.ref = "kotlinx-html" }
kotlinx-browser = { module = "org.jetbrains.kotlinx:kotlinx-browser", version.ref = "kotlinx-browser" }
kotlinx-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
kotlin-css = { module = "org.jetbrains.kotlin-wrappers:kotlin-css-jvm", version.ref = "kotlin-css-jvm" }
exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" }
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }
@@ -35,7 +37,7 @@ kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version
[bundles]
jvmMain = ["ktor-server-core", "ktor-server-netty", "ktor-server-auth", "ktor-server-content-negotiation", "ktor-serialization-kotlinx-json", "ktor-server-sessions", "ktor-server-host-common", "ktor-server-html-builder", "ktor-server-rate-limiting", "kotlinx-html", "kotlin-css", "exposed-core", "exposed-jdbc", "h2"]
jvmTest = ["ktor-test", "kotlin-test-junit"]
commonMain = ["kotlinx-serialization"]
commonMain = ["kotlinx-serialization", "kotlinx-datetime"]
commonTest = ["kotlin-test"]
jsMain = ["kotlinx-browser"]
jsTest = []

View File

@@ -1,15 +0,0 @@
@file:OptIn(ExperimentalUuidApi::class)
package online.maestoso.cofront
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
data class Identifier(val id: Uuid, val domain: String) {
override fun toString(): String {
return "$id@$domain"
}
}
fun String.toIdentifier(): Identifier {
val split = split('@', limit = 2)
return Identifier(Uuid.parse(split[0]), split[1])
}

View File

@@ -0,0 +1,7 @@
package online.maestoso.cofront.payloads
import kotlinx.serialization.Serializable
import online.maestoso.cofront.types.Member
@Serializable
data class GetMemberPayload(val member: Member)

View File

@@ -0,0 +1,26 @@
package online.maestoso.cofront.payloads
import kotlinx.serialization.Serializable
import online.maestoso.cofront.types.Identifier
import online.maestoso.cofront.types.JID
import online.maestoso.cofront.types.Member
@Serializable
data class SystemLookupPayload(
/**
* The list of current fronters.
*/
val fronters: Set<Identifier>,
/**
* The list of all members.
*/
val members: Set<Identifier>,
/**
* The list of all JIDs associated with this system.
*/
val jids: Set<JID>,
/**
* URL to a dashboard of more information to this system.
*/
val url: String,
)

View File

@@ -0,0 +1,73 @@
package online.maestoso.cofront.types
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import online.maestoso.cofront.util.toByteArray
import online.maestoso.cofront.util.toUInt
import online.maestoso.cofront.util.toULong
import kotlin.io.encoding.Base64
@Serializable(with = IdentifierSerializer::class)
data class Identifier(val system: SystemId, val member: MemberId?, val domain: String) {
data class MemberId(val serial: ULong, val time: ULong, val id: ULong)
data class SystemId(val site: UInt, val host: UInt, val time: ULong, val id: ULong)
}
object IdentifierSerializer : KSerializer<Identifier> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("memberid", PrimitiveKind.STRING)
override fun serialize(
encoder: Encoder,
value: Identifier
) {
val buf = ByteArray(48) {0}
value.system.site.toByteArray().copyInto(buf, 0)
value.system.host.toByteArray().copyInto(buf, 4)
value.system.time.toByteArray().copyInto(buf, 8)
value.system.id.toByteArray().copyInto(buf, 16)
if (value.member != null) {
value.member.serial.toByteArray().copyInto(buf, 24)
value.member.time.toByteArray().copyInto(buf, 32)
value.member.id.toByteArray().copyInto(buf, 40)
} else {
buf.dropLast(24)
}
encoder.encodeString(Base64.UrlSafe.encode(buf) + ":${value.domain}")
}
@Throws(IllegalArgumentException::class)
override fun deserialize(decoder: Decoder): Identifier {
val str = decoder.decodeString()
val i = str.indexOfFirst {it == ':'}
if(i != 24 || i != 48 || i == -1) throw IllegalArgumentException("colon not correctly placed")
val id = str.substring(0..<i)
val domain = str.substring(i..<str.length)
val buf = Base64.UrlSafe.decode(id)
val member: Identifier.MemberId? = when (buf.size) {
24 -> {
null
}
48 -> {
Identifier.MemberId(
buf.copyOfRange(24, 31).toULong(),
buf.copyOfRange(32, 39).toULong(),
buf.copyOfRange(40, 47).toULong()
)
}
else -> throw IllegalArgumentException("identifier was the wrong size: expected 24 or 48 bytes, got ${buf.size}")
}
return Identifier(
Identifier.SystemId(
buf.copyOfRange(0, 3).toUInt(),
buf.copyOfRange(4, 7).toUInt(),
buf.copyOfRange(8, 15).toULong(),
buf.copyOfRange(16, 23).toULong(),
),
member,
domain
)
}
}

View File

@@ -0,0 +1,29 @@
package online.maestoso.cofront.types
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.Serializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
@Serializable(with = JIDSerializer::class)
data class JID(val node: String, val domain: String, val resource: String) {
override fun toString(): String {
return "$node@$domain/$resource"
}
}
class JIDSerializer : KSerializer<JID> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("jid", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: JID) {
encoder.encodeString(value.toString())
}
override fun deserialize(decoder: Decoder): JID {
TODO("Not yet implemented")
}
}

View File

@@ -0,0 +1,34 @@
package online.maestoso.cofront.types
import kotlinx.serialization.Serializable
import kotlin.time.Instant
@Serializable
data class Member(
/**
Unique identifier for this member.
*/
val id: Identifier,
/**
Short but human-readable name for this member.
*/
val shortname: String,
/**
* Creation date and time for this member.
*/
val created: Instant,
/**
* The profile associated with this member.
*/
val profile: Profile,
/**
* A set of descriptions of how this member is related to other members in the system.
* This can include a variety of implementation-specific information, such as groups,
* a "root member" for a system profile, or represent interpersonal relationships.
*/
val relations: Set<MemberRelation>,
/**
* Endpoint to look up fronter/full system information.
*/
val system: String,
)

View File

@@ -0,0 +1,6 @@
package online.maestoso.cofront.types
import kotlinx.serialization.Serializable
@Serializable
data class MemberRelation(val from: Identifier, val to: Identifier, val type: String)

View File

@@ -0,0 +1,29 @@
package online.maestoso.cofront.types
import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.Serializable
import kotlin.time.Instant
@Serializable
data class Profile(
/**
* Long display name for this profile.
*/
val name: String?,
/**
* Profile's pronouns.
*/
val pronouns: String?,
/**
* Profile's preferred accent color.
*/
val color: String?,
/**
* URL to access profile picture.
*/
val avatar: String?,
/**
* This member's preferred birthday.
*/
val birthday: LocalDateTime?,
)

View File

@@ -0,0 +1,45 @@
package online.maestoso.cofront.util
fun UInt.toByteArray(): ByteArray {
val b = ByteArray(4) {0}
b[0] = this.toByte()
b[1] = (this shr 8).toByte()
b[2] = (this shr 16).toByte()
b[3] = (this shr 24).toByte()
return b
}
fun ByteArray.toUInt(): UInt {
var i = 0u
i = i or this[0].toUInt()
i = i or (this[1].toUInt() shl 8)
i = i or (this[2].toUInt() shl 16)
i = i or (this[3].toUInt() shl 24)
return i
}
fun ULong.toByteArray(): ByteArray {
val b = ByteArray(8) {0}
b[0] = this.toByte()
b[1] = (this shr 8).toByte()
b[2] = (this shr 16).toByte()
b[3] = (this shr 24).toByte()
b[4] = (this shr 32).toByte()
b[5] = (this shr 40).toByte()
b[6] = (this shr 48).toByte()
b[7] = (this shr 56).toByte()
return b
}
fun ByteArray.toULong(): ULong {
var i = 0UL
i = i or this[0].toULong()
i = i or (this[1].toULong() shl 8)
i = i or (this[2].toULong() shl 16)
i = i or (this[3].toULong() shl 24)
i = i or (this[4].toULong() shl 32)
i = i or (this[5].toULong() shl 40)
i = i or (this[6].toULong() shl 48)
i = i or (this[7].toULong() shl 56)
return i
}

View File

@@ -0,0 +1,8 @@
package online.maestoso.cofront.util
fun String.isValidDomain(): Boolean {
// regex stolen from https://stackoverflow.com/a/3824105. if it's broken its not my fault
val regex = """^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]))*$"""
.toRegex()
return regex.matches(this)
}

View File

@@ -1,11 +1,10 @@
@file:OptIn(ExperimentalUuidApi::class)
import kotlinx.browser.window
import online.maestoso.cofront.Identifier
import online.maestoso.cofront.types.Identifier
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
fun main() {
window.alert("kotlin js test")
val i = Identifier(Uuid.generateV7(), "localhost")
}

View File

@@ -1,11 +1 @@
@file:OptIn(ExperimentalUuidApi::class)
package online.maestoso.cofront.api
import online.maestoso.cofront.Identifier
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
fun test() {
val i = Identifier(Uuid.generateV7(), "localhost")
}