Thread awareness, map threads events to replies

This commit is contained in:
ariskotsomitopoulos 2021-10-25 19:00:39 +03:00
parent a7d5c6a437
commit 8f0074911a
3 changed files with 216 additions and 0 deletions

View file

@ -0,0 +1,105 @@
/*
* Copyright (c) 2021 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.network
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody
import okhttp3.ResponseBody.Companion.toResponseBody
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.network.ApiInterceptorListener
import org.matrix.android.sdk.api.network.ApiPath
import org.matrix.android.sdk.internal.di.MatrixScope
import timber.log.Timber
import javax.inject.Inject
/**
* The goal of this interceptor is to map thread events to be handled as replies.
* The interceptor is responsible for mapping a thread event:
* "m.relates_to":{
* "event_id":"$eventId",
* "rel_type":"io.element.thread"
* }
* to an equivalent reply event:
* m.relates_to":{
* "m.in_reply_to":{
* "event_id":"$eventId"
* }
*/
@MatrixScope
internal class ThreadToReplyMapInterceptor @Inject constructor() : Interceptor {
init {
Timber.d("MapThreadToReplyInterceptor.init")
}
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
if (isSyncRequest(request)) {
Timber.i(" ------> found SYNC REQUEST")
return response.body?.let {
val contentType = it.contentType()
val rawBody = it.string()
Timber.i(" ------> $rawBody")
if(rawBody.contains("\"rel_type\":\"io.element.thread\"")){
Timber.i(" ------> Thread found")
val start = rawBody.indexOf("\"rel_type\":\"io.element.thread\"") - "\"m.relates_to\":{\"event_id\":\"-GoMTnxkfmZczOPvbjcK43WqNib3wiJVaeO_vRxwHIDA\",\"".length +1
val end = rawBody.indexOf("\"rel_type\":\"io.element.thread\"") + "\"rel_type\":\"io.element.thread\"".length +2
val substr = rawBody.subSequence(start,end)
val newRaw = rawBody.replaceRange(start,end,"\"m.relates_to\":{\"m.in_reply_to\":{\"event_id\":\"\$HDddlX2bJQmVS0bN5R9HDzcrGDap18b3cFDDYjTjctc\"}},")
Timber.i(" ------> ${substr}")
Timber.i(" ------> new raw $newRaw")
val newBody = newRaw.toResponseBody(contentType)
response.newBuilder().body(newBody).build()
}else{
val newBody = rawBody.toResponseBody(contentType)
response.newBuilder().body(newBody).build()
}
} ?: response
// response.peekBody(Long.MAX_VALUE).string().let { networkResponse ->
// Timber.i(" ------> ThreadToReplyMapInterceptor $networkResponse")
// }
}
// val path = request.url.encodedPath
// if(path.contains("/sync/")){
// Timber.i("-----> SYNC REQUEST --> $responseBody")
//
//
// }
// val body = ResponseBody.create()
// val newResponse = response.newBuilder().body(body)
return response
}
/**
* Returns true if the request is a sync request, false otherwise
* Example of a sync request:
* https://matrix-client.matrix.org/_matrix/client/r0/sync?filter=0&set_presence=online&t...
*/
private fun isSyncRequest(request: Request): Boolean =
ApiPath.SYNC.method == request.method && request.url.encodedPath.contains(ApiPath.SYNC.path)
}

View file

@ -76,6 +76,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
private val cryptoService: DefaultCryptoService,
private val roomMemberEventHandler: RoomMemberEventHandler,
private val roomTypingUsersHandler: RoomTypingUsersHandler,
private val threadsAwarenessHandler: ThreadsAwarenessHandler,
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
@UserId private val userId: String,
private val timelineInput: TimelineInput) {
@ -366,6 +367,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
decryptIfNeeded(event, roomId)
}
threadsAwarenessHandler.handleIfNeeded(realm, roomId, event, ::decryptIfNeeded)
val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType)
if (event.stateKey != null) {

View file

@ -0,0 +1,108 @@
/*
* Copyright 2021 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.sync.handler.room
import io.realm.Realm
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.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
import org.matrix.android.sdk.api.session.room.model.tag.RoomTagContent
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.database.mapper.EventMapper
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.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomTagEntity
import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
import timber.log.Timber
import javax.inject.Inject
/**
* This handler is responsible for a smooth threads migration. It will map all incoming
* threads as replies. So a device without threads enabled/updated will be able to view
* threads response as replies to the orighinal message
*/
internal class ThreadsAwarenessHandler @Inject constructor(
private val permalinkFactory: PermalinkFactory
) {
fun handleIfNeeded(realm: Realm,
roomId: String,
event: Event,
decryptIfNeeded: (event: Event, roomId: String) -> Unit) {
if (!isThreadEvent(event)) return
val rootThreadEventId = getRootThreadEventId(event) ?: return
val payload = event.mxDecryptionResult?.payload?.toMutableMap() ?: return
val body = getValueFromPayload(payload, "body") ?: return
val msgType = getValueFromPayload(payload, "msgtype") ?: return
val rootThreadEventEntity = EventEntity.where(realm, eventId = rootThreadEventId).findFirst() ?: return
val rootThreadEvent = EventMapper.map(rootThreadEventEntity)
val rootThreadEventSenderId = rootThreadEvent.senderId ?: return
val rootThreadEventEventId = rootThreadEvent.eventId ?: return
Timber.i("------> Thread event detected!")
if (rootThreadEvent.isEncrypted()) {
decryptIfNeeded(rootThreadEvent, roomId)
}
val rootThreadEventBody = getValueFromPayload(rootThreadEvent.mxDecryptionResult?.payload?.toMutableMap(),"body")
val permalink = permalinkFactory.createPermalink(roomId, rootThreadEventEventId, false)
val userLink = permalinkFactory.createPermalink(rootThreadEventSenderId, false) ?: ""
val replyFormatted = LocalEchoEventFactory.REPLY_PATTERN.format(
permalink,
userLink,
rootThreadEventSenderId,
// Remove inner mx_reply tags if any
rootThreadEventBody,
body)
val messageTextContent = MessageTextContent(
msgType = msgType,
format = MessageFormat.FORMAT_MATRIX_HTML,
body = body,
formattedBody = replyFormatted
).toContent()
payload["content"] = messageTextContent
event.mxDecryptionResult = event.mxDecryptionResult?.copy(payload = payload )
}
private fun isThreadEvent(event: Event): Boolean =
event.content.toModel<MessageRelationContent>()?.relatesTo?.type == "io.element.thread"
private fun getRootThreadEventId(event: Event): String? =
event.content.toModel<MessageRelationContent>()?.relatesTo?.eventId
@Suppress("UNCHECKED_CAST")
private fun getValueFromPayload(payload: JsonDict?, key: String): String? {
val content = payload?.get("content") as? JsonDict
return content?.get(key) as? String
}
}