Merge pull request #7594 from vector-im/feature/bca/better_edit_validation
Better edit (replace handling)
This commit is contained in:
commit
035b1ebedc
1
changelog.d/7594.misc
Normal file
1
changelog.d/7594.misc
Normal file
@ -0,0 +1 @@
|
||||
Better validation of edits
|
BIN
matrix-sdk-android/src/androidTest/assets/session_42.realm
Normal file
BIN
matrix-sdk-android/src/androidTest/assets/session_42.realm
Normal file
Binary file not shown.
@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import io.realm.Realm
|
||||
import org.amshove.kluent.fail
|
||||
import org.amshove.kluent.shouldBe
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.amshove.kluent.shouldNotBe
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
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.internal.database.mapper.EventMapper
|
||||
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
|
||||
import org.matrix.android.sdk.internal.database.model.SessionRealmModule
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import org.matrix.android.sdk.internal.util.Normalizer
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class RealmSessionStoreMigration43Test {
|
||||
|
||||
@get:Rule val configurationFactory = TestRealmConfigurationFactory()
|
||||
|
||||
lateinit var context: Context
|
||||
var realm: Realm? = null
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
context = InstrumentationRegistry.getInstrumentation().context
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
realm?.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun migrationShouldBeNeeed() {
|
||||
val realmName = "session_42.realm"
|
||||
val realmConfiguration = configurationFactory.createConfiguration(
|
||||
realmName,
|
||||
"efa9ab2c77ae06b0e767ffdb1c45b12be3c77d48d94f1ac41a7cd1d637fc59ac41f869a250453074e21ce13cfe7ed535593e7d150c08ce2bad7a2ab8c7b841f0",
|
||||
SessionRealmModule(),
|
||||
43,
|
||||
null
|
||||
)
|
||||
configurationFactory.copyRealmFromAssets(context, realmName, realmName)
|
||||
|
||||
try {
|
||||
realm = Realm.getInstance(realmConfiguration)
|
||||
fail("Should need a migration")
|
||||
} catch (failure: Throwable) {
|
||||
// nop
|
||||
}
|
||||
}
|
||||
|
||||
// Database key for alias `session_db_e00482619b2597069b1f192b86de7da9`: efa9ab2c77ae06b0e767ffdb1c45b12be3c77d48d94f1ac41a7cd1d637fc59ac41f869a250453074e21ce13cfe7ed535593e7d150c08ce2bad7a2ab8c7b841f0
|
||||
// $WEJ8U6Zsx3TDZx3qmHIOKh-mXe5kqL_MnPcIkStEwwI
|
||||
// $11EtAQ8RYcudJVtw7e6B5Vm4ufCqKTOWKblY2U_wrpo
|
||||
@Test
|
||||
fun testMigration43() {
|
||||
val realmName = "session_42.realm"
|
||||
val migration = RealmSessionStoreMigration(Normalizer())
|
||||
val realmConfiguration = configurationFactory.createConfiguration(
|
||||
realmName,
|
||||
"efa9ab2c77ae06b0e767ffdb1c45b12be3c77d48d94f1ac41a7cd1d637fc59ac41f869a250453074e21ce13cfe7ed535593e7d150c08ce2bad7a2ab8c7b841f0",
|
||||
SessionRealmModule(),
|
||||
43,
|
||||
migration
|
||||
)
|
||||
configurationFactory.copyRealmFromAssets(context, realmName, realmName)
|
||||
|
||||
realm = Realm.getInstance(realmConfiguration)
|
||||
|
||||
// assert that the edit from 42 are migrated
|
||||
val editions = EventAnnotationsSummaryEntity
|
||||
.where(realm!!, "\$WEJ8U6Zsx3TDZx3qmHIOKh-mXe5kqL_MnPcIkStEwwI")
|
||||
.findFirst()
|
||||
?.editSummary
|
||||
?.editions
|
||||
|
||||
editions shouldNotBe null
|
||||
editions!!.size shouldBe 1
|
||||
val firstEdition = editions.first()
|
||||
firstEdition?.eventId shouldBeEqualTo "\$DvOyA8vJxwGfTaJG3OEJVcL4isShyaVDnprihy38W28"
|
||||
firstEdition?.isLocalEcho shouldBeEqualTo false
|
||||
|
||||
val editEvent = EventMapper.map(firstEdition!!.event!!)
|
||||
val body = editEvent.content.toModel<MessageContent>()?.body
|
||||
body shouldBeEqualTo "* Message 2 with edit"
|
||||
|
||||
// assert that the edit from 42 are migrated
|
||||
val editionsOfE2E = EventAnnotationsSummaryEntity
|
||||
.where(realm!!, "\$11EtAQ8RYcudJVtw7e6B5Vm4ufCqKTOWKblY2U_wrpo")
|
||||
.findFirst()
|
||||
?.editSummary
|
||||
?.editions
|
||||
|
||||
editionsOfE2E shouldNotBe null
|
||||
editionsOfE2E!!.size shouldBe 1
|
||||
val firstEditionE2E = editionsOfE2E.first()
|
||||
firstEditionE2E?.eventId shouldBeEqualTo "\$HUwJOQRCJwfPv7XSKvBPcvncjM0oR3q2tGIIIdv9Zts"
|
||||
firstEditionE2E?.isLocalEcho shouldBeEqualTo false
|
||||
|
||||
val editEventE2E = EventMapper.map(firstEditionE2E!!.event!!)
|
||||
val body2 = editEventE2E.getClearContent().toModel<MessageContent>()?.body
|
||||
body2 shouldBeEqualTo "* Message 2, e2e edit"
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import io.realm.Realm
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.matrix.android.sdk.internal.database.model.SessionRealmModule
|
||||
import org.matrix.android.sdk.internal.util.Normalizer
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SessionSanityMigrationTest {
|
||||
|
||||
@get:Rule val configurationFactory = TestRealmConfigurationFactory()
|
||||
|
||||
lateinit var context: Context
|
||||
var realm: Realm? = null
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
context = InstrumentationRegistry.getInstrumentation().context
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
realm?.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sessionDatabaseShouldMigrateGracefully() {
|
||||
val realmName = "session_42.realm"
|
||||
val migration = RealmSessionStoreMigration(Normalizer())
|
||||
val realmConfiguration = configurationFactory.createConfiguration(
|
||||
realmName,
|
||||
"efa9ab2c77ae06b0e767ffdb1c45b12be3c77d48d94f1ac41a7cd1d637fc59ac41f869a250453074e21ce13cfe7ed535593e7d150c08ce2bad7a2ab8c7b841f0",
|
||||
SessionRealmModule(),
|
||||
migration.schemaVersion,
|
||||
migration
|
||||
)
|
||||
configurationFactory.copyRealmFromAssets(context, realmName, realmName)
|
||||
|
||||
realm = Realm.getInstance(realmConfiguration)
|
||||
}
|
||||
}
|
@ -0,0 +1,196 @@
|
||||
/*
|
||||
* 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.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.RealmMigration
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runners.model.Statement
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.lang.IllegalStateException
|
||||
import java.util.Collections
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.Throws
|
||||
|
||||
/**
|
||||
* Based on https://github.com/realm/realm-java/blob/master/realm/realm-library/src/testUtils/java/io/realm/TestRealmConfigurationFactory.java
|
||||
*/
|
||||
class TestRealmConfigurationFactory : TemporaryFolder() {
|
||||
private val map: Map<RealmConfiguration, Boolean> = ConcurrentHashMap()
|
||||
private val configurations = Collections.newSetFromMap(map)
|
||||
@get:Synchronized private var isUnitTestFailed = false
|
||||
private var testName = ""
|
||||
private var tempFolder: File? = null
|
||||
|
||||
override fun apply(base: Statement, description: Description): Statement {
|
||||
return object : Statement() {
|
||||
@Throws(Throwable::class)
|
||||
override fun evaluate() {
|
||||
setTestName(description)
|
||||
before()
|
||||
try {
|
||||
base.evaluate()
|
||||
} catch (throwable: Throwable) {
|
||||
setUnitTestFailed()
|
||||
throw throwable
|
||||
} finally {
|
||||
after()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Throwable::class)
|
||||
override fun before() {
|
||||
Realm.init(InstrumentationRegistry.getInstrumentation().targetContext)
|
||||
super.before()
|
||||
}
|
||||
|
||||
override fun after() {
|
||||
try {
|
||||
for (configuration in configurations) {
|
||||
Realm.deleteRealm(configuration)
|
||||
}
|
||||
} catch (e: IllegalStateException) {
|
||||
// Only throws the exception caused by deleting the opened Realm if the test case itself doesn't throw.
|
||||
if (!isUnitTestFailed) {
|
||||
throw e
|
||||
}
|
||||
} finally {
|
||||
// This will delete the temp directory.
|
||||
super.after()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun create() {
|
||||
super.create()
|
||||
tempFolder = File(super.getRoot(), testName)
|
||||
check(!(tempFolder!!.exists() && !tempFolder!!.delete())) { "Could not delete folder: " + tempFolder!!.absolutePath }
|
||||
check(tempFolder!!.mkdir()) { "Could not create folder: " + tempFolder!!.absolutePath }
|
||||
}
|
||||
|
||||
override fun getRoot(): File {
|
||||
checkNotNull(tempFolder) { "the temporary folder has not yet been created" }
|
||||
return tempFolder!!
|
||||
}
|
||||
|
||||
/**
|
||||
* To be called in the [.apply].
|
||||
*/
|
||||
protected fun setTestName(description: Description) {
|
||||
testName = description.displayName
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun setUnitTestFailed() {
|
||||
isUnitTestFailed = true
|
||||
}
|
||||
|
||||
// This builder creates a configuration that is *NOT* managed.
|
||||
// You have to delete it yourself.
|
||||
private fun createConfigurationBuilder(): RealmConfiguration.Builder {
|
||||
return RealmConfiguration.Builder().directory(root)
|
||||
}
|
||||
|
||||
fun String.decodeHex(): ByteArray {
|
||||
check(length % 2 == 0) { "Must have an even length" }
|
||||
return chunked(2)
|
||||
.map { it.toInt(16).toByte() }
|
||||
.toByteArray()
|
||||
}
|
||||
|
||||
fun createConfiguration(
|
||||
name: String,
|
||||
key: String?,
|
||||
module: Any,
|
||||
schemaVersion: Long,
|
||||
migration: RealmMigration?
|
||||
): RealmConfiguration {
|
||||
val builder = createConfigurationBuilder()
|
||||
builder
|
||||
.directory(root)
|
||||
.name(name)
|
||||
.apply {
|
||||
if (key != null) {
|
||||
encryptionKey(key.decodeHex())
|
||||
}
|
||||
}
|
||||
.modules(module)
|
||||
// Allow writes on UI
|
||||
.allowWritesOnUiThread(true)
|
||||
.schemaVersion(schemaVersion)
|
||||
.apply {
|
||||
migration?.let { migration(it) }
|
||||
}
|
||||
val configuration = builder.build()
|
||||
configurations.add(configuration)
|
||||
return configuration
|
||||
}
|
||||
|
||||
// Copies a Realm file from assets to temp dir
|
||||
@Throws(IOException::class)
|
||||
fun copyRealmFromAssets(context: Context, realmPath: String, newName: String) {
|
||||
val config = RealmConfiguration.Builder()
|
||||
.directory(root)
|
||||
.name(newName)
|
||||
.build()
|
||||
copyRealmFromAssets(context, realmPath, config)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun copyRealmFromAssets(context: Context, realmPath: String, config: RealmConfiguration) {
|
||||
check(!File(config.path).exists()) { String.format(Locale.ENGLISH, "%s exists!", config.path) }
|
||||
val outFile = File(config.realmDirectory, config.realmFileName)
|
||||
copyFileFromAssets(context, realmPath, outFile)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun copyFileFromAssets(context: Context, assetPath: String?, outFile: File?) {
|
||||
var stream: InputStream? = null
|
||||
var os: FileOutputStream? = null
|
||||
try {
|
||||
stream = context.assets.open(assetPath!!)
|
||||
os = FileOutputStream(outFile)
|
||||
val buf = ByteArray(1024)
|
||||
var bytesRead: Int
|
||||
while (stream.read(buf).also { bytesRead = it } > -1) {
|
||||
os.write(buf, 0, bytesRead)
|
||||
}
|
||||
} finally {
|
||||
if (stream != null) {
|
||||
try {
|
||||
stream.close()
|
||||
} catch (ignore: IOException) {
|
||||
}
|
||||
}
|
||||
if (os != null) {
|
||||
try {
|
||||
os.close()
|
||||
} catch (ignore: IOException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -38,5 +38,4 @@ data class AggregatedAnnotation(
|
||||
override val limited: Boolean? = false,
|
||||
override val count: Int? = 0,
|
||||
val chunk: List<RelationChunkInfo>? = null
|
||||
|
||||
) : UnsignedRelationInfo
|
||||
|
@ -19,7 +19,8 @@ import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* <code>
|
||||
* Server side relation aggregation.
|
||||
* ```
|
||||
* {
|
||||
* "m.annotation": {
|
||||
* "chunk": [
|
||||
@ -43,12 +44,13 @@ import com.squareup.moshi.JsonClass
|
||||
* "count": 1
|
||||
* }
|
||||
* }
|
||||
* </code>
|
||||
* ```
|
||||
*/
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
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,46 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
fun Event.toValidDecryptedEvent(): ValidDecryptedEvent? {
|
||||
if (!this.isEncrypted()) return null
|
||||
val decryptedContent = this.getDecryptedContent() ?: return null
|
||||
val eventId = this.eventId ?: return null
|
||||
val roomId = this.roomId ?: return null
|
||||
val type = this.getDecryptedType() ?: return null
|
||||
val senderKey = this.getSenderKey() ?: return null
|
||||
val algorithm = this.content?.get("algorithm") as? String ?: return null
|
||||
|
||||
// copy the relation as it's in clear in the encrypted content
|
||||
val updatedContent = this.content.get("m.relates_to")?.let {
|
||||
decryptedContent.toMutableMap().apply {
|
||||
put("m.relates_to", it)
|
||||
}
|
||||
} ?: decryptedContent
|
||||
return ValidDecryptedEvent(
|
||||
type = type,
|
||||
eventId = eventId,
|
||||
clearContent = updatedContent,
|
||||
prevContent = this.prevContent,
|
||||
originServerTs = this.originServerTs ?: 0,
|
||||
cryptoSenderKey = senderKey,
|
||||
roomId = roomId,
|
||||
unsignedData = this.unsignedData,
|
||||
redacts = this.redacts,
|
||||
algorithm = algorithm
|
||||
)
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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 org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent
|
||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
||||
|
||||
data class ValidDecryptedEvent(
|
||||
val type: String,
|
||||
val eventId: String,
|
||||
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 ValidDecryptedEvent.getRelationContent(): RelationDefaultContent? {
|
||||
return clearContent.toModel<MessageRelationContent?>()?.relatesTo
|
||||
}
|
@ -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>,
|
||||
|
@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.room.timeline
|
||||
|
||||
import org.matrix.android.sdk.BuildConfig
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.events.model.Content
|
||||
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.RelationType
|
||||
@ -142,13 +143,21 @@ fun TimelineEvent.getEditedEventId(): String? {
|
||||
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()
|
||||
// XXX
|
||||
// Polls/Beacon are not message contents like others as there is no msgtype subtype to discriminate moshi parsing
|
||||
// so toModel<MessageContent> won't parse them correctly
|
||||
// It's discriminated on event type instead. Maybe it shouldn't be MessageContent at all to avoid confusion?
|
||||
in EventType.POLL_START -> (getLastEditNewContent() ?: root.getClearContent()).toModel<MessagePollContent>()
|
||||
in EventType.STATE_ROOM_BEACON_INFO -> (getLastEditNewContent() ?: root.getClearContent()).toModel<MessageBeaconInfoContent>()
|
||||
in EventType.BEACON_LOCATION_DATA -> (getLastEditNewContent() ?: root.getClearContent()).toModel<MessageBeaconLocationDataContent>()
|
||||
else -> (getLastEditNewContent() ?: root.getClearContent()).toModel()
|
||||
}
|
||||
}
|
||||
|
||||
fun TimelineEvent.getLastEditNewContent(): Content? {
|
||||
return annotations?.editSummary?.latestEdit?.getClearContent()?.toModel<MessageContent>()?.newContent
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if it's a reply.
|
||||
*/
|
||||
|
@ -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,
|
||||
|
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.database.mapper
|
||||
|
||||
import org.matrix.android.sdk.api.session.room.model.EditAggregatedSummary
|
||||
import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntity
|
||||
import org.matrix.android.sdk.internal.database.model.EditionOfEvent
|
||||
|
||||
internal object EditAggregatedSummaryEntityMapper {
|
||||
|
||||
fun map(summary: EditAggregatedSummaryEntity?): EditAggregatedSummary? {
|
||||
summary ?: return null
|
||||
/**
|
||||
* The most recent event is determined by comparing origin_server_ts;
|
||||
* if two or more replacement events have identical origin_server_ts,
|
||||
* the event with the lexicographically largest event_id is treated as more recent.
|
||||
*/
|
||||
val latestEdition = summary.editions.sortedWith(compareBy<EditionOfEvent> { it.timestamp }.thenBy { it.eventId })
|
||||
.lastOrNull() ?: return null
|
||||
val editEvent = latestEdition.event
|
||||
|
||||
return EditAggregatedSummary(
|
||||
latestEdit = editEvent?.asDomain(),
|
||||
sourceEvents = summary.editions.filter { editionOfEvent -> !editionOfEvent.isLocalEcho }
|
||||
.map { editionOfEvent -> editionOfEvent.eventId },
|
||||
localEchos = summary.editions.filter { editionOfEvent -> editionOfEvent.isLocalEcho }
|
||||
.map { editionOfEvent -> editionOfEvent.eventId },
|
||||
lastEditTs = latestEdition.timestamp
|
||||
)
|
||||
}
|
||||
}
|
@ -16,7 +16,6 @@
|
||||
|
||||
package org.matrix.android.sdk.internal.database.mapper
|
||||
|
||||
import org.matrix.android.sdk.api.session.room.model.EditAggregatedSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.ReactionAggregatedSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedSummary
|
||||
@ -35,18 +34,7 @@ internal object EventAnnotationsSummaryMapper {
|
||||
it.sourceLocalEcho.toList()
|
||||
)
|
||||
},
|
||||
editSummary = annotationsSummary.editSummary
|
||||
?.let {
|
||||
val latestEdition = it.editions.maxByOrNull { editionOfEvent -> editionOfEvent.timestamp } ?: return@let null
|
||||
EditAggregatedSummary(
|
||||
latestContent = ContentMapper.map(latestEdition.content),
|
||||
sourceEvents = it.editions.filter { editionOfEvent -> !editionOfEvent.isLocalEcho }
|
||||
.map { editionOfEvent -> editionOfEvent.eventId },
|
||||
localEchos = it.editions.filter { editionOfEvent -> editionOfEvent.isLocalEcho }
|
||||
.map { editionOfEvent -> editionOfEvent.eventId },
|
||||
lastEditTs = latestEdition.timestamp
|
||||
)
|
||||
},
|
||||
editSummary = EditAggregatedSummaryEntityMapper.map(annotationsSummary.editSummary),
|
||||
referencesAggregatedSummary = annotationsSummary.referencesSummaryEntity?.let {
|
||||
ReferencesAggregatedSummary(
|
||||
ContentMapper.map(it.content),
|
||||
|
@ -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", 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", String::class.java)
|
||||
.setRequired("senderId", true)
|
||||
.addField(EditionOfEventFields.TIMESTAMP, Long::class.java)
|
||||
.addField(EditionOfEventFields.IS_LOCAL_ECHO, Boolean::class.java)
|
||||
|
||||
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (c) 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.database.migration
|
||||
|
||||
import io.realm.DynamicRealm
|
||||
import org.matrix.android.sdk.internal.database.model.EditionOfEventFields
|
||||
import org.matrix.android.sdk.internal.database.model.EventEntityFields
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
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")
|
||||
?.addRealmObjectField(EditionOfEventFields.EVENT.`$`, realm.schema.get("EventEntity")!!)
|
||||
?.transform { dynamicObject ->
|
||||
realm.where("EventEntity")
|
||||
.equalTo(EventEntityFields.EVENT_ID, dynamicObject.getString(EditionOfEventFields.EVENT_ID))
|
||||
.equalTo(EventEntityFields.SENDER, dynamicObject.getString("senderId"))
|
||||
.findFirst()
|
||||
.let {
|
||||
dynamicObject.setObject(EditionOfEventFields.EVENT.`$`, it)
|
||||
}
|
||||
}
|
||||
?.removeField("senderId")
|
||||
?.removeField("content")
|
||||
}
|
||||
}
|
@ -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,126 @@
|
||||
/*
|
||||
* 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.LocalEcho
|
||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||
import org.matrix.android.sdk.api.session.events.model.getRelationContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.events.model.toValidDecryptedEvent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import timber.log.Timber
|
||||
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 {
|
||||
Timber.v("###REPLACE valide event $originalEvent replaced $replaceEvent")
|
||||
// we might not know the original event at that time. In this case we can't perform the validation
|
||||
// Edits should be revalidated when the original event is received
|
||||
if (originalEvent == null) {
|
||||
return EditValidity.Unknown
|
||||
}
|
||||
|
||||
if (LocalEcho.isLocalEchoId(replaceEvent.eventId.orEmpty())) {
|
||||
// Don't validate local echo
|
||||
return EditValidity.Unknown
|
||||
}
|
||||
|
||||
if (originalEvent.roomId != replaceEvent.roomId) {
|
||||
return EditValidity.Invalid("original event and replacement event must have the same room_id")
|
||||
}
|
||||
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.getRelationContent()?.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.getRelationContent()?.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
|
||||
}
|
||||
}
|
@ -42,6 +42,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 +73,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 +81,14 @@ 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 verification itself, just timeline display
|
||||
EventType.KEY_VERIFICATION_DONE,
|
||||
EventType.KEY_VERIFICATION_CANCEL,
|
||||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
EventType.KEY_VERIFICATION_MAC,
|
||||
// TODO Add ?
|
||||
// EventType.KEY_VERIFICATION_READY,
|
||||
EventType.KEY_VERIFICATION_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,74 +165,31 @@ 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
|
||||
// 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>()
|
||||
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)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
RelationType.RESPONSE -> {
|
||||
// can we / should we do we something for UTD response??
|
||||
Timber.w("## UTD response in room $roomId related to ${encryptedEventContent.relatesTo.eventId}")
|
||||
}
|
||||
} 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)
|
||||
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}")
|
||||
}
|
||||
}
|
||||
// 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 }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
EventType.REDACTION -> {
|
||||
val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() }
|
||||
@ -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
|
||||
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,
|
||||
)
|
||||
)
|
||||
}
|
||||
@ -326,17 +301,17 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
||||
// ok it has already been managed
|
||||
Timber.v("###REPLACE Receiving remote echo of edit (edit already done)")
|
||||
existingSummary.editions.firstOrNull { it.eventId == txId }?.let {
|
||||
it.eventId = event.eventId
|
||||
it.eventId = eventId
|
||||
it.timestamp = event.originServerTs ?: clock.epochMillis()
|
||||
it.isLocalEcho = false
|
||||
it.event = EventEntity.where(realm, eventId).findFirst()
|
||||
}
|
||||
} else {
|
||||
Timber.v("###REPLACE Computing aggregated edit summary (isLocalEcho:$isLocalEcho)")
|
||||
existingSummary.editions.add(
|
||||
EditionOfEvent(
|
||||
senderId = event.senderId ?: "",
|
||||
eventId = event.eventId,
|
||||
content = ContentMapper.map(newContent),
|
||||
eventId = eventId,
|
||||
event = EventEntity.where(realm, eventId).findFirst(),
|
||||
timestamp = if (isLocalEcho) {
|
||||
clock.epochMillis()
|
||||
} else {
|
||||
@ -501,7 +476,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
||||
}
|
||||
val sourceToDiscard = eventSummary.editSummary?.editions?.firstOrNull { it.eventId == redacted.eventId }
|
||||
if (sourceToDiscard == null) {
|
||||
Timber.w("Redaction of a replace that was not known in aggregation $sourceToDiscard")
|
||||
Timber.w("Redaction of a replace that was not known in aggregation")
|
||||
return
|
||||
}
|
||||
// Need to remove this event from the edition list
|
||||
@ -599,12 +574,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
|
||||
|
@ -25,6 +25,8 @@ import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||
import org.matrix.android.sdk.api.session.events.model.getRelationContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
@ -49,6 +51,7 @@ import org.matrix.android.sdk.internal.database.mapper.asDomain
|
||||
import org.matrix.android.sdk.internal.database.mapper.toEntity
|
||||
import org.matrix.android.sdk.internal.database.model.ChunkEntity
|
||||
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
|
||||
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
|
||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||
import org.matrix.android.sdk.internal.database.model.EventInsertType
|
||||
import org.matrix.android.sdk.internal.database.model.RoomEntity
|
||||
@ -486,23 +489,41 @@ internal class RoomSyncHandler @Inject constructor(
|
||||
cryptoService.onLiveEvent(roomEntity.roomId, event, isInitialSync)
|
||||
|
||||
// Try to remove local echo
|
||||
event.unsignedData?.transactionId?.also {
|
||||
val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it)
|
||||
event.unsignedData?.transactionId?.also { txId ->
|
||||
val sendingEventEntity = roomEntity.sendingTimelineEvents.find(txId)
|
||||
if (sendingEventEntity != null) {
|
||||
Timber.v("Remove local echo for tx:$it")
|
||||
Timber.v("Remove local echo for tx:$txId")
|
||||
roomEntity.sendingTimelineEvents.remove(sendingEventEntity)
|
||||
if (event.isEncrypted() && event.content?.get("algorithm") as? String == MXCRYPTO_ALGORITHM_MEGOLM) {
|
||||
// updated with echo decryption, to avoid seeing it decrypt again
|
||||
// updated with echo decryption, to avoid seeing txId decrypt again
|
||||
val adapter = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java)
|
||||
sendingEventEntity.root?.decryptionResultJson?.let { json ->
|
||||
eventEntity.decryptionResultJson = json
|
||||
event.mxDecryptionResult = adapter.fromJson(json)
|
||||
}
|
||||
}
|
||||
// also update potential edit that could refer to that event?
|
||||
// If not display will flicker :/
|
||||
val relationContent = event.getRelationContent()
|
||||
if (relationContent?.type == RelationType.REPLACE) {
|
||||
relationContent.eventId?.let { targetId ->
|
||||
EventAnnotationsSummaryEntity.where(realm, roomId, targetId)
|
||||
.findFirst()
|
||||
?.editSummary
|
||||
?.editions
|
||||
?.forEach {
|
||||
if (it.eventId == txId) {
|
||||
// just do that, the aggregation processor will to the rest
|
||||
it.event = eventEntity
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finally delete the local echo
|
||||
sendingEventEntity.deleteOnCascade(true)
|
||||
} else {
|
||||
Timber.v("Can't find corresponding local echo for tx:$it")
|
||||
Timber.v("Can't find corresponding local echo for tx:$txId")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,114 @@
|
||||
/*
|
||||
* 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.database.mapper
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.realm.RealmList
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.amshove.kluent.shouldNotBe
|
||||
import org.junit.Test
|
||||
import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntity
|
||||
import org.matrix.android.sdk.internal.database.model.EditionOfEvent
|
||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||
|
||||
class EditAggregatedSummaryEntityMapperTest {
|
||||
|
||||
@Test
|
||||
fun `test mapping summary entity to model`() {
|
||||
val edits = RealmList<EditionOfEvent>(
|
||||
EditionOfEvent(
|
||||
timestamp = 0L,
|
||||
eventId = "e0",
|
||||
isLocalEcho = false,
|
||||
event = mockEvent("e0")
|
||||
),
|
||||
EditionOfEvent(
|
||||
timestamp = 1L,
|
||||
eventId = "e1",
|
||||
isLocalEcho = false,
|
||||
event = mockEvent("e1")
|
||||
),
|
||||
EditionOfEvent(
|
||||
timestamp = 30L,
|
||||
eventId = "e2",
|
||||
isLocalEcho = true,
|
||||
event = mockEvent("e2")
|
||||
)
|
||||
)
|
||||
val fakeSummaryEntity = mockk<EditAggregatedSummaryEntity> {
|
||||
every { editions } returns edits
|
||||
}
|
||||
|
||||
val mapped = EditAggregatedSummaryEntityMapper.map(fakeSummaryEntity)
|
||||
mapped shouldNotBe null
|
||||
mapped!!.sourceEvents.size shouldBeEqualTo 2
|
||||
mapped.localEchos.size shouldBeEqualTo 1
|
||||
mapped.localEchos.first() shouldBeEqualTo "e2"
|
||||
|
||||
mapped.lastEditTs shouldBeEqualTo 30L
|
||||
mapped.latestEdit?.eventId shouldBeEqualTo "e2"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `event with lexicographically largest event_id is treated as more recent`() {
|
||||
val lowerId = "\$Albatross"
|
||||
val higherId = "\$Zebra"
|
||||
|
||||
(higherId > lowerId) shouldBeEqualTo true
|
||||
val timestamp = 1669288766745L
|
||||
val edits = RealmList<EditionOfEvent>(
|
||||
EditionOfEvent(
|
||||
timestamp = timestamp,
|
||||
eventId = lowerId,
|
||||
isLocalEcho = false,
|
||||
event = mockEvent(lowerId)
|
||||
),
|
||||
EditionOfEvent(
|
||||
timestamp = timestamp,
|
||||
eventId = higherId,
|
||||
isLocalEcho = false,
|
||||
event = mockEvent(higherId)
|
||||
),
|
||||
EditionOfEvent(
|
||||
timestamp = 1L,
|
||||
eventId = "e2",
|
||||
isLocalEcho = true,
|
||||
event = mockEvent("e2")
|
||||
)
|
||||
)
|
||||
|
||||
val fakeSummaryEntity = mockk<EditAggregatedSummaryEntity> {
|
||||
every { editions } returns edits
|
||||
}
|
||||
val mapped = EditAggregatedSummaryEntityMapper.map(fakeSummaryEntity)
|
||||
mapped!!.lastEditTs shouldBeEqualTo timestamp
|
||||
mapped.latestEdit?.eventId shouldBeEqualTo higherId
|
||||
}
|
||||
|
||||
private fun mockEvent(eventId: String): EventEntity {
|
||||
return EventEntity().apply {
|
||||
this.eventId = eventId
|
||||
this.content = """
|
||||
{
|
||||
"body" : "Hello",
|
||||
"msgtype": "text"
|
||||
}
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,134 @@
|
||||
/*
|
||||
* 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.event
|
||||
|
||||
import org.amshove.kluent.shouldBe
|
||||
import org.amshove.kluent.shouldNotBe
|
||||
import org.junit.Test
|
||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.events.model.toValidDecryptedEvent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
|
||||
|
||||
class ValidDecryptedEventTest {
|
||||
|
||||
private val fakeEvent = Event(
|
||||
type = EventType.ENCRYPTED,
|
||||
eventId = "\$eventId",
|
||||
roomId = "!fakeRoom",
|
||||
content = EncryptedEventContent(
|
||||
algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
|
||||
ciphertext = "AwgBEpACQEKOkd4Gp0+gSXG4M+btcrnPgsF23xs/lUmS2I4YjmqF...",
|
||||
sessionId = "TO2G4u2HlnhtbIJk",
|
||||
senderKey = "5e3EIqg3JfooZnLQ2qHIcBarbassQ4qXblai0",
|
||||
deviceId = "FAKEE"
|
||||
).toContent()
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `A failed to decrypt message should give a null validated decrypted event`() {
|
||||
fakeEvent.toValidDecryptedEvent() shouldBe null
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Mismatch sender key detection`() {
|
||||
val decryptedEvent = fakeEvent
|
||||
.apply {
|
||||
mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = mapOf(
|
||||
"type" to EventType.MESSAGE,
|
||||
"content" to mapOf(
|
||||
"body" to "some message",
|
||||
"msgtype" to "m.text"
|
||||
),
|
||||
),
|
||||
senderKey = "the_real_sender_key",
|
||||
)
|
||||
}
|
||||
|
||||
val validDecryptedEvent = decryptedEvent.toValidDecryptedEvent()
|
||||
validDecryptedEvent shouldNotBe null
|
||||
|
||||
fakeEvent.content!!["senderKey"] shouldNotBe "the_real_sender_key"
|
||||
validDecryptedEvent!!.cryptoSenderKey shouldBe "the_real_sender_key"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Mixed content event should be detected`() {
|
||||
val mixedEvent = Event(
|
||||
type = EventType.ENCRYPTED,
|
||||
eventId = "\$eventd ",
|
||||
roomId = "!fakeRoo",
|
||||
content = mapOf(
|
||||
"algorithm" to "m.megolm.v1.aes-sha2",
|
||||
"ciphertext" to "AwgBEpACQEKOkd4Gp0+gSXG4M+btcrnPgsF23xs/lUmS2I4YjmqF...",
|
||||
"sessionId" to "TO2G4u2HlnhtbIJk",
|
||||
"senderKey" to "5e3EIqg3JfooZnLQ2qHIcBarbassQ4qXblai0",
|
||||
"deviceId" to "FAKEE",
|
||||
"body" to "some message",
|
||||
"msgtype" to "m.text"
|
||||
).toContent()
|
||||
)
|
||||
|
||||
val unValidatedContent = mixedEvent.getClearContent().toModel<MessageTextContent>()
|
||||
unValidatedContent?.body shouldBe "some message"
|
||||
|
||||
mixedEvent.toValidDecryptedEvent()?.clearContent?.toModel<MessageTextContent>() shouldBe null
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Basic field validation`() {
|
||||
val decryptedEvent = fakeEvent
|
||||
.apply {
|
||||
mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = mapOf(
|
||||
"type" to EventType.MESSAGE,
|
||||
"content" to mapOf(
|
||||
"body" to "some message",
|
||||
"msgtype" to "m.text"
|
||||
),
|
||||
),
|
||||
senderKey = "the_real_sender_key",
|
||||
)
|
||||
}
|
||||
|
||||
decryptedEvent.toValidDecryptedEvent() shouldNotBe null
|
||||
decryptedEvent.copy(roomId = null).toValidDecryptedEvent() shouldBe null
|
||||
decryptedEvent.copy(eventId = null).toValidDecryptedEvent() shouldBe null
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `A clear event is not a valid decrypted event`() {
|
||||
val mockTextEvent = Event(
|
||||
type = EventType.MESSAGE,
|
||||
eventId = "eventId",
|
||||
roomId = "!fooe:example.com",
|
||||
content = mapOf(
|
||||
"body" to "some message",
|
||||
"msgtype" to "m.text"
|
||||
),
|
||||
originServerTs = 1000,
|
||||
senderId = "@anne:example.com",
|
||||
)
|
||||
mockTextEvent.toValidDecryptedEvent() shouldBe null
|
||||
}
|
||||
}
|
@ -0,0 +1,372 @@
|
||||
/*
|
||||
* Copyright (c) 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 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 EventEditValidatorTest {
|
||||
|
||||
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(
|
||||
content = encryptedEvent.content!!.toMutableMap().apply {
|
||||
put(
|
||||
"m.relates_to",
|
||||
mapOf(
|
||||
"rel_type" to "m.replace",
|
||||
"event_id" to mockTextEvent.eventId
|
||||
)
|
||||
)
|
||||
}
|
||||
).apply {
|
||||
mxDecryptionResult = encryptedEditEvent.mxDecryptionResult!!.copy(
|
||||
payload = mapOf(
|
||||
"type" to EventType.MESSAGE,
|
||||
"content" to mapOf(
|
||||
"body" to "some message",
|
||||
"msgtype" to "m.text",
|
||||
),
|
||||
)
|
||||
)
|
||||
},
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
@ -23,8 +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.toContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
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.message.MessageContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody
|
||||
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
|
||||
@ -226,16 +224,14 @@ class LocalEchoEventFactoryTests {
|
||||
).toMessageTextContent().toContent()
|
||||
}
|
||||
return TimelineEvent(
|
||||
root = A_START_EVENT,
|
||||
root = A_START_EVENT.copy(
|
||||
type = EventType.MESSAGE,
|
||||
content = textContent
|
||||
),
|
||||
localId = 1234,
|
||||
eventId = AN_EVENT_ID,
|
||||
displayIndex = 0,
|
||||
senderInfo = SenderInfo(A_USER_ID_1, A_USER_ID_1, true, null),
|
||||
annotations = if (textContent != null) {
|
||||
EventAnnotationsSummary(
|
||||
editSummary = EditAggregatedSummary(latestContent = textContent, emptyList(), emptyList())
|
||||
)
|
||||
} else null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -38,9 +38,9 @@ internal class FakeMonarchy {
|
||||
init {
|
||||
mockkStatic("org.matrix.android.sdk.internal.util.MonarchyKt")
|
||||
coEvery {
|
||||
instance.awaitTransaction(any<suspend (Realm) -> Any>())
|
||||
} coAnswers {
|
||||
secondArg<suspend (Realm) -> Any>().invoke(fakeRealm.instance)
|
||||
instance.awaitTransaction(any<(Realm) -> Any>())
|
||||
} answers {
|
||||
secondArg<(Realm) -> Any>().invoke(fakeRealm.instance)
|
||||
}
|
||||
coEvery {
|
||||
instance.doWithRealm(any())
|
||||
|
@ -19,7 +19,6 @@ package org.matrix.android.sdk.test.fakes
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.slot
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import org.matrix.android.sdk.internal.database.awaitTransaction
|
||||
@ -33,9 +32,8 @@ internal class FakeRealmConfiguration {
|
||||
val instance = mockk<RealmConfiguration>()
|
||||
|
||||
fun <T> givenAwaitTransaction(realm: Realm) {
|
||||
val transaction = slot<suspend (Realm) -> T>()
|
||||
coEvery { awaitTransaction(instance, capture(transaction)) } coAnswers {
|
||||
secondArg<suspend (Realm) -> T>().invoke(realm)
|
||||
coEvery { awaitTransaction(instance, any<(Realm) -> T>()) } answers {
|
||||
secondArg<(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,
|
||||
|
@ -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
|
||||
@ -72,8 +74,8 @@ class MessageInformationDataFactory @Inject constructor(
|
||||
prevDisplayableEvent?.root?.localDateTime()?.toLocalDate() != date.toLocalDate()
|
||||
|
||||
val time = dateFormatter.format(event.root.originServerTs, DateFormatKind.MESSAGE_SIMPLE)
|
||||
val e2eDecoration = getE2EDecoration(roomSummary, event)
|
||||
|
||||
val e2eDecoration = getE2EDecoration(roomSummary, params.lastEdit ?: event.root)
|
||||
val senderId = getSenderId(event)
|
||||
// SendState Decoration
|
||||
val sendStateDecoration = if (isSentByMe) {
|
||||
getSendStateDecoration(
|
||||
@ -89,7 +91,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,
|
||||
@ -131,6 +133,14 @@ class MessageInformationDataFactory @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun getSenderId(event: TimelineEvent) = if (event.isEncrypted()) {
|
||||
event.root.toValidDecryptedEvent()?.let {
|
||||
session.cryptoService().deviceWithIdentityKey(it.cryptoSenderKey, it.algorithm)?.userId
|
||||
} ?: event.root.senderId.orEmpty()
|
||||
} else {
|
||||
event.root.senderId.orEmpty()
|
||||
}
|
||||
|
||||
private fun getSendStateDecoration(
|
||||
event: TimelineEvent,
|
||||
lastSentEventWithoutReadReceipts: String?,
|
||||
@ -148,34 +158,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 +212,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 +224,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…
x
Reference in New Issue
Block a user