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 1dedcce8b6..7f043dc0f7 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 @@ -19,7 +19,7 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass /** - * + * ``` * { * "m.annotation": { * "chunk": [ @@ -43,7 +43,7 @@ import com.squareup.moshi.JsonClass * "count": 1 * } * } - * + * ``` */ @JsonClass(generateAdapter = true) 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 index 0cee077807..b305bf19b0 100644 --- 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 @@ -16,6 +16,9 @@ package org.matrix.android.sdk.api.session.events.model +import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent + data class ValidDecryptedEvent( val type: String, val eventId: String, @@ -29,25 +32,6 @@ data class ValidDecryptedEvent( 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 - ) +fun ValidDecryptedEvent.getRelationContent(): RelationDefaultContent? { + return clearContent.toModel()?.relatesTo } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EditAggregatedSummaryEntityMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EditAggregatedSummaryEntityMapper.kt new file mode 100644 index 0000000000..8c209f2f2a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EditAggregatedSummaryEntityMapper.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2020 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.database.mapper + +import org.matrix.android.sdk.api.session.room.model.EditAggregatedSummary +import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.EditionOfEvent + +internal object EditAggregatedSummaryEntityMapper { + + fun map(summary: EditAggregatedSummaryEntity?): EditAggregatedSummary? { + summary ?: return 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 { it.timestamp }.thenBy { it.eventId }) + .lastOrNull() ?: return null + val editEvent = latestEdition.event + + return EditAggregatedSummary( + latestEdit = editEvent?.asDomain(), + sourceEvents = summary.editions.filter { editionOfEvent -> !editionOfEvent.isLocalEcho } + .map { editionOfEvent -> editionOfEvent.eventId }, + 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/mapper/EventAnnotationsSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt index 5fb70ad1ee..d4bb5791a0 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 @@ -16,11 +16,9 @@ package org.matrix.android.sdk.internal.database.mapper -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,27 +34,7 @@ internal object EventAnnotationsSummaryMapper { it.sourceLocalEcho.toList() ) }, - editSummary = annotationsSummary.editSummary - ?.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( - latestEdit = editEvent?.asDomain(), - sourceEvents = summary.editions.filter { editionOfEvent -> !editionOfEvent.isLocalEcho } - .map { editionOfEvent -> editionOfEvent.eventId }, - localEchos = summary.editions.filter { editionOfEvent -> editionOfEvent.isLocalEcho } - .map { editionOfEvent -> editionOfEvent.eventId }, - lastEditTs = latestEdition.timestamp - ) - }, + editSummary = EditAggregatedSummaryEntityMapper.map(annotationsSummary.editSummary), referencesAggregatedSummary = annotationsSummary.referencesSummaryEntity?.let { ReferencesAggregatedSummary( ContentMapper.map(it.content), 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 42a47a9a27..f85a0661c2 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("content"/**EditionOfEventFields.CONTENT*/, String::class.java) + .addField("content", String::class.java) .addField(EditionOfEventFields.EVENT_ID, String::class.java) .setRequired(EditionOfEventFields.EVENT_ID, true) - .addField("senderId" /*EditionOfEventFields.SENDER_ID*/, String::class.java) - .setRequired("senderId" /*EditionOfEventFields.SENDER_ID*/, true) + .addField("senderId", String::class.java) + .setRequired("senderId", 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/session/events/EventExt.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/events/EventExt.kt index 91e709e464..63409a15bb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/events/EventExt.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/events/EventExt.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.events import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.ValidDecryptedEvent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.RoomMemberContent @@ -33,3 +34,32 @@ internal fun Event.getFixedRoomMemberContent(): RoomMemberContent? { content } } + +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 + + // copy the relation as it's in clear in the encrypted content + val updatedContent = this.content.get("m.relates_to")?.let { + decryptedContent.toMutableMap().apply { + put("m.relates_to", it) + } + } ?: decryptedContent + return ValidDecryptedEvent( + type = type, + eventId = eventId, + clearContent = updatedContent, + 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/internal/session/room/EventEditValidator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt index 940da25f11..ea14aacfe4 100644 --- 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 @@ -17,11 +17,14 @@ 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.LocalEcho import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.getRelationContent 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 org.matrix.android.sdk.internal.session.events.toValidDecryptedEvent +import timber.log.Timber import javax.inject.Inject internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCryptoStore) { @@ -47,12 +50,18 @@ internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCrypto * If the original event was encrypted, the replacement should be too. */ fun validateEdit(originalEvent: Event?, replaceEvent: Event): EditValidity { + Timber.v("###REPLACE valide event $originalEvent replaced $replaceEvent") // 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 (LocalEcho.isLocalEchoId(replaceEvent.eventId.orEmpty())) { + // Don't validate local echo + return EditValidity.Unknown + } + if (originalEvent.roomId != replaceEvent.roomId) { return EditValidity.Invalid("original event and replacement event must have the same room_id") } @@ -71,7 +80,7 @@ internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCrypto val originalCryptoSenderId = cryptoStore.deviceWithIdentityKey(originalDecrypted.cryptoSenderKey)?.userId val editCryptoSenderId = cryptoStore.deviceWithIdentityKey(replaceDecrypted.cryptoSenderKey)?.userId - if (originalDecrypted.clearContent.toModel()?.relatesTo?.type == RelationType.REPLACE) { + if (originalDecrypted.getRelationContent()?.type == RelationType.REPLACE) { return EditValidity.Invalid("The original event must not, itself, have a rel_type of m.replace ") } @@ -96,7 +105,7 @@ internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCrypto return EditValidity.Invalid("replacement event must have an m.new_content property") } } else { - if (originalEvent.content.toModel()?.relatesTo?.type == RelationType.REPLACE) { + if (originalEvent.getRelationContent()?.type == RelationType.REPLACE) { return EditValidity.Invalid("The original event must not, itself, have a rel_type of m.replace ") } 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 837d00720b..48e75821bd 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,6 +23,7 @@ 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 @@ -81,13 +82,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor( 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 + // It's not participating in verification itself, 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_KEY, EventType.ENCRYPTED @@ -168,28 +168,28 @@ 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, 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}") -// } -// } + // 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 -> { val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() } @@ -256,7 +256,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( relatedEventId: String? ) { val eventId = event.eventId ?: return - val targetEventId = relatedEventId ?: return // ?: content.relatesTo?.eventId ?: return + val targetEventId = relatedEventId ?: return val editedEvent = EventEntity.where(realm, targetEventId).findFirst() when (val validity = editValidator.validateEdit(editedEvent?.asDomain(), event)) { @@ -301,15 +301,16 @@ internal class EventRelationsAggregationProcessor @Inject constructor( // ok it has already been managed Timber.v("###REPLACE Receiving remote echo of edit (edit already done)") existingSummary.editions.firstOrNull { it.eventId == txId }?.let { - it.eventId = event.eventId + it.eventId = eventId it.timestamp = event.originServerTs ?: clock.epochMillis() it.isLocalEcho = false + it.event = EventEntity.where(realm, eventId).findFirst() } } else { Timber.v("###REPLACE Computing aggregated edit summary (isLocalEcho:$isLocalEcho)") existingSummary.editions.add( EditionOfEvent( - eventId = event.eventId, + eventId = eventId, event = EventEntity.where(realm, eventId).findFirst(), timestamp = if (isLocalEcho) { clock.epochMillis() @@ -343,7 +344,8 @@ 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 diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/EditAggregationSummaryMapperTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/EditAggregationSummaryMapperTest.kt new file mode 100644 index 0000000000..12ff9c1d37 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/EditAggregationSummaryMapperTest.kt @@ -0,0 +1,78 @@ +/* + * 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.mapper + +import io.mockk.every +import io.mockk.mockk +import io.realm.RealmList +import org.amshove.kluent.shouldBe +import org.amshove.kluent.shouldNotBe +import org.junit.Test +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.EventEntity + +class EditAggregationSummaryMapperTest { + + @Test + fun test() { + val edits = RealmList( + EditionOfEvent( + timestamp = 0L, + eventId = "e0", + isLocalEcho = false, + event = mockEvent("e0") + ), + EditionOfEvent( + timestamp = 1L, + eventId = "e1", + isLocalEcho = false, + event = mockEvent("e1") + ), + EditionOfEvent( + timestamp = 30L, + eventId = "e2", + isLocalEcho = true, + event = mockEvent("e2") + ) + ) + val fakeSummaryEntity = mockk { + every { editions } returns edits + } + + val mapped = EditAggregatedSummaryEntityMapper.map(fakeSummaryEntity) + mapped shouldNotBe null + mapped!!.sourceEvents.size shouldBe 2 + mapped.localEchos.size shouldBe 1 + mapped.localEchos.first() shouldBe "e2" + + mapped.lastEditTs shouldBe 30L + mapped.latestEdit?.eventId shouldBe "e2" + } + + private fun mockEvent(eventId: String): EventEntity { + return EventEntity().apply { + this.eventId = eventId + this.content = """ + { + "body" : "Hello", + "msgtype": "text" + } + """.trimIndent() + } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/event/ValidDecryptedEventTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/event/ValidDecryptedEventTest.kt new file mode 100644 index 0000000000..8dead42f60 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/event/ValidDecryptedEventTest.kt @@ -0,0 +1,134 @@ +/* + * 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.event + +import org.amshove.kluent.shouldBe +import org.amshove.kluent.shouldNotBe +import org.junit.Test +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM +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.api.session.events.model.content.EncryptedEventContent +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.MessageTextContent +import org.matrix.android.sdk.internal.session.events.toValidDecryptedEvent + +class ValidDecryptedEventTest { + + val fakeEvent = Event( + type = EventType.ENCRYPTED, + eventId = "\$eventId", + roomId = "!fakeRoom", + content = EncryptedEventContent( + algorithm = MXCRYPTO_ALGORITHM_MEGOLM, + ciphertext = "AwgBEpACQEKOkd4Gp0+gSXG4M+btcrnPgsF23xs/lUmS2I4YjmqF...", + sessionId = "TO2G4u2HlnhtbIJk", + senderKey = "5e3EIqg3JfooZnLQ2qHIcBarbassQ4qXblai0", + deviceId = "FAKEE" + ).toContent() + ) + + @Test + fun `A failed to decrypt message should give a null validated decrypted event`() { + fakeEvent.toValidDecryptedEvent() shouldBe null + } + + @Test + fun `Mismatch sender key detection`() { + val decryptedEvent = fakeEvent + .apply { + mxDecryptionResult = OlmDecryptionResult( + payload = mapOf( + "type" to EventType.MESSAGE, + "content" to mapOf( + "body" to "some message", + "msgtype" to "m.text" + ), + ), + senderKey = "the_real_sender_key", + ) + } + + val validDecryptedEvent = decryptedEvent.toValidDecryptedEvent() + validDecryptedEvent shouldNotBe null + + fakeEvent.content!!["senderKey"] shouldNotBe "the_real_sender_key" + validDecryptedEvent!!.cryptoSenderKey shouldBe "the_real_sender_key" + } + + @Test + fun `Mixed content event should be detected`() { + val mixedEvent = Event( + type = EventType.ENCRYPTED, + eventId = "\$eventd ", + roomId = "!fakeRoo", + content = mapOf( + "algorithm" to "m.megolm.v1.aes-sha2", + "ciphertext" to "AwgBEpACQEKOkd4Gp0+gSXG4M+btcrnPgsF23xs/lUmS2I4YjmqF...", + "sessionId" to "TO2G4u2HlnhtbIJk", + "senderKey" to "5e3EIqg3JfooZnLQ2qHIcBarbassQ4qXblai0", + "deviceId" to "FAKEE", + "body" to "some message", + "msgtype" to "m.text" + ).toContent() + ) + + val unValidatedContent = mixedEvent.getClearContent().toModel() + unValidatedContent?.body shouldBe "some message" + + mixedEvent.toValidDecryptedEvent()?.clearContent?.toModel() shouldBe null + } + + @Test + fun `Basic field validation`() { + val decryptedEvent = fakeEvent + .apply { + mxDecryptionResult = OlmDecryptionResult( + payload = mapOf( + "type" to EventType.MESSAGE, + "content" to mapOf( + "body" to "some message", + "msgtype" to "m.text" + ), + ), + senderKey = "the_real_sender_key", + ) + } + + decryptedEvent.toValidDecryptedEvent() shouldNotBe null + decryptedEvent.copy(roomId = null).toValidDecryptedEvent() shouldBe null + decryptedEvent.copy(eventId = null).toValidDecryptedEvent() shouldBe null + } + + @Test + fun `A clear event is not a valid decrypted event`() { + val mockTextEvent = Event( + type = EventType.MESSAGE, + eventId = "eventId", + roomId = "!fooe:example.com", + content = mapOf( + "body" to "some message", + "msgtype" to "m.text" + ), + originServerTs = 1000, + senderId = "@anne:example.com", + ) + mockTextEvent.toValidDecryptedEvent() shouldBe null + } +} 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/EventEditValidatorTest.kt similarity index 96% rename from matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EditValidationTest.kt rename to matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EventEditValidatorTest.kt index 429e6625ab..0ae712bff1 100644 --- 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/EventEditValidatorTest.kt @@ -26,7 +26,7 @@ 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 { +class EventEditValidatorTest { private val mockTextEvent = Event( type = EventType.MESSAGE, @@ -180,17 +180,23 @@ class EditValidationTest { validator .validateEdit( - encryptedEvent.copy().apply { + encryptedEvent.copy( + content = encryptedEvent.content!!.toMutableMap().apply { + put( + "m.relates_to", + mapOf( + "rel_type" to "m.replace", + "event_id" to mockTextEvent.eventId + ) + ) + } + ).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 - ) ), ) ) 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 f5545b7e76..9ad7032262 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 @@ -19,7 +19,6 @@ package org.matrix.android.sdk.test.fakes import io.mockk.coEvery import io.mockk.mockk import io.mockk.mockkStatic -import io.mockk.slot import io.realm.Realm import io.realm.RealmConfiguration import org.matrix.android.sdk.internal.database.awaitTransaction @@ -33,8 +32,7 @@ internal class FakeRealmConfiguration { val instance = mockk() fun givenAwaitTransaction(realm: Realm) { - val transaction = slot<(Realm) -> T>() - coEvery { awaitTransaction(instance, capture(transaction)) } answers { + coEvery { awaitTransaction(instance, any<(Realm) -> T>()) } answers { secondArg<(Realm) -> T>().invoke(realm) } } 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 d5d38f47a3..373410775b 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, params.event.annotations?.editSummary?.latestEdit) + val informationData = messageInformationDataFactory.create(params) 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/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index a969c294f5..3c8b342a1a 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 @@ -37,7 +37,6 @@ 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 @@ -45,6 +44,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageVerification import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.hasBeenEdited +import org.matrix.android.sdk.internal.session.events.toValidDecryptedEvent import javax.inject.Inject /** @@ -57,7 +57,7 @@ class MessageInformationDataFactory @Inject constructor( private val reactionsSummaryFactory: ReactionsSummaryFactory ) { - fun create(params: TimelineItemFactoryParams, lastEdit: Event? = null): MessageInformationData { + fun create(params: TimelineItemFactoryParams): MessageInformationData { val event = params.event val nextDisplayableEvent = params.nextDisplayableEvent val prevDisplayableEvent = params.prevDisplayableEvent @@ -74,14 +74,8 @@ class MessageInformationDataFactory @Inject constructor( prevDisplayableEvent?.root?.localDateTime()?.toLocalDate() != date.toLocalDate() val time = dateFormatter.format(event.root.originServerTs, DateFormatKind.MESSAGE_SIMPLE) - 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() - } + val e2eDecoration = getE2EDecoration(roomSummary, params.lastEdit ?: event.root) + val senderId = getSenderId(event) // SendState Decoration val sendStateDecoration = if (isSentByMe) { getSendStateDecoration( @@ -139,6 +133,14 @@ class MessageInformationDataFactory @Inject constructor( ) } + private fun getSenderId(event: TimelineEvent) = if (event.isEncrypted()) { + event.root.toValidDecryptedEvent()?.let { + session.cryptoService().deviceWithIdentityKey(it.cryptoSenderKey, it.algorithm)?.userId + } ?: event.root.senderId.orEmpty() + } else { + event.root.senderId.orEmpty() + } + private fun getSendStateDecoration( event: TimelineEvent, lastSentEventWithoutReadReceipts: String?,