Merge pull request #20 from ouchadam/support-image-messages

Support image messages
This commit is contained in:
Adam Brown 2022-04-01 20:53:08 +01:00 committed by GitHub
commit 77efb58562
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 611 additions and 220 deletions

View File

@ -0,0 +1,64 @@
package app.dapk.st.messenger
import android.util.Base64
import app.dapk.st.matrix.sync.RoomEvent
import coil.bitmap.BitmapPool
import coil.decode.DataSource
import coil.decode.Options
import coil.fetch.FetchResult
import coil.fetch.Fetcher
import coil.fetch.SourceResult
import coil.size.Size
import okhttp3.OkHttpClient
import okhttp3.Request
import okio.Buffer
import java.security.MessageDigest
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
private const val CRYPTO_BUFFER_SIZE = 32 * 1024
private const val CIPHER_ALGORITHM = "AES/CTR/NoPadding"
private const val SECRET_KEY_SPEC_ALGORITHM = "AES"
private const val MESSAGE_DIGEST_ALGORITHM = "SHA-256"
class DecryptingFetcher : Fetcher<RoomEvent.Image> {
private val http = OkHttpClient()
override suspend fun fetch(pool: BitmapPool, data: RoomEvent.Image, size: Size, options: Options): FetchResult {
val keys = data.imageMeta.keys!!
val key = Base64.decode(keys.k.replace('-', '+').replace('_', '/'), Base64.DEFAULT)
val initVectorBytes = Base64.decode(keys.iv, Base64.DEFAULT)
val decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
val ivParameterSpec = IvParameterSpec(initVectorBytes)
decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec)
val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM)
var read: Int
val d = ByteArray(CRYPTO_BUFFER_SIZE)
var decodedBytes: ByteArray
val response = http.newCall(Request.Builder().url(data.imageMeta.url).build()).execute()
val outputStream = Buffer()
response.body()?.let {
it.byteStream().use {
read = it.read(d)
while (read != -1) {
messageDigest.update(d, 0, read)
decodedBytes = decryptCipher.update(d, 0, read)
outputStream.write(decodedBytes)
read = it.read(d)
}
}
}
return SourceResult(outputStream, null, DataSource.NETWORK)
}
override fun key(data: RoomEvent.Image) = data.imageMeta.url
}

View File

@ -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.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))
} }
} }

View File

@ -9,10 +9,10 @@ import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import app.dapk.st.design.components.SmallTalkTheme
import app.dapk.st.core.DapkActivity import app.dapk.st.core.DapkActivity
import app.dapk.st.core.module import app.dapk.st.core.module
import app.dapk.st.core.viewModel import app.dapk.st.core.viewModel
import app.dapk.st.design.components.SmallTalkTheme
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize

View File

