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

Feature/edit e2e #384

Merged
merged 3 commits into from
Jul 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Changes in RiotX 0.2.1 (2019-XX-XX)
Features:
- Message Editing: View edit history (#121)
- Rooms filtering (#304)
- Edit in encrypted room

Improvements:
- Handle click on redacted events: view source and create permalink
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,12 @@ interface RelationService {
* Edit a reply. This is a special case because replies contains fallback text as a prefix.
* This method will take the new body (stripped from fallbacks) and re-add them before sending.
* @param replyToEdit The event to edit
* @param originalSenderId the sender of the message that this reply (being edited) is relating to
* @param originalEventId the event id that this reply (being edited) is relating to
* @param originalTimelineEvent the message that this reply (being edited) is relating to
* @param newBodyText The edited body (stripped from in reply to content)
* @param compatibilityBodyText The text that will appear on clients that don't support yet edition
*/
fun editReply(replyToEdit: TimelineEvent,
originalSenderId: String?,
originalEventId : String,
originalTimelineEvent: TimelineEvent,
newBodyText: String,
compatibilityBodyText: String = "* $newBodyText"): Cancelable

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.isReply
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent

/**
* This data class is a wrapper around an Event. It allows to get useful data in the context of a timeline.
Expand Down Expand Up @@ -94,7 +95,7 @@ fun TimelineEvent.getLastMessageContent(): MessageContent? = annotations?.editSu

fun TimelineEvent.getTextEditableContent(): String? {
val originalContent = root.getClearContent().toModel<MessageContent>() ?: return null
val isReply = originalContent.isReply()
val isReply = originalContent.isReply() || root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId != null
val lastContent = getLastMessageContent()
return if (isReply) {
return extractUsefulTextFromReply(lastContent?.body ?: "")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.crypto.model.event

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent

/**
* Class representing an encrypted event content
Expand Down Expand Up @@ -52,5 +53,8 @@ data class EncryptedEventContent(
* The session id
*/
@Json(name = "session_id")
val sessionId: String? = null
val sessionId: String? = null,

//Relation context is in clear in encrypted message
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ package im.vector.matrix.android.internal.session.room

import arrow.core.Try
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.*
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.relation.ReactionContent
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.matrix.android.internal.database.mapper.ContentMapper
import im.vector.matrix.android.internal.database.mapper.EventMapper
import im.vector.matrix.android.internal.database.model.*
Expand All @@ -43,7 +47,9 @@ internal interface EventRelationsAggregationTask : Task<EventRelationsAggregatio
/**
* Called by EventRelationAggregationUpdater, when new events that can affect relations are inserted in base.
*/
internal class DefaultEventRelationsAggregationTask @Inject constructor(private val monarchy: Monarchy) : EventRelationsAggregationTask {
internal class DefaultEventRelationsAggregationTask @Inject constructor(
private val monarchy: Monarchy,
private val cryptoService: CryptoService) : EventRelationsAggregationTask {

//OPT OUT serer aggregation until API mature enough
private val SHOULD_HANDLE_SERVER_AGREGGATION = false
Expand Down Expand Up @@ -86,14 +92,43 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(private
}
}

EventAnnotationsSummaryEntity.where(realm, event.eventId ?: "").findFirst()?.let {
TimelineEventEntity.where(realm,eventId = event.eventId ?: "").findFirst()?.let { tet ->
EventAnnotationsSummaryEntity.where(realm, event.eventId
?: "").findFirst()?.let {
TimelineEventEntity.where(realm, eventId = event.eventId
?: "").findFirst()?.let { tet ->
tet.annotations = it
}
}


}

EventType.ENCRYPTED -> {
//Relation type is in clear
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE) {
//we need to decrypt if needed
if (event.mxDecryptionResult == null) {
try {
val result = cryptoService.decryptEvent(event, event.roomId)
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
} catch (e: MXCryptoError) {
Copy link
Member

Choose a reason for hiding this comment

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

do something here?

Timber.w("Failed to decrypt e2e replace")
//TODO -> we should keep track of this and retry, or aggregation will be broken
}
}
event.getClearContent().toModel<MessageContent>()?.let {
Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
//A replace!
handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
}
}
}
EventType.REDACTION -> {
val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() }
?: return@forEach
Expand Down Expand Up @@ -125,9 +160,9 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(private

}

private fun handleReplace(realm: Realm, event: Event, content: MessageContent, roomId: String, isLocalEcho: Boolean) {
private fun handleReplace(realm: Realm, event: Event, content: MessageContent, roomId: String, isLocalEcho: Boolean, relatedEventId: String? = null) {
val eventId = event.eventId ?: return
val targetEventId = content.relatesTo?.eventId ?: return
val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return
val newContent = content.newContent ?: return
//ok, this is a replace
var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,24 +61,25 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M
}

val redactionEventEntity = EventEntity.where(realm, eventId = redactionEvent.eventId
?: "").findFirst()
?: return
?: "").findFirst()
?: return
val isLocalEcho = redactionEventEntity.sendState == SendState.UNSENT
Timber.v("Redact event for ${redactionEvent.redacts} localEcho=$isLocalEcho")

val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst()
?: return
?: return

val allowedKeys = computeAllowedKeys(eventToPrune.type)
if (allowedKeys.isNotEmpty()) {
val prunedContent = ContentMapper.map(eventToPrune.content)?.filterKeys { key -> allowedKeys.contains(key) }
eventToPrune.content = ContentMapper.map(prunedContent)
} else {
when (eventToPrune.type) {
EventType.ENCRYPTED,
EventType.MESSAGE -> {
Timber.d("REDACTION for message ${eventToPrune.eventId}")
val unsignedData = EventMapper.map(eventToPrune).unsignedData
?: UnsignedData(null, null)
?: UnsignedData(null, null)

//was this event a m.replace
// val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>()
Expand All @@ -89,6 +90,8 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M
val modified = unsignedData.copy(redactedEvent = redactionEvent)
eventToPrune.content = ContentMapper.map(emptyMap())
eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified)
eventToPrune.decryptionResultJson = null
eventToPrune.decryptionErrorCode = null

}
// EventType.REACTION -> {
Expand All @@ -112,14 +115,14 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M
EventType.STATE_ROOM_CREATE -> listOf("creator")
EventType.STATE_ROOM_JOIN_RULES -> listOf("join_rule")
EventType.STATE_ROOM_POWER_LEVELS -> listOf("users",
"users_default",
"events",
"events_default",
"state_default",
"ban",
"kick",
"redact",
"invite")
"users_default",
"events",
"events_default",
"state_default",
"ban",
"kick",
"redact",
"invite")
EventType.STATE_ROOM_ALIASES -> listOf("aliases")
EventType.STATE_CANONICAL_ALIAS -> listOf("alias")
EventType.FEEDBACK -> listOf("type", "target_event_id")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,32 +127,47 @@ internal class DefaultRelationService @Inject constructor(private val context: C
.also {
saveLocalEcho(it)
}
val workRequest = createSendEventWork(event)
TimelineSendEventWorkCommon.postWork(context, roomId, workRequest)
return CancelableWork(context, workRequest.id)
if (cryptoService.isRoomEncrypted(roomId)) {
val encryptWork = createEncryptEventWork(event, listOf("m.relates_to"))
val workRequest = createSendEventWork(event)
TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, workRequest)
return CancelableWork(context, encryptWork.id)

} else {
val workRequest = createSendEventWork(event)
TimelineSendEventWorkCommon.postWork(context, roomId, workRequest)
return CancelableWork(context, workRequest.id)
}

}

override fun editReply(replyToEdit: TimelineEvent,
originalSenderId: String?,
originalEventId: String,
originalEvent: TimelineEvent,
newBodyText: String,
compatibilityBodyText: String): Cancelable {
val event = eventFactory
.createReplaceTextOfReply(roomId,
replyToEdit,
originalSenderId, originalEventId,
originalEvent,
newBodyText, true, MessageType.MSGTYPE_TEXT, compatibilityBodyText)
.also {
saveLocalEcho(it)
}
val workRequest = createSendEventWork(event)
TimelineSendEventWorkCommon.postWork(context, roomId, workRequest)
return CancelableWork(context, workRequest.id)
if (cryptoService.isRoomEncrypted(roomId)) {
val encryptWork = createEncryptEventWork(event, listOf("m.relates_to"))
val workRequest = createSendEventWork(event)
TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, workRequest)
return CancelableWork(context, encryptWork.id)

} else {
val workRequest = createSendEventWork(event)
TimelineSendEventWorkCommon.postWork(context, roomId, workRequest)
return CancelableWork(context, workRequest.id)
}
}

override fun fetchEditHistory(eventId: String, callback: MatrixCallback<List<Event>>) {
val params = FetchEditHistoryTask.Params(roomId, eventId)
val params = FetchEditHistoryTask.Params(roomId, cryptoService.isRoomEncrypted(roomId), eventId)
fetchEditHistoryTask.configureWith(params)
.dispatchTo(callback)
.executeBy(taskExecutor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ internal interface FetchEditHistoryTask : Task<FetchEditHistoryTask.Params, List

data class Params(
val roomId: String,
val isRoomEncrypted: Boolean,
val eventId: String
)
}
Expand All @@ -40,9 +41,14 @@ internal class DefaultFetchEditHistoryTask @Inject constructor(

override suspend fun execute(params: FetchEditHistoryTask.Params): Try<List<Event>> {
return executeRequest<RelationsResponse> {
apiCall = roomAPI.getRelations(params.roomId, params.eventId, RelationType.REPLACE, EventType.MESSAGE)
apiCall = roomAPI.getRelations(params.roomId,
params.eventId,
RelationType.REPLACE,
if (params.isRoomEncrypted) EventType.ENCRYPTED else EventType.MESSAGE)
}.map { resp ->
resp.chunks
val events = resp.chunks.toMutableList()
resp.originalEvent?.let { events.add(it) }
events
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import im.vector.matrix.android.api.session.events.model.Event
@JsonClass(generateAdapter = true)
internal data class RelationsResponse(
@Json(name = "chunk") val chunks: List<Event>,
@Json(name = "original_event") val originalEvent: Event?,
@Json(name = "next_batch") val nextBatch: String?,
@Json(name = "prev_batch") val prevBatch: String?
)
Original file line number Diff line number Diff line change
Expand Up @@ -105,28 +105,28 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
}

fun createReplaceTextOfReply(roomId: String, eventReplaced: TimelineEvent,
originalSenderId: String?,
originalEventId: String,
originalEvent: TimelineEvent,
newBodyText: String,
newBodyAutoMarkdown: Boolean,
msgType: String,
compatibilityText: String): Event {
val permalink = PermalinkFactory.createPermalink(roomId, originalEventId)
val userLink = originalSenderId?.let { PermalinkFactory.createPermalink(it) } ?: ""
val permalink = PermalinkFactory.createPermalink(roomId, originalEvent.root.eventId ?: "")
val userLink = originalEvent.root.senderId?.let { PermalinkFactory.createPermalink(it) }
?: ""

val body = bodyForReply(eventReplaced.getLastMessageContent(), eventReplaced.root.getClearContent().toModel())
val body = bodyForReply(originalEvent.getLastMessageContent(), originalEvent.root.getClearContent().toModel())
val replyFormatted = REPLY_PATTERN.format(
permalink,
stringProvider.getString(R.string.message_reply_to_prefix),
userLink,
originalSenderId,
originalEvent.senderName ?: originalEvent.root.senderId,
body.takeFormatted(),
createTextContent(newBodyText, newBodyAutoMarkdown).takeFormatted()
)
//
// > <@alice:example.org> This is the original body
//
val replyFallback = buildReplyFallback(body, originalSenderId, newBodyText)
val replyFallback = buildReplyFallback(body, originalEvent.root.senderId ?: "", newBodyText)

return createEvent(roomId,
MessageTextContent(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.message.getFileUrl
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.matrix.rx.rx
import im.vector.riotx.core.intent.getFilenameFromUri
import im.vector.riotx.core.platform.VectorViewModel
Expand Down Expand Up @@ -229,9 +230,12 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro

//is original event a reply?
val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel<MessageContent>()?.relatesTo?.inReplyTo?.eventId
?: state.sendMode.timelineEvent.root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId
if (inReplyTo != null) {
//TODO check if same content?
room.editReply(state.sendMode.timelineEvent, room.getTimeLineEvent(inReplyTo)?.root?.senderId, inReplyTo, action.text)
room.getTimeLineEvent(inReplyTo)?.let {
room.editReply(state.sendMode.timelineEvent, it, action.text)
}
} else {
val messageContent: MessageContent? =
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
Expand Down
Loading