commonMain.net.folivo.trixnity.client.store.TimelineEvent.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of trixnity-client-jvm Show documentation
Show all versions of trixnity-client-jvm Show documentation
Multiplatform Kotlin SDK for matrix-protocol
package net.folivo.trixnity.client.store
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.*
import net.folivo.trixnity.core.model.EventId
import net.folivo.trixnity.core.model.events.ClientEvent
import net.folivo.trixnity.core.model.events.RedactedEventContent
import net.folivo.trixnity.core.model.events.RoomEventContent
import net.folivo.trixnity.core.model.events.UnknownEventContent
import net.folivo.trixnity.core.serialization.events.EventContentSerializerMapping
import net.folivo.trixnity.core.serialization.events.RedactedEventContentSerializer
import net.folivo.trixnity.core.serialization.events.UnknownEventContentSerializer
data class TimelineEvent(
val event: ClientEvent.RoomEvent<*>,
/**
* - event is not encrypted -> original content
* - event is encrypted
* - not yet decrypted -> null
* - successfully decrypted -> Result.Success
* - failure in decryption -> Result.Failure (contains TimelineEventContentError)
*
* The content may be replaced by another event.
*/
val content: Result? = if (event.isEncrypted) null else Result.success(event.content),
val previousEventId: EventId?,
val nextEventId: EventId?,
val gap: Gap?,
) {
@OptIn(ExperimentalSerializationApi::class)
@Serializable
@JsonClassDiscriminator("position")
sealed interface Gap {
val batchBefore: String?
val batchAfter: String?
@Serializable
@SerialName("before")
data class GapBefore(
override val batchBefore: String,
) : Gap {
@Transient
override val batchAfter: String? = null
}
@Serializable
@SerialName("both")
data class GapBoth(
override val batchBefore: String,
override val batchAfter: String,
) : Gap
@Serializable
@SerialName("after")
data class GapAfter(
override val batchAfter: String,
) : Gap {
@Transient
override val batchBefore: String? = null
}
val hasGapBefore: Boolean
get() = batchBefore != null
val hasGapAfter: Boolean
get() = batchAfter != null
val hasGapBoth: Boolean
get() = batchBefore != null && batchAfter != null
fun removeGapBefore() = batchAfter?.let { GapAfter(it) }
fun removeGapAfter() = batchBefore?.let { GapBefore(it) }
}
sealed interface TimelineEventContentError {
data object DecryptionTimeout : TimelineEventContentError,
RuntimeException("timeout while decrypting TimelineEvent")
data object DecryptionAlgorithmNotSupported : TimelineEventContentError,
RuntimeException("algorithm not supported for decrypting event")
data class DecryptionError(
val error: Throwable,
) : TimelineEventContentError, RuntimeException("error while decrypting TimelineEvent", error)
data object NoContent : TimelineEventContentError, RuntimeException("no content found to replace TimelineEvent")
}
}
class TimelineEventSerializer(
private val mappings: Set>,
private val storeTimelineEventContentUnencrypted: Boolean,
) : KSerializer {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("TimelineEventSerializer")
private val contentDeserializers: Map> by lazy {
mappings.associate { it.type to it.serializer }
}
@OptIn(ExperimentalSerializationApi::class)
override fun deserialize(decoder: Decoder): TimelineEvent {
require(decoder is JsonDecoder)
val jsonObject = decoder.decodeJsonElement().jsonObject
val event: ClientEvent.RoomEvent<*> = checkNotNull(jsonObject["event"]).jsonObject.let {
val serializer = decoder.json.serializersModule.getContextual(ClientEvent.RoomEvent::class)
checkNotNull(serializer)
decoder.json.decodeFromJsonElement(serializer, it)
}
val content: Result? = (jsonObject["content"] as? JsonObject)?.let {
val type = checkNotNull(it["type"]).jsonPrimitive.content
val serializer = contentDeserializers[type] ?: UnknownEventContentSerializer(type)
val value = checkNotNull(it["value"])
Result.success(decoder.json.decodeFromJsonElement(serializer, value))
}
val previousEventId: EventId? = jsonObject["previousEventId"]?.let {
decoder.json.decodeFromJsonElement(it)
}
val nextEventId: EventId? = jsonObject["nextEventId"]?.let {
decoder.json.decodeFromJsonElement(it)
}
val gap: TimelineEvent.Gap? = jsonObject["gap"]?.let {
decoder.json.decodeFromJsonElement(it)
}
return if (content != null) {
TimelineEvent(
event = event,
content = content,
previousEventId = previousEventId,
nextEventId = nextEventId,
gap = gap
)
} else {
TimelineEvent(
event = event,
previousEventId = previousEventId,
nextEventId = nextEventId,
gap = gap
)
}
}
@OptIn(ExperimentalSerializationApi::class)
override fun serialize(encoder: Encoder, value: TimelineEvent) {
require(encoder is JsonEncoder)
val event: JsonElement = value.event.let {
val serializer = encoder.json.serializersModule.getContextual(ClientEvent.RoomEvent::class)
checkNotNull(serializer)
encoder.json.encodeToJsonElement(serializer, it)
}
val content: JsonObject? = value.content?.getOrNull()?.let { content ->
if (!storeTimelineEventContentUnencrypted || !value.isEncrypted) {
null
} else {
var type: String? = null
var serializer: KSerializer? = null
val mapping = mappings.firstOrNull { it.kClass.isInstance(content) }
when {
mapping != null -> {
type = mapping.type
serializer = mapping.serializer
}
content is RedactedEventContent -> {
type = content.eventType
serializer = RedactedEventContentSerializer(type)
}
content is UnknownEventContent -> {
type = content.eventType
serializer = UnknownEventContentSerializer(type)
}
}
if (type == null || serializer == null) {
null
} else {
@Suppress("UNCHECKED_CAST")
val jsonValue =
encoder.json.encodeToJsonElement(serializer as KSerializer, content)
JsonObject(buildMap {
put("type", JsonPrimitive(type))
put("value", jsonValue)
})
}
}
}
val previousEventId: JsonElement? = value.previousEventId?.let {
encoder.json.encodeToJsonElement(it)
}
val nextEventId: JsonElement? = value.nextEventId?.let {
encoder.json.encodeToJsonElement(it)
}
val gap: JsonElement? = value.gap?.let {
encoder.json.encodeToJsonElement(it)
}
encoder.encodeJsonElement(JsonObject(buildMap {
put("event", event)
if (content != null) put("content", content)
if (previousEventId != null) put("previousEventId", previousEventId)
if (nextEventId != null) put("nextEventId", nextEventId)
if (gap != null) put("gap", gap)
}))
}
}