From e4587dd764aba6a64a82bd3b6d823b789c5c4de5 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Tue, 22 Jul 2025 04:03:52 +0200 Subject: [PATCH 01/10] Improve sync response strategy --- .../clientserverapi/model/sync/Sync.kt | 212 +++++++++++++++++- .../model/sync/SyncResponseSerializer.kt | 110 +-------- 2 files changed, 208 insertions(+), 114 deletions(-) diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/sync/Sync.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/sync/Sync.kt index cccca57d0..5a165d620 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/sync/Sync.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/sync/Sync.kt @@ -2,21 +2,54 @@ package net.folivo.trixnity.clientserverapi.model.sync import io.ktor.resources.* import kotlinx.serialization.Contextual +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.mapSerialDescriptor +import kotlinx.serialization.encoding.CompositeDecoder.Companion.DECODE_DONE +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.encoding.decodeStructure import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonEncoder +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.JsonTransformingSerializer +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.contextual +import kotlinx.serialization.modules.overwriteWith +import kotlinx.serialization.serializer import net.folivo.trixnity.core.HttpMethod import net.folivo.trixnity.core.HttpMethodType.GET import net.folivo.trixnity.core.MatrixEndpoint import net.folivo.trixnity.core.model.RoomId import net.folivo.trixnity.core.model.UserId -import net.folivo.trixnity.core.model.events.ClientEvent.* +import net.folivo.trixnity.core.model.events.ClientEvent.EphemeralEvent +import net.folivo.trixnity.core.model.events.ClientEvent.GlobalAccountDataEvent +import net.folivo.trixnity.core.model.events.ClientEvent.RoomAccountDataEvent +import net.folivo.trixnity.core.model.events.ClientEvent.RoomEvent import net.folivo.trixnity.core.model.events.ClientEvent.RoomEvent.StateEvent +import net.folivo.trixnity.core.model.events.ClientEvent.StrippedStateEvent +import net.folivo.trixnity.core.model.events.ClientEvent.ToDeviceEvent import net.folivo.trixnity.core.model.events.m.Presence import net.folivo.trixnity.core.model.events.m.PresenceEventContent import net.folivo.trixnity.core.model.keys.KeyAlgorithm +import net.folivo.trixnity.core.serialization.events.DefaultEventContentSerializerMappings +import net.folivo.trixnity.core.serialization.events.EphemeralEventSerializer import net.folivo.trixnity.core.serialization.events.EventContentSerializerMappings +import net.folivo.trixnity.core.serialization.events.MessageEventSerializer +import net.folivo.trixnity.core.serialization.events.RoomAccountDataEventSerializer +import net.folivo.trixnity.core.serialization.events.RoomEventSerializer +import net.folivo.trixnity.core.serialization.events.StateBaseEventSerializer +import net.folivo.trixnity.core.serialization.events.StateEventSerializer +import net.folivo.trixnity.core.serialization.events.StrippedStateEventSerializer /** * @see matrix spec @@ -36,7 +69,7 @@ data class Sync( mappings: EventContentSerializerMappings, json: Json, value: Response? - ): KSerializer = SyncResponseSerializer + ): KSerializer = Response.serializer() @Serializable data class Response( @@ -49,12 +82,107 @@ data class Sync( @SerialName("device_one_time_keys_count") val oneTimeKeysCount: OneTimeKeysCount? = null, @SerialName("device_unused_fallback_key_types") val unusedFallbackKeyTypes: UnusedFallbackKeyTypes? = null, ) { + abstract class RoomsMapSerializer( + valueDescriptor: SerialDescriptor, + val valueSerializer: (Json) -> KSerializer, + ) : KSerializer> { + private val keySerializer = String.serializer() + + @OptIn(ExperimentalSerializationApi::class) + override val descriptor: SerialDescriptor = mapSerialDescriptor( + keySerializer.descriptor, + valueDescriptor, + ) + + override fun serialize( + encoder: Encoder, + value: Map, + ) = TODO() + + @OptIn(ExperimentalSerializationApi::class) + override fun deserialize(decoder: Decoder): Map { + return decoder.decodeStructure(descriptor) { + buildMap { + var key: RoomId? = null + loop@ while (true) { + val index = decodeElementIndex(descriptor) + when { + index == DECODE_DONE -> break@loop + index % 2 == 0 -> { + key = RoomId(decodeSerializableElement(descriptor, index, keySerializer)) + } + index % 2 == 1 -> { + requireNotNull(key) + require(decoder is JsonDecoder) + val json = Json { + encodeDefaults = decoder.json.configuration.encodeDefaults + explicitNulls = decoder.json.configuration.explicitNulls + ignoreUnknownKeys = decoder.json.configuration.ignoreUnknownKeys + isLenient = decoder.json.configuration.isLenient + prettyPrint = decoder.json.configuration.prettyPrint + prettyPrintIndent = decoder.json.configuration.prettyPrintIndent + coerceInputValues = decoder.json.configuration.coerceInputValues + classDiscriminator = decoder.json.configuration.classDiscriminator + classDiscriminatorMode = decoder.json.configuration.classDiscriminatorMode + useAlternativeNames = decoder.json.configuration.useAlternativeNames + namingStrategy = decoder.json.configuration.namingStrategy + decodeEnumsCaseInsensitive = decoder.json.configuration.decodeEnumsCaseInsensitive + allowTrailingComma = decoder.json.configuration.allowTrailingComma + allowComments = decoder.json.configuration.allowComments + allowSpecialFloatingPointValues = decoder.json.configuration.allowSpecialFloatingPointValues + allowStructuredMapKeys = decoder.json.configuration.allowStructuredMapKeys + useArrayPolymorphism = decoder.json.configuration.useArrayPolymorphism + serializersModule = decoder.serializersModule.overwriteWith(buildSerializersModule(key)) + } + put(key, decodeSerializableElement(descriptor, index, valueSerializer(json))) + } + else -> throw SerializationException("Unexpected index $index") + } + } + } + } + } + } + + class RoomsKnockSerializer: RoomsMapSerializer( + Rooms.KnockedRoom.serializer().descriptor, + { ContextualSerializer(it, it.serializersModule.serializer()) }, + ) + + class RoomsJoinSerializer: RoomsMapSerializer( + Rooms.JoinedRoom.serializer().descriptor, + { ContextualSerializer(it, it.serializersModule.serializer()) }, + ) + + class RoomsInviteSerializer: RoomsMapSerializer( + Rooms.InvitedRoom.serializer().descriptor, + { ContextualSerializer(it, it.serializersModule.serializer()) }, + ) + class RoomsLeaveSerializer: RoomsMapSerializer( + Rooms.LeftRoom.serializer().descriptor, + { ContextualSerializer(it, it.serializersModule.serializer()) }, + ) + + class ContextualSerializer(val json: Json, val serializer: KSerializer): KSerializer { + override val descriptor: SerialDescriptor get() = serializer.descriptor + + override fun serialize(encoder: Encoder, value: T) { + require(encoder is JsonEncoder) + encoder.encodeJsonElement(json.encodeToJsonElement(serializer, value)) + } + + override fun deserialize(decoder: Decoder): T { + require(decoder is JsonDecoder) + return json.decodeFromJsonElement(serializer, decoder.decodeJsonElement()) + } + } + @Serializable data class Rooms( - @SerialName("knock") val knock: Map? = null, - @SerialName("join") val join: Map? = null, - @SerialName("invite") val invite: Map? = null, - @SerialName("leave") val leave: Map? = null + @SerialName("knock") val knock: @Serializable(with = RoomsKnockSerializer::class) Map? = null, + @SerialName("join") val join: @Serializable(with = RoomsJoinSerializer::class) Map? = null, + @SerialName("invite") val invite: @Serializable(with = RoomsInviteSerializer::class) Map? = null, + @SerialName("leave") val leave: @Serializable(with = RoomsLeaveSerializer::class) Map? = null ) { @Serializable data class KnockedRoom( @@ -150,6 +278,78 @@ data class Sync( @SerialName("events") val events: List<@Contextual ToDeviceEvent<*>>? = null ) } + + companion object { + internal fun addRoomIdToEvent(event: JsonObject, roomId: RoomId): JsonObject { + return JsonObject(buildMap { + putAll(event) + put("room_id", JsonPrimitive(roomId.full)) + val unsigned = event["unsigned"]?.jsonObject + if (unsigned != null) { + val aggregations = unsigned["m.relations"]?.jsonObject + val newAggregations = + if (aggregations != null) { + val thread = aggregations["m.thread"]?.jsonObject + if (thread != null) { + val latestEvent = thread["latest_event"]?.jsonObject + if (latestEvent != null) { + JsonObject(buildMap { + putAll(aggregations) + put("m.thread", JsonObject(buildMap { + putAll(thread) + put("latest_event", addRoomIdToEvent(latestEvent, roomId)) + })) + }) + } else null + } else null + } else null + val redactedBecause = unsigned["redacted_because"]?.jsonObject + val newRedactedBecause = + if (redactedBecause != null) { + addRoomIdToEvent(redactedBecause, roomId) + } else null + put("unsigned", JsonObject(buildMap { + putAll(unsigned) + if (newAggregations != null) { + put("m.relations", newAggregations) + } + if (newRedactedBecause != null) { + put("redacted_because", newRedactedBecause) + } + })) + } + }) + } + + + fun buildSerializersModule(roomId: RoomId): SerializersModule { + val mappings = DefaultEventContentSerializerMappings + val messageEventSerializer = WithRoomIdSerializer(roomId, MessageEventSerializer(mappings.message)) + val stateEventSerializer = WithRoomIdSerializer(roomId, StateEventSerializer(mappings.state)) + val roomEventSerializer = WithRoomIdSerializer(roomId, RoomEventSerializer(messageEventSerializer, stateEventSerializer)) + val strippedStateEventSerializer = WithRoomIdSerializer(roomId, StrippedStateEventSerializer(mappings.state)) + val stateBaseEventSerializer = WithRoomIdSerializer(roomId, StateBaseEventSerializer(stateEventSerializer, strippedStateEventSerializer)) + val ephemeralEventSerializer = WithRoomIdSerializer(roomId, EphemeralEventSerializer(mappings.ephemeral)) + val roomAccountDataEventSerializer = WithRoomIdSerializer(roomId, RoomAccountDataEventSerializer(mappings.roomAccountData)) + return SerializersModule { + contextual(roomEventSerializer) + contextual(messageEventSerializer) + contextual(stateEventSerializer) + contextual(strippedStateEventSerializer) + contextual(stateBaseEventSerializer) + contextual(ephemeralEventSerializer) + contextual(roomAccountDataEventSerializer) + } + } + } + + class WithRoomIdSerializer(private val roomId: RoomId, serializer: KSerializer) + : JsonTransformingSerializer(serializer) { + override fun transformDeserialize(element: JsonElement): JsonElement { + require(element is JsonObject) + return addRoomIdToEvent(element, roomId) + } + } } typealias OneTimeKeysCount = Map diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/sync/SyncResponseSerializer.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/sync/SyncResponseSerializer.kt index 7d907ba3b..51304e27f 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/sync/SyncResponseSerializer.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/sync/SyncResponseSerializer.kt @@ -3,111 +3,5 @@ package net.folivo.trixnity.clientserverapi.model.sync import kotlinx.serialization.json.* // TODO maybe this could be solved completely with contextual serializers -object SyncResponseSerializer : JsonTransformingSerializer(Sync.Response.serializer()) { - override fun transformDeserialize(element: JsonElement): JsonElement { - require(element is JsonObject) - val rooms = element["rooms"] ?: return element - require(rooms is JsonObject) - val roomsWithEventIds = JsonObject(buildMap { - putAll(rooms) - putAndConvertEventMap("join", rooms["join"], setOf("timeline", "state", "ephemeral", "account_data")) - putAndConvertEventMap("leave", rooms["leave"], setOf("timeline", "state", "account_data")) - putAndConvertEventMap("knock", rooms["knock"], setOf("knock_state")) - putAndConvertEventMap("invite", rooms["invite"], setOf("invite_state")) - }) - return JsonObject(buildMap { - putAll(element) - put("rooms", roomsWithEventIds) - }) - } - - private fun MutableMap.putAndConvertEventMap( - key: String, - source: JsonElement?, - eventContainerKeys: Set - ) { - if (source != null) { - require(source is JsonObject) - put(key, convertEventMap(source, eventContainerKeys)) - } - } - - private fun convertEventMap(source: JsonObject, eventContainerKeys: Set): JsonObject { - return JsonObject(buildMap { - source.forEach { - val roomId = it.key - val room = it.value - require(room is JsonObject) - put(roomId, JsonObject(buildMap { - putAll(room) - eventContainerKeys.forEach { eventContainerKey -> - putAndConvertEventMapContent(eventContainerKey, room[eventContainerKey], roomId) - } - })) - } - }) - } - - private fun MutableMap.putAndConvertEventMapContent( - key: String, - source: JsonElement?, - roomId: String, - ) { - if (source != null) { - require(source is JsonObject) - val events = source["events"] - if (events != null) { - require(events is JsonArray) - put(key, JsonObject(buildMap { - putAll(source) - put("events", addRoomIdToEvents(events, roomId)) - })) - } - } - } - - private fun addRoomIdToEvents(source: JsonArray, roomId: String): JsonArray { - return JsonArray(source.map { addRoomIdToEvent(it.jsonObject, roomId) }) - } - - private fun addRoomIdToEvent(event: JsonObject, roomId: String): JsonObject { - return JsonObject(buildMap { - putAll(event) - put("room_id", JsonPrimitive(roomId)) - val unsigned = event["unsigned"]?.jsonObject - if (unsigned != null) { - val aggregations = unsigned["m.relations"]?.jsonObject - val newAggregations = - if (aggregations != null) { - val thread = aggregations["m.thread"]?.jsonObject - if (thread != null) { - val latestEvent = thread["latest_event"]?.jsonObject - if (latestEvent != null) { - JsonObject(buildMap { - putAll(aggregations) - put("m.thread", JsonObject(buildMap { - putAll(thread) - put("latest_event", addRoomIdToEvent(latestEvent, roomId)) - })) - }) - } else null - } else null - } else null - val redactedBecause = unsigned["redacted_because"]?.jsonObject - val newRedactedBecause = - if (redactedBecause != null) { - addRoomIdToEvent(redactedBecause, roomId) - } else null - put("unsigned", JsonObject(buildMap { - putAll(unsigned) - if (newAggregations != null) { - put("m.relations", newAggregations) - } - if (newRedactedBecause != null) { - put("redacted_because", newRedactedBecause) - } - })) - } - }) - } -} \ No newline at end of file +@Deprecated("Replaced by Sync.Response.serializer()", replaceWith = ReplaceWith("Sync.Response.serializer()")) +object SyncResponseSerializer : JsonTransformingSerializer(Sync.Response.serializer()) \ No newline at end of file -- GitLab From dd324d2db7095224193b49ec67e5f2f43f2053bc Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Mon, 4 Aug 2025 11:19:56 +0200 Subject: [PATCH 02/10] Simplify json builder usage --- .../clientserverapi/model/sync/Sync.kt | 23 ++++--------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/sync/Sync.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/sync/Sync.kt index 5a165d620..8a89a5517 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/sync/Sync.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/sync/Sync.kt @@ -1,5 +1,6 @@ package net.folivo.trixnity.clientserverapi.model.sync +import io.ktor.http.ContentType.Application.Json import io.ktor.resources.* import kotlinx.serialization.Contextual import kotlinx.serialization.ExperimentalSerializationApi @@ -114,25 +115,9 @@ data class Sync( index % 2 == 1 -> { requireNotNull(key) require(decoder is JsonDecoder) - val json = Json { - encodeDefaults = decoder.json.configuration.encodeDefaults - explicitNulls = decoder.json.configuration.explicitNulls - ignoreUnknownKeys = decoder.json.configuration.ignoreUnknownKeys - isLenient = decoder.json.configuration.isLenient - prettyPrint = decoder.json.configuration.prettyPrint - prettyPrintIndent = decoder.json.configuration.prettyPrintIndent - coerceInputValues = decoder.json.configuration.coerceInputValues - classDiscriminator = decoder.json.configuration.classDiscriminator - classDiscriminatorMode = decoder.json.configuration.classDiscriminatorMode - useAlternativeNames = decoder.json.configuration.useAlternativeNames - namingStrategy = decoder.json.configuration.namingStrategy - decodeEnumsCaseInsensitive = decoder.json.configuration.decodeEnumsCaseInsensitive - allowTrailingComma = decoder.json.configuration.allowTrailingComma - allowComments = decoder.json.configuration.allowComments - allowSpecialFloatingPointValues = decoder.json.configuration.allowSpecialFloatingPointValues - allowStructuredMapKeys = decoder.json.configuration.allowStructuredMapKeys - useArrayPolymorphism = decoder.json.configuration.useArrayPolymorphism - serializersModule = decoder.serializersModule.overwriteWith(buildSerializersModule(key)) + val json = Json(decoder.json) { + serializersModule = decoder.serializersModule + .overwriteWith(buildSerializersModule(key)) } put(key, decodeSerializableElement(descriptor, index, valueSerializer(json))) } -- GitLab From 1f6aa025bf46726d1d0c3070956503d2b56a763d Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Mon, 4 Aug 2025 11:44:08 +0200 Subject: [PATCH 03/10] Avoid some allocations --- .../trixnity/clientserverapi/model/sync/Sync.kt | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/sync/Sync.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/sync/Sync.kt index 8a89a5517..c1e75d20f 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/sync/Sync.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/sync/Sync.kt @@ -129,26 +129,27 @@ data class Sync( } } - class RoomsKnockSerializer: RoomsMapSerializer( + private object RoomsKnockSerializer: RoomsMapSerializer( Rooms.KnockedRoom.serializer().descriptor, { ContextualSerializer(it, it.serializersModule.serializer()) }, ) - class RoomsJoinSerializer: RoomsMapSerializer( + private object RoomsJoinSerializer: RoomsMapSerializer( Rooms.JoinedRoom.serializer().descriptor, { ContextualSerializer(it, it.serializersModule.serializer()) }, ) - class RoomsInviteSerializer: RoomsMapSerializer( + private object RoomsInviteSerializer: RoomsMapSerializer( Rooms.InvitedRoom.serializer().descriptor, { ContextualSerializer(it, it.serializersModule.serializer()) }, ) - class RoomsLeaveSerializer: RoomsMapSerializer( + + private object RoomsLeaveSerializer: RoomsMapSerializer( Rooms.LeftRoom.serializer().descriptor, { ContextualSerializer(it, it.serializersModule.serializer()) }, ) - class ContextualSerializer(val json: Json, val serializer: KSerializer): KSerializer { + private class ContextualSerializer(val json: Json, val serializer: KSerializer): KSerializer { override val descriptor: SerialDescriptor get() = serializer.descriptor override fun serialize(encoder: Encoder, value: T) { @@ -307,7 +308,7 @@ data class Sync( } - fun buildSerializersModule(roomId: RoomId): SerializersModule { + private fun buildSerializersModule(roomId: RoomId): SerializersModule { val mappings = DefaultEventContentSerializerMappings val messageEventSerializer = WithRoomIdSerializer(roomId, MessageEventSerializer(mappings.message)) val stateEventSerializer = WithRoomIdSerializer(roomId, StateEventSerializer(mappings.state)) @@ -328,7 +329,7 @@ data class Sync( } } - class WithRoomIdSerializer(private val roomId: RoomId, serializer: KSerializer) + private class WithRoomIdSerializer(private val roomId: RoomId, serializer: KSerializer) : JsonTransformingSerializer(serializer) { override fun transformDeserialize(element: JsonElement): JsonElement { require(element is JsonObject) -- GitLab From 205eb2c58092f1188bfdc4797cb4e0b9d9e18d00 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Tue, 5 Aug 2025 11:19:51 +0200 Subject: [PATCH 04/10] Implement serialization as well --- .../trixnity/clientserverapi/model/sync/Sync.kt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/sync/Sync.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/sync/Sync.kt index c1e75d20f..c247e5630 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/sync/Sync.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/sync/Sync.kt @@ -1,6 +1,5 @@ package net.folivo.trixnity.clientserverapi.model.sync -import io.ktor.http.ContentType.Application.Json import io.ktor.resources.* import kotlinx.serialization.Contextual import kotlinx.serialization.ExperimentalSerializationApi @@ -8,7 +7,7 @@ import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.SerializationException -import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.builtins.MapSerializer import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.mapSerialDescriptor import kotlinx.serialization.encoding.CompositeDecoder.Companion.DECODE_DONE @@ -85,9 +84,9 @@ data class Sync( ) { abstract class RoomsMapSerializer( valueDescriptor: SerialDescriptor, - val valueSerializer: (Json) -> KSerializer, + private val valueSerializer: (Json) -> KSerializer, ) : KSerializer> { - private val keySerializer = String.serializer() + private val keySerializer = RoomId.serializer() @OptIn(ExperimentalSerializationApi::class) override val descriptor: SerialDescriptor = mapSerialDescriptor( @@ -98,7 +97,11 @@ data class Sync( override fun serialize( encoder: Encoder, value: Map, - ) = TODO() + ) { + require(encoder is JsonEncoder) + val mapSerializer = MapSerializer(keySerializer, valueSerializer(encoder.json)) + encoder.encodeSerializableValue(mapSerializer, value) + } @OptIn(ExperimentalSerializationApi::class) override fun deserialize(decoder: Decoder): Map { @@ -110,7 +113,7 @@ data class Sync( when { index == DECODE_DONE -> break@loop index % 2 == 0 -> { - key = RoomId(decodeSerializableElement(descriptor, index, keySerializer)) + key = decodeSerializableElement(descriptor, index, keySerializer) } index % 2 == 1 -> { requireNotNull(key) -- GitLab From 8ebde134a5b10323aa95d462ba22860de4596919 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Tue, 5 Aug 2025 12:17:46 +0200 Subject: [PATCH 05/10] decode JSON from source if possible --- .../net/folivo/trixnity/api/client/MatrixApiClient.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/trixnity-api-client/src/commonMain/kotlin/net/folivo/trixnity/api/client/MatrixApiClient.kt b/trixnity-api-client/src/commonMain/kotlin/net/folivo/trixnity/api/client/MatrixApiClient.kt index 7ee9fe94c..0f34549cb 100644 --- a/trixnity-api-client/src/commonMain/kotlin/net/folivo/trixnity/api/client/MatrixApiClient.kt +++ b/trixnity-api-client/src/commonMain/kotlin/net/folivo/trixnity/api/client/MatrixApiClient.kt @@ -11,8 +11,10 @@ import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* +import kotlinx.io.Source import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json +import kotlinx.serialization.json.io.decodeFromSource import kotlinx.serialization.serializer import net.folivo.trixnity.core.* import net.folivo.trixnity.core.HttpMethod @@ -111,9 +113,10 @@ open class MatrixApiClient( request.execute { response -> responseHandler( when { - responseSerializer != null -> json.decodeFromString(responseSerializer, response.bodyAsText()) + responseSerializer != null -> + json.decodeFromSource(responseSerializer, response.body()) endpoint.responseContentType == ContentType.Application.Json -> - json.decodeFromString(serializer(), response.bodyAsText()) + json.decodeFromSource(serializer(), response.body()) else -> response.body() } -- GitLab From 318307bad87843be9b106cd5b33d888ffd7a6667 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Tue, 5 Aug 2025 12:21:12 +0200 Subject: [PATCH 06/10] Add minimal test --- .../sync/CommonSyncResponseSerializerTest.kt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/model/sync/CommonSyncResponseSerializerTest.kt diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/model/sync/CommonSyncResponseSerializerTest.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/model/sync/CommonSyncResponseSerializerTest.kt new file mode 100644 index 000000000..6e4fba22d --- /dev/null +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/model/sync/CommonSyncResponseSerializerTest.kt @@ -0,0 +1,19 @@ +package net.folivo.trixnity.clientserverapi.model.sync + +import net.folivo.trixnity.core.serialization.createMatrixEventJson +import kotlin.test.Test +import kotlin.test.assertEquals + +class CommonSyncResponseSerializerTest { + @Test + fun test() { + val json = createMatrixEventJson() + val value = json.decodeFromString(Sync.Response.serializer(), + "{\"next_batch\":\"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\",\"device_lists\":{\"changed\":[\"@xxxxxxxx-xxxxxxxxxxxx:matrix.org\"]},\"device_one_time_keys_count\":{\"signed_curve25519\":50},\"org.matrix.msc2732.device_unused_fallback_key_types\":[\"signed_curve25519\"],\"device_unused_fallback_key_types\":[\"signed_curve25519\"],\"rooms\":{\"join\":{\"!xxxxxxxxxxxxxxxx:matrix.org\":{\"timeline\":{\"events\":[{\"content\":{\"algorithm\":\"m.megolm.v1.aes-sha2\",\"ciphertext\":\"AwgGEoAB54CgH052RDgJQpaoo0El/sfLtVLURAQGGu6QUnEyjKUjRul80IZmZYsmwxTC2Bs6yDAc0EOWXN8o3vHWSfQsYYwTRkXlMTssVLKGUnqRQajsEtQT0kqNda6WvejITkuZmY2ThUAssVK5NjEGdtaT+vit//zaHG/XAm24Rs9NMcHObrwv/sbQHPt2htPiWVHLXQtakXyU810BIKNxUR5ILT4qUW/e6Z6eZUinKwKnpy2nWkJIJPki9o3zpGjMTKcq4e28j3X/vwc\",\"device_id\":\"TCXQVOAEIN\",\"sender_key\":\"Yy+JlhZtawC9oqLnOjFEUwzl/879kwyi8ivZ3u99a14\",\"session_id\":\"BGCwyqkDmnAe5L+dTw7wQNV8NPlPI2xoxHxwhfMMYtY\"},\"origin_server_ts\":1753141908323,\"sender\":\"@xxxxxxxx-xxxxxxxxxxxx:matrix.org\",\"type\":\"m.room.encrypted\",\"unsigned\":{\"membership\":\"join\",\"age\":254,\"transaction_id\":\"m1753141907962.0\"},\"event_id\":\"\$xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\"}],\"prev_batch\":\"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\",\"limited\":false},\"state\":{\"events\":[{\"content\":{\"avatar_url\":\"mxc://matrix.org/xxxxxxxxxxxxxxxx\",\"displayname\":\"justJanne\",\"membership\":\"join\"},\"origin_server_ts\":1748954082755,\"sender\":\"@xxxxxxxx-xxxxxxxxxxxx:matrix.org\",\"state_key\":\"@xxxxxxxx-xxxxxxxxxxxx:matrix.org\",\"type\":\"m.room.member\",\"unsigned\":{\"replaces_state\":\"\$xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\",\"age\":4187825822},\"event_id\":\"\$xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\"}]},\"account_data\":{\"events\":[]},\"ephemeral\":{\"events\":[]},\"unread_notifications\":{\"notification_count\":0,\"highlight_count\":0},\"summary\":{}}}}}" + ) + assertEquals( + expected = "{\"next_batch\":\"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\",\"rooms\":{\"join\":{\"!xxxxxxxxxxxxxxxx:matrix.org\":{\"summary\":{},\"state\":{\"events\":[{\"content\":{\"avatar_url\":\"mxc://matrix.org/xxxxxxxxxxxxxxxx\",\"displayname\":\"justJanne\",\"membership\":\"join\"},\"event_id\":\"\$xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\",\"origin_server_ts\":1748954082755,\"room_id\":\"!xxxxxxxxxxxxxxxx:matrix.org\",\"sender\":\"@xxxxxxxx-xxxxxxxxxxxx:matrix.org\",\"state_key\":\"@xxxxxxxx-xxxxxxxxxxxx:matrix.org\",\"type\":\"m.room.member\",\"unsigned\":{\"age\":4187825822}}]},\"timeline\":{\"events\":[{\"content\":{\"algorithm\":\"m.megolm.v1.aes-sha2\",\"ciphertext\":\"AwgGEoAB54CgH052RDgJQpaoo0El/sfLtVLURAQGGu6QUnEyjKUjRul80IZmZYsmwxTC2Bs6yDAc0EOWXN8o3vHWSfQsYYwTRkXlMTssVLKGUnqRQajsEtQT0kqNda6WvejITkuZmY2ThUAssVK5NjEGdtaT+vit//zaHG/XAm24Rs9NMcHObrwv/sbQHPt2htPiWVHLXQtakXyU810BIKNxUR5ILT4qUW/e6Z6eZUinKwKnpy2nWkJIJPki9o3zpGjMTKcq4e28j3X/vwc\",\"device_id\":\"TCXQVOAEIN\",\"sender_key\":\"Yy+JlhZtawC9oqLnOjFEUwzl/879kwyi8ivZ3u99a14\",\"session_id\":\"BGCwyqkDmnAe5L+dTw7wQNV8NPlPI2xoxHxwhfMMYtY\"},\"event_id\":\"\$xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\",\"origin_server_ts\":1753141908323,\"room_id\":\"!xxxxxxxxxxxxxxxx:matrix.org\",\"sender\":\"@xxxxxxxx-xxxxxxxxxxxx:matrix.org\",\"type\":\"m.room.encrypted\",\"unsigned\":{\"age\":254,\"membership\":\"join\",\"transaction_id\":\"m1753141907962.0\"}}],\"limited\":false,\"prev_batch\":\"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\"},\"ephemeral\":{\"events\":[]},\"account_data\":{\"events\":[]},\"unread_notifications\":{\"highlight_count\":0,\"notification_count\":0}}}},\"device_lists\":{\"changed\":[\"@xxxxxxxx-xxxxxxxxxxxx:matrix.org\"]},\"device_one_time_keys_count\":{\"signed_curve25519\":50},\"device_unused_fallback_key_types\":[\"signed_curve25519\"]}", + actual = json.encodeToString(Sync.Response.serializer(), value), + ) + } +} -- GitLab From daa1a1470d09c382e16fd7d51e05f77bfbf95617 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Tue, 5 Aug 2025 13:30:29 +0200 Subject: [PATCH 07/10] Add tests for addRoomIdToEvent --- .../model/sync/SyncAddRoomIdTest.kt | 184 ++++++++++++++++++ ...rTest.kt => SyncResponseSerializerTest.kt} | 4 +- 2 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/model/sync/SyncAddRoomIdTest.kt rename trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/model/sync/{CommonSyncResponseSerializerTest.kt => SyncResponseSerializerTest.kt} (98%) diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/model/sync/SyncAddRoomIdTest.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/model/sync/SyncAddRoomIdTest.kt new file mode 100644 index 000000000..b5f5771d0 --- /dev/null +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/model/sync/SyncAddRoomIdTest.kt @@ -0,0 +1,184 @@ +package net.folivo.trixnity.clientserverapi.model.sync + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import net.folivo.trixnity.core.model.RoomId +import kotlin.test.Test +import kotlin.test.assertEquals + +class SyncAddRoomIdTest { + @Test + fun addsRoomIdToMessage() { + assertEquals( + expected = Json.parseToJsonElement( + "{\"content\":{\"body\":\"Message\",\"m.mentions\":{},\"msgtype\":\"m.text\"},\"origin_server_ts\":1754390485300,\"sender\":\"@janne-koschinski:matrix.org\",\"type\":\"m.room.message\",\"unsigned\":{\"membership\":\"join\",\"age\":8385,\"transaction_id\":\"m1754390484474.11\"},\"event_id\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\",\"room_id\":\"!kEPxfTHRGAIEMjppiX:matrix.org\"}" + ), + actual = Sync.addRoomIdToEvent( + roomId = RoomId("!kEPxfTHRGAIEMjppiX:matrix.org"), + event = Json.parseToJsonElement( + "{\"content\":{\"body\":\"Message\",\"m.mentions\":{},\"msgtype\":\"m.text\"},\"origin_server_ts\":1754390485300,\"sender\":\"@janne-koschinski:matrix.org\",\"type\":\"m.room.message\",\"unsigned\":{\"membership\":\"join\",\"age\":8385,\"transaction_id\":\"m1754390484474.11\"},\"event_id\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\"}", + ) as JsonObject + ) + ) + } + + @Test + fun addsRoomIdToEdit() { + assertEquals( + expected = Json.parseToJsonElement( + "{\"type\":\"m.room.message\",\"content\":{\"msgtype\":\"m.text\",\"body\":\"* Edit\",\"m.new_content\":{\"msgtype\":\"m.text\",\"body\":\"Edit\",\"m.mentions\":{}},\"m.mentions\":{},\"m.relates_to\":{\"rel_type\":\"m.replace\",\"event_id\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\"}},\"event_id\":\"\$f7rUNQQk_euyvEmOIwUzLYV_1XPXmzX6N-GNsuub4qk\",\"user_id\":\"@janne-koschinski:matrix.org\",\"sender\":\"@janne-koschinski:matrix.org\",\"room_id\":\"!kEPxfTHRGAIEMjppiX:matrix.org\",\"origin_server_ts\":1754390532635}" + ), + actual = Sync.addRoomIdToEvent( + roomId = RoomId("!kEPxfTHRGAIEMjppiX:matrix.org"), + event = Json.parseToJsonElement( + "{\"type\":\"m.room.message\",\"content\":{\"msgtype\":\"m.text\",\"body\":\"* Edit\",\"m.new_content\":{\"msgtype\":\"m.text\",\"body\":\"Edit\",\"m.mentions\":{}},\"m.mentions\":{},\"m.relates_to\":{\"rel_type\":\"m.replace\",\"event_id\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\"}},\"event_id\":\"\$f7rUNQQk_euyvEmOIwUzLYV_1XPXmzX6N-GNsuub4qk\",\"user_id\":\"@janne-koschinski:matrix.org\",\"sender\":\"@janne-koschinski:matrix.org\",\"origin_server_ts\":1754390532635}", + ) as JsonObject + ) + ) + } + + @Test + fun addsRoomIdToReply() { + assertEquals( + expected = Json.parseToJsonElement( + "{\"content\":{\"body\":\"Reply\",\"m.mentions\":{},\"m.relates_to\":{\"m.in_reply_to\":{\"event_id\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\"}},\"msgtype\":\"m.text\"},\"origin_server_ts\":1754390489981,\"sender\":\"@janne-koschinski:matrix.org\",\"type\":\"m.room.message\",\"unsigned\":{\"membership\":\"join\",\"age\":1853,\"transaction_id\":\"m1754390489106.12\"},\"event_id\":\"\$6mmtLgBQ-Ae8l7NSsHKYkgtn8O6EOwKxSAKEE_PK0bY\",\"room_id\":\"!kEPxfTHRGAIEMjppiX:matrix.org\"}" + ), + actual = Sync.addRoomIdToEvent( + roomId = RoomId("!kEPxfTHRGAIEMjppiX:matrix.org"), + event = Json.parseToJsonElement( + "{\"content\":{\"body\":\"Reply\",\"m.mentions\":{},\"m.relates_to\":{\"m.in_reply_to\":{\"event_id\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\"}},\"msgtype\":\"m.text\"},\"origin_server_ts\":1754390489981,\"sender\":\"@janne-koschinski:matrix.org\",\"type\":\"m.room.message\",\"unsigned\":{\"membership\":\"join\",\"age\":1853,\"transaction_id\":\"m1754390489106.12\"},\"event_id\":\"\$6mmtLgBQ-Ae8l7NSsHKYkgtn8O6EOwKxSAKEE_PK0bY\"}" + ) as JsonObject + ) + ) + } + + @Test + fun addsRoomIdToEmptyThreadRoot() { + // Missing + assertEquals( + expected = Json.parseToJsonElement( + "{\"content\":{\"body\":\"Message\",\"m.mentions\":{},\"msgtype\":\"m.text\"},\"origin_server_ts\":1754390485300,\"sender\":\"@janne-koschinski:matrix.org\",\"type\":\"m.room.message\",\"unsigned\":{\"membership\":\"join\",\"age\":8385,\"transaction_id\":\"m1754390484474.11\",\"m.relations\":{\"m.thread\":{\"count\":0,\"current_user_participated\":false}}},\"event_id\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\",\"room_id\":\"!kEPxfTHRGAIEMjppiX:matrix.org\"}" + ), + actual = Sync.addRoomIdToEvent( + roomId = RoomId("!kEPxfTHRGAIEMjppiX:matrix.org"), + event = Json.parseToJsonElement( + "{\"content\":{\"body\":\"Message\",\"m.mentions\":{},\"msgtype\":\"m.text\"},\"origin_server_ts\":1754390485300,\"sender\":\"@janne-koschinski:matrix.org\",\"type\":\"m.room.message\",\"unsigned\":{\"membership\":\"join\",\"age\":8385,\"transaction_id\":\"m1754390484474.11\",\"m.relations\":{\"m.thread\":{\"count\":0,\"current_user_participated\":false}}},\"event_id\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\"}" + ) as JsonObject + ) + ) + // Null + assertEquals( + expected = Json.parseToJsonElement( + "{\"content\":{\"body\":\"Message\",\"m.mentions\":{},\"msgtype\":\"m.text\"},\"origin_server_ts\":1754390485300,\"sender\":\"@janne-koschinski:matrix.org\",\"type\":\"m.room.message\",\"unsigned\":{\"membership\":\"join\",\"age\":8385,\"transaction_id\":\"m1754390484474.11\",\"m.relations\":{\"m.thread\":{\"latest_event\":null,\"count\":0,\"current_user_participated\":false}}},\"event_id\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\",\"room_id\":\"!kEPxfTHRGAIEMjppiX:matrix.org\"}" + ), + actual = Sync.addRoomIdToEvent( + roomId = RoomId("!kEPxfTHRGAIEMjppiX:matrix.org"), + event = Json.parseToJsonElement( + "{\"content\":{\"body\":\"Message\",\"m.mentions\":{},\"msgtype\":\"m.text\"},\"origin_server_ts\":1754390485300,\"sender\":\"@janne-koschinski:matrix.org\",\"type\":\"m.room.message\",\"unsigned\":{\"membership\":\"join\",\"age\":8385,\"transaction_id\":\"m1754390484474.11\",\"m.relations\":{\"m.thread\":{\"latest_event\":null,\"count\":0,\"current_user_participated\":false}}},\"event_id\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\"}" + ) as JsonObject + ) + ) + // Undefined + assertEquals( + expected = Json.parseToJsonElement( + "{\"content\":{\"body\":\"Message\",\"m.mentions\":{},\"msgtype\":\"m.text\"},\"origin_server_ts\":1754390485300,\"sender\":\"@janne-koschinski:matrix.org\",\"type\":\"m.room.message\",\"unsigned\":{\"membership\":\"join\",\"age\":8385,\"transaction_id\":\"m1754390484474.11\",\"m.relations\":{\"m.thread\":{\"latest_event\":undefined,\"count\":0,\"current_user_participated\":false}}},\"event_id\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\",\"room_id\":\"!kEPxfTHRGAIEMjppiX:matrix.org\"}" + ), + actual = Sync.addRoomIdToEvent( + roomId = RoomId("!kEPxfTHRGAIEMjppiX:matrix.org"), + event = Json.parseToJsonElement( + "{\"content\":{\"body\":\"Message\",\"m.mentions\":{},\"msgtype\":\"m.text\"},\"origin_server_ts\":1754390485300,\"sender\":\"@janne-koschinski:matrix.org\",\"type\":\"m.room.message\",\"unsigned\":{\"membership\":\"join\",\"age\":8385,\"transaction_id\":\"m1754390484474.11\",\"m.relations\":{\"m.thread\":{\"latest_event\":undefined,\"count\":0,\"current_user_participated\":false}}},\"event_id\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\"}" + ) as JsonObject + ) + ) + } + + @Test + fun addsRoomIdToThreadRoot() { + assertEquals( + expected = Json.parseToJsonElement( + "{\"content\":{\"body\":\"Message\",\"m.mentions\":{},\"msgtype\":\"m.text\"},\"origin_server_ts\":1754390485300,\"sender\":\"@janne-koschinski:matrix.org\",\"type\":\"m.room.message\",\"unsigned\":{\"membership\":\"join\",\"age\":8385,\"transaction_id\":\"m1754390484474.11\",\"m.relations\":{\"m.thread\":{\"latest_event\":{\"content\":{\"body\":\"Thread\",\"m.mentions\":{},\"m.relates_to\":{\"event_id\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\",\"is_falling_back\":true,\"m.in_reply_to\":{\"event_id\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\"},\"rel_type\":\"m.thread\"},\"msgtype\":\"m.text\"},\"origin_server_ts\":1754390493121,\"room_id\":\"!kEPxfTHRGAIEMjppiX:matrix.org\",\"sender\":\"@janne-koschinski:matrix.org\",\"type\":\"m.room.message\",\"unsigned\":{\"age\":564,\"transaction_id\":\"m1754390492376.13\"},\"event_id\":\"\$dkt-fE304ytkg_nUf-jEN9eCpA9rUZ8iOGFgxbTyroY\",\"user_id\":\"@janne-koschinski:matrix.org\",\"age\":564},\"count\":1,\"current_user_participated\":true}}},\"event_id\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\",\"room_id\":\"!kEPxfTHRGAIEMjppiX:matrix.org\"}" + ), + actual = Sync.addRoomIdToEvent( + roomId = RoomId("!kEPxfTHRGAIEMjppiX:matrix.org"), + event = Json.parseToJsonElement( + "{\"content\":{\"body\":\"Message\",\"m.mentions\":{},\"msgtype\":\"m.text\"},\"origin_server_ts\":1754390485300,\"sender\":\"@janne-koschinski:matrix.org\",\"type\":\"m.room.message\",\"unsigned\":{\"membership\":\"join\",\"age\":8385,\"transaction_id\":\"m1754390484474.11\",\"m.relations\":{\"m.thread\":{\"latest_event\":{\"content\":{\"body\":\"Thread\",\"m.mentions\":{},\"m.relates_to\":{\"event_id\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\",\"is_falling_back\":true,\"m.in_reply_to\":{\"event_id\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\"},\"rel_type\":\"m.thread\"},\"msgtype\":\"m.text\"},\"origin_server_ts\":1754390493121,\"sender\":\"@janne-koschinski:matrix.org\",\"type\":\"m.room.message\",\"unsigned\":{\"age\":564,\"transaction_id\":\"m1754390492376.13\"},\"event_id\":\"\$dkt-fE304ytkg_nUf-jEN9eCpA9rUZ8iOGFgxbTyroY\",\"user_id\":\"@janne-koschinski:matrix.org\",\"age\":564},\"count\":1,\"current_user_participated\":true}}},\"event_id\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\"}" + ) as JsonObject + ) + ) + } + + @Test + fun addsRoomIdToThreadMessage() { + assertEquals( + expected = Json.parseToJsonElement( + "{\"content\":{\"body\":\"Thread\",\"m.mentions\":{},\"m.relates_to\":{\"event_id\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\",\"is_falling_back\":true,\"m.in_reply_to\":{\"event_id\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\"},\"rel_type\":\"m.thread\"},\"msgtype\":\"m.text\"},\"origin_server_ts\":1754390493121,\"sender\":\"@janne-koschinski:matrix.org\",\"type\":\"m.room.message\",\"unsigned\":{\"membership\":\"join\",\"age\":564,\"transaction_id\":\"m1754390492376.13\"},\"event_id\":\"\$dkt-fE304ytkg_nUf-jEN9eCpA9rUZ8iOGFgxbTyroY\",\"room_id\":\"!kEPxfTHRGAIEMjppiX:matrix.org\"}" + ), + actual = Sync.addRoomIdToEvent( + roomId = RoomId("!kEPxfTHRGAIEMjppiX:matrix.org"), + event = Json.parseToJsonElement( + "{\"content\":{\"body\":\"Thread\",\"m.mentions\":{},\"m.relates_to\":{\"event_id\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\",\"is_falling_back\":true,\"m.in_reply_to\":{\"event_id\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\"},\"rel_type\":\"m.thread\"},\"msgtype\":\"m.text\"},\"origin_server_ts\":1754390493121,\"sender\":\"@janne-koschinski:matrix.org\",\"type\":\"m.room.message\",\"unsigned\":{\"membership\":\"join\",\"age\":564,\"transaction_id\":\"m1754390492376.13\"},\"event_id\":\"\$dkt-fE304ytkg_nUf-jEN9eCpA9rUZ8iOGFgxbTyroY\"}" + ) as JsonObject + ) + ) + } + + @Test + fun addsRoomIdToRedactedMessage() { + assertEquals( + expected = Json.parseToJsonElement( + "{\"content\":{},\"origin_server_ts\":1754390485300,\"sender\":\"@janne-koschinski:matrix.org\",\"type\":\"m.room.message\",\"unsigned\":{\"membership\":\"join\",\"age\":911,\"transaction_id\":\"m1754390739783.15\",\"redacted_because\":{\"content\":{\"redacts\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\"},\"origin_server_ts\":1754391184760,\"redacts\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\",\"sender\":\"@janne-koschinski:matrix.org\",\"type\":\"m.room.redaction\",\"unsigned\":{\"membership\":\"join\",\"age\":8385,\"transaction_id\":\"m1754390484474.11\"},\"event_id\":\"\$DPV7jd4H4Q4CMD26W-6UIvmB_uUbyy26mToMGuYAr2k\",\"room_id\":\"!kEPxfTHRGAIEMjppiX:matrix.org\"}},\"event_id\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\",\"room_id\":\"!kEPxfTHRGAIEMjppiX:matrix.org\"}" + ), + actual = Sync.addRoomIdToEvent( + roomId = RoomId("!kEPxfTHRGAIEMjppiX:matrix.org"), + event = Json.parseToJsonElement( + "{\"content\":{},\"origin_server_ts\":1754390485300,\"sender\":\"@janne-koschinski:matrix.org\",\"type\":\"m.room.message\",\"unsigned\":{\"membership\":\"join\",\"age\":911,\"transaction_id\":\"m1754390739783.15\",\"redacted_because\":{\"content\":{\"redacts\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\"},\"origin_server_ts\":1754391184760,\"redacts\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\",\"sender\":\"@janne-koschinski:matrix.org\",\"type\":\"m.room.redaction\",\"unsigned\":{\"membership\":\"join\",\"age\":8385,\"transaction_id\":\"m1754390484474.11\"},\"event_id\":\"\$DPV7jd4H4Q4CMD26W-6UIvmB_uUbyy26mToMGuYAr2k\"}},\"event_id\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\"}" + ) as JsonObject + ) + ) + } + + @Test + fun addsRoomIdToRedaction() { + assertEquals( + expected = Json.parseToJsonElement( + "{\"content\":{\"redacts\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\"},\"origin_server_ts\":1754391184760,\"redacts\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\",\"sender\":\"@janne-koschinski:matrix.org\",\"type\":\"m.room.redaction\",\"unsigned\":{\"membership\":\"join\",\"age\":386,\"transaction_id\":\"m1754391184606.16\"},\"event_id\":\"\$DPV7jd4H4Q4CMD26W-6UIvmB_uUbyy26mToMGuYAr2k\",\"room_id\":\"!kEPxfTHRGAIEMjppiX:matrix.org\"}" + ), + actual = Sync.addRoomIdToEvent( + roomId = RoomId("!kEPxfTHRGAIEMjppiX:matrix.org"), + event = Json.parseToJsonElement( + "{\"content\":{\"redacts\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\"},\"origin_server_ts\":1754391184760,\"redacts\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\",\"sender\":\"@janne-koschinski:matrix.org\",\"type\":\"m.room.redaction\",\"unsigned\":{\"membership\":\"join\",\"age\":386,\"transaction_id\":\"m1754391184606.16\"},\"event_id\":\"\$DPV7jd4H4Q4CMD26W-6UIvmB_uUbyy26mToMGuYAr2k\"}" + ) as JsonObject + ) + ) + } + + @Test + fun addsRoomIdToMessageWithReaction() { + assertEquals( + expected = Json.parseToJsonElement( + "{\"content\":{\"body\":\"Message\",\"m.mentions\":{},\"msgtype\":\"m.text\"},\"origin_server_ts\":1754390485300,\"sender\":\"@janne-koschinski:matrix.org\",\"type\":\"m.room.message\",\"unsigned\":{\"membership\":\"join\",\"age\":8385,\"transaction_id\":\"m1754390484474.11\",\"m.relations\":{\"org.matrix.msc2675.annotation\": [{\"key\":\"👍\",\"origin_server_ts\":1754392124424,\"count\":1}]}},\"event_id\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\",\"room_id\":\"!kEPxfTHRGAIEMjppiX:matrix.org\"}" + ), + actual = Sync.addRoomIdToEvent( + roomId = RoomId("!kEPxfTHRGAIEMjppiX:matrix.org"), + event = Json.parseToJsonElement( + "{\"content\":{\"body\":\"Message\",\"m.mentions\":{},\"msgtype\":\"m.text\"},\"origin_server_ts\":1754390485300,\"sender\":\"@janne-koschinski:matrix.org\",\"type\":\"m.room.message\",\"unsigned\":{\"membership\":\"join\",\"age\":8385,\"transaction_id\":\"m1754390484474.11\",\"m.relations\":{\"org.matrix.msc2675.annotation\": [{\"key\":\"👍\",\"origin_server_ts\":1754392124424,\"count\":1}]}},\"event_id\":\"\$osCnOKPfvz3RurR7MsIw7IAOUMTHRS7e4OUakktZlz0\"}" + ) as JsonObject + ) + ) + } + + @Test + fun addsRoomIdToReaction() { + assertEquals( + expected = Json.parseToJsonElement( + "{\"content\":{\"m.relates_to\":{\"event_id\":\"\$-jJeIc76jw5AXMEsTY3p_nn3vSs7mtavdWOkA0aUZ_0\",\"key\":\"👍️\",\"rel_type\":\"m.annotation\"}},\"origin_server_ts\":1754392124424,\"sender\":\"@janne-koschinski:matrix.org\",\"type\":\"m.reaction\",\"unsigned\":{\"membership\":\"join\",\"age\":755,\"transaction_id\":\"m1754392124307.20\"},\"event_id\":\"\$J6XPX4JxrFBTXpvnw-_hC-WE-STfr6HbBdi8bvajbRY\",\"room_id\":\"!kEPxfTHRGAIEMjppiX:matrix.org\"}" + ), + actual = Sync.addRoomIdToEvent( + roomId = RoomId("!kEPxfTHRGAIEMjppiX:matrix.org"), + event = Json.parseToJsonElement( + "{\"content\":{\"m.relates_to\":{\"event_id\":\"\$-jJeIc76jw5AXMEsTY3p_nn3vSs7mtavdWOkA0aUZ_0\",\"key\":\"👍️\",\"rel_type\":\"m.annotation\"}},\"origin_server_ts\":1754392124424,\"sender\":\"@janne-koschinski:matrix.org\",\"type\":\"m.reaction\",\"unsigned\":{\"membership\":\"join\",\"age\":755,\"transaction_id\":\"m1754392124307.20\"},\"event_id\":\"\$J6XPX4JxrFBTXpvnw-_hC-WE-STfr6HbBdi8bvajbRY\"}" + ) as JsonObject + ) + ) + } +} diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/model/sync/CommonSyncResponseSerializerTest.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/model/sync/SyncResponseSerializerTest.kt similarity index 98% rename from trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/model/sync/CommonSyncResponseSerializerTest.kt rename to trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/model/sync/SyncResponseSerializerTest.kt index 6e4fba22d..a0ac75f56 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/model/sync/CommonSyncResponseSerializerTest.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/model/sync/SyncResponseSerializerTest.kt @@ -4,9 +4,9 @@ import net.folivo.trixnity.core.serialization.createMatrixEventJson import kotlin.test.Test import kotlin.test.assertEquals -class CommonSyncResponseSerializerTest { +class SyncResponseSerializerTest { @Test - fun test() { + fun testSimpleSync() { val json = createMatrixEventJson() val value = json.decodeFromString(Sync.Response.serializer(), "{\"next_batch\":\"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\",\"device_lists\":{\"changed\":[\"@xxxxxxxx-xxxxxxxxxxxx:matrix.org\"]},\"device_one_time_keys_count\":{\"signed_curve25519\":50},\"org.matrix.msc2732.device_unused_fallback_key_types\":[\"signed_curve25519\"],\"device_unused_fallback_key_types\":[\"signed_curve25519\"],\"rooms\":{\"join\":{\"!xxxxxxxxxxxxxxxx:matrix.org\":{\"timeline\":{\"events\":[{\"content\":{\"algorithm\":\"m.megolm.v1.aes-sha2\",\"ciphertext\":\"AwgGEoAB54CgH052RDgJQpaoo0El/sfLtVLURAQGGu6QUnEyjKUjRul80IZmZYsmwxTC2Bs6yDAc0EOWXN8o3vHWSfQsYYwTRkXlMTssVLKGUnqRQajsEtQT0kqNda6WvejITkuZmY2ThUAssVK5NjEGdtaT+vit//zaHG/XAm24Rs9NMcHObrwv/sbQHPt2htPiWVHLXQtakXyU810BIKNxUR5ILT4qUW/e6Z6eZUinKwKnpy2nWkJIJPki9o3zpGjMTKcq4e28j3X/vwc\",\"device_id\":\"TCXQVOAEIN\",\"sender_key\":\"Yy+JlhZtawC9oqLnOjFEUwzl/879kwyi8ivZ3u99a14\",\"session_id\":\"BGCwyqkDmnAe5L+dTw7wQNV8NPlPI2xoxHxwhfMMYtY\"},\"origin_server_ts\":1753141908323,\"sender\":\"@xxxxxxxx-xxxxxxxxxxxx:matrix.org\",\"type\":\"m.room.encrypted\",\"unsigned\":{\"membership\":\"join\",\"age\":254,\"transaction_id\":\"m1753141907962.0\"},\"event_id\":\"\$xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\"}],\"prev_batch\":\"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\",\"limited\":false},\"state\":{\"events\":[{\"content\":{\"avatar_url\":\"mxc://matrix.org/xxxxxxxxxxxxxxxx\",\"displayname\":\"justJanne\",\"membership\":\"join\"},\"origin_server_ts\":1748954082755,\"sender\":\"@xxxxxxxx-xxxxxxxxxxxx:matrix.org\",\"state_key\":\"@xxxxxxxx-xxxxxxxxxxxx:matrix.org\",\"type\":\"m.room.member\",\"unsigned\":{\"replaces_state\":\"\$xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\",\"age\":4187825822},\"event_id\":\"\$xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\"}]},\"account_data\":{\"events\":[]},\"ephemeral\":{\"events\":[]},\"unread_notifications\":{\"notification_count\":0,\"highlight_count\":0},\"summary\":{}}}}}" -- GitLab From 67681f63d4e914cfb171c19f21a77cb9e622ce03 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Tue, 5 Aug 2025 13:31:38 +0200 Subject: [PATCH 08/10] Improve pattern matching to avoid case for impossible branch --- .../folivo/trixnity/clientserverapi/model/sync/Sync.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/sync/Sync.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/sync/Sync.kt index c247e5630..7653dd425 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/sync/Sync.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/sync/Sync.kt @@ -110,12 +110,15 @@ data class Sync( var key: RoomId? = null loop@ while (true) { val index = decodeElementIndex(descriptor) + // We've got keys and values interleaved, + // with keys in even positions and values in odd positions + val isKey = index % 2 == 0 when { index == DECODE_DONE -> break@loop - index % 2 == 0 -> { + isKey -> { key = decodeSerializableElement(descriptor, index, keySerializer) } - index % 2 == 1 -> { + else -> { requireNotNull(key) require(decoder is JsonDecoder) val json = Json(decoder.json) { @@ -124,7 +127,6 @@ data class Sync( } put(key, decodeSerializableElement(descriptor, index, valueSerializer(json))) } - else -> throw SerializationException("Unexpected index $index") } } } -- GitLab From fa6cb1137677670d6f2d6606e93b38201adbf714 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Tue, 5 Aug 2025 13:31:59 +0200 Subject: [PATCH 09/10] Fix issue where null/undefined/missing values were treated differently --- .../folivo/trixnity/clientserverapi/model/sync/Sync.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/sync/Sync.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/sync/Sync.kt index 7653dd425..7e9b0c22d 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/sync/Sync.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/sync/Sync.kt @@ -275,14 +275,14 @@ data class Sync( return JsonObject(buildMap { putAll(event) put("room_id", JsonPrimitive(roomId.full)) - val unsigned = event["unsigned"]?.jsonObject + val unsigned = event["unsigned"] as? JsonObject if (unsigned != null) { - val aggregations = unsigned["m.relations"]?.jsonObject + val aggregations = unsigned["m.relations"] as? JsonObject val newAggregations = if (aggregations != null) { - val thread = aggregations["m.thread"]?.jsonObject + val thread = aggregations["m.thread"] as? JsonObject if (thread != null) { - val latestEvent = thread["latest_event"]?.jsonObject + val latestEvent = thread["latest_event"] as? JsonObject if (latestEvent != null) { JsonObject(buildMap { putAll(aggregations) @@ -294,7 +294,7 @@ data class Sync( } else null } else null } else null - val redactedBecause = unsigned["redacted_because"]?.jsonObject + val redactedBecause = unsigned["redacted_because"] as? JsonObject val newRedactedBecause = if (redactedBecause != null) { addRoomIdToEvent(redactedBecause, roomId) -- GitLab From 02ab3773fd838f89698db55c4108985e1ca83d48 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Tue, 5 Aug 2025 13:48:30 +0200 Subject: [PATCH 10/10] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 290d4e263..e4b471c31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fix OOM gradle dependencies tasks +- Fix OOM during initial sync due to sync response parsing ### Security -- GitLab