better replace handling

This commit is contained in:
Valere 2022-10-26 17:05:31 +02:00
parent 5eb64b43d3
commit 891709ef41
33 changed files with 774 additions and 212 deletions

View file

@ -38,5 +38,4 @@ data class AggregatedAnnotation(
override val limited: Boolean? = false, override val limited: Boolean? = false,
override val count: Int? = 0, override val count: Int? = 0,
val chunk: List<RelationChunkInfo>? = null val chunk: List<RelationChunkInfo>? = null
) : UnsignedRelationInfo ) : UnsignedRelationInfo

View file

@ -50,5 +50,6 @@ import com.squareup.moshi.JsonClass
data class AggregatedRelations( data class AggregatedRelations(
@Json(name = "m.annotation") val annotations: AggregatedAnnotation? = null, @Json(name = "m.annotation") val annotations: AggregatedAnnotation? = null,
@Json(name = "m.reference") val references: DefaultUnsignedRelationInfo? = null, @Json(name = "m.reference") val references: DefaultUnsignedRelationInfo? = null,
@Json(name = "m.replace") val replaces: AggregatedReplace? = null,
@Json(name = RelationType.THREAD) val latestThread: LatestThreadUnsignedRelation? = null @Json(name = RelationType.THREAD) val latestThread: LatestThreadUnsignedRelation? = null
) )

View file

@ -0,0 +1,33 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.events.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Note that there can be multiple events with an m.replace relationship to a given event (for example, if an event is edited multiple times).
* These should be aggregated by the homeserver.
* https://spec.matrix.org/v1.4/client-server-api/#server-side-aggregation-of-mreplace-relationships
*
*/
@JsonClass(generateAdapter = true)
data class AggregatedReplace(
@Json(name = "event_id") val eventId: String? = null,
@Json(name = "origin_server_ts") val originServerTs: Long? = null,
@Json(name = "sender") val senderId: String? = null,
)

View file

@ -0,0 +1,53 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.events.model
data class ValidDecryptedEvent(
val type: String,
val eventId: String,
val clearContent: Content,
val prevContent: Content? = null,
val originServerTs: Long,
val cryptoSenderKey: String,
val roomId: String,
val unsignedData: UnsignedData? = null,
val redacts: String? = null,
val algorithm: String,
)
fun Event.toValidDecryptedEvent(): ValidDecryptedEvent? {
if (!this.isEncrypted()) return null
val decryptedContent = this.getDecryptedContent() ?: return null
val eventId = this.eventId ?: return null
val roomId = this.roomId ?: return null
val type = this.getDecryptedType() ?: return null
val senderKey = this.getSenderKey() ?: return null
val algorithm = this.content?.get("algorithm") as? String ?: return null
return ValidDecryptedEvent(
type = type,
eventId = eventId,
clearContent = decryptedContent,
prevContent = this.prevContent,
originServerTs = this.originServerTs ?: 0,
cryptoSenderKey = senderKey,
roomId = roomId,
unsignedData = this.unsignedData,
redacts = this.redacts,
algorithm = algorithm
)
}

View file

