diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneMigrator.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneMigrator.kt index 2022a62263f..4321959532c 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneMigrator.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneMigrator.kt @@ -89,6 +89,11 @@ internal class OneOnOneMigratorImpl( return getResolvedMLSOneOnOne(user.id) .flatMap { mlsConversation -> if (user.activeOneOnOneConversationId == mlsConversation) { + kaliumLogger.d( + "active one-on-one already resolved to MLS " + + "${mlsConversation.toLogString()}, " + + "user = ${user.id.toLogString()}" + ) return@flatMap Either.Right(mlsConversation) } @@ -136,6 +141,10 @@ internal class OneOnOneMigratorImpl( // We can theoretically have more than one proteus 1-1 conversation with // team members since there was no backend safeguards against this proteusOneOnOneConversations.foldToEitherWhileRight(Unit) { proteusOneOnOneConversation, _ -> + kaliumLogger.d( + "migrating proteus ${proteusOneOnOneConversation.toLogString()} " + + "to MLS conv ${targetConversation.toLogString()}" + ) messageRepository.moveMessagesToAnotherConversation( originalConversation = proteusOneOnOneConversation, targetConversation = targetConversation diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Messages.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Messages.sq index 90308374887..df57e78e627 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Messages.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Messages.sq @@ -91,7 +91,7 @@ CREATE TABLE MessageRestrictedAssetContent ( asset_size INTEGER NOT NULL, asset_name TEXT NOT NULL, - FOREIGN KEY (message_id, conversation_id) REFERENCES Message(id, conversation_id) ON DELETE CASCADE, + FOREIGN KEY (message_id, conversation_id) REFERENCES Message(id, conversation_id) ON DELETE CASCADE ON UPDATE CASCADE, PRIMARY KEY (message_id, conversation_id) ); diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Reactions.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Reactions.sq index 50e7c3a23bb..983d9c1ce71 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Reactions.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Reactions.sq @@ -6,7 +6,7 @@ CREATE TABLE Reaction ( sender_id TEXT AS QualifiedIDEntity NOT NULL, emoji TEXT NOT NULL, date TEXT NOT NULL, - FOREIGN KEY (message_id, conversation_id) REFERENCES Message(id, conversation_id) ON DELETE CASCADE, + FOREIGN KEY (message_id, conversation_id) REFERENCES Message(id, conversation_id) ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY (sender_id) REFERENCES User(qualified_id) ON DELETE CASCADE, PRIMARY KEY (message_id, conversation_id, sender_id, emoji) ); diff --git a/persistence/src/commonMain/db_user/migrations/94.sqm b/persistence/src/commonMain/db_user/migrations/94.sqm new file mode 100644 index 00000000000..386072d971b --- /dev/null +++ b/persistence/src/commonMain/db_user/migrations/94.sqm @@ -0,0 +1,47 @@ +CREATE TABLE Reaction_temp AS +SELECT * FROM Reaction; + +DROP TABLE Reaction; + +CREATE TABLE Reaction ( + message_id TEXT NOT NULL, + conversation_id TEXT AS QualifiedIDEntity NOT NULL, + sender_id TEXT AS QualifiedIDEntity NOT NULL, + emoji TEXT NOT NULL, + date TEXT NOT NULL, + FOREIGN KEY (message_id, conversation_id) REFERENCES Message(id, conversation_id) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (sender_id) REFERENCES User(qualified_id) ON DELETE CASCADE, + PRIMARY KEY (message_id, conversation_id, sender_id, emoji) +); + +INSERT INTO Reaction(message_id, conversation_id, sender_id, emoji, date) +SELECT message_id, conversation_id, sender_id, emoji, date +FROM Reaction_temp; + +DROP TABLE Reaction_temp; + +CREATE INDEX reaction_sender_index ON Reaction(sender_id); +CREATE INDEX reaction_emoji_index ON Reaction(emoji); + +CREATE TABLE MessageRestrictedAssetContent_temp AS +SELECT * FROM MessageRestrictedAssetContent; + +DROP TABLE MessageRestrictedAssetContent; + +CREATE TABLE MessageRestrictedAssetContent ( + message_id TEXT NOT NULL, + conversation_id TEXT AS QualifiedIDEntity NOT NULL, + + asset_mime_type TEXT NOT NULL, + asset_size INTEGER NOT NULL, + asset_name TEXT NOT NULL, + + FOREIGN KEY (message_id, conversation_id) REFERENCES Message(id, conversation_id) ON DELETE CASCADE ON UPDATE CASCADE, + PRIMARY KEY (message_id, conversation_id) +); + +INSERT INTO MessageRestrictedAssetContent(message_id, conversation_id, asset_mime_type, asset_size, asset_name) +SELECT message_id, conversation_id, asset_mime_type, asset_size, asset_name +FROM MessageRestrictedAssetContent_temp; + +DROP TABLE MessageRestrictedAssetContent_temp; diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOTest.kt index da4724ba739..d37c9999f4c 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOTest.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOTest.kt @@ -27,10 +27,12 @@ import com.wire.kalium.persistence.dao.asset.AssetEntity import com.wire.kalium.persistence.dao.asset.AssetTransferStatusEntity import com.wire.kalium.persistence.dao.conversation.ConversationDAO import com.wire.kalium.persistence.dao.conversation.ConversationEntity +import com.wire.kalium.persistence.dao.reaction.ReactionDAO import com.wire.kalium.persistence.dao.receipt.ReceiptDAO import com.wire.kalium.persistence.dao.receipt.ReceiptTypeEntity import com.wire.kalium.persistence.dao.unread.UnreadEventTypeEntity import com.wire.kalium.persistence.utils.IgnoreIOS +import com.wire.kalium.persistence.utils.stubs.allMessageEntities import com.wire.kalium.persistence.utils.stubs.newConversationEntity import com.wire.kalium.persistence.utils.stubs.newRegularMessageEntity import com.wire.kalium.persistence.utils.stubs.newSystemMessageEntity @@ -49,6 +51,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertIs import kotlin.test.assertNotNull +import kotlin.test.assertNull import kotlin.test.assertTrue import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.seconds @@ -61,6 +64,7 @@ class MessageDAOTest : BaseDatabaseTest() { private lateinit var userDAO: UserDAO private lateinit var receiptDao: ReceiptDAO private lateinit var assetDao: AssetDAO + private lateinit var reactionDao: ReactionDAO private val conversationEntity1 = newConversationEntity("Test1") private val conversationEntity2 = newConversationEntity("Test2") @@ -80,6 +84,7 @@ class MessageDAOTest : BaseDatabaseTest() { userDAO = db.userDAO receiptDao = db.receiptDAO assetDao = db.assetDAO + reactionDao = db.reactionDAO } @Test @@ -2344,6 +2349,32 @@ class MessageDAOTest : BaseDatabaseTest() { assertEquals(messages.size, assetStatuses.size) } + @Test + fun givenAllTypesOfMessages_whenMovingToAnotherConversation_thenItSucceeds() = runTest { + // Given + insertInitialData() + val messages = allMessageEntities(conversationId = conversationEntity1.id, senderUserId = userEntity1.id) + val firstEmoji = "🫡" + messageDAO.insertOrIgnoreMessages(messages) + reactionDao.insertReaction( + messages.first().id, + messages.first().conversationId, + userEntity1.id, + Instant.DISTANT_PAST, + firstEmoji + ) + + // When + val exception = kotlin.runCatching { + messageDAO.moveMessages(conversationEntity1.id, conversationEntity2.id) + }.exceptionOrNull() + + // Then + assertNull(exception, "Expected no exception but got: ${exception?.message}") + val result = messageDAO.getMessagesByConversationAndVisibility(conversationEntity2.id, 100, 0).first() + assertEquals(messages.size, result.size) + } + private suspend fun insertInitialData() { userDAO.upsertUsers(listOf(userEntity1, userEntity2)) conversationDAO.insertConversation( diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/utils/stubs/MessageStubs.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/utils/stubs/MessageStubs.kt index 5c2760274a7..31f973cf5f0 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/utils/stubs/MessageStubs.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/utils/stubs/MessageStubs.kt @@ -21,10 +21,13 @@ package com.wire.kalium.persistence.utils.stubs import com.wire.kalium.persistence.dao.QualifiedIDEntity import com.wire.kalium.persistence.dao.UserDetailsEntity import com.wire.kalium.persistence.dao.UserIDEntity +import com.wire.kalium.persistence.dao.conversation.ConversationEntity +import com.wire.kalium.persistence.dao.message.ButtonEntity import com.wire.kalium.persistence.dao.message.MessageEntity import com.wire.kalium.persistence.dao.message.MessageEntityContent import com.wire.kalium.persistence.dao.message.draft.MessageDraftEntity import kotlinx.datetime.Instant +import kotlin.random.Random @Suppress("LongParameterList") fun newRegularMessageEntity( @@ -93,3 +96,243 @@ fun newDraftMessageEntity( quotedMessageId: String? = null, selectedMentionList: List = emptyList() ) = MessageDraftEntity(conversationId, text, editMessageId, quotedMessageId, selectedMentionList) + +fun allMessageEntities( + conversationId: QualifiedIDEntity = QualifiedIDEntity("convId", "convDomain"), + senderUserId: QualifiedIDEntity, +): List { + return listOf( + newRegularMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage1", + content = MessageEntityContent.Text( + "@John @John", + linkPreview = listOf( + MessageEntity.LinkPreview( + "https://www.wire.com", + 0, + "https://www.wire.com", + "Wire", + "Wire is the most secure collaboration platform", + ) + ), + mentions = listOf( + MessageEntity.Mention(0, 4, QualifiedIDEntity("senderId", "senderDomain")), + MessageEntity.Mention(6, 10, QualifiedIDEntity("senderId", "senderDomain")) + ), + quotedMessageId = "testMessage2", + isQuoteVerified = true, + quotedMessage = null + ) + ), + + newRegularMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage2", + content = MessageEntityContent.Asset( + 1000, + assetName = "test name", + assetMimeType = "image/png", + assetOtrKey = byteArrayOf(1), + assetSha256Key = byteArrayOf(1), + assetId = "assetId", + assetToken = "", + assetDomain = "convDomain", + assetEncryptionAlgorithm = "", + assetWidth = null, + assetHeight = 0, + ), + ), + newRegularMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage3", + content = MessageEntityContent.Knock(false) + ), + newRegularMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage4", + content = MessageEntityContent.Location( + latitude = 42.0f, + longitude = -42.0f, + name = "someSecretLocation", + zoom = 20 + ) + ), + newRegularMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage5", + content = MessageEntityContent.Unknown(typeName = null, Random.nextBytes(1000)) + ), + newRegularMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage6", + content = MessageEntityContent.FailedDecryption( + null, + 333, + false, + QualifiedIDEntity("senderId", "senderDomain"), + "someClient" + ) + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage7", + content = MessageEntityContent.MLSWrongEpochWarning + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage8", + content = MessageEntityContent.MemberChange( + listOf(UserIDEntity("value", "domain")), + MessageEntity.MemberChangeType.REMOVED + ) + ), + newRegularMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage9", + content = MessageEntityContent.RestrictedAsset("", 0, "name") + ), + newRegularMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage10", + content = MessageEntityContent.Composite( + MessageEntityContent.Text("text"), + listOf( + ButtonEntity("text1", "id1", false), + ButtonEntity("tex2", "id2", false), + ButtonEntity("tex3", "id3", false), + ButtonEntity("tex4", "id4", false) + ) + ) + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage11", + content = MessageEntityContent.MissedCall + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage12", + content = MessageEntityContent.CryptoSessionReset + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage13", + content = MessageEntityContent.ConversationRenamed("newName") + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage14", + content = MessageEntityContent.TeamMemberRemoved("someUser") + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage15", + content = MessageEntityContent.NewConversationReceiptMode(true) + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage16", + content = MessageEntityContent.ConversationReceiptModeChanged(false) + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage17", + content = MessageEntityContent.ConversationMessageTimerChanged(6000) + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage18", + content = MessageEntityContent.ConversationProtocolChanged(ConversationEntity.Protocol.MIXED) + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage19", + content = MessageEntityContent.ConversationProtocolChangedDuringACall + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage20", + content = MessageEntityContent.HistoryLostProtocolChanged + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage21", + content = MessageEntityContent.HistoryLost + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage22", + content = MessageEntityContent.ConversationCreated + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage23", + content = MessageEntityContent.ConversationDegradedMLS + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage24", + content = MessageEntityContent.ConversationVerifiedMLS + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage25", + content = MessageEntityContent.ConversationDegradedProteus + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage26", + content = MessageEntityContent.ConversationVerifiedProteus + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage27", + content = MessageEntityContent.ConversationStartedUnverifiedWarning + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage28", + content = MessageEntityContent.Federation( + listOf("otherDomain"), + MessageEntity.FederationType.DELETE + ) + ), + newSystemMessageEntity( + conversationId = conversationId, + senderUserId = senderUserId, + id = "testMessage29", + content = MessageEntityContent.LegalHold( + listOf(QualifiedIDEntity("otherId", "otherDomain")), MessageEntity.LegalHoldType.ENABLED_FOR_MEMBERS + ) + ), + ) +}