diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/UserConfigRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/UserConfigRepository.kt index 231d532b370..57d6d3a43b7 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/UserConfigRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/UserConfigRepository.kt @@ -125,6 +125,8 @@ interface UserConfigRepository { suspend fun setCRLExpirationTime(url: String, timestamp: ULong) suspend fun getCRLExpirationTime(url: String): ULong? suspend fun observeCertificateExpirationTime(url: String): Flow> + suspend fun setShouldNotifyForRevokedCertificate(shouldNotify: Boolean) + suspend fun observeShouldNotifyForRevokedCertificate(): Flow> } @Suppress("TooManyFunctions") @@ -447,4 +449,10 @@ internal class UserConfigDataSource internal constructor( override suspend fun observeCertificateExpirationTime(url: String): Flow> = userConfigDAO.observeCertificateExpirationTime(url).wrapStorageRequest() + override suspend fun setShouldNotifyForRevokedCertificate(shouldNotify: Boolean) { + userConfigDAO.setShouldNotifyForRevokedCertificate(shouldNotify) + } + + override suspend fun observeShouldNotifyForRevokedCertificate(): Flow> = + userConfigDAO.observeShouldNotifyForRevokedCertificate().wrapStorageRequest() } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageRepository.kt index 8aa6514a047..a596e78cce4 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageRepository.kt @@ -201,12 +201,21 @@ internal interface MessageRepository { ): Either suspend fun getEphemeralMessagesMarkedForDeletion(): Either> + + suspend fun getEphemeralMessagesMarkedForEndDeletion(): Either> + suspend fun markSelfDeletionStartDate( conversationId: ConversationId, messageUuid: String, deletionStartDate: Instant ): Either + suspend fun markSelfDeletionEndDate( + conversationId: ConversationId, + messageUuid: String, + deletionEndDate: Instant + ): Either + suspend fun observeMessageVisibility( messageUuid: String, conversationId: ConversationId @@ -614,6 +623,13 @@ internal class MessageDataSource internal constructor ( messageDAO.getEphemeralMessagesMarkedForDeletion().map(messageMapper::fromEntityToMessage) } + override suspend fun getEphemeralMessagesMarkedForEndDeletion(): Either> = + wrapStorageRequest { + messageDAO + .getEphemeralMessagedMarkedForEndDeletion() + .map(messageMapper::fromEntityToMessage) + } + override suspend fun markSelfDeletionStartDate( conversationId: ConversationId, messageUuid: String, @@ -624,6 +640,16 @@ internal class MessageDataSource internal constructor ( } } + override suspend fun markSelfDeletionEndDate( + conversationId: ConversationId, + messageUuid: String, + deletionEndDate: Instant + ): Either { + return wrapStorageRequest { + messageDAO.updateSelfDeletionEndDate(conversationId.toDao(), messageUuid, deletionEndDate) + } + } + override suspend fun observeMessageVisibility( messageUuid: String, conversationId: ConversationId diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt index a70bee3be7b..bcf4ae7e67a 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt @@ -301,6 +301,8 @@ import com.wire.kalium.logic.feature.user.ObserveE2EIRequiredUseCase import com.wire.kalium.logic.feature.user.ObserveE2EIRequiredUseCaseImpl import com.wire.kalium.logic.feature.user.ObserveFileSharingStatusUseCase import com.wire.kalium.logic.feature.user.ObserveFileSharingStatusUseCaseImpl +import com.wire.kalium.logic.feature.user.e2ei.ObserveShouldNotifyForRevokedCertificateUseCase +import com.wire.kalium.logic.feature.user.e2ei.ObserveShouldNotifyForRevokedCertificateUseCaseImpl import com.wire.kalium.logic.feature.user.SyncContactsUseCase import com.wire.kalium.logic.feature.user.SyncContactsUseCaseImpl import com.wire.kalium.logic.feature.user.SyncSelfUserUseCase @@ -310,6 +312,8 @@ import com.wire.kalium.logic.feature.user.UpdateSupportedProtocolsAndResolveOneO import com.wire.kalium.logic.feature.user.UpdateSupportedProtocolsUseCase import com.wire.kalium.logic.feature.user.UpdateSupportedProtocolsUseCaseImpl import com.wire.kalium.logic.feature.user.UserScope +import com.wire.kalium.logic.feature.user.e2ei.MarkNotifyForRevokedCertificateAsNotifiedUseCase +import com.wire.kalium.logic.feature.user.e2ei.MarkNotifyForRevokedCertificateAsNotifiedUseCaseImpl import com.wire.kalium.logic.feature.user.guestroomlink.MarkGuestLinkFeatureFlagAsNotChangedUseCase import com.wire.kalium.logic.feature.user.guestroomlink.MarkGuestLinkFeatureFlagAsNotChangedUseCaseImpl import com.wire.kalium.logic.feature.user.guestroomlink.ObserveGuestRoomLinkFeatureFlagUseCase @@ -1630,6 +1634,7 @@ class UserSessionScope internal constructor( globalScope.serverConfigRepository, userStorage, userPropertyRepository, + messages.deleteEphemeralMessageEndDate, oneOnOneResolver, this, userScopedLogger @@ -1693,6 +1698,7 @@ class UserSessionScope internal constructor( val users: UserScope get() = UserScope( userRepository, + userConfigRepository, accountRepository, searchUserRepository, syncManager, @@ -1756,6 +1762,12 @@ class UserSessionScope internal constructor( val observeFileSharingStatus: ObserveFileSharingStatusUseCase get() = ObserveFileSharingStatusUseCaseImpl(userConfigRepository) + val observeShouldNotifyForRevokedCertificate: ObserveShouldNotifyForRevokedCertificateUseCase + by lazy { ObserveShouldNotifyForRevokedCertificateUseCaseImpl(userConfigRepository) } + + val markNotifyForRevokedCertificateAsNotified: MarkNotifyForRevokedCertificateAsNotifiedUseCase + by lazy { MarkNotifyForRevokedCertificateAsNotifiedUseCaseImpl(userConfigRepository) } + val markGuestLinkFeatureFlagAsNotChanged: MarkGuestLinkFeatureFlagAsNotChangedUseCase get() = MarkGuestLinkFeatureFlagAsNotChangedUseCaseImpl(userConfigRepository) @@ -1953,6 +1965,9 @@ class UserSessionScope internal constructor( launch { updateSelfClientCapabilityToLegalHoldConsent() } + launch { + users.observeCertificateRevocationForSelfClient() + } } fun onDestroy() { diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt index 08cf3b661dc..e9fd633e5f1 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt @@ -61,6 +61,7 @@ import com.wire.kalium.logic.feature.conversation.messagetimer.UpdateMessageTime import com.wire.kalium.logic.feature.conversation.mls.OneOnOneResolver import com.wire.kalium.logic.feature.message.MessageSender import com.wire.kalium.logic.feature.message.SendConfirmationUseCase +import com.wire.kalium.logic.feature.message.ephemeral.DeleteEphemeralMessagesAfterEndDateUseCase import com.wire.kalium.logic.feature.team.DeleteTeamConversationUseCase import com.wire.kalium.logic.feature.team.DeleteTeamConversationUseCaseImpl import com.wire.kalium.logic.sync.SyncManager @@ -90,6 +91,7 @@ class ConversationScope internal constructor( private val serverConfigRepository: ServerConfigRepository, private val userStorage: UserStorage, userPropertyRepository: UserPropertyRepository, + private val deleteEphemeralMessageEndDate: DeleteEphemeralMessagesAfterEndDateUseCase, private val oneOnOneResolver: OneOnOneResolver, private val scope: CoroutineScope, private val kaliumLogger: KaliumLogger @@ -123,6 +125,7 @@ class ConversationScope internal constructor( get() = NotifyConversationIsOpenUseCaseImpl( oneOnOneResolver, conversationRepository, + deleteEphemeralMessageEndDate, kaliumLogger ) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/NotifyConversationIsOpenUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/NotifyConversationIsOpenUseCase.kt index fd8f7f8f3c9..ad2575e020b 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/NotifyConversationIsOpenUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/NotifyConversationIsOpenUseCase.kt @@ -22,6 +22,7 @@ import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.conversation.ConversationRepository import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.feature.conversation.mls.OneOnOneResolver +import com.wire.kalium.logic.feature.message.ephemeral.DeleteEphemeralMessagesAfterEndDateUseCase import com.wire.kalium.logic.functional.Either import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first @@ -43,6 +44,7 @@ interface NotifyConversationIsOpenUseCase { internal class NotifyConversationIsOpenUseCaseImpl( private val oneOnOneResolver: OneOnOneResolver, private val conversationRepository: ConversationRepository, + private val deleteEphemeralMessageEndDate: DeleteEphemeralMessagesAfterEndDateUseCase, private val kaliumLogger: KaliumLogger ) : NotifyConversationIsOpenUseCase { @@ -61,5 +63,8 @@ internal class NotifyConversationIsOpenUseCaseImpl( ) oneOnOneResolver.resolveOneOnOneConversationWithUser(conversation.otherUser) } + + // Delete Ephemeral Messages that has passed the end date + deleteEphemeralMessageEndDate() } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/usecase/ObserveCertificateRevocationForSelfClientUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/usecase/ObserveCertificateRevocationForSelfClientUseCase.kt new file mode 100644 index 00000000000..0dd332e36eb --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/usecase/ObserveCertificateRevocationForSelfClientUseCase.kt @@ -0,0 +1,47 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.e2ei.usecase + +import com.wire.kalium.logic.configuration.UserConfigRepository +import com.wire.kalium.logic.data.id.CurrentClientIdProvider +import com.wire.kalium.logic.feature.e2ei.CertificateStatus +import com.wire.kalium.logic.functional.map + +/** + * Use case to observe certificate revocation for self client. + */ +interface ObserveCertificateRevocationForSelfClientUseCase { + suspend operator fun invoke() +} + +@Suppress("LongParameterList") +internal class ObserveCertificateRevocationForSelfClientUseCaseImpl( + private val userConfigRepository: UserConfigRepository, + private val currentClientIdProvider: CurrentClientIdProvider, + private val getE2eiCertificate: GetE2eiCertificateUseCase +) : ObserveCertificateRevocationForSelfClientUseCase { + override suspend fun invoke() { + currentClientIdProvider().map { clientId -> + getE2eiCertificate(clientId).run { + if (this is GetE2EICertificateUseCaseResult.Success && certificate.status == CertificateStatus.REVOKED) { + userConfigRepository.setShouldNotifyForRevokedCertificate(true) + } + } + } + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageScope.kt index 1bde0918f2a..8ff1befcd3f 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageScope.kt @@ -59,6 +59,8 @@ import com.wire.kalium.logic.feature.message.composite.SendButtonActionMessageUs import com.wire.kalium.logic.feature.message.composite.SendButtonMessageUseCase import com.wire.kalium.logic.feature.message.ephemeral.DeleteEphemeralMessageForSelfUserAsReceiverUseCaseImpl import com.wire.kalium.logic.feature.message.ephemeral.DeleteEphemeralMessageForSelfUserAsSenderUseCaseImpl +import com.wire.kalium.logic.feature.message.ephemeral.DeleteEphemeralMessagesAfterEndDateUseCase +import com.wire.kalium.logic.feature.message.ephemeral.DeleteEphemeralMessagesAfterEndDateUseCaseImpl import com.wire.kalium.logic.feature.message.ephemeral.EnqueueMessageSelfDeletionUseCase import com.wire.kalium.logic.feature.message.ephemeral.EnqueueMessageSelfDeletionUseCaseImpl import com.wire.kalium.logic.feature.message.ephemeral.EphemeralMessageDeletionHandler @@ -157,6 +159,10 @@ class MessageScope internal constructor( ephemeralMessageDeletionHandler = ephemeralMessageDeletionHandler ) + val deleteEphemeralMessageEndDate: DeleteEphemeralMessagesAfterEndDateUseCase = DeleteEphemeralMessagesAfterEndDateUseCaseImpl( + ephemeralMessageDeletionHandler = ephemeralMessageDeletionHandler + ) + internal val messageSender: MessageSender get() = MessageSenderImpl( messageRepository, diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/ephemeral/DeleteEphemeralMessageEndDateUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/ephemeral/DeleteEphemeralMessageEndDateUseCase.kt new file mode 100644 index 00000000000..67b5edf2391 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/ephemeral/DeleteEphemeralMessageEndDateUseCase.kt @@ -0,0 +1,34 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.message.ephemeral + +/** + * This use case deletes ephemeral [Message] that have an end date set and it has passed current time + */ +interface DeleteEphemeralMessagesAfterEndDateUseCase { + suspend operator fun invoke() +} + +internal class DeleteEphemeralMessagesAfterEndDateUseCaseImpl( + private val ephemeralMessageDeletionHandler: EphemeralMessageDeletionHandler +) : DeleteEphemeralMessagesAfterEndDateUseCase { + + override suspend fun invoke() { + ephemeralMessageDeletionHandler.deleteSelfDeletionMessagesFromEndDate() + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/ephemeral/EphemeralMessageDeletionHandler.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/ephemeral/EphemeralMessageDeletionHandler.kt index c68b185f5d7..f18cbccbe66 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/ephemeral/EphemeralMessageDeletionHandler.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/ephemeral/EphemeralMessageDeletionHandler.kt @@ -41,6 +41,8 @@ internal interface EphemeralMessageDeletionHandler { fun startSelfDeletion(conversationId: ConversationId, messageId: String) fun enqueueSelfDeletion(message: Message, expirationData: Message.ExpirationData) suspend fun enqueuePendingSelfDeletionMessages() + + suspend fun deleteSelfDeletionMessagesFromEndDate() } internal class EphemeralMessageDeletionHandlerImpl( @@ -195,6 +197,13 @@ internal class EphemeralMessageDeletionHandlerImpl( deletionStartDate = deletionStartMark ) + val deletionEndDate = deletionStartMark + expireAfter + messageRepository.markSelfDeletionEndDate( + conversationId = message.conversationId, + messageUuid = message.id, + deletionEndDate = deletionEndDate + ) + SelfDeletionEventLogger.log( LoggingSelfDeletionEvent.MarkingSelfSelfDeletionStartDate( message, @@ -235,4 +244,19 @@ internal class EphemeralMessageDeletionHandlerImpl( } } } + + override suspend fun deleteSelfDeletionMessagesFromEndDate() { + messageRepository.getEphemeralMessagesMarkedForEndDeletion() + .onSuccess { ephemeralMessages -> + ephemeralMessages.forEach { ephemeralMessage -> + ephemeralMessage.expirationData?.let { expirationData -> + deleteMessage( + message = ephemeralMessage, + expirationData = expirationData + ) + } + } + + } + } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UserScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UserScope.kt index 7910af415f5..71f5add0843 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UserScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UserScope.kt @@ -19,6 +19,7 @@ package com.wire.kalium.logic.feature.user +import com.wire.kalium.logic.configuration.UserConfigRepository import com.wire.kalium.logic.configuration.server.ServerConfigRepository import com.wire.kalium.logic.data.asset.AssetRepository import com.wire.kalium.logic.data.connection.ConnectionRepository @@ -54,6 +55,8 @@ import com.wire.kalium.logic.feature.e2ei.usecase.GetUserE2eiCertificateStatusUs import com.wire.kalium.logic.feature.e2ei.usecase.GetUserE2eiCertificateStatusUseCaseImpl import com.wire.kalium.logic.feature.e2ei.usecase.GetUserE2eiCertificatesUseCase import com.wire.kalium.logic.feature.e2ei.usecase.GetUserE2eiCertificatesUseCaseImpl +import com.wire.kalium.logic.feature.e2ei.usecase.ObserveCertificateRevocationForSelfClientUseCase +import com.wire.kalium.logic.feature.e2ei.usecase.ObserveCertificateRevocationForSelfClientUseCaseImpl import com.wire.kalium.logic.feature.message.MessageSender import com.wire.kalium.logic.feature.publicuser.GetAllContactsUseCase import com.wire.kalium.logic.feature.publicuser.GetAllContactsUseCaseImpl @@ -75,6 +78,7 @@ import com.wire.kalium.persistence.dao.MetadataDAO @Suppress("LongParameterList") class UserScope internal constructor( private val userRepository: UserRepository, + private val userConfigRepository: UserConfigRepository, private val accountRepository: AccountRepository, private val searchUserRepository: SearchUserRepository, private val syncManager: SyncManager, @@ -176,4 +180,11 @@ class UserScope internal constructor( val deleteAccount: DeleteAccountUseCase get() = DeleteAccountUseCase(accountRepository) val updateSupportedProtocols: UpdateSupportedProtocolsUseCase get() = updateSupportedProtocolsUseCase + + val observeCertificateRevocationForSelfClient: ObserveCertificateRevocationForSelfClientUseCase + get() = ObserveCertificateRevocationForSelfClientUseCaseImpl( + userConfigRepository = userConfigRepository, + currentClientIdProvider = clientIdProvider, + getE2eiCertificate = getE2EICertificate + ) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/e2ei/MarkNotifyForRevokedCertificateAsNotifiedUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/e2ei/MarkNotifyForRevokedCertificateAsNotifiedUseCase.kt new file mode 100644 index 00000000000..50058481030 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/e2ei/MarkNotifyForRevokedCertificateAsNotifiedUseCase.kt @@ -0,0 +1,35 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.user.e2ei + +import com.wire.kalium.logic.configuration.UserConfigRepository + +/** + * Use case that marks that the user should not be notified about revoked E2Ei certificate. + */ +interface MarkNotifyForRevokedCertificateAsNotifiedUseCase { + suspend operator fun invoke() +} + +internal class MarkNotifyForRevokedCertificateAsNotifiedUseCaseImpl( + private val userConfigRepository: UserConfigRepository +) : MarkNotifyForRevokedCertificateAsNotifiedUseCase { + override suspend operator fun invoke() { + userConfigRepository.setShouldNotifyForRevokedCertificate(false) + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/e2ei/ObserveShouldNotifyForRevokedCertificateUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/e2ei/ObserveShouldNotifyForRevokedCertificateUseCase.kt new file mode 100644 index 00000000000..f9af3cb824a --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/e2ei/ObserveShouldNotifyForRevokedCertificateUseCase.kt @@ -0,0 +1,47 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.user.e2ei + +import com.wire.kalium.logic.configuration.UserConfigRepository +import com.wire.kalium.logic.functional.fold +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf + +/** + * Use case that observes if the user should be notified about revoked E2ei certificate. + */ +interface ObserveShouldNotifyForRevokedCertificateUseCase { + suspend operator fun invoke(): Flow +} + +internal class ObserveShouldNotifyForRevokedCertificateUseCaseImpl( + private val userConfigRepository: UserConfigRepository +) : ObserveShouldNotifyForRevokedCertificateUseCase { + @OptIn(ExperimentalCoroutinesApi::class) + override suspend operator fun invoke(): Flow = + userConfigRepository.observeShouldNotifyForRevokedCertificate().flatMapLatest { + it.fold( + { flowOf(false) }, + { shouldNotify -> + flowOf(shouldNotify) + } + ) + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/asset/AssetRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/asset/AssetRepositoryTest.kt index 90914ca4f27..328a24b3530 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/asset/AssetRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/asset/AssetRepositoryTest.kt @@ -254,7 +254,7 @@ class AssetRepositoryTest { with(arrangement) { result.shouldSucceed() val expectedPath = fakeKaliumFileSystem.providePersistentAssetPath("${assetKey.value}.${assetName.fileExtension()}") - val realPath = result.value + val realPath = (result as Either.Right).value assertEquals(expectedPath, realPath) verify(assetDAO).suspendFunction(assetDAO::getAssetByKey) .with(eq(assetKey.value)) @@ -346,7 +346,8 @@ class AssetRepositoryTest { // Then with(arrangement) { result.shouldSucceed() - assertEquals(assetPath, result.value) + val realPath = (result as Either.Right).value + assertEquals(assetPath, realPath) verify(assetDAO).suspendFunction(assetDAO::getAssetByKey) .with(eq(assetKey.value)) .wasInvoked(exactly = once) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/NotifyConversationIsOpenUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/NotifyConversationIsOpenUseCaseTest.kt index 85b77011948..b2f0868a913 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/NotifyConversationIsOpenUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/NotifyConversationIsOpenUseCaseTest.kt @@ -17,6 +17,7 @@ */ package com.wire.kalium.logic.feature.conversation +import com.wire.kalium.logic.feature.message.ephemeral.DeleteEphemeralMessagesAfterEndDateUseCase import com.wire.kalium.logic.framework.TestConversationDetails import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.kaliumLogger @@ -24,8 +25,12 @@ import com.wire.kalium.logic.util.arrangement.mls.OneOnOneResolverArrangement import com.wire.kalium.logic.util.arrangement.mls.OneOnOneResolverArrangementImpl import com.wire.kalium.logic.util.arrangement.repository.ConversationRepositoryArrangement import com.wire.kalium.logic.util.arrangement.repository.ConversationRepositoryArrangementImpl +import io.mockative.Mock import io.mockative.any +import io.mockative.classOf import io.mockative.eq +import io.mockative.given +import io.mockative.mock import io.mockative.once import io.mockative.verify import kotlinx.coroutines.test.runTest @@ -40,6 +45,8 @@ class NotifyConversationIsOpenUseCaseTest { withObserveConversationDetailsByIdReturning( Either.Right(details) ) + + withDeleteEphemeralMessageEndDateSuccess() } notifyConversationIsOpenUseCase.invoke(details.conversation.id) @@ -56,6 +63,8 @@ class NotifyConversationIsOpenUseCaseTest { withObserveConversationDetailsByIdReturning( Either.Right(details) ) + + withDeleteEphemeralMessageEndDateSuccess() } notifyConversationIsOpenUseCase.invoke(details.conversation.id) @@ -75,6 +84,8 @@ class NotifyConversationIsOpenUseCaseTest { withResolveOneOnOneConversationWithUserReturning( Either.Right(details.conversation.id) ) + + withDeleteEphemeralMessageEndDateSuccess() } notifyConversationIsOpenUseCase.invoke(details.conversation.id) @@ -88,12 +99,24 @@ class NotifyConversationIsOpenUseCaseTest { private val configure: Arrangement.() -> Unit ) : OneOnOneResolverArrangement by OneOnOneResolverArrangementImpl(), ConversationRepositoryArrangement by ConversationRepositoryArrangementImpl() { + + @Mock + private val deleteEphemeralMessageEndDate = mock(classOf()) + + fun withDeleteEphemeralMessageEndDateSuccess() { + given(deleteEphemeralMessageEndDate) + .suspendFunction(deleteEphemeralMessageEndDate::invoke) + .whenInvoked() + .thenReturn(Unit) + } + fun arrange(): Pair = run { configure() this@Arrangement to NotifyConversationIsOpenUseCaseImpl( oneOnOneResolver = oneOnOneResolver, conversationRepository = conversationRepository, - kaliumLogger = kaliumLogger + kaliumLogger = kaliumLogger, + deleteEphemeralMessageEndDate = deleteEphemeralMessageEndDate ) } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/ephemeral/DeleteEphemeralMessagesAfterEndDateUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/ephemeral/DeleteEphemeralMessagesAfterEndDateUseCaseTest.kt new file mode 100644 index 00000000000..3971979034d --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/ephemeral/DeleteEphemeralMessagesAfterEndDateUseCaseTest.kt @@ -0,0 +1,61 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.message.ephemeral + +import io.mockative.Mock +import io.mockative.given +import io.mockative.mock +import io.mockative.once +import io.mockative.verify +import kotlinx.coroutines.test.runTest +import kotlin.test.Test + +class DeleteEphemeralMessagesAfterEndDateUseCaseTest { + + @Test + fun givenDeleteEphemeralMessagesUseCase_whenInvoking_ThenEphemeralHandlerIsCalled() = runTest { + val (arrangement, useCase) = Arrangement() + .withDeleteSelfDeletionMessagesFromEndDateSuccess() + .arrange() + + // when + useCase.invoke() + + // then + verify(arrangement.ephemeralMessageDeletionHandler) + .suspendFunction(arrangement.ephemeralMessageDeletionHandler::deleteSelfDeletionMessagesFromEndDate) + .wasInvoked(exactly = once) + } + + private class Arrangement { + + @Mock + val ephemeralMessageDeletionHandler = mock(EphemeralMessageDeletionHandler::class) + + fun withDeleteSelfDeletionMessagesFromEndDateSuccess() = apply { + given(ephemeralMessageDeletionHandler) + .suspendFunction(ephemeralMessageDeletionHandler::deleteSelfDeletionMessagesFromEndDate) + .whenInvoked() + .thenReturn(Unit) + } + + fun arrange() = this to DeleteEphemeralMessagesAfterEndDateUseCaseImpl( + ephemeralMessageDeletionHandler = ephemeralMessageDeletionHandler + ) + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/ephemeral/EphemeralMessageDeletionHandlerTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/ephemeral/EphemeralMessageDeletionHandlerTest.kt index 52fef0eaabd..b661078ce53 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/ephemeral/EphemeralMessageDeletionHandlerTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/ephemeral/EphemeralMessageDeletionHandlerTest.kt @@ -69,6 +69,7 @@ class EphemeralMessageDeletionHandlerTest { dispatcher = testDispatcher ).withMessageRepositoryReturningMessage(oneSecondEphemeralMessage) .withMessageRepositoryMarkingSelfDeletionStartDate() + .withMessageRepositoryMarkingSelfDeletionEndDate() .withDeletingMessage() .arrange() @@ -86,6 +87,10 @@ class EphemeralMessageDeletionHandlerTest { .with(eq(oneSecondEphemeralMessage.conversationId), eq(oneSecondEphemeralMessage.id), any()) .wasInvoked(exactly = once) + verify(arrangement.messageRepository) + .suspendFunction(arrangement.messageRepository::markSelfDeletionEndDate) + .with(eq(oneSecondEphemeralMessage.conversationId), eq(oneSecondEphemeralMessage.id), any()) + verify(arrangement.messageRepository) .suspendFunction(arrangement.messageRepository::getMessageById) .with(eq(oneSecondEphemeralMessage.conversationId), eq(oneSecondEphemeralMessage.id)) @@ -108,6 +113,7 @@ class EphemeralMessageDeletionHandlerTest { val (arrangement, ephemeralMessageDeletionHandler) = Arrangement(this, testDispatcher) .withMessageRepositoryReturningMessage(oneSecondEphemeralMessage) .withMessageRepositoryMarkingSelfDeletionStartDate() + .withMessageRepositoryMarkingSelfDeletionEndDate() .withDeletingMessage() .arrange() @@ -126,6 +132,10 @@ class EphemeralMessageDeletionHandlerTest { .with(eq(oneSecondEphemeralMessage.conversationId), eq(oneSecondEphemeralMessage.id), any()) .wasInvoked(exactly = once) + verify(arrangement.messageRepository) + .suspendFunction(arrangement.messageRepository::markSelfDeletionEndDate) + .with(eq(oneSecondEphemeralMessage.conversationId), eq(oneSecondEphemeralMessage.id), any()) + verify(arrangement.messageRepository) .suspendFunction(arrangement.messageRepository::getMessageById) .with(eq(oneSecondEphemeralMessage.conversationId), eq(oneSecondEphemeralMessage.id)) @@ -170,6 +180,7 @@ class EphemeralMessageDeletionHandlerTest { dispatcher = testDispatcher ).withMessageRepositoryReturningMessage(oneSecondEphemeralMessage) .withMessageRepositoryMarkingSelfDeletionStartDate() + .withMessageRepositoryMarkingSelfDeletionEndDate() .withDeletingMessage() .arrange() @@ -209,6 +220,7 @@ class EphemeralMessageDeletionHandlerTest { dispatcher = testDispatcher ).withMessageRepositoryReturningMessage(oneSecondEphemeralMessage) .withMessageRepositoryMarkingSelfDeletionStartDate() + .withMessageRepositoryMarkingSelfDeletionEndDate() .withDeletingMessage() .arrange() @@ -245,6 +257,7 @@ class EphemeralMessageDeletionHandlerTest { val (arrangement, ephemeralMessageDeletionHandler) = Arrangement(this, testDispatcher) .withMessageRepositoryReturningMessage(oneSecondEphemeralMessage) .withMessageRepositoryMarkingSelfDeletionStartDate() + .withMessageRepositoryMarkingSelfDeletionEndDate() .withDeletingMessage() .arrange() @@ -280,6 +293,7 @@ class EphemeralMessageDeletionHandlerTest { val (arrangement, ephemeralMessageDeletionHandler) = Arrangement(this, testDispatcher) .withMessageRepositoryReturningMessage(oneSecondEphemeralMessage) .withMessageRepositoryMarkingSelfDeletionStartDate() + .withMessageRepositoryMarkingSelfDeletionEndDate() .withDeletingMessage() .arrange() @@ -331,6 +345,7 @@ class EphemeralMessageDeletionHandlerTest { val (arrangement, ephemeralMessageDeletionHandler) = Arrangement(this, testDispatcher) .withMessageRepositoryReturningPendingEphemeralMessages(messages = pendingMessagesToDelete) .withMessageRepositoryMarkingSelfDeletionStartDate() + .withMessageRepositoryMarkingSelfDeletionEndDate() .withDeletingMessage() .arrange() @@ -379,6 +394,7 @@ class EphemeralMessageDeletionHandlerTest { val (arrangement, ephemeralMessageDeletionHandler) = Arrangement(this, testDispatcher) .withMessageRepositoryReturningPendingEphemeralMessages(messages = pendingMessagesToDelete) .withMessageRepositoryMarkingSelfDeletionStartDate() + .withMessageRepositoryMarkingSelfDeletionEndDate() .withDeletingMessage() .arrange() @@ -449,6 +465,7 @@ class EphemeralMessageDeletionHandlerTest { dispatcher = testDispatcher ).withMessageRepositoryReturningPendingEphemeralMessages(messages = pendingMessagesToDelete) .withMessageRepositoryMarkingSelfDeletionStartDate() + .withMessageRepositoryMarkingSelfDeletionEndDate() .withDeletingMessage() .arrange() @@ -545,6 +562,7 @@ class EphemeralMessageDeletionHandlerTest { dispatcher = testDispatcher ).withMessageRepositoryReturningPendingEphemeralMessages(messages = pendingMessagesToDelete) .withMessageRepositoryMarkingSelfDeletionStartDate() + .withMessageRepositoryMarkingSelfDeletionEndDate() .withDeletingMessage() .arrange() @@ -618,6 +636,7 @@ class EphemeralMessageDeletionHandlerTest { messages = pendingMessagesToDeletePastTheTime + pendingMessagesToDeleteBeforeTime ) .withMessageRepositoryMarkingSelfDeletionStartDate() + .withMessageRepositoryMarkingSelfDeletionEndDate() .withDeletingMessage() .arrange() // when @@ -669,6 +688,7 @@ class EphemeralMessageDeletionHandlerTest { messages = pendingMessagesToDeletePastTheTime + pendingMessagesToDeleteBeforeTime ) .withMessageRepositoryMarkingSelfDeletionStartDate() + .withMessageRepositoryMarkingSelfDeletionEndDate() .withDeletingMessage() .arrange() // when @@ -703,6 +723,7 @@ class EphemeralMessageDeletionHandlerTest { dispatcher = testDispatcher ).withMessageRepositoryReturningMessage(oneSecondEphemeralMessage) .withMessageRepositoryMarkingSelfDeletionStartDate() + .withMessageRepositoryMarkingSelfDeletionEndDate() .withDeletingMessage() .arrange() @@ -720,6 +741,10 @@ class EphemeralMessageDeletionHandlerTest { .with(any()) .wasNotInvoked() + verify(arrangement.messageRepository) + .suspendFunction(arrangement.messageRepository::markSelfDeletionEndDate) + .with(eq(oneSecondEphemeralMessage.conversationId), eq(oneSecondEphemeralMessage.id), any()) + verify(arrangement.messageRepository) .suspendFunction(arrangement.messageRepository::getMessageById) .with(eq(oneSecondEphemeralMessage.conversationId), eq(oneSecondEphemeralMessage.id)) @@ -767,6 +792,15 @@ class EphemeralMessageDeletionHandlerTest { return this } + fun withMessageRepositoryMarkingSelfDeletionEndDate(): Arrangement { + given(messageRepository) + .suspendFunction(messageRepository::markSelfDeletionEndDate) + .whenInvokedWith(any(), any(), any()) + .then { _, _, _ -> Either.Right(Unit) } + + return this + } + fun withDeletingMessage(): Arrangement { given(deleteEphemeralMessageForSelfUserAsReceiver) .suspendFunction(deleteEphemeralMessageForSelfUserAsReceiver::invoke) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/e2ei/MarkNotifyForRevokedCertificateAsNotifiedUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/e2ei/MarkNotifyForRevokedCertificateAsNotifiedUseCaseTest.kt new file mode 100644 index 00000000000..44bfd2e2023 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/e2ei/MarkNotifyForRevokedCertificateAsNotifiedUseCaseTest.kt @@ -0,0 +1,63 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.user.e2ei + +import com.wire.kalium.logic.configuration.UserConfigRepository +import io.mockative.Mock +import io.mockative.classOf +import io.mockative.eq +import io.mockative.given +import io.mockative.mock +import io.mockative.verify +import kotlinx.coroutines.test.runTest +import kotlin.test.Test + +class MarkNotifyForRevokedCertificateAsNotifiedUseCaseTest { + + @Test + fun givenUserConfigRepository_whenRunningUseCase_thenSetShouldNotifyForRevokedCertificateOnce() = + runTest { + val (arrangement, markNotifyForRevokedCertificateAsNotified) = Arrangement() + .withUserConfigRepository() + .arrange() + + markNotifyForRevokedCertificateAsNotified.invoke() + + verify(arrangement.userConfigRepository) + .function(arrangement.userConfigRepository::setShouldNotifyForRevokedCertificate) + .with(eq(false)) + .wasInvoked() + } + + internal class Arrangement { + + @Mock + val userConfigRepository = mock(classOf()) + + fun arrange() = this to MarkNotifyForRevokedCertificateAsNotifiedUseCaseImpl( + userConfigRepository = userConfigRepository + ) + + fun withUserConfigRepository() = apply { + given(userConfigRepository) + .function(userConfigRepository::setShouldNotifyForRevokedCertificate) + .whenInvokedWith(eq(false)) + .thenReturn(Unit) + } + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/e2ei/ObserveShouldNotifyForRevokedCertificateUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/e2ei/ObserveShouldNotifyForRevokedCertificateUseCaseTest.kt new file mode 100644 index 00000000000..a7daf079a5e --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/e2ei/ObserveShouldNotifyForRevokedCertificateUseCaseTest.kt @@ -0,0 +1,81 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.user.e2ei + +import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.configuration.UserConfigRepository +import com.wire.kalium.logic.functional.Either +import io.mockative.Mock +import io.mockative.classOf +import io.mockative.given +import io.mockative.mock +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class ObserveShouldNotifyForRevokedCertificateUseCaseTest { + + @Test + fun givenUserConfigRepositoryFailure_whenRunningUseCase_thenEmitFalse() = runTest { + val (_, observeShouldNotifyForRevokedCertificate) = Arrangement() + .withUserConfigRepositoryFailure() + .arrange() + + val result = observeShouldNotifyForRevokedCertificate.invoke() + + assertEquals(false, result.first()) + } + + @Test + fun givenUserConfigRepositorySuccess_whenRunningUseCase_thenEmitSameValueOfRepository() = + runTest { + val (_, observeShouldNotifyForRevokedCertificate) = Arrangement() + .withUserConfigRepositorySuccess() + .arrange() + + val result = observeShouldNotifyForRevokedCertificate.invoke() + + assertEquals(true, result.first()) + } + + internal class Arrangement { + + @Mock + val userConfigRepository = mock(classOf()) + + fun arrange() = this to ObserveShouldNotifyForRevokedCertificateUseCaseImpl( + userConfigRepository = userConfigRepository + ) + + fun withUserConfigRepositoryFailure() = apply { + given(userConfigRepository) + .suspendFunction(userConfigRepository::observeShouldNotifyForRevokedCertificate) + .whenInvoked() + .thenReturn(flowOf(Either.Left(StorageFailure.DataNotFound))) + } + + fun withUserConfigRepositorySuccess() = apply { + given(userConfigRepository) + .suspendFunction(userConfigRepository::observeShouldNotifyForRevokedCertificate) + .whenInvoked() + .thenReturn(flowOf(Either.Right(true))) + } + } +} diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessageDetailsView.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessageDetailsView.sq index fe7bd9e7bde..e606319529c 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessageDetailsView.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessageDetailsView.sq @@ -24,6 +24,7 @@ Message.visibility AS visibility, Message.expects_read_confirmation AS expectsReadConfirmation, Message.expire_after_millis AS expireAfterMillis, Message.self_deletion_start_date AS selfDeletionStartDate, +Message.self_deletion_end_date AS selfDeletionEndDate, IFNULL ((SELECT COUNT (*) FROM Receipt WHERE message_id = Message.id AND type = "READ"), 0) AS readCount, User.name AS senderName, User.handle AS senderHandle, 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 40d461841d4..dc3cd3d293f 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 @@ -26,6 +26,7 @@ CREATE TABLE Message ( expects_read_confirmation INTEGER AS Boolean NOT NULL DEFAULT(0), expire_after_millis INTEGER DEFAULT(NULL), self_deletion_start_date INTEGER AS Instant DEFAULT(NULL), + self_deletion_end_date INTEGER AS Instant DEFAULT(NULL), FOREIGN KEY (conversation_id) REFERENCES Conversation(qualified_id) ON DELETE CASCADE, FOREIGN KEY (sender_user_id) REFERENCES User(qualified_id), @@ -40,6 +41,7 @@ CREATE INDEX message_sender_user_index ON Message(sender_user_id); CREATE INDEX message_conversation_index ON Message(conversation_id); CREATE INDEX message_status_index ON Message(status); CREATE INDEX message_expire_after_millis_index ON Message(expire_after_millis, self_deletion_start_date); +CREATE INDEX message_self_deletion_end_date ON Message(self_deletion_end_date); CREATE TABLE MessageMention ( message_id TEXT NOT NULL, @@ -567,6 +569,18 @@ UPDATE Message SET self_deletion_start_date = ? WHERE conversation_id = ? AND id = ?; +markSelfDeletionEndDate: +UPDATE Message +SET self_deletion_end_date = ? +WHERE conversation_id = ? AND id = ?; + +selectAllEphemeralMessagesMarkedForEndDeletion: +SELECT * FROM MessageDetailsView +WHERE expireAfterMillis NOT NULL +AND selfDeletionEndDate NOT NULL +AND visibility = "VISIBLE" +AND selfDeletionEndDate <= STRFTIME('%s', 'now') * 1000; -- Checks if message end date is lower than current time in millis + insertMessageRecipientsFailure: INSERT OR IGNORE INTO MessageRecipientFailure(message_id, conversation_id, recipient_failure_list, recipient_failure_type) VALUES(?, ?, ?, ?); diff --git a/persistence/src/commonMain/db_user/migrations/73.sqm b/persistence/src/commonMain/db_user/migrations/73.sqm new file mode 100644 index 00000000000..25fe4170c8b --- /dev/null +++ b/persistence/src/commonMain/db_user/migrations/73.sqm @@ -0,0 +1,167 @@ +import kotlinx.datetime.Instant; + +ALTER TABLE Message ADD COLUMN self_deletion_end_date INTEGER AS Instant DEFAULT(NULL); + +CREATE INDEX message_self_deletion_end_date ON Message(self_deletion_end_date); + +DROP VIEW IF EXISTS MessageDetailsView; + +CREATE VIEW IF NOT EXISTS MessageDetailsView +AS SELECT +Message.id AS id, +Message.conversation_id AS conversationId, +Message.content_type AS contentType, +Message.creation_date AS date, +Message.sender_user_id AS senderUserId, +Message.sender_client_id AS senderClientId, +Message.status AS status, +Message.last_edit_date AS lastEditTimestamp, +Message.visibility AS visibility, +Message.expects_read_confirmation AS expectsReadConfirmation, +Message.expire_after_millis AS expireAfterMillis, +Message.self_deletion_start_date AS selfDeletionStartDate, +Message.self_deletion_end_date AS selfDeletionEndDate, +IFNULL ((SELECT COUNT (*) FROM Receipt WHERE message_id = Message.id AND type = "READ"), 0) AS readCount, +User.name AS senderName, +User.handle AS senderHandle, +User.email AS senderEmail, +User.phone AS senderPhone, +User.accent_id AS senderAccentId, +User.team AS senderTeamId, +User.connection_status AS senderConnectionStatus, +User.preview_asset_id AS senderPreviewAssetId, +User.complete_asset_id AS senderCompleteAssetId, +User.user_availability_status AS senderAvailabilityStatus, +User.user_type AS senderUserType, +User.bot_service AS senderBotService, +User.deleted AS senderIsDeleted, +(Message.sender_user_id == SelfUser.id) AS isSelfMessage, +TextContent.text_body AS text, +TextContent.is_quoting_self AS isQuotingSelfUser, +AssetContent.asset_size AS assetSize, +AssetContent.asset_name AS assetName, +AssetContent.asset_mime_type AS assetMimeType, +AssetContent.asset_upload_status AS assetUploadStatus, +AssetContent.asset_download_status AS assetDownloadStatus, +AssetContent.asset_otr_key AS assetOtrKey, +AssetContent.asset_sha256 AS assetSha256, +AssetContent.asset_id AS assetId, +AssetContent.asset_token AS assetToken, +AssetContent.asset_domain AS assetDomain, +AssetContent.asset_encryption_algorithm AS assetEncryptionAlgorithm, +AssetContent.asset_width AS assetWidth, +AssetContent.asset_height AS assetHeight, +AssetContent.asset_duration_ms AS assetDuration, +AssetContent.asset_normalized_loudness AS assetNormalizedLoudness, +MissedCallContent.caller_id AS callerId, +MemberChangeContent.member_change_list AS memberChangeList, +MemberChangeContent.member_change_type AS memberChangeType, +UnknownContent.unknown_type_name AS unknownContentTypeName, +UnknownContent.unknown_encoded_data AS unknownContentData, +RestrictedAssetContent.asset_mime_type AS restrictedAssetMimeType, +RestrictedAssetContent.asset_size AS restrictedAssetSize, +RestrictedAssetContent.asset_name AS restrictedAssetName, +FailedToDecryptContent.unknown_encoded_data AS failedToDecryptData, +FailedToDecryptContent.is_decryption_resolved AS isDecryptionResolved, +ConversationNameChangedContent.conversation_name AS conversationName, +'{' || IFNULL( + (SELECT GROUP_CONCAT('"' || emoji || '":' || count) + FROM ( + SELECT COUNT(*) count, Reaction.emoji emoji + FROM Reaction + WHERE Reaction.message_id = Message.id + AND Reaction.conversation_id = Message.conversation_id + GROUP BY Reaction.emoji + )), + '') +|| '}' AS allReactionsJson, +IFNULL( + (SELECT '[' || GROUP_CONCAT('"' || Reaction.emoji || '"') || ']' + FROM Reaction + WHERE Reaction.message_id = Message.id + AND Reaction.conversation_id = Message.conversation_id + AND Reaction.sender_id = SelfUser.id + ), + '[]' +) AS selfReactionsJson, +IFNULL( + (SELECT '[' || GROUP_CONCAT( + '{"start":' || start || ', "length":' || length || + ', "userId":{"value":"' || replace(substr(user_id, 0, instr(user_id, '@')), '@', '') || '"' || + ',"domain":"' || replace(substr(user_id, instr(user_id, '@')+1, length(user_id)), '@', '') || '"' || + '}' || '}') || ']' + FROM MessageMention + WHERE MessageMention.message_id = Message.id + AND MessageMention.conversation_id = Message.conversation_id + ), + '[]' +) AS mentions, +TextContent.quoted_message_id AS quotedMessageId, +QuotedMessage.sender_user_id AS quotedSenderId, +TextContent.is_quote_verified AS isQuoteVerified, +QuotedSender.name AS quotedSenderName, +QuotedMessage.creation_date AS quotedMessageDateTime, +QuotedMessage.last_edit_date AS quotedMessageEditTimestamp, +QuotedMessage.visibility AS quotedMessageVisibility, +QuotedMessage.content_type AS quotedMessageContentType, +QuotedTextContent.text_body AS quotedTextBody, +QuotedAssetContent.asset_mime_type AS quotedAssetMimeType, +QuotedAssetContent.asset_name AS quotedAssetName, +QuotedLocationContent.name AS quotedLocationName, + +NewConversationReceiptMode.receipt_mode AS newConversationReceiptMode, + +ConversationReceiptModeChanged.receipt_mode AS conversationReceiptModeChanged, +ConversationTimerChangedContent.message_timer AS messageTimerChanged, +FailedRecipientsWithNoClients.recipient_failure_list AS recipientsFailedWithNoClientsList, +FailedRecipientsDeliveryFailed.recipient_failure_list AS recipientsFailedDeliveryList, + +IFNULL( + (SELECT '[' || + GROUP_CONCAT('{"text":"' || text || '", "id":"' || id || '""is_selected":' || is_selected || '}') + || ']' + FROM ButtonContent + WHERE ButtonContent.message_id = Message.id + AND ButtonContent.conversation_id = Message.conversation_id + ), + '[]' +) AS buttonsJson, +FederationTerminatedContent.domain_list AS federationDomainList, +FederationTerminatedContent.federation_type AS federationType, +ConversationProtocolChangedContent.protocol AS conversationProtocolChanged, +ConversationLocationContent.latitude AS latitude, +ConversationLocationContent.longitude AS longitude, +ConversationLocationContent.name AS locationName, +ConversationLocationContent.zoom AS locationZoom, +LegalHoldContent.legal_hold_member_list AS legalHoldMemberList, +LegalHoldContent.legal_hold_type AS legalHoldType + +FROM Message +JOIN User ON Message.sender_user_id = User.qualified_id +LEFT JOIN MessageTextContent AS TextContent ON Message.id = TextContent.message_id AND Message.conversation_id = TextContent.conversation_id +LEFT JOIN MessageAssetContent AS AssetContent ON Message.id = AssetContent.message_id AND Message.conversation_id = AssetContent.conversation_id +LEFT JOIN MessageMissedCallContent AS MissedCallContent ON Message.id = MissedCallContent.message_id AND Message.conversation_id = MissedCallContent.conversation_id +LEFT JOIN MessageMemberChangeContent AS MemberChangeContent ON Message.id = MemberChangeContent.message_id AND Message.conversation_id = MemberChangeContent.conversation_id +LEFT JOIN MessageUnknownContent AS UnknownContent ON Message.id = UnknownContent.message_id AND Message.conversation_id = UnknownContent.conversation_id +LEFT JOIN MessageRestrictedAssetContent AS RestrictedAssetContent ON Message.id = RestrictedAssetContent.message_id AND RestrictedAssetContent.conversation_id = RestrictedAssetContent.conversation_id +LEFT JOIN MessageFailedToDecryptContent AS FailedToDecryptContent ON Message.id = FailedToDecryptContent.message_id AND Message.conversation_id = FailedToDecryptContent.conversation_id +LEFT JOIN MessageConversationChangedContent AS ConversationNameChangedContent ON Message.id = ConversationNameChangedContent.message_id AND Message.conversation_id = ConversationNameChangedContent.conversation_id +LEFT JOIN MessageRecipientFailure AS FailedRecipientsWithNoClients ON Message.id = FailedRecipientsWithNoClients.message_id AND Message.conversation_id = FailedRecipientsWithNoClients.conversation_id AND FailedRecipientsWithNoClients.recipient_failure_type = 'NO_CLIENTS_TO_DELIVER' +LEFT JOIN MessageRecipientFailure AS FailedRecipientsDeliveryFailed ON Message.id = FailedRecipientsDeliveryFailed.message_id AND Message.conversation_id = FailedRecipientsDeliveryFailed.conversation_id AND FailedRecipientsDeliveryFailed.recipient_failure_type = 'MESSAGE_DELIVERY_FAILED' + +-- joins for quoted messages +LEFT JOIN Message AS QuotedMessage ON QuotedMessage.id = TextContent.quoted_message_id AND QuotedMessage.conversation_id = TextContent.conversation_id +LEFT JOIN User AS QuotedSender ON QuotedMessage.sender_user_id = QuotedSender.qualified_id +LEFT JOIN MessageTextContent AS QuotedTextContent ON QuotedTextContent.message_id = QuotedMessage.id AND QuotedMessage.conversation_id = TextContent.conversation_id +LEFT JOIN MessageAssetContent AS QuotedAssetContent ON QuotedAssetContent.message_id = QuotedMessage.id AND QuotedMessage.conversation_id = TextContent.conversation_id +LEFT JOIN MessageConversationLocationContent AS QuotedLocationContent ON QuotedLocationContent.message_id = QuotedMessage.id AND QuotedMessage.conversation_id = TextContent.conversation_id +-- end joins for quoted messages +LEFT JOIN MessageNewConversationReceiptModeContent AS NewConversationReceiptMode ON Message.id = NewConversationReceiptMode.message_id AND Message.conversation_id = NewConversationReceiptMode.conversation_id +LEFT JOIN MessageConversationReceiptModeChangedContent AS ConversationReceiptModeChanged ON Message.id = ConversationReceiptModeChanged.message_id AND Message.conversation_id = ConversationReceiptModeChanged.conversation_id +LEFT JOIN MessageConversationTimerChangedContent AS ConversationTimerChangedContent ON Message.id = ConversationTimerChangedContent.message_id AND Message.conversation_id = ConversationTimerChangedContent.conversation_id +LEFT JOIN MessageFederationTerminatedContent AS FederationTerminatedContent ON Message.id = FederationTerminatedContent.message_id AND Message.conversation_id = FederationTerminatedContent.conversation_id +LEFT JOIN MessageConversationProtocolChangedContent AS ConversationProtocolChangedContent ON Message.id = ConversationProtocolChangedContent.message_id AND Message.conversation_id = ConversationProtocolChangedContent.conversation_id +LEFT JOIN MessageConversationLocationContent AS ConversationLocationContent ON Message.id = ConversationLocationContent.message_id AND Message.conversation_id = ConversationLocationContent.conversation_id +LEFT JOIN MessageLegalHoldContent AS LegalHoldContent ON Message.id = LegalHoldContent.message_id AND Message.conversation_id = LegalHoldContent.conversation_id +LEFT JOIN SelfUser; +-- TODO: Remove IFNULL functions above if we can force SQLDelight to not unpack as notnull diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAO.kt index dfddce7c7dc..6e2c739eeab 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAO.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAO.kt @@ -126,8 +126,12 @@ interface MessageDAO { suspend fun getEphemeralMessagesMarkedForDeletion(): List + suspend fun getEphemeralMessagedMarkedForEndDeletion(): List + suspend fun updateSelfDeletionStartDate(conversationId: QualifiedIDEntity, messageId: String, selfDeletionStartDate: Instant) + suspend fun updateSelfDeletionEndDate(conversationId: QualifiedIDEntity, messageId: String, selfDeletionEndDate: Instant) + suspend fun getConversationUnreadEventsCount(conversationId: QualifiedIDEntity): Long suspend fun insertFailedRecipientDelivery( diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt index e582758c80c..d3e91d4f8f6 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt @@ -429,12 +429,28 @@ internal class MessageDAOImpl internal constructor( } } + override suspend fun getEphemeralMessagedMarkedForEndDeletion(): List { + return withContext(coroutineContext) { + queries.selectAllEphemeralMessagesMarkedForEndDeletion(mapper::toEntityMessageFromView).executeAsList() + } + } + override suspend fun updateSelfDeletionStartDate(conversationId: QualifiedIDEntity, messageId: String, selfDeletionStartDate: Instant) { return withContext(coroutineContext) { queries.markSelfDeletionStartDate(selfDeletionStartDate, conversationId, messageId) } } + override suspend fun updateSelfDeletionEndDate( + conversationId: QualifiedIDEntity, + messageId: String, + selfDeletionEndDate: Instant + ) { + return withContext(coroutineContext) { + queries.markSelfDeletionEndDate(selfDeletionEndDate, conversationId, messageId) + } + } + override suspend fun insertFailedRecipientDelivery( id: String, conversationsId: QualifiedIDEntity, diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageMapper.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageMapper.kt index 053349f27f4..f575d2e7203 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageMapper.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageMapper.kt @@ -421,6 +421,7 @@ object MessageMapper { expectsReadConfirmation: Boolean, expireAfterMillis: Long?, selfDeletionStartDate: Instant?, + selfDeletionEndDate: Instant?, readCount: Long, senderName: String?, senderHandle: String?, diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/unread/UserConfigDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/unread/UserConfigDAO.kt index ab1115df9fd..0b9671ba6bc 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/unread/UserConfigDAO.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/unread/UserConfigDAO.kt @@ -55,6 +55,8 @@ interface UserConfigDAO { suspend fun setCRLExpirationTime(url: String, timestamp: ULong) suspend fun getCRLsPerDomain(url: String): ULong? suspend fun observeCertificateExpirationTime(url: String): Flow + suspend fun setShouldNotifyForRevokedCertificate(shouldNotify: Boolean) + suspend fun observeShouldNotifyForRevokedCertificate(): Flow } @Suppress("TooManyFunctions") @@ -167,8 +169,16 @@ internal class UserConfigDAOImpl internal constructor( override suspend fun observeCertificateExpirationTime(url: String): Flow = metadataDAO.valueByKeyFlow(url).map { it?.toULongOrNull() } + override suspend fun setShouldNotifyForRevokedCertificate(shouldNotify: Boolean) { + metadataDAO.insertValue(shouldNotify.toString(), SHOULD_NOTIFY_FOR_REVOKED_CERTIFICATE) + } + + override suspend fun observeShouldNotifyForRevokedCertificate(): Flow = + metadataDAO.valueByKeyFlow(SHOULD_NOTIFY_FOR_REVOKED_CERTIFICATE).map { it?.toBoolean() } + private companion object { private const val SELF_DELETING_MESSAGES_KEY = "SELF_DELETING_MESSAGES" + private const val SHOULD_NOTIFY_FOR_REVOKED_CERTIFICATE = "should_notify_for_revoked_certificate" private const val MLS_MIGRATION_KEY = "MLS_MIGRATION" private const val SUPPORTED_PROTOCOLS_KEY = "SUPPORTED_PROTOCOLS" const val LEGAL_HOLD_REQUEST = "legal_hold_request" diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/TableMapper.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/TableMapper.kt index 4f787b0f849..46bd7bd4057 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/TableMapper.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/TableMapper.kt @@ -120,7 +120,8 @@ internal object TableMapper { visibilityAdapter = EnumColumnAdapter(), creation_dateAdapter = InstantTypeAdapter, last_edit_dateAdapter = InstantTypeAdapter, - self_deletion_start_dateAdapter = InstantTypeAdapter + self_deletion_start_dateAdapter = InstantTypeAdapter, + self_deletion_end_dateAdapter = InstantTypeAdapter ) val messageAssetContentAdapter = MessageAssetContent.Adapter( conversation_idAdapter = QualifiedIDAdapter, 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 f033fd6ddd6..830d4cc0358 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 @@ -2007,6 +2007,53 @@ class MessageDAOTest : BaseDatabaseTest() { assertEquals(message0.date, result[0].date) } + @Test + fun givenMessagesAreInserted_whenGettingEphemeraMessagesForEndDeletion_thenOnlyRelevantMessagesAreReturned() = runTest { + insertInitialData() + + val expectedMessages = listOf( + newRegularMessageEntity( + "1", + conversationId = conversationEntity1.id, + senderUserId = userEntity1.id, + status = MessageEntity.Status.SENT, + senderName = userEntity1.name!!, + selfDeletionStartDate = Instant.DISTANT_PAST, + expireAfterMs = 1.seconds.inWholeSeconds + ) + ) + + val allMessages = expectedMessages + listOf( + newRegularMessageEntity( + "2", + conversationId = conversationEntity1.id, + senderUserId = userEntity1.id, + status = MessageEntity.Status.SENT, + senderName = userEntity1.name!! + ), + newRegularMessageEntity( + "3", + conversationId = conversationEntity2.id, + senderUserId = userEntity2.id, + status = MessageEntity.Status.SENT, + senderName = userEntity2.name!! + ) + ) + + messageDAO.insertOrIgnoreMessages(allMessages) + + messageDAO.updateSelfDeletionEndDate( + conversationId = conversationEntity1.id, + messageId = "1", + selfDeletionEndDate = Instant.DISTANT_PAST.plus(1.seconds) + ) + + val result = messageDAO.getEphemeralMessagedMarkedForEndDeletion() + + assertEquals(result.size, 1) + assertEquals(result.first().id, "1") + } + private suspend fun insertInitialData() { userDAO.upsertUsers(listOf(userEntity1, userEntity2)) conversationDAO.insertConversation( diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageMapperTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageMapperTest.kt index d817ad73c93..7a9a96e17be 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageMapperTest.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageMapperTest.kt @@ -114,6 +114,7 @@ class MessageMapperTest { expectsReadConfirmation: Boolean = false, expireAfterMillis: Long? = null, selfDeletionStartDate: Instant? = null, + selfDeletionEndDate: Instant? = null, readCount: Long = 0, senderName: String? = null, senderHandle: String? = null, @@ -201,6 +202,7 @@ class MessageMapperTest { expectsReadConfirmation, expireAfterMillis, selfDeletionStartDate, + selfDeletionEndDate, readCount, senderName, senderHandle,