mirror of
https://github.com/element-hq/element-android
synced 2024-11-27 03:48:12 +03:00
better replace handling
This commit is contained in:
parent
5eb64b43d3
commit
891709ef41
33 changed files with 774 additions and 212 deletions
|
@ -38,5 +38,4 @@ data class AggregatedAnnotation(
|
|||
override val limited: Boolean? = false,
|
||||
override val count: Int? = 0,
|
||||
val chunk: List<RelationChunkInfo>? = null
|
||||
|
||||
) : UnsignedRelationInfo
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
)
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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>,
|
||||
|
|
|
@ -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>()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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<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
|
||||
}
|
||||
}
|
|
@ -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<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}")
|
||||
// }
|
||||
// }
|
||||
}
|
||||
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<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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue