diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedAnnotation.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedAnnotation.kt index 239f749993..5b2ab77467 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedAnnotation.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedAnnotation.kt @@ -38,5 +38,4 @@ data class AggregatedAnnotation( override val limited: Boolean? = false, override val count: Int? = 0, val chunk: List? = null - ) : UnsignedRelationInfo diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt index ae8ed3941f..1dedcce8b6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt @@ -50,5 +50,6 @@ import com.squareup.moshi.JsonClass data class AggregatedRelations( @Json(name = "m.annotation") val annotations: AggregatedAnnotation? = 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 ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedReplace.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedReplace.kt new file mode 100644 index 0000000000..2ae091a1a4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedReplace.kt @@ -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, +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/ValidDecryptedEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/ValidDecryptedEvent.kt new file mode 100644 index 0000000000..0cee077807 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/ValidDecryptedEvent.kt @@ -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 + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EditAggregatedSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EditAggregatedSummary.kt index 67bab626cb..7d445a5cc6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EditAggregatedSummary.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EditAggregatedSummary.kt @@ -15,10 +15,10 @@ */ 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( - 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) val sourceEvents: List, val localEchos: List, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt index 6f4049de36..6ceced59d7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt @@ -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.room.model.EventAnnotationsSummary 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.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.relation.RelationDefaultContent 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. */ fun TimelineEvent.getLastMessageContent(): MessageContent? { - return when (root.getClearType()) { - EventType.STICKER -> root.getClearContent().toModel() - in EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() - in EventType.STATE_ROOM_BEACON_INFO -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() - in EventType.BEACON_LOCATION_DATA -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() - else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() - } + return ( + annotations?.editSummary?.latestEdit + ?.getClearContent()?.toModel()?.newContent + ?: root.getClearContent() + ) + .toModel() } /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt index 7d263f1937..a1ea88a70c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt @@ -26,17 +26,17 @@ import kotlinx.coroutines.withContext import timber.log.Timber import kotlin.system.measureTimeMillis -internal fun CoroutineScope.asyncTransaction(monarchy: Monarchy, transaction: suspend (realm: Realm) -> T) { +internal fun CoroutineScope.asyncTransaction(monarchy: Monarchy, transaction: (realm: Realm) -> T) { asyncTransaction(monarchy.realmConfiguration, transaction) } -internal fun CoroutineScope.asyncTransaction(realmConfiguration: RealmConfiguration, transaction: suspend (realm: Realm) -> T) { +internal fun CoroutineScope.asyncTransaction(realmConfiguration: RealmConfiguration, transaction: (realm: Realm) -> T) { launch { awaitTransaction(realmConfiguration, transaction) } } -internal suspend fun awaitTransaction(config: RealmConfiguration, transaction: suspend (realm: Realm) -> T): T { +internal suspend fun awaitTransaction(config: RealmConfiguration, transaction: (realm: Realm) -> T): T { return withContext(Realm.WRITE_EXECUTOR.asCoroutineDispatcher()) { Realm.getInstance(config).use { bgRealm -> bgRealm.beginTransaction() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 30836c027e..388f962454 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -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.MigrateSessionTo041 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.database.MatrixRealmMigration import javax.inject.Inject @@ -67,7 +68,7 @@ internal class RealmSessionStoreMigration @Inject constructor( private val normalizer: Normalizer ) : MatrixRealmMigration( dbName = "Session", - schemaVersion = 42L, + schemaVersion = 43L, ) { /** * Forces all RealmSessionStoreMigration instances to be equal. @@ -119,5 +120,6 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion < 40) MigrateSessionTo040(realm).perform() if (oldVersion < 41) MigrateSessionTo041(realm).perform() if (oldVersion < 42) MigrateSessionTo042(realm).perform() + if (oldVersion < 43) MigrateSessionTo043(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt index 221abe0df5..149a2eebfe 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt @@ -83,7 +83,6 @@ internal fun ChunkEntity.addTimelineEvent( this.eventId = eventId this.roomId = roomId this.annotations = EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst() - ?.also { it.cleanUp(eventEntity.sender) } this.readReceipts = readReceiptsSummaryEntity this.displayIndex = displayIndex this.ownedByThreadChunk = ownedByThreadChunk diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt index 193710f962..0ac8dc7902 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt @@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.database.helper import io.realm.Realm import io.realm.RealmQuery import io.realm.Sort -import io.realm.kotlin.createObject import kotlinx.coroutines.runBlocking import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.MXCryptoError @@ -103,32 +102,6 @@ internal fun ThreadSummaryEntity.updateThreadSummaryLatestEvent( } } -private fun EventEntity.toTimelineEventEntity(roomMemberContentsByUser: HashMap): TimelineEventEntity { - val roomId = roomId - val eventId = eventId - val localId = TimelineEventEntity.nextId(realm) - val senderId = sender ?: "" - - val timelineEventEntity = realm.createObject().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( threadSummaryType: ThreadSummaryUpdateType, realm: Realm, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt index 6bbeb17fdd..5fb70ad1ee 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt @@ -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.ReactionAggregatedSummary 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 internal object EventAnnotationsSummaryMapper { @@ -36,13 +37,22 @@ internal object EventAnnotationsSummaryMapper { ) }, editSummary = annotationsSummary.editSummary - ?.let { - val latestEdition = it.editions.maxByOrNull { editionOfEvent -> editionOfEvent.timestamp } ?: return@let null + ?.let { summary -> + /** + * 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 { it.timestamp }.thenBy { it.eventId }) + .lastOrNull() ?: return@let null + // get the event and validate? + val editEvent = latestEdition.event + EditAggregatedSummary( - latestContent = ContentMapper.map(latestEdition.content), - sourceEvents = it.editions.filter { editionOfEvent -> !editionOfEvent.isLocalEcho } + latestEdit = editEvent?.asDomain(), + sourceEvents = summary.editions.filter { editionOfEvent -> !editionOfEvent.isLocalEcho } .map { editionOfEvent -> editionOfEvent.eventId }, - localEchos = it.editions.filter { editionOfEvent -> editionOfEvent.isLocalEcho } + localEchos = summary.editions.filter { editionOfEvent -> editionOfEvent.isLocalEcho } .map { editionOfEvent -> editionOfEvent.eventId }, lastEditTs = latestEdition.timestamp ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo008.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo008.kt index b61bf7e6fa..42a47a9a27 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo008.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo008.kt @@ -25,11 +25,11 @@ internal class MigrateSessionTo008(realm: DynamicRealm) : RealmMigrator(realm, 8 override fun doMigrate(realm: DynamicRealm) { 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) .setRequired(EditionOfEventFields.EVENT_ID, true) - .addField(EditionOfEventFields.SENDER_ID, String::class.java) - .setRequired(EditionOfEventFields.SENDER_ID, true) + .addField("senderId" /*EditionOfEventFields.SENDER_ID*/, String::class.java) + .setRequired("senderId" /*EditionOfEventFields.SENDER_ID*/, true) .addField(EditionOfEventFields.TIMESTAMP, Long::class.java) .addField(EditionOfEventFields.IS_LOCAL_ECHO, Boolean::class.java) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo043.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo043.kt new file mode 100644 index 0000000000..a27d4fda3a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo043.kt @@ -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) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EditAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EditAggregatedSummaryEntity.kt index 61acd51dd4..7b7b90f82d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EditAggregatedSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EditAggregatedSummaryEntity.kt @@ -32,9 +32,8 @@ internal open class EditAggregatedSummaryEntity( @RealmClass(embedded = true) internal open class EditionOfEvent( - var senderId: String = "", var eventId: String = "", - var content: String? = null, var timestamp: Long = 0, - var isLocalEcho: Boolean = false + var isLocalEcho: Boolean = false, + var event: EventEntity? = null, ) : RealmObject() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventAnnotationsSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventAnnotationsSummaryEntity.kt index 645998d0c0..9a201ab4e8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventAnnotationsSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventAnnotationsSummaryEntity.kt @@ -19,7 +19,6 @@ import io.realm.RealmList import io.realm.RealmObject import io.realm.annotations.PrimaryKey import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity -import timber.log.Timber internal open class EventAnnotationsSummaryEntity( @PrimaryKey @@ -32,21 +31,6 @@ internal open class EventAnnotationsSummaryEntity( var liveLocationShareAggregatedSummary: LiveLocationShareAggregatedSummaryEntity? = null, ) : 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 } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt index a650fa2d64..9741a7bd15 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt @@ -24,7 +24,7 @@ internal interface EventInsertLiveProcessor { 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. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt index b15a647421..6a4abe9d34 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt @@ -54,7 +54,7 @@ internal class CallEventProcessor @Inject constructor(private val callSignalingH return allowedTypes.contains(eventType) } - override suspend fun process(realm: Realm, event: Event) { + override fun process(realm: Realm, event: Event) { eventsToPostProcess.add(event) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt new file mode 100644 index 0000000000..dcf6ad54a0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt @@ -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 else’s messages). + * The replacement and original events must have the same type (i.e. you cannot change the original event’s 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()?.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()?.newContent == null) { + return EditValidity.Invalid("replacement event must have an m.new_content property") + } + } else { + if (originalEvent.content.toModel()?.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()?.newContent == null) { + return EditValidity.Invalid("replacement event must have an m.new_content property") + } + } + + return EditValidity.Valid + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index 24d4975eb9..ef1d8c1430 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -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.LocalEcho 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.toContent 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.mapper.ContentMapper 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.EditionOfEvent import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity @@ -72,6 +72,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( private val sessionManager: SessionManager, private val liveLocationAggregationProcessor: LiveLocationAggregationProcessor, private val pollAggregationProcessor: PollAggregationProcessor, + private val editValidator: EventEditValidator, private val clock: Clock, ) : EventInsertLiveProcessor { @@ -79,13 +80,15 @@ internal class EventRelationsAggregationProcessor @Inject constructor( EventType.MESSAGE, EventType.REDACTION, 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_CANCEL, EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_MAC, // TODO Add ? - // EventType.KEY_VERIFICATION_READY, + EventType.KEY_VERIFICATION_READY, EventType.KEY_VERIFICATION_KEY, EventType.ENCRYPTED ) + 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) } - override suspend fun process(realm: Realm, event: Event) { + override fun process(realm: Realm, event: Event) { try { // Temporary catch, should be removed val roomId = event.roomId if (roomId == null) { @@ -102,7 +105,31 @@ internal class EventRelationsAggregationProcessor @Inject constructor( return } 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 -> { // we got a reaction!! 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}") 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 } - } + // XXX do something for aggregated edits? + // it's a bit strange as it would require to do a server query to get the edition? } - val content: MessageContent? = event.content.toModel() - if (content?.relatesTo?.type == RelationType.REPLACE) { + val relationContent = event.getRelationContent() + if (relationContent?.type == RelationType.REPLACE) { Timber.v("###REPLACE in room $roomId for event ${event.eventId}") // A replace! - handleReplace(realm, event, content, roomId, isLocalEcho) + handleReplace(realm, event, roomId, isLocalEcho, relationContent.eventId) } } - EventType.KEY_VERIFICATION_DONE, EventType.KEY_VERIFICATION_CANCEL, EventType.KEY_VERIFICATION_ACCEPT, @@ -142,73 +165,30 @@ 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 -> { - // Relation type is in clear - val encryptedEventContent = event.content.toModel() - if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE || - encryptedEventContent?.relatesTo?.type == RelationType.RESPONSE - ) { - event.getClearContent().toModel()?.let { - if (encryptedEventContent.relatesTo.type == RelationType.REPLACE) { - Timber.v("###REPLACE in room $roomId for event ${event.eventId}") - // A replace! - handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) - } else if (event.getClearType() in EventType.POLL_RESPONSE) { - sessionManager.getSessionComponent(sessionId)?.session()?.let { session -> - pollAggregationProcessor.handlePollResponseEvent(session, realm, event) - } - } - } - } else if (encryptedEventContent?.relatesTo?.type == RelationType.REFERENCE) { - when (event.getClearType()) { - EventType.KEY_VERIFICATION_DONE, - EventType.KEY_VERIFICATION_CANCEL, - 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(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 } -// } +// // Relation type is in clear, it might be possible to do some things? +// // Notice that if the event is decrypted later, process be called again +// val encryptedEventContent = event.content.toModel() +// when (encryptedEventContent?.relatesTo?.type) { +// RelationType.REPLACE -> { +// Timber.v("###REPLACE in room $roomId for event ${event.eventId}") +// // A replace! +// handleReplace(realm, event, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) +// } +// RelationType.RESPONSE -> { +// // can we / should we do we something for UTD response?? +// Timber.w("## UTD response in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") +// } +// RelationType.REFERENCE -> { +// // can we / should we do we something for UTD reference?? +// Timber.w("## UTD reference in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") +// } +// RelationType.ANNOTATION -> { +// // can we / should we do we something for UTD annotation?? +// Timber.w("## UTD annotation in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") +// } // } } EventType.REDACTION -> { @@ -217,9 +197,6 @@ internal class EventRelationsAggregationProcessor @Inject constructor( when (eventToPrune.type) { EventType.MESSAGE -> { Timber.d("REDACTION for message ${eventToPrune.eventId}") -// val unsignedData = EventMapper.map(eventToPrune).unsignedData -// ?: UnsignedData(null, null) - // was this event a m.replace val contentModel = ContentMapper.map(eventToPrune.content)?.toModel() 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) { Timber.v("###REPLACE in room $roomId for event ${event.eventId}") // A replace! - handleReplace(realm, event, content, roomId, isLocalEcho) + handleReplace(realm, event, roomId, isLocalEcho, content?.relatesTo.eventId) } } in EventType.POLL_RESPONSE -> { @@ -274,23 +251,22 @@ internal class EventRelationsAggregationProcessor @Inject constructor( private fun handleReplace( realm: Realm, event: Event, - content: MessageContent, roomId: String, isLocalEcho: Boolean, - relatedEventId: String? = null + relatedEventId: String? ) { val eventId = event.eventId ?: return - val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return - val newContent = content.newContent ?: return - - // Check that the sender is the same + val targetEventId = relatedEventId ?: return // ?: content.relatesTo?.eventId ?: return val editedEvent = EventEntity.where(realm, targetEventId).findFirst() - if (editedEvent == null) { - // We do not know yet about the edited event - } else if (editedEvent.sender != event.senderId) { - // Edited by someone else, ignore - Timber.w("Ignore edition by someone else") - return + + when (val validity = editValidator.validateEdit(editedEvent?.asDomain(), event)) { + is EventEditValidator.EditValidity.Invalid -> return Unit.also { + Timber.w("Dropping invalid edit ${event.eventId}, reason:${validity.reason}") + } + 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 @@ -305,11 +281,10 @@ internal class EventRelationsAggregationProcessor @Inject constructor( .also { editSummary -> editSummary.editions.add( EditionOfEvent( - senderId = event.senderId ?: "", eventId = event.eventId, - content = ContentMapper.map(newContent), - timestamp = if (isLocalEcho) 0 else event.originServerTs ?: 0, - isLocalEcho = isLocalEcho + event = EventEntity.where(realm, eventId).findFirst(), + timestamp = if (isLocalEcho) clock.epochMillis() else event.originServerTs ?: clock.epochMillis(), + isLocalEcho = isLocalEcho, ) ) } @@ -334,9 +309,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor( Timber.v("###REPLACE Computing aggregated edit summary (isLocalEcho:$isLocalEcho)") existingSummary.editions.add( EditionOfEvent( - senderId = event.senderId ?: "", eventId = event.eventId, - content = ContentMapper.map(newContent), + event = EventEntity.where(realm, eventId).findFirst(), timestamp = if (isLocalEcho) { clock.epochMillis() } else { @@ -369,8 +343,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( * @param editions list of edition of event */ private fun handleThreadSummaryEdition( - editedEvent: EventEntity?, - replaceEvent: TimelineEventEntity?, + editedEvent: EventEntity?, replaceEvent: TimelineEventEntity?, editions: List? ) { replaceEvent ?: return @@ -599,12 +572,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor( private fun handleBeaconLocationData(event: Event, realm: Realm, roomId: String, isLocalEcho: Boolean) { event.getClearContent().toModel(catchError = true)?.let { liveLocationAggregationProcessor.handleBeaconLocationData( - realm, - event, - it, - roomId, - event.getRelationContent()?.eventId, - isLocalEcho + realm = realm, + event = event, + content = it, + roomId = roomId, + relatedEventId = event.getRelationContent()?.eventId, + isLocalEcho = isLocalEcho ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt index 03c2b2a47e..0cda6eca99 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt @@ -21,6 +21,7 @@ import io.realm.Realm import io.realm.RealmConfiguration import io.realm.kotlin.createObject 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.room.failure.CreateRoomFailure 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. * 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 { membership = Membership.JOIN chunks.add(createLocalRoomChunk(realm, roomId, createRoomBody)) @@ -148,13 +149,16 @@ internal class DefaultCreateLocalRoomTask @Inject constructor( * * @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().apply { isLastBackward = 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() for (event in eventList) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt index eb966b684c..8b5fde6ab7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt @@ -30,7 +30,7 @@ import javax.inject.Inject 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() val predecessorRoomId = createRoomContent?.predecessor?.roomId ?: return diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt index fa3479ed3c..9041ef2677 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt @@ -40,7 +40,7 @@ internal class LiveLocationShareRedactionEventProcessor @Inject constructor() : 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())) { return } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt index cc86679cbc..7968eabd30 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt @@ -46,7 +46,7 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr return eventType == EventType.REDACTION } - override suspend fun process(realm: Realm, event: Event) { + override fun process(realm: Realm, event: Event) { pruneEvent(realm, event) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt index 2b404775f0..3684bec167 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt @@ -30,7 +30,7 @@ import javax.inject.Inject 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 val createRoomContent = event.getClearContent().toModel() if (createRoomContent?.replacementRoomId == null) return diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt index 6152eacae5..af3ba80fe4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt @@ -22,7 +22,7 @@ import io.realm.RealmModel import org.matrix.android.sdk.internal.database.awaitTransaction import java.util.concurrent.atomic.AtomicReference -internal suspend fun Monarchy.awaitTransaction(transaction: suspend (realm: Realm) -> T): T { +internal suspend fun Monarchy.awaitTransaction(transaction: (realm: Realm) -> T): T { return awaitTransaction(realmConfiguration, transaction) } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EditValidationTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EditValidationTest.kt new file mode 100644 index 0000000000..99942d967e --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EditValidationTest.kt @@ -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() + 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() + 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() + 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() + 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 { + every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + mockk { + 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 { + every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + mockk { + 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 { + every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + mockk { + 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 { + every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + mockk { + 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 { + 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 { + 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 + ) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt index 93999458c6..87868e91d1 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt @@ -38,7 +38,7 @@ internal class FakeMonarchy { init { mockkStatic("org.matrix.android.sdk.internal.util.MonarchyKt") coEvery { - instance.awaitTransaction(any Any>()) + instance.awaitTransaction(any<(Realm) -> Any>()) } coAnswers { secondArg Any>().invoke(fakeRealm.instance) } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt index 15a9823c79..38b67b5261 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt @@ -33,7 +33,7 @@ internal class FakeRealmConfiguration { val instance = mockk() fun givenAwaitTransaction(realm: Realm) { - val transaction = slot T>() + val transaction = slot<(Realm) -> T>() coEvery { awaitTransaction(instance, capture(transaction)) } coAnswers { secondArg T>().invoke(realm) } diff --git a/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt b/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt index 5c3393416b..d907f39ee3 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt @@ -19,6 +19,7 @@ package im.vector.app.core.extensions import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants 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.toContent 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.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 return when (root.getClearType()) { VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> { - (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() + (annotations?.editSummary?.latestEdit?.getClearContent()?.toModel().toContent().toModel() + ?: root.getClearContent().toModel()) } else -> getLastMessageContent() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index 57ad4331ce..1f079e420b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -442,6 +442,7 @@ class TimelineEventController @Inject constructor( val timelineEventsGroup = timelineEventsGroups.getOrNull(event) val params = TimelineItemFactoryParams( event = event, + lastEdit = event.annotations?.editSummary?.latestEdit, prevEvent = prevEvent, prevDisplayableEvent = prevDisplayableEvent, nextEvent = nextEvent, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 373410775b..d5d38f47a3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -161,7 +161,7 @@ class MessageItemFactory @Inject constructor( val callback = params.callback event.root.eventId ?: return null 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 if (event.root.isRedacted()) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt index 7c02b6f058..dd4494a613 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt @@ -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.helper.TimelineEventsGroup 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 data class TimelineItemFactoryParams( val event: TimelineEvent, + val lastEdit: Event? = null, val prevEvent: TimelineEvent? = null, val prevDisplayableEvent: TimelineEvent? = null, val nextEvent: TimelineEvent? = null, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 50b4366e98..a969c294f5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -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.session.Session 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.getMsgType 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.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.RoomSummary import org.matrix.android.sdk.api.session.room.model.message.MessageType @@ -55,7 +57,7 @@ class MessageInformationDataFactory @Inject constructor( private val reactionsSummaryFactory: ReactionsSummaryFactory ) { - fun create(params: TimelineItemFactoryParams): MessageInformationData { + fun create(params: TimelineItemFactoryParams, lastEdit: Event? = null): MessageInformationData { val event = params.event val nextDisplayableEvent = params.nextDisplayableEvent val prevDisplayableEvent = params.prevDisplayableEvent @@ -72,8 +74,14 @@ class MessageInformationDataFactory @Inject constructor( prevDisplayableEvent?.root?.localDateTime()?.toLocalDate() != date.toLocalDate() 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 val sendStateDecoration = if (isSentByMe) { getSendStateDecoration( @@ -89,7 +97,7 @@ class MessageInformationDataFactory @Inject constructor( return MessageInformationData( eventId = eventId, - senderId = event.root.senderId ?: "", + senderId = senderId, sendState = event.root.sendState, time = time, 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) { // No decoration for clear room // Questionable? what if the event is E2E? 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 return E2EDecoration.NONE } val userCrossSigningInfo = session.cryptoService() .crossSigningService() - .getUserCrossSigningKeys(event.root.senderId.orEmpty()) + .getUserCrossSigningKeys(event.senderId.orEmpty()) if (userCrossSigningInfo?.isTrusted() == true) { return if (event.isEncrypted()) { // 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 } else { - val sendingDevice = event.root.getSenderKey() + val sendingDevice = event.getSenderKey() ?.let { session.cryptoService().deviceWithIdentityKey( 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 } else { when { @@ -202,8 +210,8 @@ class MessageInformationDataFactory @Inject constructor( } else { return if (!event.isEncrypted()) { e2EDecorationForClearEventInE2ERoom(event, roomSummary) - } else if (event.root.mxDecryptionResult != null) { - if (event.root.mxDecryptionResult?.isSafe == true) { + } else if (event.mxDecryptionResult != null) { + if (event.mxDecryptionResult?.isSafe == true) { E2EDecoration.NONE } else { E2EDecoration.WARN_UNSAFE_KEY @@ -214,13 +222,13 @@ class MessageInformationDataFactory @Inject constructor( } } - private fun e2EDecorationForClearEventInE2ERoom(event: TimelineEvent, roomSummary: RoomSummary) = - if (event.root.isStateEvent()) { + private fun e2EDecorationForClearEventInE2ERoom(event: Event, roomSummary: RoomSummary) = + if (event.isStateEvent()) { // Do not warn for state event, they are always in clear E2EDecoration.NONE } else { 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 (eventTs > ts) E2EDecoration.WARN_IN_CLEAR else E2EDecoration.NONE }