@ -15,10 +15,10 @@
*/ */
package org.matrix.android.sdk.api.session.room.model package org.matrix.android.sdk.api.session.room.model
import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event
data class EditAggregatedSummary( data class EditAggregatedSummary(
val latestContent: Content? = null, val latestEdit: Event? = null,
// The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk)
val sourceEvents: List<String>, val sourceEvents: List<String>,
val localEchos: List<String>, val localEchos: List<String>,

View file

@ -30,12 +30,8 @@ import org.matrix.android.sdk.api.session.events.model.isSticker
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
import org.matrix.android.sdk.api.session.room.model.ReadReceipt import org.matrix.android.sdk.api.session.room.model.ReadReceipt
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
import org.matrix.android.sdk.api.session.room.sender.SenderInfo import org.matrix.android.sdk.api.session.room.sender.SenderInfo
@ -140,13 +136,12 @@ fun TimelineEvent.getEditedEventId(): String? {
* Get last MessageContent, after a possible edition. * Get last MessageContent, after a possible edition.
*/ */
fun TimelineEvent.getLastMessageContent(): MessageContent? { fun TimelineEvent.getLastMessageContent(): MessageContent? {
return when (root.getClearType()) { return (
EventType.STICKER -> root.getClearContent().toModel<MessageStickerContent>() annotations?.editSummary?.latestEdit
in EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessagePollContent>() ?.getClearContent()?.toModel<MessageContent>()?.newContent
in EventType.STATE_ROOM_BEACON_INFO -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessageBeaconInfoContent>() ?: root.getClearContent()
in EventType.BEACON_LOCATION_DATA -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessageBeaconLocationDataContent>() )
else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() .toModel<MessageContent>()
}
} }
/** /**

View file

@ -26,17 +26,17 @@ import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
internal fun <T> CoroutineScope.asyncTransaction(monarchy: Monarchy, transaction: suspend (realm: Realm) -> T) { internal fun <T> CoroutineScope.asyncTransaction(monarchy: Monarchy, transaction: (realm: Realm) -> T) {
asyncTransaction(monarchy.realmConfiguration, transaction) asyncTransaction(monarchy.realmConfiguration, transaction)
} }
internal fun <T> CoroutineScope.asyncTransaction(realmConfiguration: RealmConfiguration, transaction: suspend (realm: Realm) -> T) { internal fun <T> CoroutineScope.asyncTransaction(realmConfiguration: RealmConfiguration, transaction: (realm: Realm) -> T) {
launch { launch {
awaitTransaction(realmConfiguration, transaction) awaitTransaction(realmConfiguration, transaction)
} }
} }
internal suspend fun <T> awaitTransaction(config: RealmConfiguration, transaction: suspend (realm: Realm) -> T): T { internal suspend fun <T> awaitTransaction(config: RealmConfiguration, transaction: (realm: Realm) -> T): T {
return withContext(Realm.WRITE_EXECUTOR.asCoroutineDispatcher()) { return withContext(Realm.WRITE_EXECUTOR.asCoroutineDispatcher()) {
Realm.getInstance(config).use { bgRealm -> Realm.getInstance(config).use { bgRealm ->
bgRealm.beginTransaction() bgRealm.beginTransaction()

View file

@ -59,6 +59,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo039
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo040 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo040
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo041 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo041
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo042 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo042
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo043
import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.Normalizer
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
import javax.inject.Inject import javax.inject.Inject
@ -67,7 +68,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
private val normalizer: Normalizer private val normalizer: Normalizer
) : MatrixRealmMigration( ) : MatrixRealmMigration(
dbName = "Session", dbName = "Session",
schemaVersion = 42L, schemaVersion = 43L,
) { ) {
/** /**
* Forces all RealmSessionStoreMigration instances to be equal. * Forces all RealmSessionStoreMigration instances to be equal.
@ -119,5 +120,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion < 40) MigrateSessionTo040(realm).perform() if (oldVersion < 40) MigrateSessionTo040(realm).perform()
if (oldVersion < 41) MigrateSessionTo041(realm).perform() if (oldVersion < 41) MigrateSessionTo041(realm).perform()
if (oldVersion < 42) MigrateSessionTo042(realm).perform() if (oldVersion < 42) MigrateSessionTo042(realm).perform()
if (oldVersion < 43) MigrateSessionTo043(realm).perform()
} }
} }

View file

@ -83,7 +83,6 @@ internal fun ChunkEntity.addTimelineEvent(
this.eventId = eventId this.eventId = eventId
this.roomId = roomId this.roomId = roomId
this.annotations = EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst() this.annotations = EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst()
?.also { it.cleanUp(eventEntity.sender) }
this.readReceipts = readReceiptsSummaryEntity this.readReceipts = readReceiptsSummaryEntity
this.displayIndex = displayIndex this.displayIndex = displayIndex
this.ownedByThreadChunk = ownedByThreadChunk this.ownedByThreadChunk = ownedByThreadChunk

View file

@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.database.helper
import io.realm.Realm import io.realm.Realm
import io.realm.RealmQuery import io.realm.RealmQuery
import io.realm.Sort import io.realm.Sort
import io.realm.kotlin.createObject
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
@ -103,32 +102,6 @@ internal fun ThreadSummaryEntity.updateThreadSummaryLatestEvent(
} }
} }
private fun EventEntity.toTimelineEventEntity(roomMemberContentsByUser: HashMap<String, RoomMemberContent?>): TimelineEventEntity {
val roomId = roomId
val eventId = eventId
val localId = TimelineEventEntity.nextId(realm)
val senderId = sender ?: ""
val timelineEventEntity = realm.createObject<TimelineEventEntity>().apply {
this.localId = localId
this.root = this@toTimelineEventEntity
this.eventId = eventId
this.roomId = roomId
this.annotations = EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst()
?.also { it.cleanUp(sender) }
this.ownedByThreadChunk = true // To skip it from the original event flow
val roomMemberContent = roomMemberContentsByUser[senderId]
this.senderAvatar = roomMemberContent?.avatarUrl
this.senderName = roomMemberContent?.displayName
isUniqueDisplayName = if (roomMemberContent?.displayName != null) {
computeIsUnique(realm, roomId, false, roomMemberContent, roomMemberContentsByUser)
} else {
true
}
}
return timelineEventEntity
}
internal fun ThreadSummaryEntity.Companion.createOrUpdate( internal fun ThreadSummaryEntity.Companion.createOrUpdate(
threadSummaryType: ThreadSummaryUpdateType, threadSummaryType: ThreadSummaryUpdateType,
realm: Realm, realm: Realm,

View file

@ -20,6 +20,7 @@ import org.matrix.android.sdk.api.session.room.model.EditAggregatedSummary
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
import org.matrix.android.sdk.api.session.room.model.ReactionAggregatedSummary import org.matrix.android.sdk.api.session.room.model.ReactionAggregatedSummary
import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedSummary import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedSummary
import org.matrix.android.sdk.internal.database.model.EditionOfEvent
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
internal object EventAnnotationsSummaryMapper { internal object EventAnnotationsSummaryMapper {
@ -36,13 +37,22 @@ internal object EventAnnotationsSummaryMapper {
) )
}, },
editSummary = annotationsSummary.editSummary editSummary = annotationsSummary.editSummary
?.let { ?.let { summary ->
val latestEdition = it.editions.maxByOrNull { editionOfEvent -> editionOfEvent.timestamp } ?: return@let null /**
* The most recent event is determined by comparing origin_server_ts;
* if two or more replacement events have identical origin_server_ts,
* the event with the lexicographically largest event_id is treated as more recent.
*/
val latestEdition = summary.editions.sortedWith(compareBy<EditionOfEvent> { it.timestamp }.thenBy { it.eventId })
.lastOrNull() ?: return@let null
// get the event and validate?
val editEvent = latestEdition.event
EditAggregatedSummary( EditAggregatedSummary(
latestContent = ContentMapper.map(latestEdition.content), latestEdit = editEvent?.asDomain(),
sourceEvents = it.editions.filter { editionOfEvent -> !editionOfEvent.isLocalEcho } sourceEvents = summary.editions.filter { editionOfEvent -> !editionOfEvent.isLocalEcho }
.map { editionOfEvent -> editionOfEvent.eventId }, .map { editionOfEvent -> editionOfEvent.eventId },
localEchos = it.editions.filter { editionOfEvent -> editionOfEvent.isLocalEcho } localEchos = summary.editions.filter { editionOfEvent -> editionOfEvent.isLocalEcho }
.map { editionOfEvent -> editionOfEvent.eventId }, .map { editionOfEvent -> editionOfEvent.eventId },
lastEditTs = latestEdition.timestamp lastEditTs = latestEdition.timestamp
) )

View file

@ -25,11 +25,11 @@ internal class MigrateSessionTo008(realm: DynamicRealm) : RealmMigrator(realm, 8
override fun doMigrate(realm: DynamicRealm) { override fun doMigrate(realm: DynamicRealm) {
val editionOfEventSchema = realm.schema.create("EditionOfEvent") val editionOfEventSchema = realm.schema.create("EditionOfEvent")
.addField(EditionOfEventFields.CONTENT, String::class.java) .addField("content"/**EditionOfEventFields.CONTENT*/, String::class.java)
.addField(EditionOfEventFields.EVENT_ID, String::class.java) .addField(EditionOfEventFields.EVENT_ID, String::class.java)
.setRequired(EditionOfEventFields.EVENT_ID, true) .setRequired(EditionOfEventFields.EVENT_ID, true)
.addField(EditionOfEventFields.SENDER_ID, String::class.java) .addField("senderId" /*EditionOfEventFields.SENDER_ID*/, String::class.java)
.setRequired(EditionOfEventFields.SENDER_ID, true) .setRequired("senderId" /*EditionOfEventFields.SENDER_ID*/, true)
.addField(EditionOfEventFields.TIMESTAMP, Long::class.java) .addField(EditionOfEventFields.TIMESTAMP, Long::class.java)
.addField(EditionOfEventFields.IS_LOCAL_ECHO, Boolean::class.java) .addField(EditionOfEventFields.IS_LOCAL_ECHO, Boolean::class.java)

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.database.migration
import io.realm.DynamicRealm
import org.matrix.android.sdk.internal.database.model.EditionOfEventFields
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventEntityFields
import org.matrix.android.sdk.internal.util.database.RealmMigrator
internal class MigrateSessionTo043(realm: DynamicRealm) : RealmMigrator(realm, 43) {
override fun doMigrate(realm: DynamicRealm) {
// content(string) & senderId(string) have been removed and replaced by a link to the actual event
realm.schema.get("EditionOfEvent")
?.removeField("senderId")
?.removeField("content")
?.addRealmObjectField(EditionOfEventFields.EVENT.`$`, realm.schema.get("EventEntity")!!)
?.transform { dynamicObject ->
realm.where(EventEntity::javaClass.name)
.equalTo(EventEntityFields.EVENT_ID, dynamicObject.getString(EditionOfEventFields.EVENT_ID))
.equalTo(EventEntityFields.SENDER, dynamicObject.getString("senderId"))
.findFirst()
.let {
dynamicObject.setObject(EditionOfEventFields.EVENT.`$`, it)
}
}
}
}

View file

@ -32,9 +32,8 @@ internal open class EditAggregatedSummaryEntity(
@RealmClass(embedded = true) @RealmClass(embedded = true)
internal open class EditionOfEvent( internal open class EditionOfEvent(
var senderId: String = "",
var eventId: String = "", var eventId: String = "",
var content: String? = null,
var timestamp: Long = 0, var timestamp: Long = 0,
var isLocalEcho: Boolean = false var isLocalEcho: Boolean = false,
var event: EventEntity? = null,
) : RealmObject() ) : RealmObject()

View file

@ -19,7 +19,6 @@ import io.realm.RealmList
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
import timber.log.Timber
internal open class EventAnnotationsSummaryEntity( internal open class EventAnnotationsSummaryEntity(
@PrimaryKey @PrimaryKey
@ -32,21 +31,6 @@ internal open class EventAnnotationsSummaryEntity(
var liveLocationShareAggregatedSummary: LiveLocationShareAggregatedSummaryEntity? = null, var liveLocationShareAggregatedSummary: LiveLocationShareAggregatedSummaryEntity? = null,
) : RealmObject() { ) : RealmObject() {
/**
* Cleanup undesired editions, done by users different from the originalEventSender.
*/
fun cleanUp(originalEventSenderId: String?) {
originalEventSenderId ?: return
editSummary?.editions?.filter {
it.senderId != originalEventSenderId
}
?.forEach {
Timber.w("Deleting an edition from ${it.senderId} of event sent by $originalEventSenderId")
it.deleteFromRealm()
}
}
companion object companion object
} }

View file

@ -24,7 +24,7 @@ internal interface EventInsertLiveProcessor {
fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean
suspend fun process(realm: Realm, event: Event) fun process(realm: Realm, event: Event)
/** /**
* Called after transaction. * Called after transaction.

View file

@ -54,7 +54,7 @@ internal class CallEventProcessor @Inject constructor(private val callSignalingH
return allowedTypes.contains(eventType) return allowedTypes.contains(eventType)
} }
override suspend fun process(realm: Realm, event: Event) { override fun process(realm: Realm, event: Event) {
eventsToPostProcess.add(event) eventsToPostProcess.add(event)
} }

View file

@ -0,0 +1,115 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.events.model.toValidDecryptedEvent
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import javax.inject.Inject
internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCryptoStore) {
sealed class EditValidity {
object Valid : EditValidity()
data class Invalid(val reason: String) : EditValidity()
object Unknown : EditValidity()
}
/**
*There are a number of requirements on replacement events, which must be satisfied for the replacement to be considered valid:
* As with all event relationships, the original event and replacement event must have the same room_id
* (i.e. you cannot send an event in one room and then an edited version in a different room).
* The original event and replacement event must have the same sender (i.e. you cannot edit someone elses messages).
* The replacement and original events must have the same type (i.e. you cannot change the original events type).
* The replacement and original events must not have a state_key property (i.e. you cannot edit state events at all).
* The original event must not, itself, have a rel_type of m.replace (i.e. you cannot edit an edit though you can send multiple edits for a single original event).
* The replacement event (once decrypted, if appropriate) must have an m.new_content property.
*
* If the original event was encrypted, the replacement should be too.
*/
fun validateEdit(originalEvent: Event?, replaceEvent: Event): EditValidity {
// we might not know the original event at that time. In this case we can't perform the validation
// Edits should be revalidated when the original event is received
if (originalEvent == null) {
return EditValidity.Unknown
}
if (originalEvent.roomId != replaceEvent.roomId) {
return EditValidity.Invalid("original event and replacement event must have the same room_id")
}
if (originalEvent.isStateEvent() || replaceEvent.isStateEvent()) {
return EditValidity.Invalid("replacement and original events must not have a state_key property")
}
// check it's from same sender
if (originalEvent.isEncrypted()) {
if (!replaceEvent.isEncrypted()) return EditValidity.Invalid("If the original event was encrypted, the replacement should be too")
val originalDecrypted = originalEvent.toValidDecryptedEvent()
?: return EditValidity.Unknown // UTD can't decide
val replaceDecrypted = replaceEvent.toValidDecryptedEvent()
?: return EditValidity.Unknown // UTD can't decide
val originalCryptoSenderId = cryptoStore.deviceWithIdentityKey(originalDecrypted.cryptoSenderKey)?.userId
val editCryptoSenderId = cryptoStore.deviceWithIdentityKey(replaceDecrypted.cryptoSenderKey)?.userId
if (originalDecrypted.clearContent.toModel<MessageContent>()?.relatesTo?.type == RelationType.REPLACE) {
return EditValidity.Invalid("The original event must not, itself, have a rel_type of m.replace ")
}
if (originalCryptoSenderId == null || editCryptoSenderId == null) {
// mm what can we do? we don't know if it's cryptographically from same user?
// let valid and UI should display send by deleted device warning?
val bestEffortOriginal = originalCryptoSenderId ?: originalEvent.senderId
val bestEffortEdit = editCryptoSenderId ?: replaceEvent.senderId
if (bestEffortOriginal != bestEffortEdit) {
return EditValidity.Invalid("original event and replacement event must have the same sender")
}
} else {
if (originalCryptoSenderId != editCryptoSenderId) {
return EditValidity.Invalid("Crypto: original event and replacement event must have the same sender")
}
}
if (originalDecrypted.type != replaceDecrypted.type) {
return EditValidity.Invalid("replacement and original events must have the same type")
}
if (replaceDecrypted.clearContent.toModel<MessageContent>()?.newContent == null) {
return EditValidity.Invalid("replacement event must have an m.new_content property")
}
} else {
if (originalEvent.content.toModel<MessageContent>()?.relatesTo?.type == RelationType.REPLACE) {
return EditValidity.Invalid("The original event must not, itself, have a rel_type of m.replace ")
}
// check the sender
if (originalEvent.senderId != replaceEvent.senderId) {
return EditValidity.Invalid("original event and replacement event must have the same sender")
}
if (originalEvent.type != replaceEvent.type) {
return EditValidity.Invalid("replacement and original events must have the same type")
}
if (replaceEvent.content.toModel<MessageContent>()?.newContent == null) {
return EditValidity.Invalid("replacement event must have an m.new_content property")
}
}
return EditValidity.Valid
}
}

View file

@ -23,7 +23,6 @@ import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.LocalEcho import org.matrix.android.sdk.api.session.events.model.LocalEcho
import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
import org.matrix.android.sdk.api.session.events.model.getRelationContent import org.matrix.android.sdk.api.session.events.model.getRelationContent
import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
@ -42,6 +41,7 @@ import org.matrix.android.sdk.internal.crypto.verification.toState
import org.matrix.android.sdk.internal.database.helper.findRootThreadEvent import org.matrix.android.sdk.internal.database.helper.findRootThreadEvent
import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.mapper.ContentMapper
import org.matrix.android.sdk.internal.database.mapper.EventMapper import org.matrix.android.sdk.internal.database.mapper.EventMapper
import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntity import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntity
import org.matrix.android.sdk.internal.database.model.EditionOfEvent import org.matrix.android.sdk.internal.database.model.EditionOfEvent
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
@ -72,6 +72,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
private val sessionManager: SessionManager, private val sessionManager: SessionManager,
private val liveLocationAggregationProcessor: LiveLocationAggregationProcessor, private val liveLocationAggregationProcessor: LiveLocationAggregationProcessor,
private val pollAggregationProcessor: PollAggregationProcessor, private val pollAggregationProcessor: PollAggregationProcessor,
private val editValidator: EventEditValidator,
private val clock: Clock, private val clock: Clock,
) : EventInsertLiveProcessor { ) : EventInsertLiveProcessor {
@ -79,13 +80,15 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
EventType.MESSAGE, EventType.MESSAGE,
EventType.REDACTION, EventType.REDACTION,
EventType.REACTION, EventType.REACTION,
// The aggregator handles verification events but just to render tiles in the timeline
// It's not participating in verfication it's self, just timeline display
EventType.KEY_VERIFICATION_DONE, EventType.KEY_VERIFICATION_DONE,
EventType.KEY_VERIFICATION_CANCEL, EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_MAC, EventType.KEY_VERIFICATION_MAC,
// TODO Add ? // TODO Add ?
// EventType.KEY_VERIFICATION_READY, EventType.KEY_VERIFICATION_READY,
EventType.KEY_VERIFICATION_KEY, EventType.KEY_VERIFICATION_KEY,
EventType.ENCRYPTED EventType.ENCRYPTED
) + EventType.POLL_START + EventType.POLL_RESPONSE + EventType.POLL_END + EventType.STATE_ROOM_BEACON_INFO + EventType.BEACON_LOCATION_DATA ) + EventType.POLL_START + EventType.POLL_RESPONSE + EventType.POLL_END + EventType.STATE_ROOM_BEACON_INFO + EventType.BEACON_LOCATION_DATA
@ -94,7 +97,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
return allowedTypes.contains(eventType) return allowedTypes.contains(eventType)
} }
override suspend fun process(realm: Realm, event: Event) { override fun process(realm: Realm, event: Event) {
try { // Temporary catch, should be removed try { // Temporary catch, should be removed
val roomId = event.roomId val roomId = event.roomId
if (roomId == null) { if (roomId == null) {
@ -102,7 +105,31 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
return return
} }
val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "") val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "")
when (event.type) {
// It might be a late decryption of the original event or a event received when back paginating?
// let's check if there is already a summary for it and do some cleaning
if (!isLocalEcho) {
EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId.orEmpty())
.findFirst()
?.editSummary
?.editions
?.forEach { editionOfEvent ->
EventEntity.where(realm, editionOfEvent.eventId).findFirst()?.asDomain()?.let { editEvent ->
when (editValidator.validateEdit(event, editEvent)) {
is EventEditValidator.EditValidity.Invalid -> {
// delete it, it was invalid
Timber.v("## Replace: Removing a previously accepted edit for event ${event.eventId}")
editionOfEvent.deleteFromRealm()
}
else -> {
// nop
}
}
}
}
}
when (event.getClearType()) {
EventType.REACTION -> { EventType.REACTION -> {
// we got a reaction!! // we got a reaction!!
Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}") Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}")
@ -113,21 +140,17 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
Timber.v("###REACTION Aggregation in room $roomId for event ${event.eventId}") Timber.v("###REACTION Aggregation in room $roomId for event ${event.eventId}")
handleInitialAggregatedRelations(realm, event, roomId, event.unsignedData.relations.annotations) handleInitialAggregatedRelations(realm, event, roomId, event.unsignedData.relations.annotations)
EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId ?: "").findFirst() // XXX do something for aggregated edits?
?.let { // it's a bit strange as it would require to do a server query to get the edition?
TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findAll()
?.forEach { tet -> tet.annotations = it }
}
} }
val content: MessageContent? = event.content.toModel() val relationContent = event.getRelationContent()
if (content?.relatesTo?.type == RelationType.REPLACE) { if (relationContent?.type == RelationType.REPLACE) {
Timber.v("###REPLACE in room $roomId for event ${event.eventId}") Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
// A replace! // A replace!
handleReplace(realm, event, content, roomId, isLocalEcho) handleReplace(realm, event, roomId, isLocalEcho, relationContent.eventId)
} }
} }
EventType.KEY_VERIFICATION_DONE, EventType.KEY_VERIFICATION_DONE,
EventType.KEY_VERIFICATION_CANCEL, EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_ACCEPT,
@ -142,72 +165,29 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
} }
} }
} }
// As for now Live event processors are not receiving UTD events.
// They will get an update if the event is decrypted later
EventType.ENCRYPTED -> { EventType.ENCRYPTED -> {
// Relation type is in clear // // Relation type is in clear, it might be possible to do some things?
val encryptedEventContent = event.content.toModel<EncryptedEventContent>() // // Notice that if the event is decrypted later, process be called again
if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE || // val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
encryptedEventContent?.relatesTo?.type == RelationType.RESPONSE // when (encryptedEventContent?.relatesTo?.type) {
) { // RelationType.REPLACE -> {
event.getClearContent().toModel<MessageContent>()?.let { // Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
if (encryptedEventContent.relatesTo.type == RelationType.REPLACE) { // // A replace!
Timber.v("###REPLACE in room $roomId for event ${event.eventId}") // handleReplace(realm, event, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
// A replace! // }
handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) // RelationType.RESPONSE -> {
} else if (event.getClearType() in EventType.POLL_RESPONSE) { // // can we / should we do we something for UTD response??
sessionManager.getSessionComponent(sessionId)?.session()?.let { session -> // Timber.w("## UTD response in room $roomId related to ${encryptedEventContent.relatesTo.eventId}")
pollAggregationProcessor.handlePollResponseEvent(session, realm, event) // }
} // RelationType.REFERENCE -> {
} // // can we / should we do we something for UTD reference??
} // Timber.w("## UTD reference in room $roomId related to ${encryptedEventContent.relatesTo.eventId}")
} else if (encryptedEventContent?.relatesTo?.type == RelationType.REFERENCE) { // }
when (event.getClearType()) { // RelationType.ANNOTATION -> {
EventType.KEY_VERIFICATION_DONE, // // can we / should we do we something for UTD annotation??
EventType.KEY_VERIFICATION_CANCEL, // Timber.w("## UTD annotation in room $roomId related to ${encryptedEventContent.relatesTo.eventId}")
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_MAC,
EventType.KEY_VERIFICATION_READY,
EventType.KEY_VERIFICATION_KEY -> {
Timber.v("## SAS REF in room $roomId for event ${event.eventId}")
encryptedEventContent.relatesTo.eventId?.let {
handleVerification(realm, event, roomId, isLocalEcho, it)
}
}
in EventType.POLL_RESPONSE -> {
event.getClearContent().toModel<MessagePollResponseContent>(catchError = true)?.let {
sessionManager.getSessionComponent(sessionId)?.session()?.let { session ->
pollAggregationProcessor.handlePollResponseEvent(session, realm, event)
}
}
}
in EventType.POLL_END -> {
sessionManager.getSessionComponent(sessionId)?.session()?.let { session ->
getPowerLevelsHelper(event.roomId)?.let {
pollAggregationProcessor.handlePollEndEvent(session, it, realm, event)
}
}
}
in EventType.BEACON_LOCATION_DATA -> {
handleBeaconLocationData(event, realm, roomId, isLocalEcho)
}
}
} else if (encryptedEventContent?.relatesTo?.type == RelationType.ANNOTATION) {
// Reaction
if (event.getClearType() == EventType.REACTION) {
// we got a reaction!!
Timber.v("###REACTION e2e in room $roomId , reaction eventID ${event.eventId}")
handleReaction(realm, event, roomId, isLocalEcho)
}
}
// HandleInitialAggregatedRelations should also be applied in encrypted messages with annotations
// else if (event.unsignedData?.relations?.annotations != null) {
// Timber.v("###REACTION e2e Aggregation in room $roomId for event ${event.eventId}")
// handleInitialAggregatedRelations(realm, event, roomId, event.unsignedData.relations.annotations)
// EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId ?: "").findFirst()
// ?.let {
// TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findAll()
// ?.forEach { tet -> tet.annotations = it }
// } // }
// } // }
} }
@ -217,9 +197,6 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
when (eventToPrune.type) { when (eventToPrune.type) {
EventType.MESSAGE -> { EventType.MESSAGE -> {
Timber.d("REDACTION for message ${eventToPrune.eventId}") Timber.d("REDACTION for message ${eventToPrune.eventId}")
// val unsignedData = EventMapper.map(eventToPrune).unsignedData
// ?: UnsignedData(null, null)
// was this event a m.replace // was this event a m.replace
val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>() val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>()
if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) { if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) {
@ -236,7 +213,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
if (content?.relatesTo?.type == RelationType.REPLACE) { if (content?.relatesTo?.type == RelationType.REPLACE) {
Timber.v("###REPLACE in room $roomId for event ${event.eventId}") Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
// A replace! // A replace!
handleReplace(realm, event, content, roomId, isLocalEcho) handleReplace(realm, event, roomId, isLocalEcho, content?.relatesTo.eventId)
} }
} }
in EventType.POLL_RESPONSE -> { in EventType.POLL_RESPONSE -> {
@ -274,23 +251,22 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
private fun handleReplace( private fun handleReplace(
realm: Realm, realm: Realm,
event: Event, event: Event,
content: MessageContent,
roomId: String, roomId: String,
isLocalEcho: Boolean, isLocalEcho: Boolean,
relatedEventId: String? = null relatedEventId: String?
) { ) {
val eventId = event.eventId ?: return val eventId = event.eventId ?: return
val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return val targetEventId = relatedEventId ?: return // ?: content.relatesTo?.eventId ?: return
val newContent = content.newContent ?: return
// Check that the sender is the same
val editedEvent = EventEntity.where(realm, targetEventId).findFirst() val editedEvent = EventEntity.where(realm, targetEventId).findFirst()
if (editedEvent == null) {
// We do not know yet about the edited event when (val validity = editValidator.validateEdit(editedEvent?.asDomain(), event)) {
} else if (editedEvent.sender != event.senderId) { is EventEditValidator.EditValidity.Invalid -> return Unit.also {
// Edited by someone else, ignore Timber.w("Dropping invalid edit ${event.eventId}, reason:${validity.reason}")
Timber.w("Ignore edition by someone else") }
return EventEditValidator.EditValidity.Unknown, // we can't drop the source event might be unknown, will be validated later
EventEditValidator.EditValidity.Valid -> {
// continue
}
} }
// ok, this is a replace // ok, this is a replace
@ -305,11 +281,10 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
.also { editSummary -> .also { editSummary ->
editSummary.editions.add( editSummary.editions.add(
EditionOfEvent( EditionOfEvent(
senderId = event.senderId ?: "",
eventId = event.eventId, eventId = event.eventId,
content = ContentMapper.map(newContent), event = EventEntity.where(realm, eventId).findFirst(),
timestamp = if (isLocalEcho) 0 else event.originServerTs ?: 0, timestamp = if (isLocalEcho) clock.epochMillis() else event.originServerTs ?: clock.epochMillis(),
isLocalEcho = isLocalEcho isLocalEcho = isLocalEcho,
) )
) )
} }
@ -334,9 +309,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
Timber.v("###REPLACE Computing aggregated edit summary (isLocalEcho:$isLocalEcho)") Timber.v("###REPLACE Computing aggregated edit summary (isLocalEcho:$isLocalEcho)")
existingSummary.editions.add( existingSummary.editions.add(
EditionOfEvent( EditionOfEvent(
senderId = event.senderId ?: "",
eventId = event.eventId, eventId = event.eventId,
content = ContentMapper.map(newContent), event = EventEntity.where(realm, eventId).findFirst(),
timestamp = if (isLocalEcho) { timestamp = if (isLocalEcho) {
clock.epochMillis() clock.epochMillis()
} else { } else {
@ -369,8 +343,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
* @param editions list of edition of event * @param editions list of edition of event
*/ */
private fun handleThreadSummaryEdition( private fun handleThreadSummaryEdition(
editedEvent: EventEntity?, editedEvent: EventEntity?, replaceEvent: TimelineEventEntity?,
replaceEvent: TimelineEventEntity?,
editions: List<EditionOfEvent>? editions: List<EditionOfEvent>?
) { ) {
replaceEvent ?: return replaceEvent ?: return
@ -599,12 +572,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
private fun handleBeaconLocationData(event: Event, realm: Realm, roomId: String, isLocalEcho: Boolean) { private fun handleBeaconLocationData(event: Event, realm: Realm, roomId: String, isLocalEcho: Boolean) {
event.getClearContent().toModel<MessageBeaconLocationDataContent>(catchError = true)?.let { event.getClearContent().toModel<MessageBeaconLocationDataContent>(catchError = true)?.let {
liveLocationAggregationProcessor.handleBeaconLocationData( liveLocationAggregationProcessor.handleBeaconLocationData(
realm, realm = realm,
event, event = event,
it, content = it,
roomId, roomId = roomId,
event.getRelationContent()?.eventId, relatedEventId = event.getRelationContent()?.eventId,
isLocalEcho isLocalEcho = isLocalEcho
) )
} }
} }

View file

@ -21,6 +21,7 @@ import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import io.realm.kotlin.createObject import io.realm.kotlin.createObject
import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.runBlocking
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
@ -95,7 +96,7 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
* Create a local room entity from the given room creation params. * Create a local room entity from the given room creation params.
* This will also generate and store in database the chunk and the events related to the room params in order to retrieve and display the local room. * This will also generate and store in database the chunk and the events related to the room params in order to retrieve and display the local room.
*/ */
private suspend fun createLocalRoomEntity(realm: Realm, roomId: String, createRoomBody: CreateRoomBody) { private fun createLocalRoomEntity(realm: Realm, roomId: String, createRoomBody: CreateRoomBody) {
RoomEntity.getOrCreate(realm, roomId).apply { RoomEntity.getOrCreate(realm, roomId).apply {
membership = Membership.JOIN membership = Membership.JOIN
chunks.add(createLocalRoomChunk(realm, roomId, createRoomBody)) chunks.add(createLocalRoomChunk(realm, roomId, createRoomBody))
@ -148,13 +149,16 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
* *
* @return a chunk entity * @return a chunk entity
*/ */
private suspend fun createLocalRoomChunk(realm: Realm, roomId: String, createRoomBody: CreateRoomBody): ChunkEntity { private fun createLocalRoomChunk(realm: Realm, roomId: String, createRoomBody: CreateRoomBody): ChunkEntity {
val chunkEntity = realm.createObject<ChunkEntity>().apply { val chunkEntity = realm.createObject<ChunkEntity>().apply {
isLastBackward = true isLastBackward = true
isLastForward = true isLastForward = true
} }
val eventList = createLocalRoomStateEventsTask.execute(CreateLocalRoomStateEventsTask.Params(createRoomBody)) // Can't suspend when using realm as it could jump thread
val eventList = runBlocking {
createLocalRoomStateEventsTask.execute(CreateLocalRoomStateEventsTask.Params(createRoomBody))
}
val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>() val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
for (event in eventList) { for (event in eventList) {

View file

@ -30,7 +30,7 @@ import javax.inject.Inject
internal class RoomCreateEventProcessor @Inject constructor() : EventInsertLiveProcessor { internal class RoomCreateEventProcessor @Inject constructor() : EventInsertLiveProcessor {
override suspend fun process(realm: Realm, event: Event) { override fun process(realm: Realm, event: Event) {
val createRoomContent = event.getClearContent().toModel<RoomCreateContent>() val createRoomContent = event.getClearContent().toModel<RoomCreateContent>()
val predecessorRoomId = createRoomContent?.predecessor?.roomId ?: return val predecessorRoomId = createRoomContent?.predecessor?.roomId ?: return

View file

@ -40,7 +40,7 @@ internal class LiveLocationShareRedactionEventProcessor @Inject constructor() :
return eventType == EventType.REDACTION && insertType != EventInsertType.LOCAL_ECHO return eventType == EventType.REDACTION && insertType != EventInsertType.LOCAL_ECHO
} }
override suspend fun process(realm: Realm, event: Event) { override fun process(realm: Realm, event: Event) {
if (event.redacts.isNullOrBlank() || LocalEcho.isLocalEchoId(event.eventId.orEmpty())) { if (event.redacts.isNullOrBlank() || LocalEcho.isLocalEchoId(event.eventId.orEmpty())) {
return return
} }

View file

@ -46,7 +46,7 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr
return eventType == EventType.REDACTION return eventType == EventType.REDACTION
} }
override suspend fun process(realm: Realm, event: Event) { override fun process(realm: Realm, event: Event) {
pruneEvent(realm, event) pruneEvent(realm, event)
} }

View file

@ -30,7 +30,7 @@ import javax.inject.Inject
internal class RoomTombstoneEventProcessor @Inject constructor() : EventInsertLiveProcessor { internal class RoomTombstoneEventProcessor @Inject constructor() : EventInsertLiveProcessor {
override suspend fun process(realm: Realm, event: Event) { override fun process(realm: Realm, event: Event) {
if (event.roomId == null) return if (event.roomId == null) return
val createRoomContent = event.getClearContent().toModel<RoomTombstoneContent>() val createRoomContent = event.getClearContent().toModel<RoomTombstoneContent>()
if (createRoomContent?.replacementRoomId == null) return if (createRoomContent?.replacementRoomId == null) return

View file

@ -22,7 +22,7 @@ import io.realm.RealmModel
import org.matrix.android.sdk.internal.database.awaitTransaction import org.matrix.android.sdk.internal.database.awaitTransaction
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
internal suspend fun <T> Monarchy.awaitTransaction(transaction: suspend (realm: Realm) -> T): T { internal suspend fun <T> Monarchy.awaitTransaction(transaction: (realm: Realm) -> T): T {
return awaitTransaction(realmConfiguration, transaction) return awaitTransaction(realmConfiguration, transaction)
} }

View file

@ -0,0 +1,366 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room
import io.mockk.every
import io.mockk.mockk
import org.amshove.kluent.shouldBeInstanceOf
import org.junit.Test
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
class EditValidationTest {
private val mockTextEvent = Event(
type = EventType.MESSAGE,
eventId = "\$WX8WlNC2reiXrwHIA_CQHmU_pSR-jhOA2xKPRcJN9wQ",
roomId = "!GXKhWsRwiWWvbQDBpe:example.com",
content = mapOf(
"body" to "some message",
"msgtype" to "m.text"
),
originServerTs = 1000,
senderId = "@alice:example.com",
)
private val mockEdit = Event(
type = EventType.MESSAGE,
eventId = "\$-SF7RWLPzRzCbHqK3ZAhIrX5Auh3B2lS5AqJiypt1p0",
roomId = "!GXKhWsRwiWWvbQDBpe:example.com",
content = mapOf(
"body" to "* some message edited",
"msgtype" to "m.text",
"m.new_content" to mapOf(
"body" to "some message edited",
"msgtype" to "m.text"
),
"m.relates_to" to mapOf(
"rel_type" to "m.replace",
"event_id" to mockTextEvent.eventId
)
),
originServerTs = 2000,
senderId = "@alice:example.com",
)
@Test
fun `edit should be valid`() {
val mockCryptoStore = mockk<IMXCryptoStore>()
val validator = EventEditValidator(mockCryptoStore)
validator
.validateEdit(mockTextEvent, mockEdit) shouldBeInstanceOf EventEditValidator.EditValidity.Valid::class
}
@Test
fun `original event and replacement event must have the same sender`() {
val mockCryptoStore = mockk<IMXCryptoStore>()
val validator = EventEditValidator(mockCryptoStore)
validator
.validateEdit(
mockTextEvent,
mockEdit.copy(senderId = "@bob:example.com")
) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class
}
@Test
fun `original event and replacement event must have the same room_id`() {
val mockCryptoStore = mockk<IMXCryptoStore>()
val validator = EventEditValidator(mockCryptoStore)
validator
.validateEdit(
mockTextEvent,
mockEdit.copy(roomId = "!someotherroom")
) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class
validator
.validateEdit(
encryptedEvent,
encryptedEditEvent.copy(roomId = "!someotherroom")
) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class
}
@Test
fun `replacement and original events must not have a state_key property`() {
val mockCryptoStore = mockk<IMXCryptoStore>()
val validator = EventEditValidator(mockCryptoStore)
validator
.validateEdit(
mockTextEvent,
mockEdit.copy(stateKey = "")
) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class
validator
.validateEdit(
mockTextEvent.copy(stateKey = ""),
mockEdit
) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class
}
@Test
fun `replacement event must have an new_content property`() {
val mockCryptoStore = mockk<IMXCryptoStore> {
every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns
mockk<CryptoDeviceInfo> {
every { userId } returns "@alice:example.com"
}
}
val validator = EventEditValidator(mockCryptoStore)
validator
.validateEdit(mockTextEvent, mockEdit.copy(
content = mockEdit.content!!.toMutableMap().apply {
this.remove("m.new_content")
}
)) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class
validator
.validateEdit(
encryptedEvent,
encryptedEditEvent.copy().apply {
mxDecryptionResult = encryptedEditEvent.mxDecryptionResult!!.copy(
payload = mapOf(
"type" to EventType.MESSAGE,
"content" to mapOf(
"body" to "* some message edited",
"msgtype" to "m.text",
"m.relates_to" to mapOf(
"rel_type" to "m.replace",
"event_id" to mockTextEvent.eventId
)
)
)
)
}
) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class
}
@Test
fun `The original event must not itself have a rel_type of m_replace`() {
val mockCryptoStore = mockk<IMXCryptoStore> {
every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns
mockk<CryptoDeviceInfo> {
every { userId } returns "@alice:example.com"
}
}
val validator = EventEditValidator(mockCryptoStore)
validator
.validateEdit(
mockTextEvent.copy(
content = mockTextEvent.content!!.toMutableMap().apply {
this["m.relates_to"] = mapOf(
"rel_type" to "m.replace",
"event_id" to mockTextEvent.eventId
)
}
),
mockEdit
) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class
validator
.validateEdit(
encryptedEvent.copy().apply {
mxDecryptionResult = encryptedEditEvent.mxDecryptionResult!!.copy(
payload = mapOf(
"type" to EventType.MESSAGE,
"content" to mapOf(
"body" to "some message",
"msgtype" to "m.text",
"m.relates_to" to mapOf(
"rel_type" to "m.replace",
"event_id" to mockTextEvent.eventId
)
),
)
)
},
encryptedEditEvent
) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class
}
@Test
fun `valid e2ee edit`() {
val mockCryptoStore = mockk<IMXCryptoStore> {
every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns
mockk<CryptoDeviceInfo> {
every { userId } returns "@alice:example.com"
}
}
val validator = EventEditValidator(mockCryptoStore)
validator
.validateEdit(
encryptedEvent,
encryptedEditEvent
) shouldBeInstanceOf EventEditValidator.EditValidity.Valid::class
}
@Test
fun `If the original event was encrypted, the replacement should be too`() {
val mockCryptoStore = mockk<IMXCryptoStore> {
every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns
mockk<CryptoDeviceInfo> {
every { userId } returns "@alice:example.com"
}
}
val validator = EventEditValidator(mockCryptoStore)
validator
.validateEdit(
encryptedEvent,
mockEdit
) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class
}
@Test
fun `encrypted, original event and replacement event must have the same sender`() {
val mockCryptoStore = mockk<IMXCryptoStore> {
every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns
mockk {
every { userId } returns "@alice:example.com"
}
every { deviceWithIdentityKey("7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI") } returns
mockk {
every { userId } returns "@bob:example.com"
}
}
val validator = EventEditValidator(mockCryptoStore)
validator
.validateEdit(
encryptedEvent,
encryptedEditEvent.copy().apply {
mxDecryptionResult = encryptedEditEvent.mxDecryptionResult!!.copy(
senderKey = "7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI"
)
}
) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class
// if sent fom a deleted device it should use the event claimed sender id
}
@Test
fun `encrypted, sent fom a deleted device, original event and replacement event must have the same sender`() {
val mockCryptoStore = mockk<IMXCryptoStore> {
every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns
mockk {
every { userId } returns "@alice:example.com"
}
every { deviceWithIdentityKey("7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI") } returns
null
}
val validator = EventEditValidator(mockCryptoStore)
validator
.validateEdit(
encryptedEvent,
encryptedEditEvent.copy().apply {
mxDecryptionResult = encryptedEditEvent.mxDecryptionResult!!.copy(
senderKey = "7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI"
)
}
) shouldBeInstanceOf EventEditValidator.EditValidity.Valid::class
validator
.validateEdit(
encryptedEvent,
encryptedEditEvent.copy(
senderId = "bob@example.com"
).apply {
mxDecryptionResult = encryptedEditEvent.mxDecryptionResult!!.copy(
senderKey = "7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI"
)
}
) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class
}
private val encryptedEditEvent = Event(
type = EventType.ENCRYPTED,
eventId = "\$-SF7RWLPzRzCbHqK3ZAhIrX5Auh3B2lS5AqJiypt1p0",
roomId = "!GXKhWsRwiWWvbQDBpe:example.com",
content = mapOf(
"algorithm" to "m.megolm.v1.aes-sha2",
"sender_key" to "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo",
"session_id" to "7tOd6xon/R2zJpy2LlSKcKWIek2jvkim0sNdnZZCWMQ",
"device_id" to "QDHBLWOTSN",
"ciphertext" to "AwgXErAC6TgQ4bV6NFldlffTWuUV1gsBYH6JLQMqG...deLfCQOSPunSSNDFdWuDkB8Cg",
"m.relates_to" to mapOf(
"rel_type" to "m.replace",
"event_id" to mockTextEvent.eventId
)
),
originServerTs = 2000,
senderId = "@alice:example.com",
).apply {
mxDecryptionResult = OlmDecryptionResult(
payload = mapOf(
"type" to EventType.MESSAGE,
"content" to mapOf(
"body" to "* some message edited",
"msgtype" to "m.text",
"m.new_content" to mapOf(
"body" to "some message edited",
"msgtype" to "m.text"
),
"m.relates_to" to mapOf(
"rel_type" to "m.replace",
"event_id" to mockTextEvent.eventId
)
)
),
senderKey = "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo",
isSafe = true
)
}
private val encryptedEvent = Event(
type = EventType.ENCRYPTED,
eventId = "\$WX8WlNC2reiXrwHIA_CQHmU_pSR-jhOA2xKPRcJN9wQ",
roomId = "!GXKhWsRwiWWvbQDBpe:example.com",
content = mapOf(
"algorithm" to "m.megolm.v1.aes-sha2",
"sender_key" to "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo",
"session_id" to "7tOd6xon/R2zJpy2LlSKcKWIek2jvkim0sNdnZZCWMQ",
"device_id" to "QDHBLWOTSN",
"ciphertext" to "AwgXErAC6TgQ4bV6NFldlffTWuUV1gsBYH6JLQMqG+4Vr...Yf0gYyhVWZY4SedF3fTMwkjmTuel4fwrmq",
),
originServerTs = 2000,
senderId = "@alice:example.com",
).apply {
mxDecryptionResult = OlmDecryptionResult(
payload = mapOf(
"type" to EventType.MESSAGE,
"content" to mapOf(
"body" to "some message",
"msgtype" to "m.text"
),
),
senderKey = "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo",
isSafe = true
)
}
}

View file

@ -38,7 +38,7 @@ internal class FakeMonarchy {
init { init {
mockkStatic("org.matrix.android.sdk.internal.util.MonarchyKt") mockkStatic("org.matrix.android.sdk.internal.util.MonarchyKt")
coEvery { coEvery {
instance.awaitTransaction(any<suspend (Realm) -> Any>()) instance.awaitTransaction(any<(Realm) -> Any>())
} coAnswers { } coAnswers {
secondArg<suspend (Realm) -> Any>().invoke(fakeRealm.instance) secondArg<suspend (Realm) -> Any>().invoke(fakeRealm.instance)
} }

View file

@ -33,7 +33,7 @@ internal class FakeRealmConfiguration {
val instance = mockk<RealmConfiguration>() val instance = mockk<RealmConfiguration>()
fun <T> givenAwaitTransaction(realm: Realm) { fun <T> givenAwaitTransaction(realm: Realm) {
val transaction = slot<suspend (Realm) -> T>() val transaction = slot<(Realm) -> T>()
coEvery { awaitTransaction(instance, capture(transaction)) } coAnswers { coEvery { awaitTransaction(instance, capture(transaction)) } coAnswers {
secondArg<suspend (Realm) -> T>().invoke(realm) secondArg<suspend (Realm) -> T>().invoke(realm)
} }

View file

@ -19,6 +19,7 @@ package im.vector.app.core.extensions
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
@ -40,7 +41,8 @@ fun TimelineEvent.getVectorLastMessageContent(): MessageContent? {
// Iterate on event types which are not part of the matrix sdk, otherwise fallback to the sdk method // Iterate on event types which are not part of the matrix sdk, otherwise fallback to the sdk method
return when (root.getClearType()) { return when (root.getClearType()) {
VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> { VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> {
(annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessageVoiceBroadcastInfoContent>() (annotations?.editSummary?.latestEdit?.getClearContent()?.toModel<MessageContent>().toContent().toModel<MessageVoiceBroadcastInfoContent>()
?: root.getClearContent().toModel<MessageVoiceBroadcastInfoContent>())
} }
else -> getLastMessageContent() else -> getLastMessageContent()
} }

View file

@ -442,6 +442,7 @@ class TimelineEventController @Inject constructor(
val timelineEventsGroup = timelineEventsGroups.getOrNull(event) val timelineEventsGroup = timelineEventsGroups.getOrNull(event)
val params = TimelineItemFactoryParams( val params = TimelineItemFactoryParams(
event = event, event = event,
lastEdit = event.annotations?.editSummary?.latestEdit,
prevEvent = prevEvent, prevEvent = prevEvent,
prevDisplayableEvent = prevDisplayableEvent, prevDisplayableEvent = prevDisplayableEvent,
nextEvent = nextEvent, nextEvent = nextEvent,

View file

@ -161,7 +161,7 @@ class MessageItemFactory @Inject constructor(
val callback = params.callback val callback = params.callback
event.root.eventId ?: return null event.root.eventId ?: return null
roomId = event.roomId roomId = event.roomId
val informationData = messageInformationDataFactory.create(params) val informationData = messageInformationDataFactory.create(params, params.event.annotations?.editSummary?.latestEdit)
val threadDetails = if (params.isFromThreadTimeline()) null else event.root.threadDetails val threadDetails = if (params.isFromThreadTimeline()) null else event.root.threadDetails
if (event.root.isRedacted()) { if (event.root.isRedacted()) {

View file

@ -19,10 +19,12 @@ package im.vector.app.features.home.room.detail.timeline.factory
import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventsGroup import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventsGroup
import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryEvents import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryEvents
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
data class TimelineItemFactoryParams( data class TimelineItemFactoryParams(
val event: TimelineEvent, val event: TimelineEvent,
val lastEdit: Event? = null,
val prevEvent: TimelineEvent? = null, val prevEvent: TimelineEvent? = null,
val prevDisplayableEvent: TimelineEvent? = null, val prevDisplayableEvent: TimelineEvent? = null,
val nextEvent: TimelineEvent? = null, val nextEvent: TimelineEvent? = null,

View file

@ -31,11 +31,13 @@ import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLay
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.verification.VerificationState import org.matrix.android.sdk.api.session.crypto.verification.VerificationState
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.getMsgType import org.matrix.android.sdk.api.session.events.model.getMsgType
import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage
import org.matrix.android.sdk.api.session.events.model.isSticker import org.matrix.android.sdk.api.session.events.model.isSticker
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.events.model.toValidDecryptedEvent
import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedContent import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedContent
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.message.MessageType
@ -55,7 +57,7 @@ class MessageInformationDataFactory @Inject constructor(
private val reactionsSummaryFactory: ReactionsSummaryFactory private val reactionsSummaryFactory: ReactionsSummaryFactory
) { ) {
fun create(params: TimelineItemFactoryParams): MessageInformationData { fun create(params: TimelineItemFactoryParams, lastEdit: Event? = null): MessageInformationData {
val event = params.event val event = params.event
val nextDisplayableEvent = params.nextDisplayableEvent val nextDisplayableEvent = params.nextDisplayableEvent
val prevDisplayableEvent = params.prevDisplayableEvent val prevDisplayableEvent = params.prevDisplayableEvent
@ -72,8 +74,14 @@ class MessageInformationDataFactory @Inject constructor(
prevDisplayableEvent?.root?.localDateTime()?.toLocalDate() != date.toLocalDate() prevDisplayableEvent?.root?.localDateTime()?.toLocalDate() != date.toLocalDate()
val time = dateFormatter.format(event.root.originServerTs, DateFormatKind.MESSAGE_SIMPLE) val time = dateFormatter.format(event.root.originServerTs, DateFormatKind.MESSAGE_SIMPLE)
val e2eDecoration = getE2EDecoration(roomSummary, event) val e2eDecoration = getE2EDecoration(roomSummary, lastEdit ?: event.root)
val senderId = if (event.isEncrypted()) {
event.root.toValidDecryptedEvent()?.let {
session.cryptoService().deviceWithIdentityKey(it.cryptoSenderKey, it.algorithm)?.userId
} ?: event.root.senderId.orEmpty()
} else {
event.root.senderId.orEmpty()
}
// SendState Decoration // SendState Decoration
val sendStateDecoration = if (isSentByMe) { val sendStateDecoration = if (isSentByMe) {
getSendStateDecoration( getSendStateDecoration(
@ -89,7 +97,7 @@ class MessageInformationDataFactory @Inject constructor(
return MessageInformationData( return MessageInformationData(
eventId = eventId, eventId = eventId,
senderId = event.root.senderId ?: "", senderId = senderId,
sendState = event.root.sendState, sendState = event.root.sendState,
time = time, time = time,
ageLocalTS = event.root.ageLocalTs, ageLocalTS = event.root.ageLocalTs,
@ -148,34 +156,34 @@ class MessageInformationDataFactory @Inject constructor(
} }
} }
private fun getE2EDecoration(roomSummary: RoomSummary?, event: TimelineEvent): E2EDecoration { private fun getE2EDecoration(roomSummary: RoomSummary?, event: Event): E2EDecoration {
if (roomSummary?.isEncrypted != true) { if (roomSummary?.isEncrypted != true) {
// No decoration for clear room // No decoration for clear room
// Questionable? what if the event is E2E? // Questionable? what if the event is E2E?
return E2EDecoration.NONE return E2EDecoration.NONE
} }
if (event.root.sendState != SendState.SYNCED) { if (event.sendState != SendState.SYNCED) {
// we don't display e2e decoration if event not synced back // we don't display e2e decoration if event not synced back
return E2EDecoration.NONE return E2EDecoration.NONE
} }
val userCrossSigningInfo = session.cryptoService() val userCrossSigningInfo = session.cryptoService()
.crossSigningService() .crossSigningService()
.getUserCrossSigningKeys(event.root.senderId.orEmpty()) .getUserCrossSigningKeys(event.senderId.orEmpty())
if (userCrossSigningInfo?.isTrusted() == true) { if (userCrossSigningInfo?.isTrusted() == true) {
return if (event.isEncrypted()) { return if (event.isEncrypted()) {
// Do not decorate failed to decrypt, or redaction (we lost sender device info) // Do not decorate failed to decrypt, or redaction (we lost sender device info)
if (event.root.getClearType() == EventType.ENCRYPTED || event.root.isRedacted()) { if (event.getClearType() == EventType.ENCRYPTED || event.isRedacted()) {
E2EDecoration.NONE E2EDecoration.NONE
} else { } else {
val sendingDevice = event.root.getSenderKey() val sendingDevice = event.getSenderKey()
?.let { ?.let {
session.cryptoService().deviceWithIdentityKey( session.cryptoService().deviceWithIdentityKey(
it, it,
event.root.content?.get("algorithm") as? String ?: "" event.content?.get("algorithm") as? String ?: ""
) )
} }
if (event.root.mxDecryptionResult?.isSafe == false) { if (event.mxDecryptionResult?.isSafe == false) {
E2EDecoration.WARN_UNSAFE_KEY E2EDecoration.WARN_UNSAFE_KEY
} else { } else {
when { when {
@ -202,8 +210,8 @@ class MessageInformationDataFactory @Inject constructor(
} else { } else {
return if (!event.isEncrypted()) { return if (!event.isEncrypted()) {
e2EDecorationForClearEventInE2ERoom(event, roomSummary) e2EDecorationForClearEventInE2ERoom(event, roomSummary)
} else if (event.root.mxDecryptionResult != null) { } else if (event.mxDecryptionResult != null) {
if (event.root.mxDecryptionResult?.isSafe == true) { if (event.mxDecryptionResult?.isSafe == true) {
E2EDecoration.NONE E2EDecoration.NONE
} else { } else {
E2EDecoration.WARN_UNSAFE_KEY E2EDecoration.WARN_UNSAFE_KEY
@ -214,13 +222,13 @@ class MessageInformationDataFactory @Inject constructor(
} }
} }
private fun e2EDecorationForClearEventInE2ERoom(event: TimelineEvent, roomSummary: RoomSummary) = private fun e2EDecorationForClearEventInE2ERoom(event: Event, roomSummary: RoomSummary) =
if (event.root.isStateEvent()) { if (event.isStateEvent()) {
// Do not warn for state event, they are always in clear // Do not warn for state event, they are always in clear
E2EDecoration.NONE E2EDecoration.NONE
} else { } else {
val ts = roomSummary.encryptionEventTs ?: 0 val ts = roomSummary.encryptionEventTs ?: 0
val eventTs = event.root.originServerTs ?: 0 val eventTs = event.originServerTs ?: 0
// If event is in clear after the room enabled encryption we should warn // If event is in clear after the room enabled encryption we should warn
if (eventTs > ts) E2EDecoration.WARN_IN_CLEAR else E2EDecoration.NONE if (eventTs > ts) E2EDecoration.WARN_IN_CLEAR else E2EDecoration.NONE
} }