From 9d60bdb915e5826786b112dea68937b4a1f22f5d Mon Sep 17 00:00:00 2001 From: mae Date: Sat, 21 Mar 2026 00:41:36 -0500 Subject: [PATCH] basic types and identifier implementation --- gradle/libs.versions.toml | 4 +- .../online/maestoso/cofront/Identifier.kt | 15 ---- .../cofront/payloads/GetMemberPayload.kt | 7 ++ .../cofront/payloads/SystemLookupPayload.kt | 26 +++++++ .../maestoso/cofront/types/Identifier.kt | 73 +++++++++++++++++++ .../online/maestoso/cofront/types/JID.kt | 29 ++++++++ .../online/maestoso/cofront/types/Member.kt | 34 +++++++++ .../maestoso/cofront/types/MemberRelation.kt | 6 ++ .../online/maestoso/cofront/types/Profile.kt | 29 ++++++++ .../maestoso/cofront/util/ByteManipulation.kt | 45 ++++++++++++ .../online/maestoso/cofront/util/URL.kt | 8 ++ src/jsMain/kotlin/Cofront.kt | 3 +- .../kotlin/online/maestoso/cofront/api/API.kt | 10 --- 13 files changed, 261 insertions(+), 28 deletions(-) delete mode 100644 src/commonMain/kotlin/online/maestoso/cofront/Identifier.kt create mode 100644 src/commonMain/kotlin/online/maestoso/cofront/payloads/GetMemberPayload.kt create mode 100644 src/commonMain/kotlin/online/maestoso/cofront/payloads/SystemLookupPayload.kt create mode 100644 src/commonMain/kotlin/online/maestoso/cofront/types/Identifier.kt create mode 100644 src/commonMain/kotlin/online/maestoso/cofront/types/JID.kt create mode 100644 src/commonMain/kotlin/online/maestoso/cofront/types/Member.kt create mode 100644 src/commonMain/kotlin/online/maestoso/cofront/types/MemberRelation.kt create mode 100644 src/commonMain/kotlin/online/maestoso/cofront/types/Profile.kt create mode 100644 src/commonMain/kotlin/online/maestoso/cofront/util/ByteManipulation.kt create mode 100644 src/commonMain/kotlin/online/maestoso/cofront/util/URL.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a39bb3b..ae3324f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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 = [] diff --git a/src/commonMain/kotlin/online/maestoso/cofront/Identifier.kt b/src/commonMain/kotlin/online/maestoso/cofront/Identifier.kt deleted file mode 100644 index f00100c..0000000 --- a/src/commonMain/kotlin/online/maestoso/cofront/Identifier.kt +++ /dev/null @@ -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]) -} \ No newline at end of file diff --git a/src/commonMain/kotlin/online/maestoso/cofront/payloads/GetMemberPayload.kt b/src/commonMain/kotlin/online/maestoso/cofront/payloads/GetMemberPayload.kt new file mode 100644 index 0000000..9ebc689 --- /dev/null +++ b/src/commonMain/kotlin/online/maestoso/cofront/payloads/GetMemberPayload.kt @@ -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) diff --git a/src/commonMain/kotlin/online/maestoso/cofront/payloads/SystemLookupPayload.kt b/src/commonMain/kotlin/online/maestoso/cofront/payloads/SystemLookupPayload.kt new file mode 100644 index 0000000..437a04d --- /dev/null +++ b/src/commonMain/kotlin/online/maestoso/cofront/payloads/SystemLookupPayload.kt @@ -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, + /** + * The list of all members. + */ + val members: Set, + /** + * The list of all JIDs associated with this system. + */ + val jids: Set, + /** + * URL to a dashboard of more information to this system. + */ + val url: String, + ) diff --git a/src/commonMain/kotlin/online/maestoso/cofront/types/Identifier.kt b/src/commonMain/kotlin/online/maestoso/cofront/types/Identifier.kt new file mode 100644 index 0000000..7dfd9a2 --- /dev/null +++ b/src/commonMain/kotlin/online/maestoso/cofront/types/Identifier.kt @@ -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 { + 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.. { + 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 + ) + } +} diff --git a/src/commonMain/kotlin/online/maestoso/cofront/types/JID.kt b/src/commonMain/kotlin/online/maestoso/cofront/types/JID.kt new file mode 100644 index 0000000..057a8d4 --- /dev/null +++ b/src/commonMain/kotlin/online/maestoso/cofront/types/JID.kt @@ -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 { + 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") + } + +} \ No newline at end of file diff --git a/src/commonMain/kotlin/online/maestoso/cofront/types/Member.kt b/src/commonMain/kotlin/online/maestoso/cofront/types/Member.kt new file mode 100644 index 0000000..d515fca --- /dev/null +++ b/src/commonMain/kotlin/online/maestoso/cofront/types/Member.kt @@ -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, + /** + * Endpoint to look up fronter/full system information. + */ + val system: String, +) diff --git a/src/commonMain/kotlin/online/maestoso/cofront/types/MemberRelation.kt b/src/commonMain/kotlin/online/maestoso/cofront/types/MemberRelation.kt new file mode 100644 index 0000000..7bea947 --- /dev/null +++ b/src/commonMain/kotlin/online/maestoso/cofront/types/MemberRelation.kt @@ -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) diff --git a/src/commonMain/kotlin/online/maestoso/cofront/types/Profile.kt b/src/commonMain/kotlin/online/maestoso/cofront/types/Profile.kt new file mode 100644 index 0000000..ed67f58 --- /dev/null +++ b/src/commonMain/kotlin/online/maestoso/cofront/types/Profile.kt @@ -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?, +) diff --git a/src/commonMain/kotlin/online/maestoso/cofront/util/ByteManipulation.kt b/src/commonMain/kotlin/online/maestoso/cofront/util/ByteManipulation.kt new file mode 100644 index 0000000..9eda13b --- /dev/null +++ b/src/commonMain/kotlin/online/maestoso/cofront/util/ByteManipulation.kt @@ -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 +} \ No newline at end of file diff --git a/src/commonMain/kotlin/online/maestoso/cofront/util/URL.kt b/src/commonMain/kotlin/online/maestoso/cofront/util/URL.kt new file mode 100644 index 0000000..9f915c1 --- /dev/null +++ b/src/commonMain/kotlin/online/maestoso/cofront/util/URL.kt @@ -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) +} \ No newline at end of file diff --git a/src/jsMain/kotlin/Cofront.kt b/src/jsMain/kotlin/Cofront.kt index 98542ca..8e1cd9d 100644 --- a/src/jsMain/kotlin/Cofront.kt +++ b/src/jsMain/kotlin/Cofront.kt @@ -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") } \ No newline at end of file diff --git a/src/jvmMain/kotlin/online/maestoso/cofront/api/API.kt b/src/jvmMain/kotlin/online/maestoso/cofront/api/API.kt index 145a7bf..a1e10b4 100644 --- a/src/jvmMain/kotlin/online/maestoso/cofront/api/API.kt +++ b/src/jvmMain/kotlin/online/maestoso/cofront/api/API.kt @@ -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") -} \ No newline at end of file