@ -1,5 +1,6 @@
package app.dapk.st.messenger package app.dapk.st.messenger
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@ -36,6 +37,7 @@ import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.matrix.sync.RoomEvent.Message import app.dapk.st.matrix.sync.RoomEvent.Message
import app.dapk.st.matrix.sync.RoomState import app.dapk.st.matrix.sync.RoomState
import app.dapk.st.navigator.Navigator import app.dapk.st.navigator.Navigator
import coil.compose.rememberImagePainter
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
@ -155,30 +157,33 @@ private fun ColumnScope.RoomContent(self: UserId, state: RoomState) {
items = state.events, items = state.events,
key = { _, item -> item.eventId.value }, key = { _, item -> item.eventId.value },
) { index, item -> ) { index, item ->
when (item) { val previousEvent = if (index != 0) state.events[index - 1] else null
is Message -> { val wasPreviousMessageSameSender = previousEvent?.author?.id == item.author.id
val wasPreviousMessageSameSender = when (val previousEvent = if (index != 0) state.events[index - 1] else null) { AlignedBubble(item, self, wasPreviousMessageSameSender) {
null -> false when (item) {
is Message -> previousEvent.author.id == item.author.id is RoomEvent.Image -> MessageImage(it as BubbleContent<RoomEvent.Image>)
is RoomEvent.Reply -> previousEvent.message.author.id == item.author.id is Message -> TextBubbleContent(it as BubbleContent<RoomEvent.Message>)
} is RoomEvent.Reply -> ReplyBubbleContent(it as BubbleContent<RoomEvent.Reply>)
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)
} }
} }
} }
} }
} }
private data class BubbleContent<T : RoomEvent>(
val shape: RoundedCornerShape,
val background: Color,
val isNotSelf: Boolean,
val message: T
)
@Composable @Composable
private fun LazyItemScope.Message(self: UserId, message: Message, wasPreviousMessageSameSender: Boolean) { private fun <T : RoomEvent> LazyItemScope.AlignedBubble(
message: T,
self: UserId,
wasPreviousMessageSameSender: Boolean,
content: @Composable (BubbleContent<T>) -> Unit
) {
when (message.author.id == self) { when (message.author.id == self) {
true -> { true -> {
Box(modifier = Modifier.fillParentMaxWidth(), contentAlignment = Alignment.TopEnd) { Box(modifier = Modifier.fillParentMaxWidth(), contentAlignment = Alignment.TopEnd) {
@ -188,7 +193,7 @@ private fun LazyItemScope.Message(self: UserId, message: Message, wasPreviousMes
isNotSelf = false, isNotSelf = false,
wasPreviousMessageSameSender = wasPreviousMessageSameSender wasPreviousMessageSameSender = wasPreviousMessageSameSender
) { ) {
TextBubbleContent(selfBackgroundShape, SmallTalkTheme.extendedColors.selfBubble, false, message) content(BubbleContent(selfBackgroundShape, SmallTalkTheme.extendedColors.selfBubble, false, message))
} }
} }
} }
@ -200,7 +205,7 @@ private fun LazyItemScope.Message(self: UserId, message: Message, wasPreviousMes
isNotSelf = true, isNotSelf = true,
wasPreviousMessageSameSender = wasPreviousMessageSameSender wasPreviousMessageSameSender = wasPreviousMessageSameSender
) { ) {
TextBubbleContent(othersBackgroundShape, SmallTalkTheme.extendedColors.othersBubble, true, message) content(BubbleContent(othersBackgroundShape, SmallTalkTheme.extendedColors.othersBubble, true, message))
} }
} }
} }
@ -208,42 +213,67 @@ private fun LazyItemScope.Message(self: UserId, message: Message, wasPreviousMes
} }
@Composable @Composable
private fun LazyItemScope.Reply(self: UserId, message: RoomEvent.Reply, wasPreviousMessageSameSender: Boolean) { private fun MessageImage(content: BubbleContent<RoomEvent.Image>) {
when (message.message.author.id == self) { Box(modifier = Modifier.padding(start = 6.dp)) {
true -> { Box(
Box(modifier = Modifier.fillParentMaxWidth(), contentAlignment = Alignment.TopEnd) { Modifier
Box(modifier = Modifier.fillParentMaxWidth(0.85f), contentAlignment = Alignment.TopEnd) { .padding(4.dp)
Bubble( .clip(content.shape)
message = message.message, .background(content.background)
isNotSelf = false, .height(IntrinsicSize.Max),
wasPreviousMessageSameSender = wasPreviousMessageSameSender ) {
) { Column(
ReplyBubbleContent(selfBackgroundShape, SmallTalkTheme.extendedColors.selfBubble, false, message) Modifier
} .padding(8.dp)
.width(IntrinsicSize.Max)
.defaultMinSize(minWidth = 50.dp)
) {
if (content.isNotSelf) {
Text(
fontSize = 11.sp,
text = content.message.author.displayName ?: content.message.author.id.value,
maxLines = 1,
color = MaterialTheme.colors.onPrimary
)
} }
}
} val width = with(LocalDensity.current) { content.message.imageMeta.width.toDp() }
false -> { val height = with(LocalDensity.current) { content.message.imageMeta.height.toDp() }
Box(modifier = Modifier.fillParentMaxWidth(0.95f), contentAlignment = Alignment.TopStart) { Spacer(modifier = Modifier.height(4.dp))
Bubble( Image(
message = message.message, modifier = Modifier.size(width, height),
isNotSelf = true, painter = rememberImagePainter(
wasPreviousMessageSameSender = wasPreviousMessageSameSender data = content.message,
) { builder = { fetcher(DecryptingFetcher()) }
ReplyBubbleContent(othersBackgroundShape, SmallTalkTheme.extendedColors.othersBubble, true, message) ),
contentDescription = null,
)
Spacer(modifier = Modifier.height(4.dp))
Row(horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
val editedPrefix = if (content.message.edited) "(edited) " else null
Text(
fontSize = 9.sp,
text = "${editedPrefix ?: ""}${content.message.time}",
textAlign = TextAlign.End,
color = MaterialTheme.colors.onPrimary,
modifier = Modifier.wrapContentSize()
)
SendStatus(content.message)
} }
} }
} }
} }
}
}
private val selfBackgroundShape = RoundedCornerShape(12.dp, 0.dp, 12.dp, 12.dp) private val selfBackgroundShape = RoundedCornerShape(12.dp, 0.dp, 12.dp, 12.dp)
private val othersBackgroundShape = RoundedCornerShape(0.dp, 12.dp, 12.dp, 12.dp) private val othersBackgroundShape = RoundedCornerShape(0.dp, 12.dp, 12.dp, 12.dp)
@Composable @Composable
private fun Bubble( private fun Bubble(
message: Message, message: RoomEvent,
isNotSelf: Boolean, isNotSelf: Boolean,
wasPreviousMessageSameSender: Boolean, wasPreviousMessageSameSender: Boolean,
content: @Composable () -> Unit content: @Composable () -> Unit
@ -270,13 +300,13 @@ private fun Bubble(
} }
@Composable @Composable
private fun TextBubbleContent(shape: RoundedCornerShape, background: Color, isNotSelf: Boolean, message: Message) { private fun TextBubbleContent(content: BubbleContent<RoomEvent.Message>) {
Box(modifier = Modifier.padding(start = 6.dp)) { Box(modifier = Modifier.padding(start = 6.dp)) {
Box( Box(
Modifier Modifier
.padding(4.dp) .padding(4.dp)
.clip(shape) .clip(content.shape)
.background(background) .background(content.background)
.height(IntrinsicSize.Max), .height(IntrinsicSize.Max),
) { ) {
Column( Column(
@ -285,16 +315,16 @@ private fun TextBubbleContent(shape: RoundedCornerShape, background: Color, isNo
.width(IntrinsicSize.Max) .width(IntrinsicSize.Max)
.defaultMinSize(minWidth = 50.dp) .defaultMinSize(minWidth = 50.dp)
) { ) {
if (isNotSelf) { if (content.isNotSelf) {
Text( Text(
fontSize = 11.sp, fontSize = 11.sp,
text = message.author.displayName ?: message.author.id.value, text = content.message.author.displayName ?: content.message.author.id.value,
maxLines = 1, maxLines = 1,
color = MaterialTheme.colors.onPrimary color = MaterialTheme.colors.onPrimary
) )
} }
Text( Text(
text = message.content, text = content.message.content,
color = MaterialTheme.colors.onPrimary, color = MaterialTheme.colors.onPrimary,
fontSize = 15.sp, fontSize = 15.sp,
modifier = Modifier.wrapContentSize(), modifier = Modifier.wrapContentSize(),
@ -303,15 +333,15 @@ private fun TextBubbleContent(shape: RoundedCornerShape, background: Color, isNo
Spacer(modifier = Modifier.height(2.dp)) Spacer(modifier = Modifier.height(2.dp))
Row(horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { Row(horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
val editedPrefix = if (message.edited) "(edited) " else null val editedPrefix = if (content.message.edited) "(edited) " else null
Text( Text(
fontSize = 9.sp, fontSize = 9.sp,
text = "${editedPrefix ?: ""}${message.time}", text = "${editedPrefix ?: ""}${content.message.time}",
textAlign = TextAlign.End, textAlign = TextAlign.End,
color = MaterialTheme.colors.onPrimary, color = MaterialTheme.colors.onPrimary,
modifier = Modifier.wrapContentSize() modifier = Modifier.wrapContentSize()
) )
SendStatus(message) SendStatus(content.message)
} }
} }
} }
@ -319,13 +349,13 @@ private fun TextBubbleContent(shape: RoundedCornerShape, background: Color, isNo
} }
@Composable @Composable
private fun ReplyBubbleContent(shape: RoundedCornerShape, background: Color, isNotSelf: Boolean, reply: RoomEvent.Reply) { private fun ReplyBubbleContent(content: BubbleContent<RoomEvent.Reply>) {
Box(modifier = Modifier.padding(start = 6.dp)) { Box(modifier = Modifier.padding(start = 6.dp)) {
Box( Box(
Modifier Modifier
.padding(4.dp) .padding(4.dp)
.clip(shape) .clip(content.shape)
.background(background) .background(content.background)
.height(IntrinsicSize.Max), .height(IntrinsicSize.Max),
) { ) {
Column( Column(
@ -336,53 +366,90 @@ private fun ReplyBubbleContent(shape: RoundedCornerShape, background: Color, isN
) { ) {
Column( Column(
Modifier Modifier
.background(if (isNotSelf) SmallTalkTheme.extendedColors.otherBubbleReplyBackground else SmallTalkTheme.extendedColors.selfBubbleReplyBackground) .background(if (content.isNotSelf) SmallTalkTheme.extendedColors.otherBubbleReplyBackground else SmallTalkTheme.extendedColors.selfBubbleReplyBackground)
.padding(4.dp) .padding(4.dp)
) { ) {
val replyName = if (!isNotSelf && reply.replyingToSelf) "You" else reply.replyingTo.author.displayName ?: reply.replyingTo.author.id.value val replyName = if (!content.isNotSelf && content.message.replyingToSelf) "You" else content.message.replyingTo.author.displayName
?: content.message.replyingTo.author.id.value
Text( Text(
fontSize = 11.sp, fontSize = 11.sp,
text = replyName, text = replyName,
maxLines = 1, maxLines = 1,
color = MaterialTheme.colors.onPrimary color = MaterialTheme.colors.onPrimary
) )
Text( when (val replyingTo = content.message.replyingTo) {
text = reply.replyingTo.content, is Message -> {
color = MaterialTheme.colors.onPrimary, Text(
fontSize = 15.sp, text = replyingTo.content,
modifier = Modifier.wrapContentSize(), color = MaterialTheme.colors.onPrimary,
textAlign = TextAlign.Start, fontSize = 15.sp,
) modifier = Modifier.wrapContentSize(),
textAlign = TextAlign.Start,
)
}
is RoomEvent.Image -> {
val width = with(LocalDensity.current) { replyingTo.imageMeta.width.toDp() }
val height = with(LocalDensity.current) { replyingTo.imageMeta.height.toDp() }
Spacer(modifier = Modifier.height(4.dp))
Image(
modifier = Modifier.size(width, height),
painter = rememberImagePainter(
data = replyingTo,
builder = { fetcher(DecryptingFetcher()) }
),
contentDescription = null,
)
Spacer(modifier = Modifier.height(4.dp))
}
}
} }
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
if (isNotSelf) { if (content.isNotSelf) {
Text( Text(
fontSize = 11.sp, fontSize = 11.sp,
text = reply.message.author.displayName ?: reply.message.author.id.value, text = content.message.message.author.displayName ?: content.message.message.author.id.value,
maxLines = 1, maxLines = 1,
color = MaterialTheme.colors.onPrimary color = MaterialTheme.colors.onPrimary
) )
} }
Text( when (val message = content.message.message) {
text = reply.message.content, is Message -> {
color = MaterialTheme.colors.onPrimary, Text(
fontSize = 15.sp, text = message.content,
modifier = Modifier.wrapContentSize(), color = MaterialTheme.colors.onPrimary,
textAlign = TextAlign.Start, fontSize = 15.sp,
) modifier = Modifier.wrapContentSize(),
textAlign = TextAlign.Start,
)
}
is RoomEvent.Image -> {
val width = with(LocalDensity.current) { message.imageMeta.width.toDp() }
val height = with(LocalDensity.current) { message.imageMeta.height.toDp() }
Spacer(modifier = Modifier.height(4.dp))
Image(
modifier = Modifier.size(width, height),
painter = rememberImagePainter(
data = content.message,
builder = { fetcher(DecryptingFetcher()) }
),
contentDescription = null,
)
Spacer(modifier = Modifier.height(4.dp))
}
}
Spacer(modifier = Modifier.height(2.dp)) Spacer(modifier = Modifier.height(2.dp))
Row(horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { Row(horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
Text( Text(
fontSize = 9.sp, fontSize = 9.sp,
text = reply.time, text = content.message.time,
textAlign = TextAlign.End, textAlign = TextAlign.End,
color = MaterialTheme.colors.onPrimary, color = MaterialTheme.colors.onPrimary,
modifier = Modifier.wrapContentSize() modifier = Modifier.wrapContentSize()
) )
SendStatus(reply.message) SendStatus(content.message.message)
} }
} }
} }
@ -391,7 +458,7 @@ private fun ReplyBubbleContent(shape: RoundedCornerShape, background: Color, isN
@Composable @Composable
private fun RowScope.SendStatus(message: Message) { private fun RowScope.SendStatus(message: RoomEvent) {
when (val meta = message.meta) { when (val meta = message.meta) {
MessageMeta.FromServer -> { MessageMeta.FromServer -> {
// last message is self // last message is self

View File

@ -4,7 +4,6 @@ import app.dapk.st.core.extensions.unsafeLazy
import app.dapk.st.matrix.common.* import app.dapk.st.matrix.common.*
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import java.time.Instant import java.time.Instant
import java.time.ZoneId import java.time.ZoneId
import java.time.ZonedDateTime import java.time.ZonedDateTime
@ -23,6 +22,8 @@ sealed class RoomEvent {
abstract val eventId: EventId abstract val eventId: EventId
abstract val utcTimestamp: Long abstract val utcTimestamp: Long
abstract val author: RoomMember
abstract val meta: MessageMeta
@Serializable @Serializable
@SerialName("message") @SerialName("message")
@ -30,8 +31,8 @@ sealed class RoomEvent {
@SerialName("event_id") override val eventId: EventId, @SerialName("event_id") override val eventId: EventId,
@SerialName("timestamp") override val utcTimestamp: Long, @SerialName("timestamp") override val utcTimestamp: Long,
@SerialName("content") val content: String, @SerialName("content") val content: String,
@SerialName("author") val author: RoomMember, @SerialName("author") override val author: RoomMember,
@SerialName("meta") val meta: MessageMeta, @SerialName("meta") override val meta: MessageMeta,
@SerialName("encrypted_content") val encryptedContent: MegOlmV1? = null, @SerialName("encrypted_content") val encryptedContent: MegOlmV1? = null,
@SerialName("edited") val edited: Boolean = false, @SerialName("edited") val edited: Boolean = false,
) : RoomEvent() { ) : RoomEvent() {
@ -44,7 +45,6 @@ sealed class RoomEvent {
@SerialName("session_id") val sessionId: SessionId, @SerialName("session_id") val sessionId: SessionId,
) )
@Transient
val time: String by unsafeLazy { val time: String by unsafeLazy {
val instant = Instant.ofEpochMilli(utcTimestamp) val instant = Instant.ofEpochMilli(utcTimestamp)
ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT) ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT)
@ -54,22 +54,59 @@ sealed class RoomEvent {
@Serializable @Serializable
@SerialName("reply") @SerialName("reply")
data class Reply( data class Reply(
@SerialName("message") val message: Message, @SerialName("message") val message: RoomEvent,
@SerialName("in_reply_to") val replyingTo: Message, @SerialName("in_reply_to") val replyingTo: RoomEvent,
) : RoomEvent() { ) : RoomEvent() {
override val eventId: EventId = message.eventId override val eventId: EventId = message.eventId
override val utcTimestamp: Long = message.utcTimestamp override val utcTimestamp: Long = message.utcTimestamp
override val author: RoomMember = message.author
override val meta: MessageMeta = message.meta
val replyingToSelf = replyingTo.author == message.author val replyingToSelf = replyingTo.author == message.author
@Transient
val time: String by unsafeLazy { val time: String by unsafeLazy {
val instant = Instant.ofEpochMilli(utcTimestamp) val instant = Instant.ofEpochMilli(utcTimestamp)
ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT) 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,
@SerialName("keys") val keys: Keys?,
) {
@Serializable
data class Keys(
@SerialName("k") val k: String,
@SerialName("iv") val iv: String,
@SerialName("v") val v: String,
@SerialName("hashes") val hashes: Map<String, String>,
)
}
}
} }
@Serializable @Serializable

View File

@ -35,7 +35,7 @@ internal class DefaultSyncService(
json: Json, json: Json,
oneTimeKeyProducer: MaybeCreateMoreKeys, oneTimeKeyProducer: MaybeCreateMoreKeys,
scope: CoroutineScope, scope: CoroutineScope,
credentialsStore: CredentialsStore, private val credentialsStore: CredentialsStore,
roomMembersService: RoomMembersService, roomMembersService: RoomMembersService,
logger: MatrixLogger, logger: MatrixLogger,
errorTracker: ErrorTracker, errorTracker: ErrorTracker,
@ -57,7 +57,7 @@ internal class DefaultSyncService(
roomMembersService, roomMembersService,
roomDataSource, roomDataSource,
TimelineEventsProcessor( TimelineEventsProcessor(
RoomEventCreator(roomMembersService, logger, errorTracker), RoomEventCreator(roomMembersService, errorTracker, RoomEventFactory(roomMembersService)),
roomEventsDecrypter, roomEventsDecrypter,
eventDecrypter, eventDecrypter,
EventLookupUseCase(roomStore) EventLookupUseCase(roomStore)
@ -69,6 +69,7 @@ internal class DefaultSyncService(
roomRefresher, roomRefresher,
roomDataSource, roomDataSource,
logger, logger,
errorTracker,
coroutineDispatchers, coroutineDispatchers,
) )
SyncUseCase( SyncUseCase(
@ -114,7 +115,7 @@ internal class DefaultSyncService(
coroutineDispatchers.withIoContext { coroutineDispatchers.withIoContext {
roomIds.map { roomIds.map {
async { async {
roomRefresher.refreshRoomContent(it)?.also { roomRefresher.refreshRoomContent(it, credentialsStore.credentials()!!)?.also {
overviewStore.persist(listOf(it.roomOverview)) overviewStore.persist(listOf(it.roomOverview))
} }
} }

View File

@ -445,13 +445,55 @@ internal sealed class ApiTimelineEvent {
@SerialName("st.decryption_status") val decryptionStatus: DecryptionStatus? = null @SerialName("st.decryption_status") val decryptionStatus: DecryptionStatus? = null
) : ApiTimelineEvent() { ) : ApiTimelineEvent() {
@Serializable @Serializable(with = ApiTimelineMessageContentDeserializer::class)
internal data class Content( internal sealed interface Content {
@SerialName("body") val body: String? = null, val relation: Relation?
@SerialName("formatted_body") val formattedBody: String? = null,
@SerialName("msgtype") val type: String? = null, @Serializable
@SerialName("m.relates_to") val relation: Relation? = null, 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("url") val url: MxUrl? = null,
@SerialName("file") val file: File? = null,
@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,
@SerialName("iv") val iv: String,
@SerialName("v") val v: String,
@SerialName("hashes") val hashes: Map<String, String>,
@SerialName("key") val key: Key,
) {
@Serializable
data class Key(
@SerialName("k") val k: String,
)
}
@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 @Serializable
data class Relation( data class Relation(
@ -512,3 +554,28 @@ internal object EncryptedContentDeserializer : KSerializer<ApiEncryptedContent>
override fun serialize(encoder: Encoder, value: ApiEncryptedContent) = TODO("Not yet implemented") override fun serialize(encoder: Encoder, value: ApiEncryptedContent) = TODO("Not yet implemented")
} }
internal object ApiTimelineMessageContentDeserializer : KSerializer<ApiTimelineEvent.TimelineMessage.Content> {
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" -> 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)
}
}

View File

@ -2,6 +2,7 @@ package app.dapk.st.matrix.sync.internal.room
import app.dapk.st.matrix.common.* import app.dapk.st.matrix.common.*
import app.dapk.st.matrix.sync.RoomEvent 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 app.dapk.st.matrix.sync.internal.request.DecryptedContent
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -11,13 +12,38 @@ internal class RoomEventsDecrypter(
private val logger: MatrixLogger, private val logger: MatrixLogger,
) { ) {
suspend fun decryptRoomEvents(events: List<RoomEvent>) = events.map { event -> suspend fun decryptRoomEvents(userCredentials: UserCredentials, events: List<RoomEvent>) = events.map { event ->
when (event) { decryptEvent(event, userCredentials)
is RoomEvent.Message -> event.decrypt() }
is RoomEvent.Reply -> RoomEvent.Reply(
message = event.message.decrypt(), private suspend fun decryptEvent(event: RoomEvent, userCredentials: UserCredentials): RoomEvent = when (event) {
replyingTo = event.replyingTo.decrypt(), 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) ?: content.url!!.convertMxUrToUrl(userCredentials.homeServer),
keys = content.file?.let { RoomEvent.Image.ImageMeta.Keys(it.key.k, it.iv, it.v, it.hashes) }
),
encryptedContent = null,
)
}
}
} }
} }
@ -35,10 +61,9 @@ internal class RoomEventsDecrypter(
private fun JsonString.toModel() = json.decodeFromString(DecryptedContent.serializer(), this.value) private fun JsonString.toModel() = json.decodeFromString(DecryptedContent.serializer(), this.value)
private fun RoomEvent.Message.copyWithDecryptedContent(decryptedContent: DecryptedContent.TimelineText) = this.copy( 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 encryptedContent = null
) )
} }
private fun RoomEvent.Message.MegOlmV1.toModel() = EncryptedMessageContent.MegOlmV1( private fun RoomEvent.Message.MegOlmV1.toModel() = EncryptedMessageContent.MegOlmV1(

View File

@ -39,7 +39,11 @@ internal class SyncEventDecrypter(
is DecryptedContent.TimelineText -> ApiTimelineEvent.TimelineMessage( is DecryptedContent.TimelineText -> ApiTimelineEvent.TimelineMessage(
event.eventId, event.eventId,
event.senderId, 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, event.utcTimestamp,
).also { logger.matrixLog("decrypted to timeline text: $it") } ).also { logger.matrixLog("decrypted to timeline text: $it") }
DecryptedContent.Ignored -> event DecryptedContent.Ignored -> event

View File

@ -4,9 +4,8 @@ import app.dapk.st.core.extensions.ErrorTracker
import app.dapk.st.core.extensions.ifOrNull import app.dapk.st.core.extensions.ifOrNull
import app.dapk.st.core.extensions.nullAndTrack import app.dapk.st.core.extensions.nullAndTrack
import app.dapk.st.matrix.common.EventId 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.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.MessageMeta
import app.dapk.st.matrix.sync.RoomEvent import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.matrix.sync.RoomMembersService import app.dapk.st.matrix.sync.RoomMembersService
@ -18,8 +17,8 @@ private typealias Lookup = suspend (EventId) -> LookupResult
internal class RoomEventCreator( internal class RoomEventCreator(
private val roomMembersService: RoomMembersService, private val roomMembersService: RoomMembersService,
private val logger: MatrixLogger,
private val errorTracker: ErrorTracker, private val errorTracker: ErrorTracker,
private val roomEventFactory: RoomEventFactory,
) { ) {
suspend fun ApiTimelineEvent.Encrypted.toRoomEvent(roomId: RoomId): RoomEvent? { 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 { return when {
this.isEdit() -> handleEdit(roomId, this.content.relation!!.eventId!!, lookup) event.content == ApiTimelineEvent.TimelineMessage.Content.Ignored -> null
this.isReply() -> handleReply(roomId, lookup) event.isEdit() -> event.handleEdit(editedEventId = event.content.relation!!.eventId!!, lookup)
else -> this.toMessage(roomId) 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? { private suspend fun ApiTimelineEvent.TimelineMessage.handleReply(replyToId: EventId, lookup: Lookup): RoomEvent {
return lookup(editedEventId).fold( val relationEvent = lookup(replyToId).fold(
onApiTimelineEvent = { onApiTimelineEvent = { it.toTextMessage() },
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) },
onRoomEvent = { it }, onRoomEvent = { it },
onEmpty = { null } onEmpty = { null }
) )
logger.matrixLog("found relation: $relationEvent")
return when (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 -> { else -> {
RoomEvent.Reply( RoomEvent.Reply(
message = this.toMessage(roomId, content = this.content.formattedBody?.stripTags() ?: "redacted"), message = roomEventFactory.mapToRoomEvent(this),
replyingTo = when (relationEvent) { replyingTo = when (relationEvent) {
is RoomEvent.Message -> relationEvent is RoomEvent.Message -> relationEvent
is RoomEvent.Reply -> relationEvent.message is RoomEvent.Reply -> relationEvent.message
is RoomEvent.Image -> relationEvent
} }
) )
} }
} }
} }
private suspend fun ApiTimelineEvent.TimelineMessage.toMessage( private suspend fun ApiTimelineEvent.TimelineMessage.toFallbackTextMessage() = this.toTextMessage(content = this.asTextContent().body ?: "redacted")
roomId: RoomId,
content: String = this.content.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, edited: Boolean = false,
utcTimestamp: Long = this.utcTimestamp, utcTimestamp: Long = this.utcTimestamp,
) = RoomEvent.Message( ) = with(roomEventFactory) { toTextMessage(roomId, content, edited, utcTimestamp) }
eventId = this.id,
content = content, private suspend fun ApiTimelineEvent.TimelineMessage.toImageMessage(
author = roomMembersService.find(roomId, this.senderId)!!, edited: Boolean = false,
utcTimestamp = utcTimestamp, utcTimestamp: Long = this.utcTimestamp,
meta = MessageMeta.FromServer, ) = with(roomEventFactory) { toImageMessage(userCredentials, roomId, edited, utcTimestamp) }
edited = edited,
)
} }
@ -128,5 +167,6 @@ private fun String.stripTags() = this.substring(this.indexOf("</mx-reply>") + "<
.replace("<em>", "") .replace("<em>", "")
.replace("</em>", "") .replace("</em>", "")
private fun ApiTimelineEvent.TimelineMessage.isEdit() = this.content.relation?.relationType == "m.replace" && this.content.relation.eventId != null 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.isReply() = this.content.relation?.inReplyTo != null
private fun ApiTimelineEvent.TimelineMessage.asTextContent() = this.content as ApiTimelineEvent.TimelineMessage.Content.Text

View File

@ -0,0 +1,61 @@
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) ?: content.url!!.convertMxUrToUrl(userCredentials.homeServer),
keys = content.file?.let { RoomEvent.Image.ImageMeta.Keys(it.key.k, it.iv, it.v, it.hashes) }
)
}
}
private fun String.stripTags() = this.substring(this.indexOf("</mx-reply>") + "</mx-reply>".length)
.trim()
.replace("<em>", "")
.replace("</em>", "")
private fun ApiTimelineEvent.TimelineMessage.asTextContent() = this.content as ApiTimelineEvent.TimelineMessage.Content.Text

View File

@ -1,9 +1,6 @@
package app.dapk.st.matrix.sync.internal.sync package app.dapk.st.matrix.sync.internal.sync
import app.dapk.st.matrix.common.MatrixLogTag import app.dapk.st.matrix.common.*
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.sync.RoomEvent import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.matrix.sync.RoomState import app.dapk.st.matrix.sync.RoomState
import app.dapk.st.matrix.sync.internal.room.RoomEventsDecrypter import app.dapk.st.matrix.sync.internal.room.RoomEventsDecrypter
@ -14,13 +11,13 @@ internal class RoomRefresher(
private val logger: MatrixLogger 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") logger.matrixLog(MatrixLogTag.SYNC, "reducing side effect: $roomId")
return when (val previousState = roomDataSource.read(roomId)) { return when (val previousState = roomDataSource.read(roomId)) {
null -> null.also { logger.matrixLog(MatrixLogTag.SYNC, "no previous state to update") } null -> null.also { logger.matrixLog(MatrixLogTag.SYNC, "no previous state to update") }
else -> { else -> {
logger.matrixLog(MatrixLogTag.SYNC, "previous state updated") 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() val lastMessage = decryptedEvents.sortedByDescending { it.utcTimestamp }.findLastMessage()
previousState.copy(events = decryptedEvents, roomOverview = previousState.roomOverview.copy(lastMessage = lastMessage)).also { previousState.copy(events = decryptedEvents, roomOverview = previousState.roomOverview.copy(lastMessage = lastMessage)).also {
@ -30,6 +27,6 @@ internal class RoomRefresher(
} }
} }
private suspend fun List<RoomEvent>.decryptEvents() = roomEventsDecrypter.decryptRoomEvents(this) private suspend fun List<RoomEvent>.decryptEvents(userCredentials: UserCredentials) = roomEventsDecrypter.decryptRoomEvents(userCredentials, this)
} }

View File

@ -1,6 +1,7 @@
package app.dapk.st.matrix.sync.internal.sync package app.dapk.st.matrix.sync.internal.sync
import app.dapk.st.core.CoroutineDispatchers import app.dapk.st.core.CoroutineDispatchers
import app.dapk.st.core.extensions.ErrorTracker
import app.dapk.st.core.withIoContextAsync import app.dapk.st.core.withIoContextAsync
import app.dapk.st.matrix.common.* import app.dapk.st.matrix.common.*
import app.dapk.st.matrix.common.MatrixLogTag.SYNC import app.dapk.st.matrix.common.MatrixLogTag.SYNC
@ -16,6 +17,7 @@ internal class SyncReducer(
private val roomRefresher: RoomRefresher, private val roomRefresher: RoomRefresher,
private val roomDataSource: RoomDataSource, private val roomDataSource: RoomDataSource,
private val logger: MatrixLogger, private val logger: MatrixLogger,
private val errorTracker: ErrorTracker,
private val coroutineDispatchers: CoroutineDispatchers, private val coroutineDispatchers: CoroutineDispatchers,
) { ) {
@ -48,14 +50,14 @@ internal class SyncReducer(
isInitialSync = isInitialSync isInitialSync = isInitialSync
) )
} }
.onFailure { logger.matrixLog(SYNC, "failed to reduce: $roomId, skipping") } .onFailure { errorTracker.track(it, "failed to reduce: $roomId, skipping") }
.getOrNull() .getOrNull()
} }
} ?: emptyList() } ?: emptyList()
val roomsWithSideEffects = sideEffects.roomsToRefresh(alreadyHandledRooms = apiUpdatedRooms?.keys ?: emptySet()).map { roomId -> val roomsWithSideEffects = sideEffects.roomsToRefresh(alreadyHandledRooms = apiUpdatedRooms?.keys ?: emptySet()).map { roomId ->
coroutineDispatchers.withIoContextAsync { coroutineDispatchers.withIoContextAsync {
roomRefresher.refreshRoomContent(roomId) roomRefresher.refreshRoomContent(roomId, userCredentials)
} }
} }

View File

@ -1,5 +1,6 @@
package app.dapk.st.matrix.sync.internal.sync 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.RoomEvent
import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent
import app.dapk.st.matrix.sync.internal.room.RoomEventsDecrypter import app.dapk.st.matrix.sync.internal.room.RoomEventsDecrypter
@ -22,13 +23,13 @@ internal class TimelineEventsProcessor(
private suspend fun processNewEvents(roomToProcess: RoomToProcess, previousEvents: List<RoomEvent>): List<RoomEvent> { private suspend fun processNewEvents(roomToProcess: RoomToProcess, previousEvents: List<RoomEvent>): List<RoomEvent> {
val decryptedTimeline = roomToProcess.apiSyncRoom.timeline.apiTimelineEvents.decryptEvents() val decryptedTimeline = roomToProcess.apiSyncRoom.timeline.apiTimelineEvents.decryptEvents()
val decryptedPreviousEvents = previousEvents.decryptEvents() val decryptedPreviousEvents = previousEvents.decryptEvents(roomToProcess.userCredentials)
val newEvents = with(roomEventCreator) { val newEvents = with(roomEventCreator) {
decryptedTimeline.value.mapNotNull { event -> decryptedTimeline.value.mapNotNull { event ->
val roomEvent = when (event) { val roomEvent = when (event) {
is ApiTimelineEvent.Encrypted -> event.toRoomEvent(roomToProcess.roomId) 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) eventLookupUseCase.lookup(eventId, decryptedTimeline, decryptedPreviousEvents)
} }
is ApiTimelineEvent.Encryption -> null is ApiTimelineEvent.Encryption -> null
@ -46,7 +47,8 @@ internal class TimelineEventsProcessor(
} }
private suspend fun List<ApiTimelineEvent>.decryptEvents() = DecryptedTimeline(eventDecrypter.decryptTimelineEvents(this)) private suspend fun List<ApiTimelineEvent>.decryptEvents() = DecryptedTimeline(eventDecrypter.decryptTimelineEvents(this))
private suspend fun List<RoomEvent>.decryptEvents() = DecryptedRoomEvents(roomEventsDecrypter.decryptRoomEvents(this)) private suspend fun List<RoomEvent>.decryptEvents(userCredentials: UserCredentials) =
DecryptedRoomEvents(roomEventsDecrypter.decryptRoomEvents(userCredentials, this))
} }

View File

@ -42,6 +42,7 @@ internal class UnreadEventsUseCase(
when (it) { when (it) {
is RoomEvent.Message -> it.author.id == selfId is RoomEvent.Message -> it.author.id == selfId
is RoomEvent.Reply -> it.message.author.id == selfId is RoomEvent.Reply -> it.message.author.id == selfId
is RoomEvent.Image -> it.author.id == selfId
} }
}.map { it.eventId } }.map { it.eventId }
roomStore.insertUnread(overview.roomId, eventsFromOthers) roomStore.insertUnread(overview.roomId, eventsFromOthers)

View File

@ -21,6 +21,7 @@ private val AN_ENCRYPTED_ROOM_REPLY = aRoomReplyMessageEvent(
replyingTo = AN_ENCRYPTED_ROOM_MESSAGE.copy(eventId = anEventId("other-event")) 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_DECRYPTED_CONTENT = DecryptedContent.TimelineText(aTimelineTextEventContent(body = A_DECRYPTED_MESSAGE_CONTENT))
private val A_USER_CREDENTIALS = aUserCredentials()
class RoomEventsDecrypterTest { class RoomEventsDecrypterTest {
@ -35,7 +36,7 @@ class RoomEventsDecrypterTest {
@Test @Test
fun `given clear message event, when decrypting, then does nothing`() = runTest { fun `given clear message event, when decrypting, then does nothing`() = runTest {
val aClearMessageEvent = aRoomMessageEvent(encryptedContent = null) val aClearMessageEvent = aRoomMessageEvent(encryptedContent = null)
val result = roomEventsDecrypter.decryptRoomEvents(listOf(aClearMessageEvent)) val result = roomEventsDecrypter.decryptRoomEvents(A_USER_CREDENTIALS, listOf(aClearMessageEvent))
result shouldBeEqualTo 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 { 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) 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)) 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 { fun `given encrypted reply event, when decrypting, then decrypts message and replyTo`() = runTest {
givenEncryptedReply(AN_ENCRYPTED_ROOM_REPLY, decryptsTo = A_DECRYPTED_CONTENT) 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( result shouldBeEqualTo listOf(
AN_ENCRYPTED_ROOM_REPLY.copy( AN_ENCRYPTED_ROOM_REPLY.copy(
message = AN_ENCRYPTED_ROOM_REPLY.message.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.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) { private fun givenEncryptedMessage(roomMessage: RoomEvent.Message, decryptsTo: DecryptedContent) {
val model = roomMessage.encryptedContent!!.toModel() val model = roomMessage.encryptedContent!!.toModel()
fakeMessageDecrypter.givenDecrypt(model) 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) { private fun givenEncryptedReply(roomReply: RoomEvent.Reply, decryptsTo: DecryptedContent) {
givenEncryptedMessage(roomReply.message, decryptsTo) givenEncryptedMessage(roomReply.message as RoomEvent.Message, decryptsTo)
givenEncryptedMessage(roomReply.replyingTo, decryptsTo) givenEncryptedMessage(roomReply.replyingTo as RoomEvent.Message, decryptsTo)
} }
} }

View File

@ -26,12 +26,13 @@ private val A_TEXT_EVENT_WITHOUT_CONTENT = anApiTimelineTextEvent(
senderId = A_SENDER.id, senderId = A_SENDER.id,
content = aTimelineTextEventContent(body = null) content = aTimelineTextEventContent(body = null)
) )
private val A_USER_CREDENTIALS = aUserCredentials()
internal class RoomEventCreatorTest { internal class RoomEventCreatorTest {
private val fakeRoomMembersService = FakeRoomMembersService() private val fakeRoomMembersService = FakeRoomMembersService()
private val roomEventCreator = RoomEventCreator(fakeRoomMembersService, FakeMatrixLogger(), FakeErrorTracker()) private val roomEventCreator = RoomEventCreator(fakeRoomMembersService, FakeErrorTracker(), RoomEventFactory(fakeRoomMembersService))
@Test @Test
fun `given Megolm encrypted event then maps to encrypted room message`() = runTest { 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 { fun `given text event then maps to room message`() = runTest {
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER) 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( result shouldBeEqualTo aRoomMessageEvent(
eventId = A_TEXT_EVENT.id, 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 { fun `given text event without body then maps to redacted room message`() = runTest {
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER) 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( result shouldBeEqualTo aRoomMessageEvent(
eventId = A_TEXT_EVENT_WITHOUT_CONTENT.id, eventId = A_TEXT_EVENT_WITHOUT_CONTENT.id,
@ -100,12 +101,12 @@ internal class RoomEventCreatorTest {
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER) fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
val editEvent = anApiTimelineTextEvent().toEditEvent(newTimestamp = 0, messageContent = A_TEXT_EVENT_MESSAGE) 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( result shouldBeEqualTo aRoomMessageEvent(
eventId = editEvent.id, eventId = editEvent.id,
utcTimestamp = editEvent.utcTimestamp, utcTimestamp = editEvent.utcTimestamp,
content = editEvent.content.body!!, content = editEvent.asTextContent().body!!,
author = A_SENDER, author = A_SENDER,
edited = true edited = true
) )
@ -118,7 +119,7 @@ internal class RoomEventCreatorTest {
val editedMessage = originalMessage.toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE) val editedMessage = originalMessage.toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE)
val lookup = givenLookup(originalMessage) 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( result shouldBeEqualTo aRoomMessageEvent(
eventId = originalMessage.id, eventId = originalMessage.id,
@ -136,7 +137,7 @@ internal class RoomEventCreatorTest {
val editedMessage = originalMessage.toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE) val editedMessage = originalMessage.toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE)
val lookup = givenLookup(originalMessage) 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( result shouldBeEqualTo aRoomMessageEvent(
eventId = originalMessage.eventId, 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 { 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) fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
val originalMessage = aRoomReplyMessageEvent(message = aRoomMessageEvent()) 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 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( result shouldBeEqualTo aRoomReplyMessageEvent(
replyingTo = originalMessage.replyingTo, replyingTo = originalMessage.replyingTo,
@ -174,7 +175,7 @@ internal class RoomEventCreatorTest {
val editedMessage = originalMessage.toEditEvent(newTimestamp = 0, messageContent = A_TEXT_EVENT_MESSAGE) val editedMessage = originalMessage.toEditEvent(newTimestamp = 0, messageContent = A_TEXT_EVENT_MESSAGE)
val lookup = givenLookup(originalMessage) 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 result shouldBeEqualTo null
} }
@ -185,22 +186,23 @@ internal class RoomEventCreatorTest {
val editedMessage = originalMessage.toEditEvent(newTimestamp = 0, messageContent = A_TEXT_EVENT_MESSAGE) val editedMessage = originalMessage.toEditEvent(newTimestamp = 0, messageContent = A_TEXT_EVENT_MESSAGE)
val lookup = givenLookup(originalMessage) 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 result shouldBeEqualTo null
} }
@Test @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) fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
val replyEvent = anApiTimelineTextEvent().toReplyEvent(messageContent = A_TEXT_EVENT_MESSAGE) 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( result shouldBeEqualTo aRoomMessageEvent(
eventId = replyEvent.id, eventId = replyEvent.id,
utcTimestamp = replyEvent.utcTimestamp, utcTimestamp = replyEvent.utcTimestamp,
content = "${replyEvent.content.body}", content = replyEvent.asTextContent().body!!,
author = A_SENDER, author = A_SENDER,
) )
} }
@ -212,13 +214,13 @@ internal class RoomEventCreatorTest {
val replyMessage = originalMessage.toReplyEvent(messageContent = A_REPLY_EVENT_MESSAGE) val replyMessage = originalMessage.toReplyEvent(messageContent = A_REPLY_EVENT_MESSAGE)
val lookup = givenLookup(originalMessage) 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( result shouldBeEqualTo aRoomReplyMessageEvent(
replyingTo = aRoomMessageEvent( replyingTo = aRoomMessageEvent(
eventId = originalMessage.id, eventId = originalMessage.id,
utcTimestamp = originalMessage.utcTimestamp, utcTimestamp = originalMessage.utcTimestamp,
content = originalMessage.content.body!!, content = originalMessage.asTextContent().body!!,
author = A_SENDER, author = A_SENDER,
), ),
message = aRoomMessageEvent( message = aRoomMessageEvent(
@ -237,7 +239,7 @@ internal class RoomEventCreatorTest {
val replyMessage = originalMessage.toReplyEvent(messageContent = A_REPLY_EVENT_MESSAGE) val replyMessage = originalMessage.toReplyEvent(messageContent = A_REPLY_EVENT_MESSAGE)
val lookup = givenLookup(originalMessage) 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( result shouldBeEqualTo aRoomReplyMessageEvent(
replyingTo = originalMessage, 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 { 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) fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
val originalMessage = aRoomReplyMessageEvent() 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 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( result shouldBeEqualTo aRoomReplyMessageEvent(
replyingTo = originalMessage.message, replyingTo = originalMessage.message,
@ -326,4 +328,6 @@ private fun ApiEncryptedContent.toMegolm(): RoomEvent.Message.MegOlmV1 {
private class FakeLookup(private val result: LookupResult) : suspend (EventId) -> LookupResult { private class FakeLookup(private val result: LookupResult) : suspend (EventId) -> LookupResult {
override suspend fun invoke(p1: EventId) = result override suspend fun invoke(p1: EventId) = result
} }
private fun ApiTimelineEvent.TimelineMessage.asTextContent() = this.content as ApiTimelineEvent.TimelineMessage.Content.Text

View File

@ -22,6 +22,7 @@ private object ARoom {
val DECRYPTED_EVENTS = listOf(MESSAGE_EVENT, DECRYPTED_EVENT) val DECRYPTED_EVENTS = listOf(MESSAGE_EVENT, DECRYPTED_EVENT)
val NEW_STATE = RoomState(aRoomOverview(lastMessage = DECRYPTED_EVENT.asLastMessage()), DECRYPTED_EVENTS) val NEW_STATE = RoomState(aRoomOverview(lastMessage = DECRYPTED_EVENT.asLastMessage()), DECRYPTED_EVENTS)
} }
private val A_USER_CREDENTIALS = aUserCredentials()
internal class RoomRefresherTest { internal class RoomRefresherTest {
@ -38,7 +39,7 @@ internal class RoomRefresherTest {
fun `given no existing room when refreshing then does nothing`() = runTest { fun `given no existing room when refreshing then does nothing`() = runTest {
fakeRoomDataSource.givenNoCachedRoom(A_ROOM_ID) fakeRoomDataSource.givenNoCachedRoom(A_ROOM_ID)
val result = roomRefresher.refreshRoomContent(aRoomId()) val result = roomRefresher.refreshRoomContent(aRoomId(), A_USER_CREDENTIALS)
result shouldBeEqualTo null result shouldBeEqualTo null
fakeRoomDataSource.verifyNoChanges() fakeRoomDataSource.verifyNoChanges()
@ -48,9 +49,9 @@ internal class RoomRefresherTest {
fun `given existing room when refreshing then processes existing state`() = runTest { fun `given existing room when refreshing then processes existing state`() = runTest {
fakeRoomDataSource.expect { it.instance.persist(RoomId(any()), any(), any()) } fakeRoomDataSource.expect { it.instance.persist(RoomId(any()), any(), any()) }
fakeRoomDataSource.givenRoom(A_ROOM_ID, ARoom.PREVIOUS_STATE) 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) fakeRoomDataSource.verifyRoomUpdated(ARoom.PREVIOUS_STATE, ARoom.NEW_STATE)
result shouldBeEqualTo ARoom.NEW_STATE result shouldBeEqualTo ARoom.NEW_STATE

View File

@ -22,6 +22,7 @@ private val A_TEXT_TIMELINE_EVENT = anApiTimelineTextEvent()
private val A_MESSAGE_ROOM_EVENT = aRoomMessageEvent(anEventId("a-message")) private val A_MESSAGE_ROOM_EVENT = aRoomMessageEvent(anEventId("a-message"))
private val AN_ENCRYPTED_ROOM_EVENT = anEncryptedRoomMessageEvent(anEventId("encrypted-message")) private val AN_ENCRYPTED_ROOM_EVENT = anEncryptedRoomMessageEvent(anEventId("encrypted-message"))
private val A_LOOKUP_EVENT_ID = anEventId("lookup-id") private val A_LOOKUP_EVENT_ID = anEventId("lookup-id")
private val A_USER_CREDENTIALS = aUserCredentials()
class TimelineEventsProcessorTest { class TimelineEventsProcessorTest {
@ -41,7 +42,7 @@ class TimelineEventsProcessorTest {
fun `given a room with no events then returns empty`() = runTest { fun `given a room with no events then returns empty`() = runTest {
val previousEvents = emptyList<RoomEvent>() val previousEvents = emptyList<RoomEvent>()
val roomToProcess = aRoomToProcess() val roomToProcess = aRoomToProcess()
fakeRoomEventsDecrypter.givenDecrypts(previousEvents) fakeRoomEventsDecrypter.givenDecrypts(A_USER_CREDENTIALS, previousEvents)
fakeSyncEventDecrypter.givenDecrypts(roomToProcess.apiSyncRoom.timeline.apiTimelineEvents) fakeSyncEventDecrypter.givenDecrypts(roomToProcess.apiSyncRoom.timeline.apiTimelineEvents)
val result = timelineEventsProcessor.process(roomToProcess, previousEvents) val result = timelineEventsProcessor.process(roomToProcess, previousEvents)
@ -54,11 +55,18 @@ class TimelineEventsProcessorTest {
val previousEvents = listOf(aRoomMessageEvent(eventId = anEventId("previous-event"))) val previousEvents = listOf(aRoomMessageEvent(eventId = anEventId("previous-event")))
val newTimelineEvents = listOf(AN_ENCRYPTED_TIMELINE_EVENT, A_TEXT_TIMELINE_EVENT) val newTimelineEvents = listOf(AN_ENCRYPTED_TIMELINE_EVENT, A_TEXT_TIMELINE_EVENT)
val roomToProcess = aRoomToProcess(apiSyncRoom = anApiSyncRoom(anApiSyncRoomTimeline(newTimelineEvents))) val roomToProcess = aRoomToProcess(apiSyncRoom = anApiSyncRoom(anApiSyncRoomTimeline(newTimelineEvents)))
fakeRoomEventsDecrypter.givenDecrypts(previousEvents) fakeRoomEventsDecrypter.givenDecrypts(A_USER_CREDENTIALS, previousEvents)
fakeSyncEventDecrypter.givenDecrypts(newTimelineEvents) fakeSyncEventDecrypter.givenDecrypts(newTimelineEvents)
fakeEventLookup.givenLookup(A_LOOKUP_EVENT_ID, DecryptedTimeline(newTimelineEvents), DecryptedRoomEvents(previousEvents), ANY_LOOKUP_RESULT) 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.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) val result = timelineEventsProcessor.process(roomToProcess, previousEvents)
@ -79,7 +87,7 @@ class TimelineEventsProcessorTest {
anIgnoredApiTimelineEvent() anIgnoredApiTimelineEvent()
) )
val roomToProcess = aRoomToProcess(apiSyncRoom = anApiSyncRoom(anApiSyncRoomTimeline(newTimelineEvents))) val roomToProcess = aRoomToProcess(apiSyncRoom = anApiSyncRoom(anApiSyncRoomTimeline(newTimelineEvents)))
fakeRoomEventsDecrypter.givenDecrypts(previousEvents) fakeRoomEventsDecrypter.givenDecrypts(A_USER_CREDENTIALS, previousEvents)
fakeSyncEventDecrypter.givenDecrypts(newTimelineEvents) fakeSyncEventDecrypter.givenDecrypts(newTimelineEvents)
val result = timelineEventsProcessor.process(roomToProcess, previousEvents) val result = timelineEventsProcessor.process(roomToProcess, previousEvents)

View File

@ -2,6 +2,7 @@ package internalfake
import app.dapk.st.matrix.common.EventId import app.dapk.st.matrix.common.EventId
import app.dapk.st.matrix.common.RoomId 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.RoomEvent
import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent
import app.dapk.st.matrix.sync.internal.sync.LookupResult import app.dapk.st.matrix.sync.internal.sync.LookupResult
@ -18,9 +19,16 @@ internal class FakeRoomEventCreator {
coEvery { with(instance) { event.toRoomEvent(roomId) } } returns result 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<suspend (EventId) -> LookupResult>() val slot = slot<suspend (EventId) -> LookupResult>()
coEvery { with(instance) { event.toRoomEvent(roomId, capture(slot)) } } answers { coEvery { with(instance) { event.toRoomEvent(userCredentials, roomId, capture(slot)) } } answers {
runBlocking { runBlocking {
if (slot.captured.invoke(eventIdToLookup) == lookupResult) { if (slot.captured.invoke(eventIdToLookup) == lookupResult) {
result result

View File

@ -1,5 +1,6 @@
package internalfake package internalfake
import app.dapk.st.matrix.common.UserCredentials
import app.dapk.st.matrix.sync.RoomEvent import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.matrix.sync.internal.room.RoomEventsDecrypter import app.dapk.st.matrix.sync.internal.room.RoomEventsDecrypter
import io.mockk.coEvery import io.mockk.coEvery
@ -8,7 +9,7 @@ import io.mockk.mockk
internal class FakeRoomEventsDecrypter { internal class FakeRoomEventsDecrypter {
val instance = mockk<RoomEventsDecrypter>() val instance = mockk<RoomEventsDecrypter>()
fun givenDecrypts(previousEvents: List<RoomEvent>, result: List<RoomEvent> = previousEvents) { fun givenDecrypts(userCredentials: UserCredentials, previousEvents: List<RoomEvent>, result: List<RoomEvent> = previousEvents) {
coEvery { instance.decryptRoomEvents(previousEvents) } returns result coEvery { instance.decryptRoomEvents(userCredentials, previousEvents) } returns result
} }
} }

View File

@ -39,9 +39,8 @@ internal fun anApiTimelineTextEvent(
internal fun aTimelineTextEventContent( internal fun aTimelineTextEventContent(
body: String? = null, body: String? = null,
formattedBody: String? = null, formattedBody: String? = null,
type: String? = null,
relation: ApiTimelineEvent.TimelineMessage.Relation? = 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( internal fun anEditRelation(originalId: EventId) = ApiTimelineEvent.TimelineMessage.Relation(
relationType = "m.replace", relationType = "m.replace",

View File

@ -15,8 +15,8 @@ fun aRoomMessageEvent(
) = RoomEvent.Message(eventId, utcTimestamp, content, author, meta, encryptedContent, edited) ) = RoomEvent.Message(eventId, utcTimestamp, content, author, meta, encryptedContent, edited)
fun aRoomReplyMessageEvent( fun aRoomReplyMessageEvent(
message: RoomEvent.Message = aRoomMessageEvent(), message: RoomEvent = aRoomMessageEvent(),
replyingTo: RoomEvent.Message = aRoomMessageEvent(eventId = anEventId("in-reply-to-id")), replyingTo: RoomEvent = aRoomMessageEvent(eventId = anEventId("in-reply-to-id")),
) = RoomEvent.Reply(message, replyingTo) ) = RoomEvent.Reply(message, replyingTo)
fun anEncryptedRoomMessageEvent( fun anEncryptedRoomMessageEvent(