better replace handling

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

View file

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

View file

@ -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
)

View file

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

View file

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

View file

@ -15,10 +15,10 @@
*/
package org.matrix.android.sdk.api.session.room.model
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<String>,
val localEchos: List<String>,

View file

@ -30,12 +30,8 @@ import org.matrix.android.sdk.api.session.events.model.isSticker
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.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<MessageStickerContent>()
in EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessagePollContent>()
in EventType.STATE_ROOM_BEACON_INFO -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessageBeaconInfoContent>()
in EventType.BEACON_LOCATION_DATA -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessageBeaconLocationDataContent>()
else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
}
return (
annotations?.editSummary?.latestEdit
?.getClearContent()?.toModel<MessageContent>()?.newContent
?: root.getClearContent()
)
.toModel<MessageContent>()
}
/**

View file

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

View file

@ -59,6 +59,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo039
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo040
import org.matrix.android.sdk.internal.database.migration.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()
}
}

View file

@ -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

View file

@ -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<String, RoomMemberContent?>): TimelineEventEntity {
val roomId = roomId
val eventId = eventId
val localId = TimelineEventEntity.nextId(realm)
val senderId = sender ?: ""
val timelineEventEntity = realm.createObject<TimelineEventEntity>().apply {
this.localId = localId
this.root = this@toTimelineEventEntity
this.eventId = eventId
this.roomId = roomId
this.annotations = EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst()
?.also { it.cleanUp(sender) }
this.ownedByThreadChunk = true // To skip it from the original event flow
val roomMemberContent = roomMemberContentsByUser[senderId]
this.senderAvatar = roomMemberContent?.avatarUrl
this.senderName = roomMemberContent?.displayName
isUniqueDisplayName = if (roomMemberContent?.displayName != null) {
computeIsUnique(realm, roomId, false, roomMemberContent, roomMemberContentsByUser)
} else {
true
}
}
return timelineEventEntity
}
internal fun ThreadSummaryEntity.Companion.createOrUpdate(
threadSummaryType: ThreadSummaryUpdateType,
realm: Realm,

View file

@ -20,6 +20,7 @@ import org.matrix.android.sdk.api.session.room.model.EditAggregatedSummary
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
import org.matrix.android.sdk.api.session.room.model.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<EditionOfEvent> { 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
)

View file

@ -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)

View file

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

View file

@ -32,9 +32,8 @@ internal open class EditAggregatedSummaryEntity(
@RealmClass(embedded = true)
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()

View file

@ -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
}

View file

@ -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.

View file

@ -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)
}

View file

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

View file

@ -23,7 +23,6 @@ import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.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,72 +165,29 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
}
}
}
// As for now Live event processors are not receiving UTD events.
// They will get an update if the event is decrypted later
EventType.ENCRYPTED -> {
// Relation type is in clear
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE ||
encryptedEventContent?.relatesTo?.type == RelationType.RESPONSE
) {
event.getClearContent().toModel<MessageContent>()?.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<MessagePollResponseContent>(catchError = true)?.let {
sessionManager.getSessionComponent(sessionId)?.session()?.let { session ->
pollAggregationProcessor.handlePollResponseEvent(session, realm, event)
}
}
}
in EventType.POLL_END -> {
sessionManager.getSessionComponent(sessionId)?.session()?.let { session ->
getPowerLevelsHelper(event.roomId)?.let {
pollAggregationProcessor.handlePollEndEvent(session, it, realm, event)
}
}
}
in EventType.BEACON_LOCATION_DATA -> {
handleBeaconLocationData(event, realm, roomId, isLocalEcho)
}
}
} else if (encryptedEventContent?.relatesTo?.type == RelationType.ANNOTATION) {
// Reaction
if (event.getClearType() == EventType.REACTION) {
// we got a reaction!!
Timber.v("###REACTION e2e in room $roomId , reaction eventID ${event.eventId}")
handleReaction(realm, event, roomId, isLocalEcho)
}
}
// HandleInitialAggregatedRelations should also be applied in encrypted messages with annotations
// else if (event.unsignedData?.relations?.annotations != null) {
// Timber.v("###REACTION e2e Aggregation in room $roomId for event ${event.eventId}")
// handleInitialAggregatedRelations(realm, event, roomId, event.unsignedData.relations.annotations)
// EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId ?: "").findFirst()
// ?.let {
// TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findAll()
// ?.forEach { tet -> tet.annotations = it }
// // 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<EncryptedEventContent>()
// 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}")
// }
// }
}
@ -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<MessageContent>()
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<EditionOfEvent>?
) {
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<MessageBeaconLocationDataContent>(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
)
}
}

View file

@ -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<ChunkEntity>().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<String, RoomMemberContent?>()
for (event in eventList) {

View file

@ -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<RoomCreateContent>()
val predecessorRoomId = createRoomContent?.predecessor?.roomId ?: return

View file

@ -40,7 +40,7 @@ internal class LiveLocationShareRedactionEventProcessor @Inject constructor() :
return eventType == EventType.REDACTION && insertType != EventInsertType.LOCAL_ECHO
}
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
}

View file

@ -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)
}

View file

@ -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<RoomTombstoneContent>()
if (createRoomContent?.replacementRoomId == null) return

View file

@ -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 <T> Monarchy.awaitTransaction(transaction: suspend (realm: Realm) -> T): T {
internal suspend fun <T> Monarchy.awaitTransaction(transaction: (realm: Realm) -> T): T {
return awaitTransaction(realmConfiguration, transaction)
}

View file

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

View file

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

View file

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

View file

@ -19,6 +19,7 @@ package im.vector.app.core.extensions
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
import im.vector.app.features.voicebroadcast.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<MessageVoiceBroadcastInfoContent>()
(annotations?.editSummary?.latestEdit?.getClearContent()?.toModel<MessageContent>().toContent().toModel<MessageVoiceBroadcastInfoContent>()
?: root.getClearContent().toModel<MessageVoiceBroadcastInfoContent>())
}
else -> getLastMessageContent()
}

View file

@ -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,

View file

@ -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()) {

View file

@ -19,10 +19,12 @@ package im.vector.app.features.home.room.detail.timeline.factory
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.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,

View file

@ -31,11 +31,13 @@ import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLay
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.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
}