Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better edit (replace handling) #7594

Merged
merged 10 commits into from
Nov 24, 2022
Merged

Conversation

BillCarsonFr
Copy link
Member

@BillCarsonFr BillCarsonFr commented Nov 16, 2022

Type of change

  • Feature
  • Bugfix
  • Technical
  • Other :

Content

This PR is improving the handling of Edits, and enforcing edit validation rules.

  • Edit validation: see EventEditValidator ensures the rules as per spec (recently updated)

  • Added proper unit tests

  • Persistence layer changes:

    • Session data migration needed (v43)
    • We now store a reference of the edit event instead of the content to allow more flexibility when validated event; lastEdit instead of LastContent (as sender/types are also needed for validation)
    • Late decryption supported with revalidation of edit chain
  • Fixed a problem in Aggregator with process calls beeing suspend, which is a problem as it's called within a realm transaction and realm instance is locked to a thread. All asyncTransaction helpers have been modified to make the blockparam not suspendable

Handling of server side aggregation is still not handled as there is ongoing discussions regarding that

Motivation and context

#7430

Screenshots / GIFs

N/A

Tests

Try some edits, polls, location sharing

Tested devices

  • Physical
  • Emulator
  • OS version(s):

Checklist

@BillCarsonFr BillCarsonFr requested review from a team and jonnyandrew and removed request for a team November 16, 2022 09:40
@BillCarsonFr BillCarsonFr force-pushed the feature/bca/better_edit_validation branch 2 times, most recently from fe947ca to 6d56967 Compare November 16, 2022 13:34
@BillCarsonFr BillCarsonFr requested review from a team, ganfra and mnaturel and removed request for jonnyandrew, a team and ganfra November 18, 2022 10:09
@bmarty bmarty changed the title Better edit (replace handnling) Better edit (replace handling) Nov 18, 2022
@@ -50,5 +50,6 @@ import com.squareup.moshi.JsonClass
data class AggregatedRelations(
@Json(name = "m.annotation") val annotations: AggregatedAnnotation? = null,
@Json(name = "m.reference") val references: DefaultUnsignedRelationInfo? = null,
@Json(name = "m.replace") val replaces: AggregatedReplace? = null,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we may need to update the related Kdoc. And it seems the preview of this doc in the IDE is not working, the <code></code> seems to not be well rendered.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed, should use ```

val algorithm: String,
)

fun Event.toValidDecryptedEvent(): ValidDecryptedEvent? {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this method should be in EventExt file instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

val algorithm: String,
)

fun Event.toValidDecryptedEvent(): ValidDecryptedEvent? {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should try to add unit tests on this method.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding this description comment! Indeed, when adding the implementation for beacon events, I was concerned about that. But I couldn't find any other solution since the timeline mecanism seems to rely a lot on MessageContent... Do you know if there is any example of events like these which are correctly implemented?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that they should not be at all MessageContent from the beginning. could use MessageRelationContent though?
Where is it causing problem?

@@ -36,13 +37,22 @@ internal object EventAnnotationsSummaryMapper {
)
},
editSummary = annotationsSummary.editSummary
?.let {
val latestEdition = it.editions.maxByOrNull { editionOfEvent -> editionOfEvent.timestamp } ?: return@let null
?.let { summary ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should take the occasion to introduce a dedicated EditAggregatedSummaryMapper (as a classical class and not an Object). We could then add unit tests on it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted as object and added tests

* 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 })
Copy link
Contributor

@mnaturel mnaturel Nov 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure the implementation is correct. I think the natural order of sorting will be the increasing order and here we would like decreasing order so that we get the most recent first. And I am also not sure about the default sorting for strings.
This is one of the reasons, I strongly think we need unit tests to validate the implementation.

What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added tests

*/
val latestEdition = summary.editions.sortedWith(compareBy<EditionOfEvent> { it.timestamp }.thenBy { it.eventId })
.lastOrNull() ?: return@let null
// get the event and validate?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is there this comment? Is there a validation step missing?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no should be validated before, or for late decryption after

@@ -25,11 +25,11 @@ internal class MigrateSessionTo008(realm: DynamicRealm) : RealmMigrator(realm, 8

override fun doMigrate(realm: DynamicRealm) {
val editionOfEventSchema = realm.schema.create("EditionOfEvent")
.addField(EditionOfEventFields.CONTENT, String::class.java)
.addField("content"/**EditionOfEventFields.CONTENT*/, String::class.java)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should add a comment about this commented code? I guess you wanted to keep the previous fields in comments for future reference?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept it to show that it was from an added then deleted class, ok to remove though

@@ -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>()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we could replace this by:
getLastEditNewContent().toModel<MessageVoiceBroadcastInfoContent>()? The difference is that in this case we would use the newContent field instead of toContent() extension method.

@@ -55,7 +57,7 @@ class MessageInformationDataFactory @Inject constructor(
private val reactionsSummaryFactory: ReactionsSummaryFactory
) {

fun create(params: TimelineItemFactoryParams): MessageInformationData {
fun create(params: TimelineItemFactoryParams, lastEdit: Event? = null): MessageInformationData {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure it is needed to add the lastEdit as a new argument. Since it is already contained in the first argument, I think we could retrieve it easily inside this method.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch

val transaction = slot<suspend (Realm) -> T>()
coEvery { awaitTransaction(instance, capture(transaction)) } coAnswers {
secondArg<suspend (Realm) -> T>().invoke(realm)
val transaction = slot<(Realm) -> T>()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realized the capture of the second argument is in fact not necessary when mocking the answer, so we could simplify this by:

fun <T> givenAwaitTransaction(realm: Realm) {
    coEvery { awaitTransaction(instance, any<(Realm) -> T>()) } answers {
        secondArg<(Realm) -> T>().invoke(realm)
    }
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed

import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent

data class TimelineItemFactoryParams(
val event: TimelineEvent,
val lastEdit: Event? = null,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This field is not used: maybe it should be used inside MessageInformationDataFactory.create()?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's used now

private val clock: Clock,
) : EventInsertLiveProcessor {

private val allowedTypes = listOf(
EventType.MESSAGE,
EventType.REDACTION,
EventType.REACTION,
// The aggregator handles verification events but just to render tiles in the timeline
// It's not participating in verfication it's self, just timeline display
EventType.KEY_VERIFICATION_DONE,
EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_MAC,
// TODO Add ?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we remove the // TODO Add ? since it seems to be done now?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

private val clock: Clock,
) : EventInsertLiveProcessor {

private val allowedTypes = listOf(
EventType.MESSAGE,
EventType.REDACTION,
EventType.REACTION,
// The aggregator handles verification events but just to render tiles in the timeline
// It's not participating in verfication it's self, just timeline display
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// It's not participating in verfication it's self, just timeline display
// It's not participating in verification itself, just timeline display

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@@ -334,9 +309,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
Timber.v("###REPLACE Computing aggregated edit summary (isLocalEcho:$isLocalEcho)")
existingSummary.editions.add(
EditionOfEvent(
senderId = event.senderId ?: "",
eventId = event.eventId,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
eventId = event.eventId,
eventId = eventId,

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@@ -369,8 +343,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
* @param editions list of edition of event
*/
private fun handleThreadSummaryEdition(
editedEvent: EventEntity?,
replaceEvent: TimelineEventEntity?,
editedEvent: EventEntity?, replaceEvent: TimelineEventEntity?,
Copy link
Contributor

@mnaturel mnaturel Nov 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it is better to have one argument per line for consistency?

Copy link
Member Author

@BillCarsonFr BillCarsonFr Nov 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Contributor

@mnaturel mnaturel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have posted some comments concerning some implementation details that should be investigated I guess.

After some tests, here the issues I found:

  • in non encrypted room, the last edited content is not rendered in the timeline for a text message for example. This is always the original content which is rendered
  • in encrypted room, not really sure but I think we might no more render the local echo of the edited content in the timeline as I have seen some short delay before rendering (it may wait for the remote content before rendering).

I have not tested the update of the app for migration of the DB. If not already done, I think the migration should be tested before merge.

* limitations under the License.
*/

package org.matrix.android.sdk.api.session.events.model
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Were you aware there was an existing EventExt in the org.matrix.android.sdk.internal.session.events package?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but it's internal, and toValidate is API

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I see. Thanks for explaining.


class ValidDecryptedEventTest {

val fakeEvent = Event(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should declare the variable as private.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

import org.matrix.android.sdk.api.session.events.model.toValidDecryptedEvent
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent

class ValidDecryptedEventTest {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding these tests!

import org.matrix.android.sdk.internal.database.model.EditionOfEvent
import org.matrix.android.sdk.internal.database.model.EventEntity

class EditAggregationSummaryMapperTest {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we rename it EditAggregatedSummaryEntityMapperTest? It took me while to find the corresponding tests and IDE does not recognize the associations between the class and the test if naming is not aligned.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

class EditAggregationSummaryMapperTest {

@Test
fun test() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a test when we have same timestamp for 2 EditionOfEvent to check we take the correct latest event in this situation?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea, added

Copy link
Contributor

@mnaturel mnaturel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the updates. I didn't expect android tests (was thinking about manual tests) on database migration but that's awesome we know have tests on this.

I have just added some small new comments. I wonder also you have seen some of my previous comments (and particularly the ones about EventEditValidatorTest)?

@bmarty
Copy link
Member

bmarty commented Nov 24, 2022

I didn't expect android tests (was thinking about manual tests) on database migration but that's awesome we now have tests on this.

Yes, that's nice, @BillCarsonFr can you explain somewhere how to create files like session_42.realm

@BillCarsonFr BillCarsonFr force-pushed the feature/bca/better_edit_validation branch from cd497b2 to bec8b5f Compare November 24, 2022 11:45
@BillCarsonFr
Copy link
Member Author

I didn't expect android tests (was thinking about manual tests) on database migration but that's awesome we now have tests on this.

Yes, that's nice, @BillCarsonFr can you explain somewhere how to create files like session_42.realm

I launched the app in the emulator (with LOG_PRIVATE_DATA to true).
I added some messages to add the entities needed to test my migration.
Then I used the Device File Explorer to export the database data/data/im.vector.app.debug/files/<hash>/disk_store.realm and save it on my disk
Then I took the key from logs searching for Database key for alias XXX

And I added that to test assets.

We should create a task to do that for crypto and auth (and probably a better one for session)
The idea would be that it has objects for each table in the base to better test the .transform() on dynamic realm

@BillCarsonFr
Copy link
Member Author

Thanks for the updates. I didn't expect android tests (was thinking about manual tests) on database migration but that's awesome we know have tests on this.

I have just added some small new comments. I wonder also you have seen some of my previous comments (and particularly the ones about EventEditValidatorTest)?

I want to handle them in another PR, to rationalize test on IMXCryptoStore and have a bunch of fake events in test.fixtures

@sonarcloud
Copy link

sonarcloud bot commented Nov 24, 2022

SonarCloud Quality Gate failed.    Quality Gate failed

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot A 0 Security Hotspots
Code Smell A 4 Code Smells

35.9% 35.9% Coverage
0.0% 0.0% Duplication

Copy link
Contributor

@mnaturel mnaturel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@BillCarsonFr BillCarsonFr merged commit 035b1eb into develop Nov 24, 2022
@BillCarsonFr BillCarsonFr deleted the feature/bca/better_edit_validation branch November 24, 2022 16:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants