diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/LocalEchoMapper.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/LocalEchoMapper.kt index f82ba88..6f445ef 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/LocalEchoMapper.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/LocalEchoMapper.kt @@ -22,9 +22,10 @@ internal class LocalEchoMapper(private val metaMapper: MetaMapper) { } } - fun RoomEvent.mergeWith(echo: MessageService.LocalEcho) = when (this) { + fun RoomEvent.mergeWith(echo: MessageService.LocalEcho): RoomEvent = when (this) { is RoomEvent.Message -> this.copy(meta = metaMapper.toMeta(echo)) - is RoomEvent.Reply -> this.copy(message = this.message.copy(meta = metaMapper.toMeta(echo))) + is RoomEvent.Reply -> this.copy(message = this.message.mergeWith(echo)) + is RoomEvent.Image -> this.copy(meta = metaMapper.toMeta(echo)) } } diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt index c4e9c3c..16ef053 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt @@ -36,6 +36,7 @@ import app.dapk.st.matrix.sync.RoomEvent import app.dapk.st.matrix.sync.RoomEvent.Message import app.dapk.st.matrix.sync.RoomState import app.dapk.st.navigator.Navigator +import coil.compose.rememberImagePainter import kotlinx.coroutines.launch @Composable @@ -155,28 +156,70 @@ private fun ColumnScope.RoomContent(self: UserId, state: RoomState) { items = state.events, key = { _, item -> item.eventId.value }, ) { index, item -> + val previousEvent = if (index != 0) state.events[index - 1] else null + val wasPreviousMessageSameSender = previousEvent?.author?.id == item.author.id + when (item) { - is Message -> { - val wasPreviousMessageSameSender = when (val previousEvent = if (index != 0) state.events[index - 1] else null) { - null -> false - is Message -> previousEvent.author.id == item.author.id - is RoomEvent.Reply -> previousEvent.message.author.id == item.author.id + is Message -> Message(self, item, wasPreviousMessageSameSender) + is RoomEvent.Reply -> Reply(self, item, wasPreviousMessageSameSender) + is RoomEvent.Image -> Image(self, item, wasPreviousMessageSameSender) + } + } + } +} + +@Composable +private fun LazyItemScope.Image(self: UserId, message: RoomEvent.Image, wasPreviousMessageSameSender: Boolean) { + when (message.author.id == self) { + true -> { + Box(modifier = Modifier.fillParentMaxWidth(), contentAlignment = Alignment.TopEnd) { + Box(modifier = Modifier.fillParentMaxWidth(0.85f), contentAlignment = Alignment.TopEnd) { + Bubble( + message = message, + isNotSelf = false, + wasPreviousMessageSameSender = wasPreviousMessageSameSender + ) { + Text(message.imageMeta.url) + androidx.compose.foundation.Image( + painter = rememberImagePainter( + data = message.imageMeta.url, + ), + contentDescription = null, + modifier = Modifier + .size(100.dp) + .align(Alignment.Center) + ) } - Message(self, item, wasPreviousMessageSameSender) } - is RoomEvent.Reply -> { - val wasPreviousMessageSameSender = when (val previousEvent = if (index != 0) state.events[index - 1] else null) { - null -> false - is Message -> previousEvent.author.id == item.message.author.id - is RoomEvent.Reply -> previousEvent.message.author.id == item.message.author.id - } - Reply(self, item, wasPreviousMessageSameSender) + } + } + false -> { + Box(modifier = Modifier.fillParentMaxWidth(0.95f), contentAlignment = Alignment.TopStart) { + Bubble( + message = message, + isNotSelf = true, + wasPreviousMessageSameSender = wasPreviousMessageSameSender + ) { + Text(message.imageMeta.url) + androidx.compose.foundation.Image( + painter = rememberImagePainter( + data = message.imageMeta.url, + builder = { + + } + ), + contentDescription = null, + modifier = Modifier + .size(100.dp) + .align(Alignment.Center) + ) } } } } } + @Composable private fun LazyItemScope.Message(self: UserId, message: Message, wasPreviousMessageSameSender: Boolean) { when (message.author.id == self) { @@ -243,7 +286,7 @@ private val othersBackgroundShape = RoundedCornerShape(0.dp, 12.dp, 12.dp, 12.dp @Composable private fun Bubble( - message: Message, + message: RoomEvent, isNotSelf: Boolean, wasPreviousMessageSameSender: Boolean, content: @Composable () -> Unit @@ -346,13 +389,17 @@ private fun ReplyBubbleContent(shape: RoundedCornerShape, background: Color, isN maxLines = 1, color = MaterialTheme.colors.onPrimary ) - Text( - text = reply.replyingTo.content, - color = MaterialTheme.colors.onPrimary, - fontSize = 15.sp, - modifier = Modifier.wrapContentSize(), - textAlign = TextAlign.Start, - ) + when (val replyingTo = reply.replyingTo) { + is Message -> { + Text( + text = replyingTo.content, + color = MaterialTheme.colors.onPrimary, + fontSize = 15.sp, + modifier = Modifier.wrapContentSize(), + textAlign = TextAlign.Start, + ) + } + } } Spacer(modifier = Modifier.height(12.dp)) @@ -365,13 +412,17 @@ private fun ReplyBubbleContent(shape: RoundedCornerShape, background: Color, isN color = MaterialTheme.colors.onPrimary ) } - Text( - text = reply.message.content, - color = MaterialTheme.colors.onPrimary, - fontSize = 15.sp, - modifier = Modifier.wrapContentSize(), - textAlign = TextAlign.Start, - ) + when (val message = reply.message) { + is Message -> { + Text( + text = message.content, + color = MaterialTheme.colors.onPrimary, + fontSize = 15.sp, + modifier = Modifier.wrapContentSize(), + textAlign = TextAlign.Start, + ) + } + } Spacer(modifier = Modifier.height(2.dp)) Row(horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { @@ -391,7 +442,7 @@ private fun ReplyBubbleContent(shape: RoundedCornerShape, background: Color, isN @Composable -private fun RowScope.SendStatus(message: Message) { +private fun RowScope.SendStatus(message: RoomEvent) { when (val meta = message.meta) { MessageMeta.FromServer -> { // last message is self diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/RoomState.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/RoomState.kt index 9eec44b..8021b96 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/RoomState.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/RoomState.kt @@ -4,7 +4,6 @@ import app.dapk.st.core.extensions.unsafeLazy import app.dapk.st.matrix.common.* import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.Transient import java.time.Instant import java.time.ZoneId import java.time.ZonedDateTime @@ -23,6 +22,8 @@ sealed class RoomEvent { abstract val eventId: EventId abstract val utcTimestamp: Long + abstract val author: RoomMember + abstract val meta: MessageMeta @Serializable @SerialName("message") @@ -30,8 +31,8 @@ sealed class RoomEvent { @SerialName("event_id") override val eventId: EventId, @SerialName("timestamp") override val utcTimestamp: Long, @SerialName("content") val content: String, - @SerialName("author") val author: RoomMember, - @SerialName("meta") val meta: MessageMeta, + @SerialName("author") override val author: RoomMember, + @SerialName("meta") override val meta: MessageMeta, @SerialName("encrypted_content") val encryptedContent: MegOlmV1? = null, @SerialName("edited") val edited: Boolean = false, ) : RoomEvent() { @@ -44,7 +45,6 @@ sealed class RoomEvent { @SerialName("session_id") val sessionId: SessionId, ) - @Transient val time: String by unsafeLazy { val instant = Instant.ofEpochMilli(utcTimestamp) ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT) @@ -54,22 +54,48 @@ sealed class RoomEvent { @Serializable @SerialName("reply") data class Reply( - @SerialName("message") val message: Message, - @SerialName("in_reply_to") val replyingTo: Message, + @SerialName("message") val message: RoomEvent, + @SerialName("in_reply_to") val replyingTo: RoomEvent, ) : RoomEvent() { override val eventId: EventId = message.eventId override val utcTimestamp: Long = message.utcTimestamp + override val author: RoomMember = message.author + override val meta: MessageMeta = message.meta val replyingToSelf = replyingTo.author == message.author - @Transient val time: String by unsafeLazy { val instant = Instant.ofEpochMilli(utcTimestamp) ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT) } } + @Serializable + @SerialName("image") + data class Image( + @SerialName("event_id") override val eventId: EventId, + @SerialName("timestamp") override val utcTimestamp: Long, + @SerialName("image_meta") val imageMeta: ImageMeta, + @SerialName("author") override val author: RoomMember, + @SerialName("meta") override val meta: MessageMeta, + @SerialName("encrypted_content") val encryptedContent: Message.MegOlmV1? = null, + @SerialName("edited") val edited: Boolean = false, + ) : RoomEvent() { + + val time: String by unsafeLazy { + val instant = Instant.ofEpochMilli(utcTimestamp) + ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT) + } + + @Serializable + data class ImageMeta( + @SerialName("width") val width: Int, + @SerialName("height") val height: Int, + @SerialName("url") val url: String, + ) + } + } @Serializable diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/DefaultSyncService.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/DefaultSyncService.kt index 699d5d1..a79bffd 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/DefaultSyncService.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/DefaultSyncService.kt @@ -35,7 +35,7 @@ internal class DefaultSyncService( json: Json, oneTimeKeyProducer: MaybeCreateMoreKeys, scope: CoroutineScope, - credentialsStore: CredentialsStore, + private val credentialsStore: CredentialsStore, roomMembersService: RoomMembersService, logger: MatrixLogger, errorTracker: ErrorTracker, @@ -57,7 +57,7 @@ internal class DefaultSyncService( roomMembersService, roomDataSource, TimelineEventsProcessor( - RoomEventCreator(roomMembersService, logger, errorTracker), + RoomEventCreator(roomMembersService, errorTracker, RoomEventFactory(roomMembersService)), roomEventsDecrypter, eventDecrypter, EventLookupUseCase(roomStore) @@ -69,6 +69,7 @@ internal class DefaultSyncService( roomRefresher, roomDataSource, logger, + errorTracker, coroutineDispatchers, ) SyncUseCase( @@ -114,7 +115,7 @@ internal class DefaultSyncService( coroutineDispatchers.withIoContext { roomIds.map { async { - roomRefresher.refreshRoomContent(it)?.also { + roomRefresher.refreshRoomContent(it, credentialsStore.credentials()!!)?.also { overviewStore.persist(listOf(it.roomOverview)) } } diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt index ad56558..0136ac2 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt @@ -445,13 +445,44 @@ internal sealed class ApiTimelineEvent { @SerialName("st.decryption_status") val decryptionStatus: DecryptionStatus? = null ) : ApiTimelineEvent() { - @Serializable - internal data class Content( - @SerialName("body") val body: String? = null, - @SerialName("formatted_body") val formattedBody: String? = null, - @SerialName("msgtype") val type: String? = null, - @SerialName("m.relates_to") val relation: Relation? = null, - ) + @Serializable(with = ApiTimelineMessageContentDeserializer::class) + internal sealed interface Content { + val relation: Relation? + + @Serializable + data class Text( + @SerialName("body") val body: String? = null, + @SerialName("formatted_body") val formattedBody: String? = null, + @SerialName("m.relates_to") override val relation: Relation? = null, + @SerialName("msgtype") val messageType: String = "m.text", + ) : Content + + @Serializable + data class Image( + @SerialName("file") val file: File, + @SerialName("info") val info: Info, + @SerialName("m.relates_to") override val relation: Relation? = null, + @SerialName("msgtype") val messageType: String = "m.image", + ) : Content { + + @Serializable + data class File( + @SerialName("url") val url: MxUrl, + ) + + @Serializable + internal data class Info( + @SerialName("h") val height: Int, + @SerialName("w") val width: Int, + ) + + } + + @Serializable + object Ignored : Content { + override val relation: Relation? = null + } + } @Serializable data class Relation( @@ -512,3 +543,31 @@ internal object EncryptedContentDeserializer : KSerializer override fun serialize(encoder: Encoder, value: ApiEncryptedContent) = TODO("Not yet implemented") } + +internal object ApiTimelineMessageContentDeserializer : KSerializer { + + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("messageContent") + + override fun deserialize(decoder: Decoder): ApiTimelineEvent.TimelineMessage.Content { + require(decoder is JsonDecoder) + val element = decoder.decodeJsonElement() + return when (element.jsonObject["msgtype"]?.jsonPrimitive?.content) { + "m.text" -> ApiTimelineEvent.TimelineMessage.Content.Text.serializer().deserialize(decoder) + "m.image" -> when (element.jsonObject["file"]) { + null -> ApiTimelineEvent.TimelineMessage.Content.Ignored + else -> ApiTimelineEvent.TimelineMessage.Content.Image.serializer().deserialize(decoder) + } + else -> { + println(element) + ApiTimelineEvent.TimelineMessage.Content.Ignored + } + } + } + + override fun serialize(encoder: Encoder, value: ApiTimelineEvent.TimelineMessage.Content) = when (value) { + ApiTimelineEvent.TimelineMessage.Content.Ignored -> {} + is ApiTimelineEvent.TimelineMessage.Content.Image -> ApiTimelineEvent.TimelineMessage.Content.Image.serializer().serialize(encoder, value) + is ApiTimelineEvent.TimelineMessage.Content.Text -> ApiTimelineEvent.TimelineMessage.Content.Text.serializer().serialize(encoder, value) + } + +} \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/RoomEventsDecrypter.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/RoomEventsDecrypter.kt index 83d9bad..418dfb1 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/RoomEventsDecrypter.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/RoomEventsDecrypter.kt @@ -2,6 +2,7 @@ package app.dapk.st.matrix.sync.internal.room import app.dapk.st.matrix.common.* import app.dapk.st.matrix.sync.RoomEvent +import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent import app.dapk.st.matrix.sync.internal.request.DecryptedContent import kotlinx.serialization.json.Json @@ -11,13 +12,37 @@ internal class RoomEventsDecrypter( private val logger: MatrixLogger, ) { - suspend fun decryptRoomEvents(events: List) = events.map { event -> - when (event) { - is RoomEvent.Message -> event.decrypt() - is RoomEvent.Reply -> RoomEvent.Reply( - message = event.message.decrypt(), - replyingTo = event.replyingTo.decrypt(), - ) + suspend fun decryptRoomEvents(userCredentials: UserCredentials, events: List) = events.map { event -> + decryptEvent(event, userCredentials) + } + + private suspend fun decryptEvent(event: RoomEvent, userCredentials: UserCredentials): RoomEvent = when (event) { + is RoomEvent.Message -> event.decrypt() + is RoomEvent.Reply -> RoomEvent.Reply( + message = decryptEvent(event.message, userCredentials), + replyingTo = decryptEvent(event.replyingTo, userCredentials), + ) + is RoomEvent.Image -> event.decrypt(userCredentials) + } + + private suspend fun RoomEvent.Image.decrypt(userCredentials: UserCredentials) = when (this.encryptedContent) { + null -> this + else -> when (val result = messageDecrypter.decrypt(this.encryptedContent.toModel())) { + is DecryptionResult.Failed -> this.also { logger.crypto("Failed to decrypt ${it.eventId}") } + is DecryptionResult.Success -> when (val model = result.payload.toModel()) { + DecryptedContent.Ignored -> this + is DecryptedContent.TimelineText -> { + val content = model.content as ApiTimelineEvent.TimelineMessage.Content.Image + this.copy( + imageMeta = RoomEvent.Image.ImageMeta( + width = content.info.width, + height = content.info.height, + url = content.file?.url?.convertMxUrToUrl(userCredentials.homeServer) ?: "" + ), + encryptedContent = null, + ) + } + } } } @@ -35,10 +60,9 @@ internal class RoomEventsDecrypter( private fun JsonString.toModel() = json.decodeFromString(DecryptedContent.serializer(), this.value) private fun RoomEvent.Message.copyWithDecryptedContent(decryptedContent: DecryptedContent.TimelineText) = this.copy( - content = decryptedContent.content.body ?: "", + content = (decryptedContent.content as ApiTimelineEvent.TimelineMessage.Content.Text).body ?: "", encryptedContent = null ) - } private fun RoomEvent.Message.MegOlmV1.toModel() = EncryptedMessageContent.MegOlmV1( diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/SyncEventDecrypter.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/SyncEventDecrypter.kt index 1631dc4..78f7c28 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/SyncEventDecrypter.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/SyncEventDecrypter.kt @@ -39,7 +39,11 @@ internal class SyncEventDecrypter( is DecryptedContent.TimelineText -> ApiTimelineEvent.TimelineMessage( event.eventId, event.senderId, - it.content.copy(relation = relation), + when (it.content) { + is ApiTimelineEvent.TimelineMessage.Content.Image -> it.content.copy(relation = relation) + is ApiTimelineEvent.TimelineMessage.Content.Text -> it.content.copy(relation = relation) + ApiTimelineEvent.TimelineMessage.Content.Ignored -> it.content + }, event.utcTimestamp, ).also { logger.matrixLog("decrypted to timeline text: $it") } DecryptedContent.Ignored -> event diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreator.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreator.kt index 90b9eb8..4d15423 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreator.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreator.kt @@ -4,9 +4,8 @@ import app.dapk.st.core.extensions.ErrorTracker import app.dapk.st.core.extensions.ifOrNull import app.dapk.st.core.extensions.nullAndTrack import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.common.MatrixLogger import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.common.matrixLog +import app.dapk.st.matrix.common.UserCredentials import app.dapk.st.matrix.sync.MessageMeta import app.dapk.st.matrix.sync.RoomEvent import app.dapk.st.matrix.sync.RoomMembersService @@ -18,8 +17,8 @@ private typealias Lookup = suspend (EventId) -> LookupResult internal class RoomEventCreator( private val roomMembersService: RoomMembersService, - private val logger: MatrixLogger, private val errorTracker: ErrorTracker, + private val roomEventFactory: RoomEventFactory, ) { suspend fun ApiTimelineEvent.Encrypted.toRoomEvent(roomId: RoomId): RoomEvent? { @@ -44,82 +43,122 @@ internal class RoomEventCreator( } } - suspend fun ApiTimelineEvent.TimelineMessage.toRoomEvent(roomId: RoomId, lookup: Lookup): RoomEvent? { + suspend fun ApiTimelineEvent.TimelineMessage.toRoomEvent(userCredentials: UserCredentials, roomId: RoomId, lookup: Lookup): RoomEvent? { + return TimelineEventMapper(userCredentials, roomId, roomEventFactory).mapToRoomEvent(this, lookup) + } +} + +internal class TimelineEventMapper( + private val userCredentials: UserCredentials, + private val roomId: RoomId, + private val roomEventFactory: RoomEventFactory, +) { + + suspend fun mapToRoomEvent(event: ApiTimelineEvent.TimelineMessage, lookup: Lookup): RoomEvent? { return when { - this.isEdit() -> handleEdit(roomId, this.content.relation!!.eventId!!, lookup) - this.isReply() -> handleReply(roomId, lookup) - else -> this.toMessage(roomId) + event.content == ApiTimelineEvent.TimelineMessage.Content.Ignored -> null + event.isEdit() -> event.handleEdit(editedEventId = event.content.relation!!.eventId!!, lookup) + event.isReply() -> event.handleReply(replyToId = event.content.relation!!.inReplyTo!!.eventId, lookup) + else -> roomEventFactory.mapToRoomEvent(event) } } - private suspend fun ApiTimelineEvent.TimelineMessage.handleEdit(roomId: RoomId, editedEventId: EventId, lookup: Lookup): RoomEvent? { - return lookup(editedEventId).fold( - onApiTimelineEvent = { - ifOrNull(this.utcTimestamp > it.utcTimestamp) { - it.toMessage( - roomId, - utcTimestamp = this.utcTimestamp, - content = this.content.body?.removePrefix(" * ")?.trim() ?: "redacted", - edited = true, - ) - } - }, - onRoomEvent = { - ifOrNull(this.utcTimestamp > it.utcTimestamp) { - when (it) { - is RoomEvent.Message -> it.edited(this) - is RoomEvent.Reply -> it.copy(message = it.message.edited(this)) - } - } - }, - onEmpty = { this.toMessage(roomId, edited = true) } - ) - } - - private fun RoomEvent.Message.edited(edit: ApiTimelineEvent.TimelineMessage) = this.copy( - content = edit.content.body?.removePrefix(" * ")?.trim() ?: "redacted", - utcTimestamp = edit.utcTimestamp, - edited = true, - ) - - private suspend fun ApiTimelineEvent.TimelineMessage.handleReply(roomId: RoomId, lookup: Lookup): RoomEvent { - val replyTo = this.content.relation!!.inReplyTo!! - - val relationEvent = lookup(replyTo.eventId).fold( - onApiTimelineEvent = { it.toMessage(roomId) }, + private suspend fun ApiTimelineEvent.TimelineMessage.handleReply(replyToId: EventId, lookup: Lookup): RoomEvent { + val relationEvent = lookup(replyToId).fold( + onApiTimelineEvent = { it.toTextMessage() }, onRoomEvent = { it }, onEmpty = { null } ) - logger.matrixLog("found relation: $relationEvent") - return when (relationEvent) { - null -> this.toMessage(roomId) + null -> when (this.content) { + is ApiTimelineEvent.TimelineMessage.Content.Image -> this.toImageMessage() + is ApiTimelineEvent.TimelineMessage.Content.Text -> this.toFallbackTextMessage() + ApiTimelineEvent.TimelineMessage.Content.Ignored -> throw IllegalStateException() + } else -> { RoomEvent.Reply( - message = this.toMessage(roomId, content = this.content.formattedBody?.stripTags() ?: "redacted"), + message = roomEventFactory.mapToRoomEvent(this), replyingTo = when (relationEvent) { is RoomEvent.Message -> relationEvent is RoomEvent.Reply -> relationEvent.message + is RoomEvent.Image -> relationEvent } ) } } } - private suspend fun ApiTimelineEvent.TimelineMessage.toMessage( - roomId: RoomId, - content: String = this.content.body ?: "redacted", + private suspend fun ApiTimelineEvent.TimelineMessage.toFallbackTextMessage() = this.toTextMessage(content = this.asTextContent().body ?: "redacted") + + private suspend fun ApiTimelineEvent.TimelineMessage.handleEdit(editedEventId: EventId, lookup: Lookup): RoomEvent? { + return lookup(editedEventId).fold( + onApiTimelineEvent = { editApiEvent(original = it, incomingEdit = this) }, + onRoomEvent = { editRoomEvent(original = it, incomingEdit = this) }, + onEmpty = { this.toTextMessage(edited = true) } + ) + } + + private fun editRoomEvent(original: RoomEvent, incomingEdit: ApiTimelineEvent.TimelineMessage): RoomEvent? { + return ifOrNull(incomingEdit.utcTimestamp > original.utcTimestamp) { + when (original) { + is RoomEvent.Message -> original.edited(incomingEdit) + is RoomEvent.Reply -> original.copy( + message = when (original.message) { + is RoomEvent.Image -> original.message + is RoomEvent.Message -> original.message.edited(incomingEdit) + is RoomEvent.Reply -> original.message + } + ) + is RoomEvent.Image -> { + // can't edit images + null + } + } + } + } + + private suspend fun editApiEvent(original: ApiTimelineEvent.TimelineMessage, incomingEdit: ApiTimelineEvent.TimelineMessage): RoomEvent? { + return ifOrNull(incomingEdit.utcTimestamp > original.utcTimestamp) { + when (original.content) { + is ApiTimelineEvent.TimelineMessage.Content.Image -> original.toImageMessage( + utcTimestamp = incomingEdit.utcTimestamp, + edited = true, + ) + is ApiTimelineEvent.TimelineMessage.Content.Text -> original.toTextMessage( + utcTimestamp = incomingEdit.utcTimestamp, + content = incomingEdit.asTextContent().body?.removePrefix(" * ")?.trim() ?: "redacted", + edited = true, + ) + ApiTimelineEvent.TimelineMessage.Content.Ignored -> null + } + } + } + + private fun RoomEvent.Message.edited(edit: ApiTimelineEvent.TimelineMessage) = this.copy( + content = edit.asTextContent().body?.removePrefix(" * ")?.trim() ?: "redacted", + utcTimestamp = edit.utcTimestamp, + edited = true, + ) + + private suspend fun RoomEventFactory.mapToRoomEvent(source: ApiTimelineEvent.TimelineMessage): RoomEvent { + return when (source.content) { + is ApiTimelineEvent.TimelineMessage.Content.Image -> source.toImageMessage(userCredentials, roomId) + is ApiTimelineEvent.TimelineMessage.Content.Text -> source.toTextMessage(roomId) + ApiTimelineEvent.TimelineMessage.Content.Ignored -> throw IllegalStateException() + } + } + + private suspend fun ApiTimelineEvent.TimelineMessage.toTextMessage( + content: String = this.asTextContent().formattedBody?.stripTags() ?: this.asTextContent().body ?: "redacted", edited: Boolean = false, utcTimestamp: Long = this.utcTimestamp, - ) = RoomEvent.Message( - eventId = this.id, - content = content, - author = roomMembersService.find(roomId, this.senderId)!!, - utcTimestamp = utcTimestamp, - meta = MessageMeta.FromServer, - edited = edited, - ) + ) = with(roomEventFactory) { toTextMessage(roomId, content, edited, utcTimestamp) } + + private suspend fun ApiTimelineEvent.TimelineMessage.toImageMessage( + edited: Boolean = false, + utcTimestamp: Long = this.utcTimestamp, + ) = with(roomEventFactory) { toImageMessage(userCredentials, roomId, edited, utcTimestamp) } } @@ -128,5 +167,6 @@ private fun String.stripTags() = this.substring(this.indexOf("") + "< .replace("", "") .replace("", "") -private fun ApiTimelineEvent.TimelineMessage.isEdit() = this.content.relation?.relationType == "m.replace" && this.content.relation.eventId != null -private fun ApiTimelineEvent.TimelineMessage.isReply() = this.content.relation?.inReplyTo != null \ No newline at end of file +private fun ApiTimelineEvent.TimelineMessage.isEdit() = this.content.relation?.relationType == "m.replace" && this.content.relation?.eventId != null +private fun ApiTimelineEvent.TimelineMessage.isReply() = this.content.relation?.inReplyTo != null +private fun ApiTimelineEvent.TimelineMessage.asTextContent() = this.content as ApiTimelineEvent.TimelineMessage.Content.Text diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventFactory.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventFactory.kt new file mode 100644 index 0000000..de508d1 --- /dev/null +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventFactory.kt @@ -0,0 +1,60 @@ +package app.dapk.st.matrix.sync.internal.sync + +import app.dapk.st.matrix.common.RoomId +import app.dapk.st.matrix.common.UserCredentials +import app.dapk.st.matrix.common.convertMxUrToUrl +import app.dapk.st.matrix.sync.MessageMeta +import app.dapk.st.matrix.sync.RoomEvent +import app.dapk.st.matrix.sync.RoomMembersService +import app.dapk.st.matrix.sync.find +import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent + +internal class RoomEventFactory( + private val roomMembersService: RoomMembersService +) { + + suspend fun ApiTimelineEvent.TimelineMessage.toTextMessage( + roomId: RoomId, + content: String = this.asTextContent().formattedBody?.stripTags() ?: this.asTextContent().body ?: "redacted", + edited: Boolean = false, + utcTimestamp: Long = this.utcTimestamp, + ) = RoomEvent.Message( + eventId = this.id, + content = content, + author = roomMembersService.find(roomId, this.senderId)!!, + utcTimestamp = utcTimestamp, + meta = MessageMeta.FromServer, + edited = edited, + ) + + suspend fun ApiTimelineEvent.TimelineMessage.toImageMessage( + userCredentials: UserCredentials, + roomId: RoomId, + edited: Boolean = false, + utcTimestamp: Long = this.utcTimestamp, + imageMeta: RoomEvent.Image.ImageMeta = this.readImageMeta(userCredentials) + ) = RoomEvent.Image( + eventId = this.id, + imageMeta = imageMeta, + author = roomMembersService.find(roomId, this.senderId)!!, + utcTimestamp = utcTimestamp, + meta = MessageMeta.FromServer, + edited = edited, + ) + + private fun ApiTimelineEvent.TimelineMessage.readImageMeta(userCredentials: UserCredentials): RoomEvent.Image.ImageMeta { + val content = this.content as ApiTimelineEvent.TimelineMessage.Content.Image + return RoomEvent.Image.ImageMeta( + content.info.width, + content.info.height, + content.file?.url?.convertMxUrToUrl(userCredentials.homeServer) + ) + } +} + +private fun String.stripTags() = this.substring(this.indexOf("") + "".length) + .trim() + .replace("", "") + .replace("", "") + +private fun ApiTimelineEvent.TimelineMessage.asTextContent() = this.content as ApiTimelineEvent.TimelineMessage.Content.Text diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomRefresher.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomRefresher.kt index 5529797..6be702a 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomRefresher.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomRefresher.kt @@ -1,9 +1,6 @@ package app.dapk.st.matrix.sync.internal.sync -import app.dapk.st.matrix.common.MatrixLogTag -import app.dapk.st.matrix.common.MatrixLogger -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.common.matrixLog +import app.dapk.st.matrix.common.* import app.dapk.st.matrix.sync.RoomEvent import app.dapk.st.matrix.sync.RoomState import app.dapk.st.matrix.sync.internal.room.RoomEventsDecrypter @@ -14,13 +11,13 @@ internal class RoomRefresher( private val logger: MatrixLogger ) { - suspend fun refreshRoomContent(roomId: RoomId): RoomState? { + suspend fun refreshRoomContent(roomId: RoomId, userCredentials: UserCredentials): RoomState? { logger.matrixLog(MatrixLogTag.SYNC, "reducing side effect: $roomId") return when (val previousState = roomDataSource.read(roomId)) { null -> null.also { logger.matrixLog(MatrixLogTag.SYNC, "no previous state to update") } else -> { logger.matrixLog(MatrixLogTag.SYNC, "previous state updated") - val decryptedEvents = previousState.events.decryptEvents() + val decryptedEvents = previousState.events.decryptEvents(userCredentials) val lastMessage = decryptedEvents.sortedByDescending { it.utcTimestamp }.findLastMessage() previousState.copy(events = decryptedEvents, roomOverview = previousState.roomOverview.copy(lastMessage = lastMessage)).also { @@ -30,6 +27,6 @@ internal class RoomRefresher( } } - private suspend fun List.decryptEvents() = roomEventsDecrypter.decryptRoomEvents(this) + private suspend fun List.decryptEvents(userCredentials: UserCredentials) = roomEventsDecrypter.decryptRoomEvents(userCredentials, this) } \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/SyncReducer.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/SyncReducer.kt index 27bbb43..62cd990 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/SyncReducer.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/SyncReducer.kt @@ -1,6 +1,7 @@ package app.dapk.st.matrix.sync.internal.sync import app.dapk.st.core.CoroutineDispatchers +import app.dapk.st.core.extensions.ErrorTracker import app.dapk.st.core.withIoContextAsync import app.dapk.st.matrix.common.* import app.dapk.st.matrix.common.MatrixLogTag.SYNC @@ -16,6 +17,7 @@ internal class SyncReducer( private val roomRefresher: RoomRefresher, private val roomDataSource: RoomDataSource, private val logger: MatrixLogger, + private val errorTracker: ErrorTracker, private val coroutineDispatchers: CoroutineDispatchers, ) { @@ -48,14 +50,14 @@ internal class SyncReducer( isInitialSync = isInitialSync ) } - .onFailure { logger.matrixLog(SYNC, "failed to reduce: $roomId, skipping") } + .onFailure { errorTracker.track(it, "failed to reduce: $roomId, skipping") } .getOrNull() } } ?: emptyList() val roomsWithSideEffects = sideEffects.roomsToRefresh(alreadyHandledRooms = apiUpdatedRooms?.keys ?: emptySet()).map { roomId -> coroutineDispatchers.withIoContextAsync { - roomRefresher.refreshRoomContent(roomId) + roomRefresher.refreshRoomContent(roomId, userCredentials) } } diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/TimelineEventsProcessor.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/TimelineEventsProcessor.kt index f7bfe0f..ee68463 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/TimelineEventsProcessor.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/TimelineEventsProcessor.kt @@ -1,5 +1,6 @@ package app.dapk.st.matrix.sync.internal.sync +import app.dapk.st.matrix.common.UserCredentials import app.dapk.st.matrix.sync.RoomEvent import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent import app.dapk.st.matrix.sync.internal.room.RoomEventsDecrypter @@ -22,13 +23,13 @@ internal class TimelineEventsProcessor( private suspend fun processNewEvents(roomToProcess: RoomToProcess, previousEvents: List): List { val decryptedTimeline = roomToProcess.apiSyncRoom.timeline.apiTimelineEvents.decryptEvents() - val decryptedPreviousEvents = previousEvents.decryptEvents() + val decryptedPreviousEvents = previousEvents.decryptEvents(roomToProcess.userCredentials) val newEvents = with(roomEventCreator) { decryptedTimeline.value.mapNotNull { event -> val roomEvent = when (event) { is ApiTimelineEvent.Encrypted -> event.toRoomEvent(roomToProcess.roomId) - is ApiTimelineEvent.TimelineMessage -> event.toRoomEvent(roomToProcess.roomId) { eventId -> + is ApiTimelineEvent.TimelineMessage -> event.toRoomEvent(roomToProcess.userCredentials, roomToProcess.roomId) { eventId -> eventLookupUseCase.lookup(eventId, decryptedTimeline, decryptedPreviousEvents) } is ApiTimelineEvent.Encryption -> null @@ -46,7 +47,8 @@ internal class TimelineEventsProcessor( } private suspend fun List.decryptEvents() = DecryptedTimeline(eventDecrypter.decryptTimelineEvents(this)) - private suspend fun List.decryptEvents() = DecryptedRoomEvents(roomEventsDecrypter.decryptRoomEvents(this)) + private suspend fun List.decryptEvents(userCredentials: UserCredentials) = + DecryptedRoomEvents(roomEventsDecrypter.decryptRoomEvents(userCredentials, this)) } diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/UnreadEventsUseCase.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/UnreadEventsUseCase.kt index 9defc92..c93f662 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/UnreadEventsUseCase.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/UnreadEventsUseCase.kt @@ -42,6 +42,7 @@ internal class UnreadEventsUseCase( when (it) { is RoomEvent.Message -> it.author.id == selfId is RoomEvent.Reply -> it.message.author.id == selfId + is RoomEvent.Image -> it.author.id == selfId } }.map { it.eventId } roomStore.insertUnread(overview.roomId, eventsFromOthers) diff --git a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/room/RoomEventsDecrypterTest.kt b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/room/RoomEventsDecrypterTest.kt index 368edf9..a366b48 100644 --- a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/room/RoomEventsDecrypterTest.kt +++ b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/room/RoomEventsDecrypterTest.kt @@ -21,6 +21,7 @@ private val AN_ENCRYPTED_ROOM_REPLY = aRoomReplyMessageEvent( replyingTo = AN_ENCRYPTED_ROOM_MESSAGE.copy(eventId = anEventId("other-event")) ) private val A_DECRYPTED_CONTENT = DecryptedContent.TimelineText(aTimelineTextEventContent(body = A_DECRYPTED_MESSAGE_CONTENT)) +private val A_USER_CREDENTIALS = aUserCredentials() class RoomEventsDecrypterTest { @@ -35,7 +36,7 @@ class RoomEventsDecrypterTest { @Test fun `given clear message event, when decrypting, then does nothing`() = runTest { val aClearMessageEvent = aRoomMessageEvent(encryptedContent = null) - val result = roomEventsDecrypter.decryptRoomEvents(listOf(aClearMessageEvent)) + val result = roomEventsDecrypter.decryptRoomEvents(A_USER_CREDENTIALS, listOf(aClearMessageEvent)) result shouldBeEqualTo listOf(aClearMessageEvent) } @@ -44,7 +45,7 @@ class RoomEventsDecrypterTest { fun `given encrypted message event, when decrypting, then applies decrypted body and removes encrypted content`() = runTest { givenEncryptedMessage(AN_ENCRYPTED_ROOM_MESSAGE, decryptsTo = A_DECRYPTED_CONTENT) - val result = roomEventsDecrypter.decryptRoomEvents(listOf(AN_ENCRYPTED_ROOM_MESSAGE)) + val result = roomEventsDecrypter.decryptRoomEvents(A_USER_CREDENTIALS, listOf(AN_ENCRYPTED_ROOM_MESSAGE)) result shouldBeEqualTo listOf(AN_ENCRYPTED_ROOM_MESSAGE.copy(content = A_DECRYPTED_MESSAGE_CONTENT, encryptedContent = null)) } @@ -53,12 +54,12 @@ class RoomEventsDecrypterTest { fun `given encrypted reply event, when decrypting, then decrypts message and replyTo`() = runTest { givenEncryptedReply(AN_ENCRYPTED_ROOM_REPLY, decryptsTo = A_DECRYPTED_CONTENT) - val result = roomEventsDecrypter.decryptRoomEvents(listOf(AN_ENCRYPTED_ROOM_REPLY)) + val result = roomEventsDecrypter.decryptRoomEvents(A_USER_CREDENTIALS, listOf(AN_ENCRYPTED_ROOM_REPLY)) result shouldBeEqualTo listOf( AN_ENCRYPTED_ROOM_REPLY.copy( - message = AN_ENCRYPTED_ROOM_REPLY.message.copy(content = A_DECRYPTED_MESSAGE_CONTENT, encryptedContent = null), - replyingTo = AN_ENCRYPTED_ROOM_REPLY.replyingTo.copy(content = A_DECRYPTED_MESSAGE_CONTENT, encryptedContent = null), + message = (AN_ENCRYPTED_ROOM_REPLY.message as RoomEvent.Message).copy(content = A_DECRYPTED_MESSAGE_CONTENT, encryptedContent = null), + replyingTo = (AN_ENCRYPTED_ROOM_REPLY.replyingTo as RoomEvent.Message).copy(content = A_DECRYPTED_MESSAGE_CONTENT, encryptedContent = null), ) ) } @@ -66,12 +67,12 @@ class RoomEventsDecrypterTest { private fun givenEncryptedMessage(roomMessage: RoomEvent.Message, decryptsTo: DecryptedContent) { val model = roomMessage.encryptedContent!!.toModel() fakeMessageDecrypter.givenDecrypt(model) - .returns(aDecryptionSuccessResult(payload = JsonString(Json.encodeToString(DecryptedContent.serializer(), decryptsTo)))) + .returns(aDecryptionSuccessResult(payload = JsonString(Json { encodeDefaults = true }.encodeToString(DecryptedContent.serializer(), decryptsTo)))) } private fun givenEncryptedReply(roomReply: RoomEvent.Reply, decryptsTo: DecryptedContent) { - givenEncryptedMessage(roomReply.message, decryptsTo) - givenEncryptedMessage(roomReply.replyingTo, decryptsTo) + givenEncryptedMessage(roomReply.message as RoomEvent.Message, decryptsTo) + givenEncryptedMessage(roomReply.replyingTo as RoomEvent.Message, decryptsTo) } } diff --git a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreatorTest.kt b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreatorTest.kt index d8d7d19..f0b9a94 100644 --- a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreatorTest.kt +++ b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreatorTest.kt @@ -26,12 +26,13 @@ private val A_TEXT_EVENT_WITHOUT_CONTENT = anApiTimelineTextEvent( senderId = A_SENDER.id, content = aTimelineTextEventContent(body = null) ) +private val A_USER_CREDENTIALS = aUserCredentials() internal class RoomEventCreatorTest { private val fakeRoomMembersService = FakeRoomMembersService() - private val roomEventCreator = RoomEventCreator(fakeRoomMembersService, FakeMatrixLogger(), FakeErrorTracker()) + private val roomEventCreator = RoomEventCreator(fakeRoomMembersService, FakeErrorTracker(), RoomEventFactory(fakeRoomMembersService)) @Test fun `given Megolm encrypted event then maps to encrypted room message`() = runTest { @@ -71,7 +72,7 @@ internal class RoomEventCreatorTest { fun `given text event then maps to room message`() = runTest { fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER) - val result = with(roomEventCreator) { A_TEXT_EVENT.toRoomEvent(A_ROOM_ID, EMPTY_LOOKUP) } + val result = with(roomEventCreator) { A_TEXT_EVENT.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) } result shouldBeEqualTo aRoomMessageEvent( eventId = A_TEXT_EVENT.id, @@ -85,7 +86,7 @@ internal class RoomEventCreatorTest { fun `given text event without body then maps to redacted room message`() = runTest { fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER) - val result = with(roomEventCreator) { A_TEXT_EVENT_WITHOUT_CONTENT.toRoomEvent(A_ROOM_ID, EMPTY_LOOKUP) } + val result = with(roomEventCreator) { A_TEXT_EVENT_WITHOUT_CONTENT.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) } result shouldBeEqualTo aRoomMessageEvent( eventId = A_TEXT_EVENT_WITHOUT_CONTENT.id, @@ -100,12 +101,12 @@ internal class RoomEventCreatorTest { fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER) val editEvent = anApiTimelineTextEvent().toEditEvent(newTimestamp = 0, messageContent = A_TEXT_EVENT_MESSAGE) - val result = with(roomEventCreator) { editEvent.toRoomEvent(A_ROOM_ID, EMPTY_LOOKUP) } + val result = with(roomEventCreator) { editEvent.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) } result shouldBeEqualTo aRoomMessageEvent( eventId = editEvent.id, utcTimestamp = editEvent.utcTimestamp, - content = editEvent.content.body!!, + content = editEvent.asTextContent().body!!, author = A_SENDER, edited = true ) @@ -118,7 +119,7 @@ internal class RoomEventCreatorTest { val editedMessage = originalMessage.toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE) val lookup = givenLookup(originalMessage) - val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_ROOM_ID, lookup) } + val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) } result shouldBeEqualTo aRoomMessageEvent( eventId = originalMessage.id, @@ -136,7 +137,7 @@ internal class RoomEventCreatorTest { val editedMessage = originalMessage.toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE) val lookup = givenLookup(originalMessage) - val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_ROOM_ID, lookup) } + val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) } result shouldBeEqualTo aRoomMessageEvent( eventId = originalMessage.eventId, @@ -151,10 +152,10 @@ internal class RoomEventCreatorTest { fun `given edited event which relates to a room reply event then only updates message`() = runTest { fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER) val originalMessage = aRoomReplyMessageEvent(message = aRoomMessageEvent()) - val editedMessage = originalMessage.message.toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE) + val editedMessage = (originalMessage.message as RoomEvent.Message).toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE) val lookup = givenLookup(originalMessage) - val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_ROOM_ID, lookup) } + val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) } result shouldBeEqualTo aRoomReplyMessageEvent( replyingTo = originalMessage.replyingTo, @@ -174,7 +175,7 @@ internal class RoomEventCreatorTest { val editedMessage = originalMessage.toEditEvent(newTimestamp = 0, messageContent = A_TEXT_EVENT_MESSAGE) val lookup = givenLookup(originalMessage) - val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_ROOM_ID, lookup) } + val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) } result shouldBeEqualTo null } @@ -185,22 +186,23 @@ internal class RoomEventCreatorTest { val editedMessage = originalMessage.toEditEvent(newTimestamp = 0, messageContent = A_TEXT_EVENT_MESSAGE) val lookup = givenLookup(originalMessage) - val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_ROOM_ID, lookup) } + val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) } result shouldBeEqualTo null } @Test - fun `given reply event with no relation then maps to new room message`() = runTest { + fun `given reply event with no relation then maps to new room message using the full body`() = runTest { fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER) val replyEvent = anApiTimelineTextEvent().toReplyEvent(messageContent = A_TEXT_EVENT_MESSAGE) - val result = with(roomEventCreator) { replyEvent.toRoomEvent(A_ROOM_ID, EMPTY_LOOKUP) } + println(replyEvent.content) + val result = with(roomEventCreator) { replyEvent.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) } result shouldBeEqualTo aRoomMessageEvent( eventId = replyEvent.id, utcTimestamp = replyEvent.utcTimestamp, - content = "${replyEvent.content.body}", + content = replyEvent.asTextContent().body!!, author = A_SENDER, ) } @@ -212,13 +214,13 @@ internal class RoomEventCreatorTest { val replyMessage = originalMessage.toReplyEvent(messageContent = A_REPLY_EVENT_MESSAGE) val lookup = givenLookup(originalMessage) - val result = with(roomEventCreator) { replyMessage.toRoomEvent(A_ROOM_ID, lookup) } + val result = with(roomEventCreator) { replyMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) } result shouldBeEqualTo aRoomReplyMessageEvent( replyingTo = aRoomMessageEvent( eventId = originalMessage.id, utcTimestamp = originalMessage.utcTimestamp, - content = originalMessage.content.body!!, + content = originalMessage.asTextContent().body!!, author = A_SENDER, ), message = aRoomMessageEvent( @@ -237,7 +239,7 @@ internal class RoomEventCreatorTest { val replyMessage = originalMessage.toReplyEvent(messageContent = A_REPLY_EVENT_MESSAGE) val lookup = givenLookup(originalMessage) - val result = with(roomEventCreator) { replyMessage.toRoomEvent(A_ROOM_ID, lookup) } + val result = with(roomEventCreator) { replyMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) } result shouldBeEqualTo aRoomReplyMessageEvent( replyingTo = originalMessage, @@ -254,10 +256,10 @@ internal class RoomEventCreatorTest { fun `given reply event which relates to another room reply event then maps to reply with the reply's message`() = runTest { fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER) val originalMessage = aRoomReplyMessageEvent() - val replyMessage = originalMessage.message.toReplyEvent(messageContent = A_REPLY_EVENT_MESSAGE) + val replyMessage = (originalMessage.message as RoomEvent.Message).toReplyEvent(messageContent = A_REPLY_EVENT_MESSAGE) val lookup = givenLookup(originalMessage) - val result = with(roomEventCreator) { replyMessage.toRoomEvent(A_ROOM_ID, lookup) } + val result = with(roomEventCreator) { replyMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) } result shouldBeEqualTo aRoomReplyMessageEvent( replyingTo = originalMessage.message, @@ -326,4 +328,6 @@ private fun ApiEncryptedContent.toMegolm(): RoomEvent.Message.MegOlmV1 { private class FakeLookup(private val result: LookupResult) : suspend (EventId) -> LookupResult { override suspend fun invoke(p1: EventId) = result -} \ No newline at end of file +} + +private fun ApiTimelineEvent.TimelineMessage.asTextContent() = this.content as ApiTimelineEvent.TimelineMessage.Content.Text diff --git a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomRefresherTest.kt b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomRefresherTest.kt index 1b5019e..60a76fb 100644 --- a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomRefresherTest.kt +++ b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomRefresherTest.kt @@ -22,6 +22,7 @@ private object ARoom { val DECRYPTED_EVENTS = listOf(MESSAGE_EVENT, DECRYPTED_EVENT) val NEW_STATE = RoomState(aRoomOverview(lastMessage = DECRYPTED_EVENT.asLastMessage()), DECRYPTED_EVENTS) } +private val A_USER_CREDENTIALS = aUserCredentials() internal class RoomRefresherTest { @@ -38,7 +39,7 @@ internal class RoomRefresherTest { fun `given no existing room when refreshing then does nothing`() = runTest { fakeRoomDataSource.givenNoCachedRoom(A_ROOM_ID) - val result = roomRefresher.refreshRoomContent(aRoomId()) + val result = roomRefresher.refreshRoomContent(aRoomId(), A_USER_CREDENTIALS) result shouldBeEqualTo null fakeRoomDataSource.verifyNoChanges() @@ -48,9 +49,9 @@ internal class RoomRefresherTest { fun `given existing room when refreshing then processes existing state`() = runTest { fakeRoomDataSource.expect { it.instance.persist(RoomId(any()), any(), any()) } fakeRoomDataSource.givenRoom(A_ROOM_ID, ARoom.PREVIOUS_STATE) - fakeRoomEventsDecrypter.givenDecrypts(ARoom.PREVIOUS_STATE.events, ARoom.DECRYPTED_EVENTS) + fakeRoomEventsDecrypter.givenDecrypts(A_USER_CREDENTIALS, ARoom.PREVIOUS_STATE.events, ARoom.DECRYPTED_EVENTS) - val result = roomRefresher.refreshRoomContent(aRoomId()) + val result = roomRefresher.refreshRoomContent(aRoomId(), A_USER_CREDENTIALS) fakeRoomDataSource.verifyRoomUpdated(ARoom.PREVIOUS_STATE, ARoom.NEW_STATE) result shouldBeEqualTo ARoom.NEW_STATE diff --git a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/TimelineEventsProcessorTest.kt b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/TimelineEventsProcessorTest.kt index 6eb1ae9..531351a 100644 --- a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/TimelineEventsProcessorTest.kt +++ b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/TimelineEventsProcessorTest.kt @@ -22,6 +22,7 @@ private val A_TEXT_TIMELINE_EVENT = anApiTimelineTextEvent() private val A_MESSAGE_ROOM_EVENT = aRoomMessageEvent(anEventId("a-message")) private val AN_ENCRYPTED_ROOM_EVENT = anEncryptedRoomMessageEvent(anEventId("encrypted-message")) private val A_LOOKUP_EVENT_ID = anEventId("lookup-id") +private val A_USER_CREDENTIALS = aUserCredentials() class TimelineEventsProcessorTest { @@ -41,7 +42,7 @@ class TimelineEventsProcessorTest { fun `given a room with no events then returns empty`() = runTest { val previousEvents = emptyList() val roomToProcess = aRoomToProcess() - fakeRoomEventsDecrypter.givenDecrypts(previousEvents) + fakeRoomEventsDecrypter.givenDecrypts(A_USER_CREDENTIALS, previousEvents) fakeSyncEventDecrypter.givenDecrypts(roomToProcess.apiSyncRoom.timeline.apiTimelineEvents) val result = timelineEventsProcessor.process(roomToProcess, previousEvents) @@ -54,11 +55,18 @@ class TimelineEventsProcessorTest { val previousEvents = listOf(aRoomMessageEvent(eventId = anEventId("previous-event"))) val newTimelineEvents = listOf(AN_ENCRYPTED_TIMELINE_EVENT, A_TEXT_TIMELINE_EVENT) val roomToProcess = aRoomToProcess(apiSyncRoom = anApiSyncRoom(anApiSyncRoomTimeline(newTimelineEvents))) - fakeRoomEventsDecrypter.givenDecrypts(previousEvents) + fakeRoomEventsDecrypter.givenDecrypts(A_USER_CREDENTIALS, previousEvents) fakeSyncEventDecrypter.givenDecrypts(newTimelineEvents) fakeEventLookup.givenLookup(A_LOOKUP_EVENT_ID, DecryptedTimeline(newTimelineEvents), DecryptedRoomEvents(previousEvents), ANY_LOOKUP_RESULT) fakeRoomEventCreator.givenCreates(A_ROOM_ID, AN_ENCRYPTED_TIMELINE_EVENT, AN_ENCRYPTED_ROOM_EVENT) - fakeRoomEventCreator.givenCreatesUsingLookup(A_ROOM_ID, A_LOOKUP_EVENT_ID, A_TEXT_TIMELINE_EVENT, A_MESSAGE_ROOM_EVENT, ANY_LOOKUP_RESULT) + fakeRoomEventCreator.givenCreatesUsingLookup( + A_USER_CREDENTIALS, + A_ROOM_ID, + A_LOOKUP_EVENT_ID, + A_TEXT_TIMELINE_EVENT, + A_MESSAGE_ROOM_EVENT, + ANY_LOOKUP_RESULT + ) val result = timelineEventsProcessor.process(roomToProcess, previousEvents) @@ -79,7 +87,7 @@ class TimelineEventsProcessorTest { anIgnoredApiTimelineEvent() ) val roomToProcess = aRoomToProcess(apiSyncRoom = anApiSyncRoom(anApiSyncRoomTimeline(newTimelineEvents))) - fakeRoomEventsDecrypter.givenDecrypts(previousEvents) + fakeRoomEventsDecrypter.givenDecrypts(A_USER_CREDENTIALS, previousEvents) fakeSyncEventDecrypter.givenDecrypts(newTimelineEvents) val result = timelineEventsProcessor.process(roomToProcess, previousEvents) diff --git a/matrix/services/sync/src/test/kotlin/internalfake/FakeRoomEventCreator.kt b/matrix/services/sync/src/test/kotlin/internalfake/FakeRoomEventCreator.kt index 9e225d3..4d0ee6b 100644 --- a/matrix/services/sync/src/test/kotlin/internalfake/FakeRoomEventCreator.kt +++ b/matrix/services/sync/src/test/kotlin/internalfake/FakeRoomEventCreator.kt @@ -2,6 +2,7 @@ package internalfake import app.dapk.st.matrix.common.EventId import app.dapk.st.matrix.common.RoomId +import app.dapk.st.matrix.common.UserCredentials import app.dapk.st.matrix.sync.RoomEvent import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent import app.dapk.st.matrix.sync.internal.sync.LookupResult @@ -18,9 +19,16 @@ internal class FakeRoomEventCreator { coEvery { with(instance) { event.toRoomEvent(roomId) } } returns result } - fun givenCreatesUsingLookup(roomId: RoomId, eventIdToLookup: EventId, event: ApiTimelineEvent.TimelineMessage, result: RoomEvent, lookupResult: LookupResult) { + fun givenCreatesUsingLookup( + userCredentials: UserCredentials, + roomId: RoomId, + eventIdToLookup: EventId, + event: ApiTimelineEvent.TimelineMessage, + result: RoomEvent, + lookupResult: LookupResult + ) { val slot = slot LookupResult>() - coEvery { with(instance) { event.toRoomEvent(roomId, capture(slot)) } } answers { + coEvery { with(instance) { event.toRoomEvent(userCredentials, roomId, capture(slot)) } } answers { runBlocking { if (slot.captured.invoke(eventIdToLookup) == lookupResult) { result diff --git a/matrix/services/sync/src/test/kotlin/internalfake/FakeRoomEventsDecrypter.kt b/matrix/services/sync/src/test/kotlin/internalfake/FakeRoomEventsDecrypter.kt index 64e8769..773832d 100644 --- a/matrix/services/sync/src/test/kotlin/internalfake/FakeRoomEventsDecrypter.kt +++ b/matrix/services/sync/src/test/kotlin/internalfake/FakeRoomEventsDecrypter.kt @@ -1,5 +1,6 @@ package internalfake +import app.dapk.st.matrix.common.UserCredentials import app.dapk.st.matrix.sync.RoomEvent import app.dapk.st.matrix.sync.internal.room.RoomEventsDecrypter import io.mockk.coEvery @@ -8,7 +9,7 @@ import io.mockk.mockk internal class FakeRoomEventsDecrypter { val instance = mockk() - fun givenDecrypts(previousEvents: List, result: List = previousEvents) { - coEvery { instance.decryptRoomEvents(previousEvents) } returns result + fun givenDecrypts(userCredentials: UserCredentials, previousEvents: List, result: List = previousEvents) { + coEvery { instance.decryptRoomEvents(userCredentials, previousEvents) } returns result } } \ No newline at end of file diff --git a/matrix/services/sync/src/test/kotlin/internalfixture/ApiSyncRoomFixture.kt b/matrix/services/sync/src/test/kotlin/internalfixture/ApiSyncRoomFixture.kt index 17e5281..8e069ea 100644 --- a/matrix/services/sync/src/test/kotlin/internalfixture/ApiSyncRoomFixture.kt +++ b/matrix/services/sync/src/test/kotlin/internalfixture/ApiSyncRoomFixture.kt @@ -39,9 +39,8 @@ internal fun anApiTimelineTextEvent( internal fun aTimelineTextEventContent( body: String? = null, formattedBody: String? = null, - type: String? = null, relation: ApiTimelineEvent.TimelineMessage.Relation? = null, -) = ApiTimelineEvent.TimelineMessage.Content(body, formattedBody, type, relation) +) = ApiTimelineEvent.TimelineMessage.Content.Text(body, formattedBody, relation) internal fun anEditRelation(originalId: EventId) = ApiTimelineEvent.TimelineMessage.Relation( relationType = "m.replace", diff --git a/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomEventFixture.kt b/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomEventFixture.kt index f8f8524..9cfab31 100644 --- a/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomEventFixture.kt +++ b/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomEventFixture.kt @@ -15,8 +15,8 @@ fun aRoomMessageEvent( ) = RoomEvent.Message(eventId, utcTimestamp, content, author, meta, encryptedContent, edited) fun aRoomReplyMessageEvent( - message: RoomEvent.Message = aRoomMessageEvent(), - replyingTo: RoomEvent.Message = aRoomMessageEvent(eventId = anEventId("in-reply-to-id")), + message: RoomEvent = aRoomMessageEvent(), + replyingTo: RoomEvent = aRoomMessageEvent(eventId = anEventId("in-reply-to-id")), ) = RoomEvent.Reply(message, replyingTo) fun anEncryptedRoomMessageEvent(