diff --git a/.github/workflows/cherry-pick-pr-to-newer-release-cycle.yml b/.github/workflows/cherry-pick-pr-to-newer-release-cycle.yml index f6dbeda7d91..9dfb71c505a 100644 --- a/.github/workflows/cherry-pick-pr-to-newer-release-cycle.yml +++ b/.github/workflows/cherry-pick-pr-to-newer-release-cycle.yml @@ -43,7 +43,7 @@ jobs: fetch-depth: 0 - name: Cherry pick to `develop` - uses: wireapp/action-auto-cherry-pick@v1.0.0 + uses: wireapp/action-auto-cherry-pick@v1.0.2 with: target-branch: develop pr-title-suffix: '🍒' diff --git a/cryptography/src/commonJvmAndroid/kotlin/com.wire.kalium.cryptography/ProteusClientCoreCryptoImpl.kt b/cryptography/src/commonJvmAndroid/kotlin/com.wire.kalium.cryptography/ProteusClientCoreCryptoImpl.kt index c6f2a91bd6f..fdc9bc2b69c 100644 --- a/cryptography/src/commonJvmAndroid/kotlin/com.wire.kalium.cryptography/ProteusClientCoreCryptoImpl.kt +++ b/cryptography/src/commonJvmAndroid/kotlin/com.wire.kalium.cryptography/ProteusClientCoreCryptoImpl.kt @@ -22,6 +22,7 @@ import com.wire.crypto.CoreCrypto import com.wire.crypto.CoreCryptoException import com.wire.crypto.client.toByteArray import com.wire.kalium.cryptography.exceptions.ProteusException +import com.wire.kalium.cryptography.exceptions.ProteusStorageMigrationException import io.ktor.util.decodeBase64Bytes import io.ktor.util.encodeBase64 import kotlinx.coroutines.sync.Mutex @@ -178,36 +179,44 @@ class ProteusClientCoreCryptoImpl private constructor( acc && File(rootDir).resolve(file).deleteRecursively() } - private suspend fun migrateFromCryptoBoxIfNecessary(coreCrypto: CoreCrypto, rootDir: String) { - if (cryptoBoxFilesExists(File(rootDir))) { - kaliumLogger.i("migrating from crypto box at: $rootDir") - coreCrypto.proteusCryptoboxMigrate(rootDir) - kaliumLogger.i("migration successful") - - if (deleteCryptoBoxFiles(rootDir)) { - kaliumLogger.i("successfully deleted old crypto box files") - } else { - kaliumLogger.e("Failed to deleted old crypto box files at $rootDir") - } - } - } - - @Suppress("TooGenericExceptionCaught") + @Suppress("TooGenericExceptionCaught", "ThrowsCount") suspend operator fun invoke(coreCrypto: CoreCrypto, rootDir: String): ProteusClientCoreCryptoImpl { try { migrateFromCryptoBoxIfNecessary(coreCrypto, rootDir) coreCrypto.proteusInit() return ProteusClientCoreCryptoImpl(coreCrypto) + } catch (exception: ProteusStorageMigrationException) { + throw exception } catch (e: CoreCryptoException) { throw ProteusException( - e.message, - ProteusException.fromProteusCode(coreCrypto.proteusLastErrorCode().toInt()), - coreCrypto.proteusLastErrorCode().toInt(), - e.cause + message = e.message, + code = ProteusException.fromProteusCode(coreCrypto.proteusLastErrorCode().toInt()), + intCode = coreCrypto.proteusLastErrorCode().toInt(), + cause = e.cause ) } catch (e: Exception) { throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, null, e.cause) } } + + @Suppress("TooGenericExceptionCaught") + private suspend fun migrateFromCryptoBoxIfNecessary(coreCrypto: CoreCrypto, rootDir: String) { + try { + if (cryptoBoxFilesExists(File(rootDir))) { + kaliumLogger.i("migrating from crypto box at: $rootDir") + coreCrypto.proteusCryptoboxMigrate(rootDir) + kaliumLogger.i("migration successful") + + if (deleteCryptoBoxFiles(rootDir)) { + kaliumLogger.i("successfully deleted old crypto box files") + } else { + kaliumLogger.e("Failed to deleted old crypto box files at $rootDir") + } + } + } catch (exception: Exception) { + kaliumLogger.e("Failed to migrate from crypto box to core crypto, exception: $exception") + throw ProteusStorageMigrationException("Failed to migrate from crypto box at $rootDir", exception) + } + } } } diff --git a/cryptography/src/commonMain/kotlin/com/wire/kalium/cryptography/exceptions/ProteusException.kt b/cryptography/src/commonMain/kotlin/com/wire/kalium/cryptography/exceptions/ProteusException.kt index 5a67b9d9e35..547d0deefa9 100644 --- a/cryptography/src/commonMain/kotlin/com/wire/kalium/cryptography/exceptions/ProteusException.kt +++ b/cryptography/src/commonMain/kotlin/com/wire/kalium/cryptography/exceptions/ProteusException.kt @@ -18,7 +18,7 @@ package com.wire.kalium.cryptography.exceptions -class ProteusException(message: String?, val code: Code, val intCode: Int?, cause: Throwable? = null) : Exception(message, cause) { +open class ProteusException(message: String?, val code: Code, val intCode: Int?, cause: Throwable? = null) : Exception(message, cause) { constructor(message: String?, code: Int, cause: Throwable? = null) : this( message, @@ -199,3 +199,6 @@ class ProteusException(message: String?, val code: Code, val intCode: Int?, caus } } } + +class ProteusStorageMigrationException(override val message: String, val rootCause: Throwable? = null) : + ProteusException(message, Int.MIN_VALUE, null) diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallMetadataProfile.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallMetadataProfile.kt index 96fd1f2d914..dd306c7b382 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallMetadataProfile.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallMetadataProfile.kt @@ -24,6 +24,7 @@ import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.user.OtherUserMinimized import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.type.UserType +import kotlinx.datetime.Instant data class CallMetadataProfile( val data: Map @@ -46,7 +47,8 @@ data class CallMetadata( val maxParticipants: Int = 0, // Was used for tracking val protocol: Conversation.ProtocolInfo, val activeSpeakers: Map> = mapOf(), - val users: List = listOf() + val users: List = listOf(), + val screenShareMetadata: CallScreenSharingMetadata = CallScreenSharingMetadata() ) { fun getFullParticipants(): List = participants.map { participant -> val user = users.firstOrNull { it.id == participant.userId } @@ -66,3 +68,14 @@ data class CallMetadata( ) } } + +/** + * [activeScreenShares] - map of user ids that share screen with the start timestamp + * [completedScreenShareDurationInMillis] - total time of already ended screen shares in milliseconds + * [uniqueSharingUsers] - set of users that were sharing a screen at least once + */ +data class CallScreenSharingMetadata( + val activeScreenShares: Map = emptyMap(), + val completedScreenShareDurationInMillis: Long = 0L, + val uniqueSharingUsers: Set = emptySet() +) diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/Conversation.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/Conversation.kt index a74f1903193..b07cf80f009 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/Conversation.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/Conversation.kt @@ -38,6 +38,8 @@ import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.type.UserType import com.wire.kalium.util.serialization.toJsonElement import kotlinx.datetime.Instant +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import kotlin.time.Duration /** @@ -258,11 +260,23 @@ data class Conversation( fun toLogMap(): Map } - data class Member(val id: UserId, val role: Role) { + @Serializable + data class Member( + @SerialName("id") val id: UserId, + @SerialName("role") val role: Role + ) { + + @Serializable sealed class Role { + + @Serializable data object Member : Role() + + @Serializable data object Admin : Role() - data class Unknown(val name: String) : Role() + + @Serializable + data class Unknown(@SerialName("name") val name: String) : Role() override fun toString(): String = when (this) { @@ -300,7 +314,6 @@ sealed class ConversationDetails(open val conversation: Conversation) { override val conversation: Conversation, val hasOngoingCall: Boolean = false, val isSelfUserMember: Boolean, - val isSelfUserCreator: Boolean, val selfRole: Conversation.Member.Role?, val isFavorite: Boolean = false // val isTeamAdmin: Boolean, TODO kubaz diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationStatus.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationStatus.kt index f8b451f6eee..445bec974c1 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationStatus.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationStatus.kt @@ -18,22 +18,29 @@ package com.wire.kalium.logic.data.conversation +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + /** * Conversation muting settings type */ -sealed class MutedConversationStatus(open val status: Int = 0) { +@Serializable +sealed class MutedConversationStatus(@SerialName("status") open val status: Int = 0) { /** * 0 -> All notifications are displayed */ + @Serializable data object AllAllowed : MutedConversationStatus(0) /** * 1 -> Only mentions and replies are displayed (normal messages muted) */ + @Serializable data object OnlyMentionsAndRepliesAllowed : MutedConversationStatus(1) /** * 3 -> No notifications are displayed */ + @Serializable data object AllMuted : MutedConversationStatus(3) } diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/logout/LogoutReason.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/logout/LogoutReason.kt index 578ce4d1617..3cfd14e6525 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/logout/LogoutReason.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/logout/LogoutReason.kt @@ -46,5 +46,11 @@ enum class LogoutReason { /** * Session Expired. */ - SESSION_EXPIRED; + SESSION_EXPIRED, + + /** + * The migration to CC failed. + * This will trigger a cleanup of the local client data and prepare for a fresh start without losing data. + */ + MIGRATION_TO_CC_FAILED } diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/Message.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/Message.kt index a188c0dabe5..5ea75254462 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/Message.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/Message.kt @@ -29,6 +29,8 @@ import com.wire.kalium.util.DateTimeUtil.toIsoDateTimeString import com.wire.kalium.util.serialization.toJsonElement import kotlinx.datetime.Clock import kotlinx.datetime.Instant +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlin.time.Duration @@ -473,15 +475,20 @@ sealed interface Message { } } + @Serializable data class ExpirationData( - val expireAfter: Duration, - val selfDeletionStatus: SelfDeletionStatus = SelfDeletionStatus.NotStarted + @SerialName("expire_after") val expireAfter: Duration, + @SerialName("self_deletion_status") val selfDeletionStatus: SelfDeletionStatus = SelfDeletionStatus.NotStarted ) { + @Serializable sealed class SelfDeletionStatus { + + @Serializable data object NotStarted : SelfDeletionStatus() - data class Started(val selfDeletionEndDate: Instant) : SelfDeletionStatus() + @Serializable + data class Started(@SerialName("self_deletion_end_date") val selfDeletionEndDate: Instant) : SelfDeletionStatus() fun toLogMap(): Map = when (this) { is NotStarted -> mutableMapOf( diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/mention/MessageMention.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/mention/MessageMention.kt index 504fac6903c..bb0b8069452 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/mention/MessageMention.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/mention/MessageMention.kt @@ -19,10 +19,13 @@ package com.wire.kalium.logic.data.message.mention import com.wire.kalium.logic.data.user.UserId +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +@Serializable data class MessageMention( - val start: Int, - val length: Int, - val userId: UserId, - val isSelfMention: Boolean + @SerialName("start") val start: Int, + @SerialName("length") val length: Int, + @SerialName("userId") val userId: UserId, + @SerialName("isSelfMention") val isSelfMention: Boolean ) diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/user/CreateUserTeam.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/user/CreateUserTeam.kt index f5e78793d5d..622d28ad298 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/user/CreateUserTeam.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/user/CreateUserTeam.kt @@ -1,3 +1,3 @@ package com.wire.kalium.logic.data.user -data class CreateUserTeam(val teamName: String) +data class CreateUserTeam(val teamId: String, val teamName: String) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9a8e027eb86..8fe82ae3248 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,7 +34,7 @@ moduleGraph = "0.7.0" # if you update sqlDelight check if https://github.com/cashapp/sqldelight/issues/4154 is fixed # and delete the workaround in the dev.mk file sqldelight = "2.0.1" -sqlcipher-android = "4.6.1" +sqlcipher-android = "4.5.6" pbandk = "0.14.2" turbine = "1.1.0" avs = "10.0.1" diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallRepository.kt index e5f8bc07ec4..f45ce704881 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallRepository.kt @@ -40,6 +40,7 @@ import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.FederatedIdMapper import com.wire.kalium.logic.data.id.GroupID import com.wire.kalium.logic.data.id.QualifiedClientID +import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.data.id.SubconversationId import com.wire.kalium.logic.data.id.toCrypto @@ -441,6 +442,8 @@ internal class CallDataSource( val currentParticipantIds = call.participants.map { it.userId }.toSet() val newParticipantIds = participants.map { it.userId }.toSet() + val sharingScreenParticipantIds = participants.filter { it.isSharingScreen } + .map { participant -> participant.id } val updatedUsers = call.users.toMutableList() @@ -456,7 +459,11 @@ internal class CallDataSource( this[conversationId] = call.copy( participants = participants, maxParticipants = max(call.maxParticipants, participants.size + 1), - users = updatedUsers + users = updatedUsers, + screenShareMetadata = updateScreenSharingMetadata( + metadata = call.screenShareMetadata, + usersCurrentlySharingScreen = sharingScreenParticipantIds + ) ) } @@ -479,6 +486,43 @@ internal class CallDataSource( } } + /** + * Manages call sharing metadata for analytical purposes by tracking the following: + * - **Active Screen Shares**: Maintains a record of currently active screen shares with their start times (local to the device). + * - **Completed Screen Share Duration**: Accumulates the total duration of screen shares that have already ended. + * - **Unique Sharing Users**: Keeps a unique list of all users who have shared their screen during the call. + * + * To update the metadata, the following steps are performed: + * 1. **Calculate Ended Screen Share Time**: Determine the total time for users who stopped sharing since the last update. + * 2. **Update Active Shares**: Filter out inactive shares and add any new ones, associating them with the current start time. + * 3. **Track Unique Users**: Append ids to current set in order to keep track of unique users. + */ + private fun updateScreenSharingMetadata( + metadata: CallScreenSharingMetadata, + usersCurrentlySharingScreen: List + ): CallScreenSharingMetadata { + val now = DateTimeUtil.currentInstant() + + val alreadyEndedScreenSharesTimeInMillis = metadata.activeScreenShares + .filterKeys { id -> id !in usersCurrentlySharingScreen } + .values + .sumOf { startTime -> DateTimeUtil.calculateMillisDifference(startTime, now) } + + val updatedShares = metadata.activeScreenShares + .filterKeys { id -> id in usersCurrentlySharingScreen } + .plus( + usersCurrentlySharingScreen + .filterNot { id -> metadata.activeScreenShares.containsKey(id) } + .associateWith { now } + ) + + return metadata.copy( + activeScreenShares = updatedShares, + completedScreenShareDurationInMillis = metadata.completedScreenShareDurationInMillis + alreadyEndedScreenSharesTimeInMillis, + uniqueSharingUsers = metadata.uniqueSharingUsers.plus(usersCurrentlySharingScreen.map { id -> id.toString() }) + ) + } + private fun clearStaleParticipantTimeout(participant: ParticipantMinimized) { callingLogger.i("Clear stale participant timer") val qualifiedClient = QualifiedClientID(ClientId(participant.clientId), participant.id) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ProteusClientProvider.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ProteusClientProvider.kt index 013deac601f..26f2f6ece6d 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ProteusClientProvider.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ProteusClientProvider.kt @@ -18,9 +18,11 @@ package com.wire.kalium.logic.data.client +import com.wire.kalium.cryptography.CoreCryptoCentral import com.wire.kalium.cryptography.ProteusClient import com.wire.kalium.cryptography.coreCryptoCentral import com.wire.kalium.cryptography.cryptoboxProteusClient +import com.wire.kalium.cryptography.exceptions.ProteusStorageMigrationException import com.wire.kalium.logger.KaliumLogLevel import com.wire.kalium.logger.obfuscateId import com.wire.kalium.logic.CoreFailure @@ -58,20 +60,15 @@ class ProteusClientProviderImpl( private val userId: UserId, private val passphraseStorage: PassphraseStorage, private val kaliumConfigs: KaliumConfigs, - private val dispatcher: KaliumDispatcher = KaliumDispatcherImpl + private val dispatcher: KaliumDispatcher = KaliumDispatcherImpl, + private val proteusMigrationRecoveryHandler: ProteusMigrationRecoveryHandler ) : ProteusClientProvider { private var _proteusClient: ProteusClient? = null private val mutex = Mutex() override suspend fun clearLocalFiles() { - mutex.withLock { - withContext(dispatcher.io) { - _proteusClient?.close() - _proteusClient = null - FileUtil.deleteDirectory(rootProteusPath) - } - } + mutex.withLock { removeLocalFiles() } } override suspend fun getOrCreate(): ProteusClient { @@ -109,7 +106,6 @@ class ProteusClientProviderImpl( databaseKey = SecurityHelperImpl(passphraseStorage).proteusDBSecret(userId).value ) } catch (e: Exception) { - val logMap = mapOf( "userId" to userId.value.obfuscateId(), "exception" to e, @@ -119,7 +115,7 @@ class ProteusClientProviderImpl( kaliumLogger.logStructuredJson(KaliumLogLevel.ERROR, TAG, logMap) throw e } - central.proteusClient() + getCentralProteusClientOrError(central) } else { cryptoboxProteusClient( rootDir = rootProteusPath, @@ -129,6 +125,34 @@ class ProteusClientProviderImpl( } } + private suspend fun getCentralProteusClientOrError(central: CoreCryptoCentral): ProteusClient { + return try { + central.proteusClient() + } catch (exception: ProteusStorageMigrationException) { + proteusMigrationRecoveryHandler.clearClientData { removeLocalFiles() } + val logMap = mapOf( + "userId" to userId.value.obfuscateId(), + "exception" to exception, + "message" to exception.message, + "stackTrace" to exception.stackTraceToString() + ) + kaliumLogger.withTextTag(TAG).logStructuredJson(KaliumLogLevel.ERROR, "Proteus storage migration failed", logMap) + throw exception + } + } + + /** + * Actually deletes the proteus local files. + * Important! It is the caller responsibility to use the mutex, DON'T add a mutex here or it will be dead lock it. + */ + private suspend fun removeLocalFiles() { + withContext(dispatcher.io) { + _proteusClient?.close() + _proteusClient = null + FileUtil.deleteDirectory(rootProteusPath) + } + } + private companion object { const val TAG = "ProteusClientProvider" } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ProteusMigrationRecoveryHandler.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ProteusMigrationRecoveryHandler.kt new file mode 100644 index 00000000000..9fe6880ecc4 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ProteusMigrationRecoveryHandler.kt @@ -0,0 +1,30 @@ +/* + * 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.data.client + +import com.wire.kalium.logic.data.logout.LogoutReason + +/** + * Handles the migration error of a proteus client storage from CryptoBox to CoreCrypto. + * It will perform a logout, using [LogoutReason.MIGRATION_TO_CC_FAILED] as the reason. + * + * This achieves that the client data is cleared and the user is logged out without losing content. + */ +interface ProteusMigrationRecoveryHandler { + suspend fun clearClientData(clearLocalFiles: suspend () -> Unit) +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt index 35bd5b96fd0..6382559ee23 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt @@ -272,7 +272,6 @@ internal class ConversationMapperImpl( conversation = fromConversationViewToEntity(daoModel), hasOngoingCall = callStatus != null, // todo: we can do better! isSelfUserMember = isMember, - isSelfUserCreator = isCreator == 1L, selfRole = selfRole?.let { conversationRoleMapper.fromDAO(it) }, isFavorite = isFavorite ) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventMapper.kt index 58e95922ffa..9bbffa82e2c 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventMapper.kt @@ -456,7 +456,7 @@ class EventMapper( } @Suppress("MagicNumber") - private fun mapConversationMutedStatus(status: Int?) = when (status) { + private fun mapConversationMutedStatus(status: Int?): MutedConversationStatus = when (status) { 0 -> MutedConversationStatus.AllAllowed 1 -> MutedConversationStatus.OnlyMentionsAndRepliesAllowed 3 -> MutedConversationStatus.AllMuted diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/session/SessionMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/session/SessionMapper.kt index 8092e6f3e4b..ef9c8eee0de 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/session/SessionMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/session/SessionMapper.kt @@ -107,6 +107,7 @@ internal class SessionMapperImpl : SessionMapper { LogoutReason.REMOVED_CLIENT -> LogoutReasonEntity.REMOVED_CLIENT LogoutReason.DELETED_ACCOUNT -> LogoutReasonEntity.DELETED_ACCOUNT LogoutReason.SESSION_EXPIRED -> LogoutReasonEntity.SESSION_EXPIRED + LogoutReason.MIGRATION_TO_CC_FAILED -> LogoutReasonEntity.MIGRATION_TO_CC_FAILED } override fun toSsoIdEntity(ssoId: SsoId?): SsoIdEntity? = @@ -140,6 +141,7 @@ internal class SessionMapperImpl : SessionMapper { LogoutReasonEntity.REMOVED_CLIENT -> LogoutReason.REMOVED_CLIENT LogoutReasonEntity.DELETED_ACCOUNT -> LogoutReason.DELETED_ACCOUNT LogoutReasonEntity.SESSION_EXPIRED -> LogoutReason.SESSION_EXPIRED + LogoutReasonEntity.MIGRATION_TO_CC_FAILED -> LogoutReason.MIGRATION_TO_CC_FAILED } override fun fromEntityToProxyCredentialsDTO(proxyCredentialsEntity: ProxyCredentialsEntity): ProxyCredentialsDTO = diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserRepository.kt index 35360a5aaa8..e8760f83b79 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserRepository.kt @@ -166,6 +166,7 @@ interface UserRepository { suspend fun getUsersMinimizedByQualifiedIDs(userIds: List): Either> suspend fun getNameAndHandle(userId: UserId): Either suspend fun migrateUserToTeam(teamName: String): Either + suspend fun updateTeamId(userId: UserId, teamId: TeamId): Either } @Suppress("LongParameterList", "TooManyFunctions") @@ -652,7 +653,7 @@ internal class UserDataSource internal constructor( override suspend fun migrateUserToTeam(teamName: String): Either { return wrapApiRequest { upgradePersonalToTeamApi.migrateToTeam(teamName) }.map { dto -> - CreateUserTeam(dto.teamName) + CreateUserTeam(dto.teamId, dto.teamName) } .onSuccess { kaliumLogger.d("Migrated user to team") @@ -663,6 +664,10 @@ internal class UserDataSource internal constructor( } } + override suspend fun updateTeamId(userId: UserId, teamId: TeamId): Either = wrapStorageRequest { + userDAO.updateTeamId(userId.toDao(), teamId.value) + } + companion object { internal const val SELF_USER_ID_KEY = "selfUserID" 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 451a14b6779..e7e1a1637a8 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 @@ -51,6 +51,7 @@ import com.wire.kalium.logic.data.client.MLSClientProvider import com.wire.kalium.logic.data.client.MLSClientProviderImpl import com.wire.kalium.logic.data.client.ProteusClientProvider import com.wire.kalium.logic.data.client.ProteusClientProviderImpl +import com.wire.kalium.logic.data.client.ProteusMigrationRecoveryHandler import com.wire.kalium.logic.data.client.remote.ClientRemoteDataSource import com.wire.kalium.logic.data.client.remote.ClientRemoteRepository import com.wire.kalium.logic.data.connection.ConnectionDataSource @@ -189,6 +190,7 @@ import com.wire.kalium.logic.feature.client.IsAllowedToRegisterMLSClientUseCase import com.wire.kalium.logic.feature.client.IsAllowedToRegisterMLSClientUseCaseImpl import com.wire.kalium.logic.feature.client.MLSClientManager import com.wire.kalium.logic.feature.client.MLSClientManagerImpl +import com.wire.kalium.logic.feature.client.ProteusMigrationRecoveryHandlerImpl import com.wire.kalium.logic.feature.client.RegisterMLSClientUseCase import com.wire.kalium.logic.feature.client.RegisterMLSClientUseCaseImpl import com.wire.kalium.logic.feature.connection.ConnectionScope @@ -560,6 +562,10 @@ class UserSessionScope internal constructor( } } + private val invalidateTeamId = { + _teamId = Either.Left(CoreFailure.Unknown(Throwable("NotInitialized"))) + } + private val selfTeamId = SelfTeamIdProvider { teamId() } private val accessTokenRepository: AccessTokenRepository @@ -629,12 +635,17 @@ class UserSessionScope internal constructor( private val updateKeyingMaterialThresholdProvider: UpdateKeyingMaterialThresholdProvider get() = UpdateKeyingMaterialThresholdProviderImpl(kaliumConfigs) + private val proteusMigrationRecoveryHandler: ProteusMigrationRecoveryHandler by lazy { + ProteusMigrationRecoveryHandlerImpl(lazy { logout }) + } + val proteusClientProvider: ProteusClientProvider by lazy { ProteusClientProviderImpl( rootProteusPath = rootPathsProvider.rootProteusPath(userId), userId = userId, passphraseStorage = globalPreferences.passphraseStorage, - kaliumConfigs = kaliumConfigs + kaliumConfigs = kaliumConfigs, + proteusMigrationRecoveryHandler = proteusMigrationRecoveryHandler ) } @@ -932,11 +943,12 @@ class UserSessionScope internal constructor( kaliumFileSystem = kaliumFileSystem ) - private val eventGatherer: EventGatherer get() = EventGathererImpl( - eventRepository = eventRepository, - incrementalSyncRepository = incrementalSyncRepository, - logger = userScopedLogger - ) + private val eventGatherer: EventGatherer + get() = EventGathererImpl( + eventRepository = eventRepository, + incrementalSyncRepository = incrementalSyncRepository, + logger = userScopedLogger + ) private val eventProcessor: EventProcessor by lazy { EventProcessorImpl( @@ -1899,6 +1911,9 @@ class UserSessionScope internal constructor( val search: SearchScope by lazy { SearchScope( + mlsPublicKeysRepository = mlsPublicKeysRepository, + getDefaultProtocol = getDefaultProtocol, + getConversationProtocolInfo = conversations.getConversationProtocolInfo, searchUserRepository = searchUserRepository, selfUserId = userId, sessionRepository = globalScope.sessionRepository, @@ -2097,7 +2112,7 @@ class UserSessionScope internal constructor( ) val migrateFromPersonalToTeam: MigrateFromPersonalToTeamUseCase - get() = MigrateFromPersonalToTeamUseCaseImpl(userRepository) + get() = MigrateFromPersonalToTeamUseCaseImpl(userId, userRepository, invalidateTeamId) internal val getProxyCredentials: GetProxyCredentialsUseCase get() = GetProxyCredentialsUseCaseImpl(sessionManager) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/auth/LogoutUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/auth/LogoutUseCase.kt index d3be95db8b5..4609d23c225 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/auth/LogoutUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/auth/LogoutUseCase.kt @@ -31,6 +31,7 @@ import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.client.ClearClientDataUseCase import com.wire.kalium.logic.feature.session.DeregisterTokenUseCase import com.wire.kalium.logic.featureFlags.KaliumConfigs +import com.wire.kalium.logic.kaliumLogger import com.wire.kalium.logic.sync.UserSessionWorkScheduler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.cancel @@ -106,6 +107,9 @@ internal class LogoutUseCaseImpl @Suppress("LongParameterList") constructor( } LogoutReason.SELF_SOFT_LOGOUT -> clearCurrentClientIdAndFirebaseTokenFlag() + LogoutReason.MIGRATION_TO_CC_FAILED -> prepareForCoreCryptoMigrationRecovery() + }.also { + kaliumLogger.withTextTag(TAG).d("Logout reason: $reason") } userConfigRepository.clearE2EISettings() @@ -115,6 +119,13 @@ internal class LogoutUseCaseImpl @Suppress("LongParameterList") constructor( }.let { if (waitUntilCompletes) it.join() else it } } + private suspend fun prepareForCoreCryptoMigrationRecovery() { + clearClientDataUseCase() + logoutRepository.clearClientRelatedLocalMetadata() + clientRepository.clearRetainedClientId() + pushTokenRepository.setUpdateFirebaseTokenFlag(true) + } + private suspend fun clearCurrentClientIdAndFirebaseTokenFlag() { clientRepository.clearCurrentClientId() clientRepository.clearNewClients() @@ -146,5 +157,6 @@ internal class LogoutUseCaseImpl @Suppress("LongParameterList") constructor( companion object { const val CLEAR_DATA_DELAY = 1000L + const val TAG = "LogoutUseCase" } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/ProteusMigrationRecoveryHandlerImpl.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/ProteusMigrationRecoveryHandlerImpl.kt new file mode 100644 index 00000000000..c2b7e42bfbb --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/ProteusMigrationRecoveryHandlerImpl.kt @@ -0,0 +1,52 @@ +/* + * 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.client + +import com.wire.kalium.logic.data.client.ProteusMigrationRecoveryHandler +import com.wire.kalium.logic.data.logout.LogoutReason +import com.wire.kalium.logic.feature.auth.LogoutUseCase +import com.wire.kalium.logic.kaliumLogger + +internal class ProteusMigrationRecoveryHandlerImpl( + private val logoutUseCase: Lazy +) : ProteusMigrationRecoveryHandler { + + /** + * Handles the migration error of a proteus client storage from CryptoBox to CoreCrypto. + * It will perform a logout, using [LogoutReason.MIGRATION_TO_CC_FAILED] as the reason. + * + * This achieves that the client data is cleared and the user is logged out without losing content. + */ + @Suppress("TooGenericExceptionCaught") + override suspend fun clearClientData(clearLocalFiles: suspend () -> Unit) { + try { + kaliumLogger.withTextTag(TAG).i("Starting the recovery from failed Proteus storage migration") + clearLocalFiles() + logoutUseCase.value(LogoutReason.MIGRATION_TO_CC_FAILED, true) + } catch (e: Exception) { + kaliumLogger.withTextTag(TAG).e("Fatal, error while clearing client data: $e") + throw e + } finally { + kaliumLogger.withTextTag(TAG).i("Finished the recovery from failed Proteus storage migration") + } + } + + private companion object { + const val TAG = "ProteusMigrationRecoveryHandler" + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/RegisterClientUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/RegisterClientUseCase.kt index 9b2a097ddc8..c92236cf238 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/RegisterClientUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/RegisterClientUseCase.kt @@ -36,6 +36,7 @@ import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.flatMap import com.wire.kalium.logic.functional.fold import com.wire.kalium.logic.functional.map +import com.wire.kalium.logic.kaliumLogger import com.wire.kalium.network.exceptions.AuthenticationCodeFailure import com.wire.kalium.network.exceptions.KaliumException import com.wire.kalium.network.exceptions.authenticationCodeFailure @@ -146,8 +147,9 @@ class RegisterClientUseCaseImpl @OptIn(DelicateKaliumApi::class) internal constr verificationCode, modelPostfix, ) - }.fold({ - RegisterClientResult.Failure.Generic(it) + }.fold({ error -> + kaliumLogger.withTextTag(TAG).e("There was an error while registering the client $error") + RegisterClientResult.Failure.Generic(error) }, { registerClientParam -> clientRepository.registerClient(registerClientParam) // todo? separate this in mls client usesCase register! separate everything @@ -241,4 +243,8 @@ class RegisterClientUseCaseImpl @OptIn(DelicateKaliumApi::class) internal constr } } } + + private companion object { + const val TAG = "RegisterClientUseCase" + } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/IsFederationSearchAllowedUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/IsFederationSearchAllowedUseCase.kt new file mode 100644 index 00000000000..964b17c01e8 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/IsFederationSearchAllowedUseCase.kt @@ -0,0 +1,82 @@ +/* + * 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.search + +import com.wire.kalium.logic.data.conversation.Conversation +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.mls.MLSPublicKeys +import com.wire.kalium.logic.data.mlspublickeys.MLSPublicKeysRepository +import com.wire.kalium.logic.data.user.SupportedProtocol +import com.wire.kalium.logic.feature.conversation.GetConversationProtocolInfoUseCase +import com.wire.kalium.logic.feature.user.GetDefaultProtocolUseCase +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.util.KaliumDispatcher +import com.wire.kalium.util.KaliumDispatcherImpl +import kotlinx.coroutines.withContext + +/** + * Check if FederatedSearchIsAllowed according to MLS configuration of the backend + * and the conversation protocol if a [ConversationId] is provided. + */ +interface IsFederationSearchAllowedUseCase { + suspend operator fun invoke(conversationId: ConversationId?): Boolean +} + +@Suppress("FunctionNaming") +internal fun IsFederationSearchAllowedUseCase( + mlsPublicKeysRepository: MLSPublicKeysRepository, + getDefaultProtocol: GetDefaultProtocolUseCase, + getConversationProtocolInfo: GetConversationProtocolInfoUseCase, + dispatcher: KaliumDispatcher = KaliumDispatcherImpl +) = object : IsFederationSearchAllowedUseCase { + + override suspend operator fun invoke(conversationId: ConversationId?): Boolean = withContext(dispatcher.io) { + val isMlsConfiguredForBackend = hasMLSKeysConfiguredForBackend() + when (isMlsConfiguredForBackend) { + true -> isConversationProtocolAbleToFederate(conversationId) + false -> true + } + } + + private suspend fun hasMLSKeysConfiguredForBackend(): Boolean { + return when (val mlsKeysResult = mlsPublicKeysRepository.getKeys()) { + is Either.Left -> false + is Either.Right -> { + val mlsKeys: MLSPublicKeys = mlsKeysResult.value + mlsKeys.removal != null && mlsKeys.removal?.isNotEmpty() == true + } + } + } + + /** + * MLS is enabled, then we need to check if the protocol for the conversation is able to federate. + */ + private suspend fun isConversationProtocolAbleToFederate(conversationId: ConversationId?): Boolean { + val isProteusTeam = getDefaultProtocol() == SupportedProtocol.PROTEUS + val isOtherDomainAllowed: Boolean = conversationId?.let { + when (val result = getConversationProtocolInfo(it)) { + is GetConversationProtocolInfoUseCase.Result.Failure -> !isProteusTeam + + is GetConversationProtocolInfoUseCase.Result.Success -> + !isProteusTeam && result.protocolInfo !is Conversation.ProtocolInfo.Proteus + } + } ?: !isProteusTeam + return isOtherDomainAllowed + } + +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/SearchScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/SearchScope.kt index 662514c9447..bb5c11a9ff9 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/SearchScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/SearchScope.kt @@ -17,12 +17,19 @@ */ package com.wire.kalium.logic.feature.search +import com.wire.kalium.logic.data.mlspublickeys.MLSPublicKeysRepository import com.wire.kalium.logic.data.publicuser.SearchUserRepository import com.wire.kalium.logic.data.session.SessionRepository import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.conversation.GetConversationProtocolInfoUseCase +import com.wire.kalium.logic.feature.user.GetDefaultProtocolUseCase import com.wire.kalium.logic.featureFlags.KaliumConfigs +@Suppress("LongParameterList") class SearchScope internal constructor( + private val mlsPublicKeysRepository: MLSPublicKeysRepository, + private val getDefaultProtocol: GetDefaultProtocolUseCase, + private val getConversationProtocolInfo: GetConversationProtocolInfoUseCase, private val searchUserRepository: SearchUserRepository, private val sessionRepository: SessionRepository, private val selfUserId: UserId, @@ -42,4 +49,7 @@ class SearchScope internal constructor( kaliumConfigs.maxRemoteSearchResultCount ) val federatedSearchParser: FederatedSearchParser get() = FederatedSearchParser(sessionRepository, selfUserId) + + val isFederationSearchAllowedUseCase: IsFederationSearchAllowedUseCase + get() = IsFederationSearchAllowedUseCase(mlsPublicKeysRepository, getDefaultProtocol, getConversationProtocolInfo) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/migration/MigrateFromPersonalToTeamUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/migration/MigrateFromPersonalToTeamUseCase.kt index 8d3e841940e..5f2ca1c35bf 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/migration/MigrateFromPersonalToTeamUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/migration/MigrateFromPersonalToTeamUseCase.kt @@ -18,8 +18,12 @@ package com.wire.kalium.logic.feature.user.migration import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.id.TeamId +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.NetworkFailure import com.wire.kalium.logic.data.user.UserRepository import com.wire.kalium.logic.functional.fold +import com.wire.kalium.network.exceptions.KaliumException /** * Use case to migrate user personal account to team account. @@ -30,23 +34,68 @@ interface MigrateFromPersonalToTeamUseCase { } sealed class MigrateFromPersonalToTeamResult { - data class Success(val teamName: String) : MigrateFromPersonalToTeamResult() - data class Error(val failure: CoreFailure) : MigrateFromPersonalToTeamResult() + data object Success : MigrateFromPersonalToTeamResult() + data class Error(val failure: MigrateFromPersonalToTeamFailure) : + MigrateFromPersonalToTeamResult() +} + +sealed class MigrateFromPersonalToTeamFailure { + + data class UnknownError(val coreFailure: CoreFailure) : MigrateFromPersonalToTeamFailure() + class UserAlreadyInTeam : MigrateFromPersonalToTeamFailure() { + companion object { + const val ERROR_LABEL = "user-already-in-a-team" + } + } + + data object NoNetwork : MigrateFromPersonalToTeamFailure() } internal class MigrateFromPersonalToTeamUseCaseImpl internal constructor( + private val selfUserId: UserId, private val userRepository: UserRepository, + private val invalidateTeamId: () -> Unit ) : MigrateFromPersonalToTeamUseCase { override suspend operator fun invoke( teamName: String, ): MigrateFromPersonalToTeamResult { - return userRepository.migrateUserToTeam(teamName) - .fold( - { error -> return MigrateFromPersonalToTeamResult.Error(error) }, - { success -> - // TODO Invalidate team id in memory so UserSessionScope.selfTeamId got updated data WPB-12187 - MigrateFromPersonalToTeamResult.Success(teamName = success.teamName) + return userRepository.migrateUserToTeam(teamName).fold({ error -> + return when (error) { + is NetworkFailure.ServerMiscommunication -> { + if (error.kaliumException is KaliumException.InvalidRequestError) { + val response = error.kaliumException.errorResponse + if (response.label == MigrateFromPersonalToTeamFailure.UserAlreadyInTeam.ERROR_LABEL) { + MigrateFromPersonalToTeamResult.Error( + MigrateFromPersonalToTeamFailure.UserAlreadyInTeam() + ) + } else { + MigrateFromPersonalToTeamResult.Error( + MigrateFromPersonalToTeamFailure.UnknownError(error) + ) + } + } else { + MigrateFromPersonalToTeamResult.Error( + MigrateFromPersonalToTeamFailure.UnknownError(error) + ) + } + } + + is NetworkFailure.NoNetworkConnection -> { + MigrateFromPersonalToTeamResult.Error(MigrateFromPersonalToTeamFailure.NoNetwork) + } + + else -> { + MigrateFromPersonalToTeamResult.Error( + MigrateFromPersonalToTeamFailure.UnknownError( + error + ) + ) } - ) + } + }, { user -> + userRepository.updateTeamId(selfUserId, TeamId(user.teamId)) + invalidateTeamId() + MigrateFromPersonalToTeamResult.Success + }) } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncCriteriaProvider.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncCriteriaProvider.kt index 49f0567d0a1..fb9f20448c0 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncCriteriaProvider.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncCriteriaProvider.kt @@ -116,6 +116,7 @@ internal class SlowSlowSyncCriteriaProviderImpl( LogoutReason.SESSION_EXPIRED -> "Logout: SESSION_EXPIRED" LogoutReason.REMOVED_CLIENT -> "Logout: REMOVED_CLIENT" LogoutReason.DELETED_ACCOUNT -> "Logout: DELETED_ACCOUNT" + LogoutReason.MIGRATION_TO_CC_FAILED -> "Logout: MIGRATION_TO_CC_FAILED" null -> null }?.let { MissingRequirement(it) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallRepositoryTest.kt index 1b0b223aa64..e1c9cb91886 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallRepositoryTest.kt @@ -23,6 +23,7 @@ import com.wire.kalium.cryptography.CryptoQualifiedClientId import com.wire.kalium.cryptography.MLSClient import com.wire.kalium.logic.StorageFailure import com.wire.kalium.logic.data.call.CallRepositoryTest.Arrangement.Companion.callerId +import com.wire.kalium.logic.data.call.CallRepositoryTest.Arrangement.Companion.participant import com.wire.kalium.logic.data.call.mapper.CallMapperImpl import com.wire.kalium.logic.data.client.MLSClientProvider import com.wire.kalium.logic.data.conversation.ClientId @@ -84,6 +85,7 @@ import kotlinx.datetime.Clock import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse +import kotlin.test.assertNotNull import kotlin.test.assertTrue import kotlin.time.DurationUnit import kotlin.time.toDuration @@ -174,7 +176,6 @@ class CallRepositoryTest { Arrangement.groupConversation, false, isSelfUserMember = true, - isSelfUserCreator = true, selfRole = Conversation.Member.Role.Member ) ) @@ -212,7 +213,6 @@ class CallRepositoryTest { ConversationDetails.Group( Arrangement.groupConversation, isSelfUserMember = true, - isSelfUserCreator = true, selfRole = Conversation.Member.Role.Member ) ) @@ -266,7 +266,6 @@ class CallRepositoryTest { ConversationDetails.Group( Arrangement.groupConversation, isSelfUserMember = true, - isSelfUserCreator = true, selfRole = Conversation.Member.Role.Member ) ) @@ -309,7 +308,6 @@ class CallRepositoryTest { ConversationDetails.Group( Arrangement.groupConversation, isSelfUserMember = true, - isSelfUserCreator = true, selfRole = Conversation.Member.Role.Member ) ) @@ -366,7 +364,6 @@ class CallRepositoryTest { ConversationDetails.Group( Arrangement.groupConversation, isSelfUserMember = true, - isSelfUserCreator = true, selfRole = Conversation.Member.Role.Member ) ) @@ -1514,6 +1511,158 @@ class CallRepositoryTest { ) } + @Test + fun givenCallWithParticipantsNotSharingScreen_whenOneStartsToShare_thenSharingMetadataHasProperValues() = runTest { + // given + val otherParticipant = participant.copy(id = QualifiedID("anotherParticipantId", "participantDomain")) + val participantsList = listOf(participant, otherParticipant) + val (_, callRepository) = Arrangement() + .givenGetKnownUserMinimizedSucceeds() + .arrange() + callRepository.updateCallMetadataProfileFlow( + callMetadataProfile = CallMetadataProfile( + data = mapOf(Arrangement.conversationId to createCallMetadata().copy(participants = participantsList)) + ) + ) + + // when + callRepository.updateCallParticipants( + Arrangement.conversationId, + listOf(participant, otherParticipant.copy(isSharingScreen = true)) + ) + val callMetadata = callRepository.getCallMetadataProfile()[Arrangement.conversationId] + + // then + assertNotNull(callMetadata) + assertEquals(0L, callMetadata.screenShareMetadata.completedScreenShareDurationInMillis) + assertTrue(callMetadata.screenShareMetadata.activeScreenShares.containsKey(otherParticipant.id)) + } + + @Test + fun givenCallWithParticipantsNotSharingScreen_whenTwoStartsAndOneStops_thenSharingMetadataHasProperValues() = runTest { + // given + val (_, callRepository) = Arrangement() + .arrange() + val secondParticipant = participant.copy(id = QualifiedID("secondParticipantId", "participantDomain")) + val thirdParticipant = participant.copy(id = QualifiedID("thirdParticipantId", "participantDomain")) + val participantsList = listOf(participant, secondParticipant, thirdParticipant) + callRepository.updateCallMetadataProfileFlow( + callMetadataProfile = CallMetadataProfile( + data = mapOf(Arrangement.conversationId to createCallMetadata().copy(participants = participantsList)) + ) + ) + + // when + callRepository.updateCallParticipants( + Arrangement.conversationId, + listOf(participant, secondParticipant.copy(isSharingScreen = true), thirdParticipant.copy(isSharingScreen = true)) + ) + callRepository.updateCallParticipants( + Arrangement.conversationId, + listOf(participant, secondParticipant, thirdParticipant.copy(isSharingScreen = true)) + ) + val callMetadata = callRepository.getCallMetadataProfile()[Arrangement.conversationId] + + // then + assertNotNull(callMetadata) + assertTrue(callMetadata.screenShareMetadata.activeScreenShares.size == 1) + assertTrue(callMetadata.screenShareMetadata.activeScreenShares.containsKey(thirdParticipant.id)) + } + + @Test + fun givenCallWithParticipantsSharingScreen_whenOneStopsToShare_thenSharingMetadataHasProperValues() = runTest { + // given + val (_, callRepository) = Arrangement() + .arrange() + val otherParticipant = participant.copy(id = QualifiedID("anotherParticipantId", "participantDomain")) + val participantsList = listOf(participant, otherParticipant) + callRepository.updateCallMetadataProfileFlow( + callMetadataProfile = CallMetadataProfile( + data = mapOf(Arrangement.conversationId to createCallMetadata().copy(participants = participantsList)) + ) + ) + callRepository.updateCallParticipants( + Arrangement.conversationId, + listOf(participant, otherParticipant.copy(isSharingScreen = true)) + ) + + // when + callRepository.updateCallParticipants( + Arrangement.conversationId, + listOf(participant, otherParticipant.copy(isSharingScreen = false)) + ) + val callMetadata = callRepository.getCallMetadataProfile()[Arrangement.conversationId] + + // then + assertNotNull(callMetadata) + assertTrue(callMetadata.screenShareMetadata.activeScreenShares.isEmpty()) + } + + @Test + fun givenCallWithParticipantsSharingScreen_whenTheSameParticipantIsSharingMultipleTime_thenSharingMetadataHasUserIdOnlyOnce() = + runTest { + // given + val (_, callRepository) = Arrangement() + .arrange() + val otherParticipant = participant.copy(id = QualifiedID("anotherParticipantId", "participantDomain")) + val participantsList = listOf(participant, otherParticipant) + callRepository.updateCallMetadataProfileFlow( + callMetadataProfile = CallMetadataProfile( + data = mapOf(Arrangement.conversationId to createCallMetadata().copy(participants = participantsList)) + ) + ) + + // when + callRepository.updateCallParticipants( + Arrangement.conversationId, + listOf(participant, otherParticipant.copy(isSharingScreen = true)) + ) + callRepository.updateCallParticipants( + Arrangement.conversationId, + listOf(participant, otherParticipant.copy(isSharingScreen = false)) + ) + callRepository.updateCallParticipants( + Arrangement.conversationId, + listOf(participant, otherParticipant.copy(isSharingScreen = true)) + ) + val callMetadata = callRepository.getCallMetadataProfile()[Arrangement.conversationId] + + // then + assertNotNull(callMetadata) + assertTrue(callMetadata.screenShareMetadata.uniqueSharingUsers.size == 1) + assertTrue(callMetadata.screenShareMetadata.uniqueSharingUsers.contains(otherParticipant.id.toString())) + } + + @Test + fun givenCallWithParticipantsSharingScreen_whenTwoParticipantsAreSharing_thenSharingMetadataHasBothOfUsersIds() = runTest { + // given + val (_, callRepository) = Arrangement() + .arrange() + val secondParticipant = participant.copy(id = QualifiedID("secondParticipantId", "participantDomain")) + val thirdParticipant = participant.copy(id = QualifiedID("thirdParticipantId", "participantDomain")) + val participantsList = listOf(participant, secondParticipant, thirdParticipant) + callRepository.updateCallMetadataProfileFlow( + callMetadataProfile = CallMetadataProfile( + data = mapOf(Arrangement.conversationId to createCallMetadata().copy(participants = participantsList)) + ) + ) + + // when + callRepository.updateCallParticipants( + Arrangement.conversationId, + listOf(participant, secondParticipant.copy(isSharingScreen = true), thirdParticipant.copy(isSharingScreen = true)) + ) + val callMetadata = callRepository.getCallMetadataProfile()[Arrangement.conversationId] + + // then + assertNotNull(callMetadata) + assertTrue(callMetadata.screenShareMetadata.uniqueSharingUsers.size == 2) + assertEquals( + setOf(secondParticipant.id.toString(), thirdParticipant.id.toString()), + callMetadata.screenShareMetadata.uniqueSharingUsers + ) + } + private fun provideCall(id: ConversationId, status: CallStatus) = Call( conversationId = id, status = status, diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/auth/LogoutUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/auth/LogoutUseCaseTest.kt index c680b29fa84..f443b8a5914 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/auth/LogoutUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/auth/LogoutUseCaseTest.kt @@ -51,7 +51,6 @@ import io.mockative.every import io.mockative.mock import io.mockative.once import io.mockative.time -import io.mockative.verify import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope @@ -342,6 +341,39 @@ class LogoutUseCaseTest { }.wasInvoked(exactly = calls.size.time) } + @Test + fun givenAMigrationFailedLogout_whenLoggingOut_thenExecuteAllRequiredActions() = runTest { + val reason = LogoutReason.MIGRATION_TO_CC_FAILED + val (arrangement, logoutUseCase) = Arrangement() + .withLogoutResult(Either.Right(Unit)) + .withSessionLogoutResult(Either.Right(Unit)) + .withAllValidSessionsResult(Either.Right(listOf(Arrangement.VALID_ACCOUNT_INFO))) + .withDeregisterTokenResult(DeregisterTokenUseCase.Result.Success) + .withClearCurrentClientIdResult(Either.Right(Unit)) + .withClearRetainedClientIdResult(Either.Right(Unit)) + .withUserSessionScopeGetResult(null) + .withFirebaseTokenUpdate() + .withNoOngoingCalls() + .arrange() + + logoutUseCase.invoke(reason) + arrangement.globalTestScope.advanceUntilIdle() + + coVerify { + arrangement.clearClientDataUseCase.invoke() + }.wasInvoked(exactly = once) + coVerify { + arrangement.logoutRepository.clearClientRelatedLocalMetadata() + }.wasInvoked(exactly = once) + + coVerify { + arrangement.clientRepository.clearRetainedClientId() + }.wasInvoked(exactly = once) + coVerify { + arrangement.pushTokenRepository.setUpdateFirebaseTokenFlag(eq(true)) + }.wasInvoked(exactly = once) + } + private class Arrangement { @Mock val logoutRepository = mock(LogoutRepository::class) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallOnConversationChangeUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallOnConversationChangeUseCaseTest.kt index 6212866cc8e..77cbac0d93a 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallOnConversationChangeUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallOnConversationChangeUseCaseTest.kt @@ -249,7 +249,6 @@ class EndCallOnConversationChangeUseCaseTest { conversation = conversation, hasOngoingCall = true, isSelfUserMember = false, - isSelfUserCreator = false, selfRole = null ) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationDetailsUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationDetailsUseCaseTest.kt index 3bf203d73db..3ff8776be81 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationDetailsUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationDetailsUseCaseTest.kt @@ -81,7 +81,6 @@ class ObserveConversationDetailsUseCaseTest { ConversationDetails.Group( conversation, isSelfUserMember = true, - isSelfUserCreator = true, selfRole = Conversation.Member.Role.Member ) ), @@ -89,7 +88,6 @@ class ObserveConversationDetailsUseCaseTest { ConversationDetails.Group( conversation.copy(name = "New Name"), isSelfUserMember = true, - isSelfUserCreator = true, selfRole = Conversation.Member.Role.Member ) ) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationListDetailsUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationListDetailsUseCaseTest.kt index e2f9044bf6c..a3340b10fef 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationListDetailsUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationListDetailsUseCaseTest.kt @@ -65,7 +65,6 @@ class ObserveConversationListDetailsUseCaseTest { ConversationDetails.Group( groupConversation, isSelfUserMember = true, - isSelfUserCreator = true, selfRole = Conversation.Member.Role.Member ) @@ -100,14 +99,12 @@ class ObserveConversationListDetailsUseCaseTest { ConversationDetails.Group( groupConversation1, isSelfUserMember = true, - isSelfUserCreator = true, selfRole = Conversation.Member.Role.Member ) val groupConversationDetails2 = ConversationDetails.Group( groupConversation2, isSelfUserMember = true, - isSelfUserCreator = true, selfRole = Conversation.Member.Role.Member ) @@ -141,7 +138,6 @@ class ObserveConversationListDetailsUseCaseTest { val groupConversationDetails = ConversationDetails.Group( conversation = groupConversation, isSelfUserMember = true, - isSelfUserCreator = true, selfRole = Conversation.Member.Role.Member ) @@ -175,7 +171,6 @@ class ObserveConversationListDetailsUseCaseTest { ConversationDetails.Group( groupConversation, isSelfUserMember = true, - isSelfUserCreator = true, selfRole = Conversation.Member.Role.Member ) ) @@ -223,7 +218,6 @@ class ObserveConversationListDetailsUseCaseTest { val groupConversationDetails = ConversationDetails.Group( groupConversation, isSelfUserMember = true, - isSelfUserCreator = true, selfRole = Conversation.Member.Role.Member ) @@ -258,7 +252,6 @@ class ObserveConversationListDetailsUseCaseTest { val groupConversationDetails = ConversationDetails.Group( groupConversation, isSelfUserMember = true, - isSelfUserCreator = true, selfRole = Conversation.Member.Role.Member ) @@ -287,7 +280,6 @@ class ObserveConversationListDetailsUseCaseTest { val groupConversationDetails = ConversationDetails.Group( groupConversation, isSelfUserMember = true, - isSelfUserCreator = true, selfRole = Conversation.Member.Role.Member ) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/search/IsFederationSearchAllowedUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/search/IsFederationSearchAllowedUseCaseTest.kt new file mode 100644 index 00000000000..0ac583443fd --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/search/IsFederationSearchAllowedUseCaseTest.kt @@ -0,0 +1,166 @@ +/* + * 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.search + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.mls.MLSPublicKeys +import com.wire.kalium.logic.data.mlspublickeys.MLSPublicKeysRepository +import com.wire.kalium.logic.data.user.SupportedProtocol +import com.wire.kalium.logic.feature.conversation.GetConversationProtocolInfoUseCase +import com.wire.kalium.logic.feature.user.GetDefaultProtocolUseCase +import com.wire.kalium.logic.framework.TestConversation +import com.wire.kalium.logic.framework.TestConversation.PROTEUS_PROTOCOL_INFO +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.util.KaliumDispatcherImpl +import io.mockative.Mock +import io.mockative.any +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.every +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class IsFederationSearchAllowedUseCaseTest { + + @Test + fun givenMLSIsNotConfigured_whenInvokingIsFederationSearchAllowed_thenReturnTrue() = runTest { + val (arrangement, isFederationSearchAllowedUseCase) = Arrangement() + .withMLSConfiguredForBackend(isConfigured = false) + .arrange() + + val isAllowed = isFederationSearchAllowedUseCase(conversationId = null) + + assertEquals(true, isAllowed) + coVerify { arrangement.mlsPublicKeysRepository.getKeys() }.wasInvoked(once) + coVerify { arrangement.getDefaultProtocol.invoke() }.wasNotInvoked() + coVerify { arrangement.getConversationProtocolInfo.invoke(any()) }.wasNotInvoked() + } + + @Test + fun givenMLSIsConfiguredAndAMLSTeamWithEmptyKeys_whenInvokingIsFederationSearchAllowed_thenReturnTrue() = runTest { + val (arrangement, isFederationSearchAllowedUseCase) = Arrangement() + .withEmptyMlsKeys() + .arrange() + + val isAllowed = isFederationSearchAllowedUseCase(conversationId = null) + + assertEquals(true, isAllowed) + coVerify { arrangement.mlsPublicKeysRepository.getKeys() }.wasInvoked(once) + coVerify { arrangement.getDefaultProtocol.invoke() }.wasNotInvoked() + coVerify { arrangement.getConversationProtocolInfo.invoke(any()) }.wasNotInvoked() + } + + @Test + fun givenMLSIsConfiguredAndAMLSTeam_whenInvokingIsFederationSearchAllowed_thenReturnTrue() = runTest { + val (arrangement, isFederationSearchAllowedUseCase) = Arrangement() + .withMLSConfiguredForBackend(isConfigured = true) + .withDefaultProtocol(SupportedProtocol.MLS) + .arrange() + + val isAllowed = isFederationSearchAllowedUseCase(conversationId = null) + + assertEquals(true, isAllowed) + coVerify { arrangement.mlsPublicKeysRepository.getKeys() }.wasInvoked(once) + coVerify { arrangement.getDefaultProtocol.invoke() }.wasInvoked(once) + coVerify { arrangement.getConversationProtocolInfo.invoke(any()) }.wasNotInvoked() + } + + @Test + fun givenMLSIsConfiguredAndAMLSTeamAndProteusProtocol_whenInvokingIsFederationSearchAllowed_thenReturnFalse() = runTest { + val (arrangement, isFederationSearchAllowedUseCase) = Arrangement() + .withMLSConfiguredForBackend(isConfigured = true) + .withDefaultProtocol(SupportedProtocol.MLS) + .withConversationProtocolInfo(GetConversationProtocolInfoUseCase.Result.Success(PROTEUS_PROTOCOL_INFO)) + .arrange() + + val isAllowed = isFederationSearchAllowedUseCase(conversationId = TestConversation.ID) + + assertEquals(false, isAllowed) + coVerify { arrangement.mlsPublicKeysRepository.getKeys() }.wasInvoked(once) + coVerify { arrangement.getDefaultProtocol.invoke() }.wasInvoked(once) + coVerify { arrangement.getConversationProtocolInfo.invoke(any()) }.wasInvoked(once) + } + + @Test + fun givenMLSIsConfiguredAndAProteusTeamAndProteusProtocol_whenInvokingIsFederationSearchAllowed_thenReturnFalse() = runTest { + val (arrangement, isFederationSearchAllowedUseCase) = Arrangement() + .withMLSConfiguredForBackend(isConfigured = true) + .withDefaultProtocol(SupportedProtocol.PROTEUS) + .withConversationProtocolInfo(GetConversationProtocolInfoUseCase.Result.Success(PROTEUS_PROTOCOL_INFO)) + .arrange() + + val isAllowed = isFederationSearchAllowedUseCase(conversationId = TestConversation.ID) + + assertEquals(false, isAllowed) + coVerify { arrangement.mlsPublicKeysRepository.getKeys() }.wasInvoked(once) + coVerify { arrangement.getDefaultProtocol.invoke() }.wasInvoked(once) + coVerify { arrangement.getConversationProtocolInfo.invoke(any()) }.wasInvoked(once) + } + + private class Arrangement { + + @Mock + val mlsPublicKeysRepository = mock(MLSPublicKeysRepository::class) + + @Mock + val getDefaultProtocol = mock(GetDefaultProtocolUseCase::class) + + @Mock + val getConversationProtocolInfo = mock(GetConversationProtocolInfoUseCase::class) + + private val MLS_PUBLIC_KEY = MLSPublicKeys( + removal = mapOf( + "ed25519" to "gRNvFYReriXbzsGu7zXiPtS8kaTvhU1gUJEV9rdFHVw=" + ) + ) + + fun withDefaultProtocol(protocol: SupportedProtocol) = apply { + every { getDefaultProtocol.invoke() }.returns(protocol) + } + + suspend fun withConversationProtocolInfo(protocolInfo: GetConversationProtocolInfoUseCase.Result) = apply { + coEvery { getConversationProtocolInfo(any()) }.returns(protocolInfo) + } + + suspend fun withMLSConfiguredForBackend(isConfigured: Boolean = true) = apply { + coEvery { mlsPublicKeysRepository.getKeys() }.returns( + if (isConfigured) { + Either.Right(MLS_PUBLIC_KEY) + } else { + Either.Left(CoreFailure.Unknown(RuntimeException("MLS is not configured"))) + } + ) + } + + suspend fun withEmptyMlsKeys() = apply { + coEvery { mlsPublicKeysRepository.getKeys() }.returns(Either.Right(MLSPublicKeys(emptyMap()))) + } + + fun arrange() = this to IsFederationSearchAllowedUseCase( + mlsPublicKeysRepository = mlsPublicKeysRepository, + getDefaultProtocol = getDefaultProtocol, + getConversationProtocolInfo = getConversationProtocolInfo, + dispatcher = KaliumDispatcherImpl + ) + } +} + + diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/migration/MigrateFromPersonalToTeamUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/migration/MigrateFromPersonalToTeamUseCaseTest.kt index 425af5a8d1e..23f8ce4db9f 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/migration/MigrateFromPersonalToTeamUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/migration/MigrateFromPersonalToTeamUseCaseTest.kt @@ -20,8 +20,10 @@ package com.wire.kalium.logic.feature.user.migration import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.NetworkFailure +import com.wire.kalium.logic.StorageFailure import com.wire.kalium.logic.data.user.CreateUserTeam import com.wire.kalium.logic.data.user.UserRepository +import com.wire.kalium.logic.framework.TestUser import com.wire.kalium.logic.functional.Either import com.wire.kalium.network.api.model.ErrorResponse import com.wire.kalium.network.exceptions.KaliumException @@ -29,63 +31,73 @@ import io.ktor.http.HttpStatusCode import io.mockative.Mock import io.mockative.any import io.mockative.coEvery +import io.mockative.coVerify import io.mockative.mock +import io.mockative.once import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertIs +import kotlin.test.assertTrue class MigrateFromPersonalToTeamUseCaseTest { @Test fun givenRepositorySucceeds_whenMigratingUserToTeam_thenShouldPropagateSuccess() = runTest { - val (_, useCase) = Arrangement().withSuccessRepository().arrange() + val (arrangement, useCase) = Arrangement() + .withUpdateTeamIdReturning(Either.Right(Unit)) + .withMigrationSuccess() + .arrange() val result = useCase(teamName = "teamName") + coVerify { + arrangement.userRepository.updateTeamId(any(), any()) + }.wasInvoked(exactly = once) + assertTrue(arrangement.isCachedTeamIdInvalidated) assertIs(result) } @Test - fun givenRepositoryFailsWithNoNetworkConnection_whenMigratingUserToTeam_thenShouldPropagateFailure() = + fun givenRepositoryFailsWithNoNetworkConnection_whenMigratingUserToTeam_thenShouldPropagateNoNetworkFailure() = runTest { - val coreFailure = NetworkFailure.NoNetworkConnection(null) - val (_, useCase) = Arrangement().withRepositoryReturning(Either.Left(coreFailure)) + val (_, useCase) = Arrangement() + .withMigrationNoNetworkFailure() .arrange() val result = useCase(teamName = "teamName") assertIs(result) - assertIs(result.failure) + assertIs(result.failure) } @Test - fun givenRepositoryFailsWithUserAlreadyInTeam_whenMigratingUserToTeam_thenShouldPropagateFailure() = + fun givenRepositoryFailsWithUserAlreadyInTeam_whenMigratingUserToTeam_thenShouldPropagateUserAlreadyInTeamFailure() = runTest { - val (_, useCase) = Arrangement().withUserAlreadyInTeamRepository().arrange() + val (_, useCase) = Arrangement() + .withUserAlreadyInTeamFailure() + .arrange() val result = useCase(teamName = "teamName") assertIs(result) - assertIs(result.failure) - val serverMiscommunication = result.failure as NetworkFailure.ServerMiscommunication - val invalidRequestError = - serverMiscommunication.kaliumException as KaliumException.InvalidRequestError - val errorLabel = invalidRequestError.errorResponse.label - - assertEquals("user-already-in-a-team", errorLabel) + assertIs(result.failure) } @Test - fun givenRepositoryFailsWithNotFound_whenMigratingUserToTeam_thenShouldPropagateFailure() = + fun givenRepositoryFailsWithUnknownError_whenMigratingUserToTeam_thenShouldPropagateUnknownFailure() = runTest { - val (_, useCase) = Arrangement().withNotFoundRepository().arrange() + val (_, useCase) = Arrangement() + .withMigrationUserNotFoundFailure() + .arrange() val result = useCase(teamName = "teamName") assertIs(result) - assertIs(result.failure) - val serverMiscommunication = result.failure as NetworkFailure.ServerMiscommunication + assertIs(result.failure) + val coreFailure = + (result.failure as MigrateFromPersonalToTeamFailure.UnknownError).coreFailure + val serverMiscommunication = coreFailure as NetworkFailure.ServerMiscommunication val invalidRequestError = serverMiscommunication.kaliumException as KaliumException.InvalidRequestError val errorLabel = invalidRequestError.errorResponse.label @@ -98,54 +110,61 @@ class MigrateFromPersonalToTeamUseCaseTest { @Mock val userRepository: UserRepository = mock(UserRepository::class) - suspend fun withSuccessRepository() = apply { + var isCachedTeamIdInvalidated = false + + suspend fun withMigrationSuccess() = apply { coEvery { userRepository.migrateUserToTeam(any()) }.returns( Either.Right( - CreateUserTeam("teamName") + CreateUserTeam("teamId", "teamName") ) ) } - suspend fun withUserAlreadyInTeamRepository() = apply { - coEvery { userRepository.migrateUserToTeam(any()) }.returns( - Either.Left( - NetworkFailure.ServerMiscommunication( - KaliumException.InvalidRequestError( - ErrorResponse( - HttpStatusCode.Forbidden.value, - message = "Switching teams is not allowed", - label = "user-already-in-a-team", - ) + suspend fun withUserAlreadyInTeamFailure() = withMigrationReturning( + Either.Left( + NetworkFailure.ServerMiscommunication( + KaliumException.InvalidRequestError( + ErrorResponse( + HttpStatusCode.Forbidden.value, + message = "Switching teams is not allowed", + label = "user-already-in-a-team", ) ) ) ) - } + ) - suspend fun withNotFoundRepository() = apply { - coEvery { userRepository.migrateUserToTeam(any()) }.returns( - Either.Left( - NetworkFailure.ServerMiscommunication( - KaliumException.InvalidRequestError( - ErrorResponse( - HttpStatusCode.NotFound.value, - message = "User not found", - label = "not-found", - ) + + suspend fun withMigrationUserNotFoundFailure() = withMigrationReturning( + Either.Left( + NetworkFailure.ServerMiscommunication( + KaliumException.InvalidRequestError( + ErrorResponse( + HttpStatusCode.NotFound.value, + message = "User not found", + label = "not-found", ) ) ) ) + ) + + suspend fun withMigrationNoNetworkFailure() = withMigrationReturning( + Either.Left(NetworkFailure.NoNetworkConnection(null)) + ) + + suspend fun withMigrationReturning(result: Either) = apply { + coEvery { userRepository.migrateUserToTeam(any()) }.returns(result) } - suspend fun withRepositoryReturning(result: Either) = - apply { - coEvery { userRepository.migrateUserToTeam(any()) }.returns(result) - } + suspend fun withUpdateTeamIdReturning(result: Either) = apply { + coEvery { userRepository.updateTeamId(any(), any()) }.returns(result) + } - fun arrange() = this to MigrateFromPersonalToTeamUseCaseImpl( - userRepository = userRepository - ) + fun arrange() = this to MigrateFromPersonalToTeamUseCaseImpl(selfUserId = TestUser.SELF.id, + userRepository = userRepository, + invalidateTeamId = { + isCachedTeamIdInvalidated = true + }) } - -} \ No newline at end of file +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversation.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversation.kt index ecd484c1d2c..6f36299004d 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversation.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversation.kt @@ -149,7 +149,6 @@ object TestConversation { userDeleted = false, connectionStatus = null, otherUserId = null, - isCreator = 0L, lastNotificationDate = null, protocolInfo = protocolInfo, creatorId = "someValue", @@ -315,7 +314,6 @@ object TestConversation { userDeleted = false, connectionStatus = null, otherUserId = null, - isCreator = 0L, lastNotificationDate = null, protocolInfo = ConversationEntity.ProtocolInfo.Proteus, creatorId = "someValue", diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversationDetails.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversationDetails.kt index 1372c6fc446..1dce7148793 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversationDetails.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversationDetails.kt @@ -45,7 +45,6 @@ object TestConversationDetails { val CONVERSATION_GROUP = ConversationDetails.Group( conversation = TestConversation.GROUP(), - isSelfUserCreator = true, isSelfUserMember = true, selfRole = Conversation.Member.Role.Member ) diff --git a/logic/src/jvmTest/kotlin/com/wire/kalium/logic/data/client/ProteusClientProviderTest.kt b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/data/client/ProteusClientProviderTest.kt new file mode 100644 index 00000000000..8d4c8caa2f5 --- /dev/null +++ b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/data/client/ProteusClientProviderTest.kt @@ -0,0 +1,73 @@ +package com.wire.kalium.logic.data.client + +import com.wire.kalium.cryptography.exceptions.ProteusStorageMigrationException +import com.wire.kalium.logic.featureFlags.KaliumConfigs +import com.wire.kalium.logic.framework.TestUser +import com.wire.kalium.persistence.dbPassphrase.PassphraseStorage +import com.wire.kalium.util.FileUtil +import com.wire.kalium.util.KaliumDispatcherImpl +import io.mockative.Mock +import io.mockative.any +import io.mockative.coVerify +import io.mockative.every +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.test.runTest +import org.junit.Test +import java.nio.file.Paths +import kotlin.io.path.createDirectory +import kotlin.io.path.createFile +import kotlin.io.path.exists + +class ProteusClientProviderTest { + + @Test + fun givenGettingOrCreatingAProteusClient_whenMigrationPerformedAndFails_thenCatchErrorAndStartRecovery() = runTest { + // given + val (arrangement, proteusClientProvider) = Arrangement() + .withCorruptedProteusStorage() + .arrange() + + // when - then + try { + proteusClientProvider.getOrCreate() + } catch (e: ProteusStorageMigrationException) { + coVerify { arrangement.proteusMigrationRecoveryHandler.clearClientData(any()) }.wasInvoked(once) + } + } + + private class Arrangement { + + @Mock + val passphraseStorage = mock(PassphraseStorage::class) + + @Mock + val proteusMigrationRecoveryHandler = mock(ProteusMigrationRecoveryHandler::class) + + init { + every { passphraseStorage.getPassphrase(any()) }.returns("passphrase") + } + + /** + * Corrupted because it's just an empty file called "prekeys". + * But nothing to migrate, this is just to test that we are calling recovery. + */ + fun withCorruptedProteusStorage() = apply { + val rootProteusPath = Paths.get("/tmp/rootProteusPath") + if (rootProteusPath.exists()) { + FileUtil.deleteDirectory(rootProteusPath.toString()) + } + rootProteusPath.createDirectory() + rootProteusPath.resolve("prekeys").createFile() + } + + fun arrange() = this to ProteusClientProviderImpl( + rootProteusPath = "/tmp/rootProteusPath", + userId = TestUser.USER_ID, + passphraseStorage = passphraseStorage, + kaliumConfigs = KaliumConfigs(encryptProteusStorage = true), + dispatcher = KaliumDispatcherImpl, + proteusMigrationRecoveryHandler = proteusMigrationRecoveryHandler + ) + } +} diff --git a/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/client/ProteusMigrationRecoveryHandlerTest.kt b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/client/ProteusMigrationRecoveryHandlerTest.kt new file mode 100644 index 00000000000..7a52607adb6 --- /dev/null +++ b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/client/ProteusMigrationRecoveryHandlerTest.kt @@ -0,0 +1,36 @@ +package com.wire.kalium.logic.feature.client + +import com.wire.kalium.logic.data.logout.LogoutReason +import com.wire.kalium.logic.feature.auth.LogoutUseCase +import io.mockative.Mock +import io.mockative.coVerify +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ProteusMigrationRecoveryHandlerTest { + + @Test + fun givenGettingOrCreatingAProteusClient_whenMigrationPerformedAndFails_thenCatchErrorAndStartRecovery() = runTest { + // given + val (arrangement, proteusMigrationRecoveryHandler) = Arrangement().arrange() + + // when + val clearLocalFiles: suspend () -> Unit = { } + proteusMigrationRecoveryHandler.clearClientData(clearLocalFiles) + + // then + coVerify { arrangement.logoutUseCase(LogoutReason.MIGRATION_TO_CC_FAILED, true) }.wasInvoked(once) + } + + private class Arrangement { + + @Mock + val logoutUseCase = mock(LogoutUseCase::class) + + fun arrange() = this to ProteusMigrationRecoveryHandlerImpl( + lazy { logoutUseCase } + ) + } +} diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/BackendMetaDataUtil.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/BackendMetaDataUtil.kt index ac9a4767514..be435578cf3 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/BackendMetaDataUtil.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/BackendMetaDataUtil.kt @@ -23,11 +23,11 @@ import com.wire.kalium.network.api.unbound.versioning.VersionInfoDTO // They are not truly constants as set is not a primitive type, yet are treated as one in this context @Suppress("MagicNumber") -val SupportedApiVersions = setOf(0, 1, 2, 4, 5) +val SupportedApiVersions = setOf(0, 1, 2, 4, 5, 6) // They are not truly constants as set is not a primitive type, yet are treated as one in this context @Suppress("MagicNumber") -val DevelopmentApiVersions = setOf(6, 7) +val DevelopmentApiVersions = setOf(7) // You can use scripts/generate_new_api_version.sh or gradle task network:generateNewApiVersion to // bump API version and generate all needed classes diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetails.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetails.sq index 66403d5116a..b521bb3c216 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetails.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetails.sq @@ -58,10 +58,6 @@ CASE (Conversation.type) WHEN 'ONE_ON_ONE' THEN User.active_one_on_one_conversation_id WHEN 'CONNECTION_PENDING' THEN connection_user.active_one_on_one_conversation_id END AS otherUserActiveConversationId, -CASE - WHEN (SelfUser.id LIKE (Conversation.creator_id || '@%')) THEN 1 - ELSE 0 -END AS isCreator, CASE (Conversation.type) WHEN 'ONE_ON_ONE' THEN coalesce(User.active_one_on_one_conversation_id = Conversation.qualified_id, 0) ELSE 1 diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Users.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Users.sq index 56dd1db0d6f..385d13f2602 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Users.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Users.sq @@ -273,3 +273,6 @@ SELECT active_one_on_one_conversation_id FROM User WHERE qualified_id = :userId; selectNamesAndHandle: SELECT name, handle FROM User WHERE qualified_id = :userId; + +updateTeamId: +UPDATE User SET team = ? WHERE qualified_id = ?; diff --git a/persistence/src/commonMain/db_user/migrations/93.sqm b/persistence/src/commonMain/db_user/migrations/93.sqm new file mode 100644 index 00000000000..30dc8b3fa11 --- /dev/null +++ b/persistence/src/commonMain/db_user/migrations/93.sqm @@ -0,0 +1,127 @@ +DROP VIEW IF EXISTS ConversationDetails; + +CREATE VIEW IF NOT EXISTS ConversationDetails AS +SELECT +Conversation.qualified_id AS qualifiedId, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.name + WHEN 'CONNECTION_PENDING' THEN connection_user.name + ELSE Conversation.name +END AS name, +Conversation.type, +Call.status AS callStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.preview_asset_id + WHEN 'CONNECTION_PENDING' THEN connection_user.preview_asset_id +END AS previewAssetId, +Conversation.muted_status AS mutedStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.team + ELSE Conversation.team_id +END AS teamId, +CASE (Conversation.type) + WHEN 'CONNECTION_PENDING' THEN Connection.last_update_date + ELSE Conversation.last_modified_date +END AS lastModifiedDate, +Conversation.last_read_date AS lastReadDate, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.user_availability_status + WHEN 'CONNECTION_PENDING' THEN connection_user.user_availability_status +END AS userAvailabilityStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.user_type + WHEN 'CONNECTION_PENDING' THEN connection_user.user_type +END AS userType, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.bot_service + WHEN 'CONNECTION_PENDING' THEN connection_user.bot_service +END AS botService, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.deleted + WHEN 'CONNECTION_PENDING' THEN connection_user.deleted +END AS userDeleted, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.defederated + WHEN 'CONNECTION_PENDING' THEN connection_user.defederated +END AS userDefederated, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.supported_protocols + WHEN 'CONNECTION_PENDING' THEN connection_user.supported_protocols +END AS userSupportedProtocols, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.connection_status + WHEN 'CONNECTION_PENDING' THEN connection_user.connection_status +END AS connectionStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.qualified_id + WHEN 'CONNECTION_PENDING' THEN connection_user.qualified_id +END AS otherUserId, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.active_one_on_one_conversation_id + WHEN 'CONNECTION_PENDING' THEN connection_user.active_one_on_one_conversation_id +END AS otherUserActiveConversationId, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN coalesce(User.active_one_on_one_conversation_id = Conversation.qualified_id, 0) + ELSE 1 +END AS isActive, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.accent_id + ELSE 0 +END AS accentId, +Conversation.last_notified_date AS lastNotifiedMessageDate, +memberRole. role AS selfRole, +Conversation.protocol, +Conversation.mls_cipher_suite, +Conversation.mls_epoch, +Conversation.mls_group_id, +Conversation.mls_last_keying_material_update_date, +Conversation.mls_group_state, +Conversation.access_list, +Conversation.access_role_list, +Conversation.mls_proposal_timer, +Conversation.muted_time, +Conversation.creator_id, +Conversation.receipt_mode, +Conversation.message_timer, +Conversation.user_message_timer, +Conversation.incomplete_metadata, +Conversation.archived, +Conversation.archived_date_time, +Conversation.verification_status AS mls_verification_status, +Conversation.proteus_verification_status, +Conversation.legal_hold_status, +SelfUser.id AS selfUserId, +CASE + WHEN Conversation.type = 'GROUP' THEN + CASE + WHEN memberRole.role IS NOT NULL THEN 1 + ELSE 0 + END + WHEN Conversation.type = 'ONE_ON_ONE' THEN + CASE + WHEN User.defederated = 1 THEN 0 + WHEN User.deleted = 1 THEN 0 + WHEN User.connection_status = 'BLOCKED' THEN 0 + WHEN Conversation.legal_hold_status = 'DEGRADED' THEN 0 + ELSE 1 + END + ELSE 0 +END AS interactionEnabled, +LabeledConversation.folder_id IS NOT NULL AS isFavorite +FROM Conversation +LEFT JOIN SelfUser +LEFT JOIN Member ON Conversation.qualified_id = Member.conversation + AND Conversation.type IS 'ONE_ON_ONE' + AND Member.user IS NOT SelfUser.id +LEFT JOIN Member AS memberRole ON Conversation.qualified_id = memberRole.conversation + AND memberRole.user IS SelfUser.id +LEFT JOIN User ON User.qualified_id = Member.user +LEFT JOIN Connection ON Connection.qualified_conversation = Conversation.qualified_id + AND (Connection.status = 'SENT' + OR Connection.status = 'PENDING' + OR Connection.status = 'NOT_CONNECTED' + AND Conversation.type IS 'CONNECTION_PENDING') +LEFT JOIN User AS connection_user ON Connection.qualified_to = connection_user.qualified_id +LEFT JOIN Call ON Call.id IS (SELECT id FROM Call WHERE Call.conversation_id = Conversation.qualified_id AND Call.status IS 'STILL_ONGOING' ORDER BY created_at DESC LIMIT 1) +LEFT JOIN ConversationFolder AS FavoriteFolder ON FavoriteFolder.folder_type = 'FAVORITE' +LEFT JOIN LabeledConversation ON LabeledConversation.conversation_id = Conversation.qualified_id AND LabeledConversation.folder_id = FavoriteFolder.id; diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAO.kt index 5c535cd5ccb..c92b50621e0 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAO.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAO.kt @@ -316,4 +316,5 @@ interface UserDAO { suspend fun getOneOnOnConversationId(userId: UserIDEntity): QualifiedIDEntity? suspend fun getUsersMinimizedByQualifiedIDs(qualifiedIDs: List): List suspend fun getNameAndHandle(userId: UserIDEntity): NameAndHandleEntity? + suspend fun updateTeamId(userId: UserIDEntity, teamId: String) } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAOImpl.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAOImpl.kt index c0c3789d3a0..f601dcc701c 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAOImpl.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAOImpl.kt @@ -473,4 +473,8 @@ class UserDAOImpl internal constructor( override suspend fun getNameAndHandle(userId: UserIDEntity): NameAndHandleEntity? = withContext(queriesContext) { userQueries.selectNamesAndHandle(userId, ::NameAndHandleEntity).executeAsOneOrNull() } + + override suspend fun updateTeamId(userId: UserIDEntity, teamId: String) { + userQueries.updateTeamId(teamId, userId) + } } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsMapper.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsMapper.kt index 8e77a9aa642..d59c32c6c2f 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsMapper.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsMapper.kt @@ -53,7 +53,6 @@ data object ConversationDetailsWithEventsMapper { connectionStatus: ConnectionEntity.State?, otherUserId: QualifiedIDEntity?, otherUserActiveConversationId: QualifiedIDEntity?, - isCreator: Long, isActive: Long, accentId: Int?, lastNotifiedMessageDate: Instant?, @@ -128,7 +127,6 @@ data object ConversationDetailsWithEventsMapper { connectionStatus = connectionStatus, otherUserId = otherUserId, otherUserActiveConversationId = otherUserActiveConversationId, - isCreator = isCreator, isActive = isActive, accentId = accentId, lastNotifiedMessageDate = lastNotifiedMessageDate, diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationMapper.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationMapper.kt index d421e6b0de0..4cb050db21d 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationMapper.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationMapper.kt @@ -49,7 +49,6 @@ data object ConversationMapper { connectionStatus: ConnectionEntity.State?, otherUserId: QualifiedIDEntity?, otherUserActiveConversationId: QualifiedIDEntity?, - isCreator: Long, isActive: Long, accentId: Int?, lastNotifiedMessageDate: Instant?, @@ -90,7 +89,6 @@ data object ConversationMapper { mlsLastKeyingMaterialUpdateDate, mlsCipherSuite ), - isCreator = isCreator, mutedStatus = mutedStatus, mutedTime = mutedTime, creatorId = creatorId, diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationViewEntity.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationViewEntity.kt index d9ebd64303b..8e5e247226e 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationViewEntity.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationViewEntity.kt @@ -47,7 +47,6 @@ data class ConversationViewEntity( val userDefederated: Boolean?, val connectionStatus: ConnectionEntity.State? = ConnectionEntity.State.NOT_CONNECTED, val otherUserId: QualifiedIDEntity?, - val isCreator: Long, val lastNotificationDate: Instant?, val selfRole: MemberEntity.Role?, val protocolInfo: ConversationEntity.ProtocolInfo, diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/model/LogoutReason.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/model/LogoutReason.kt index 6d0ed44cc61..dc306760bab 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/model/LogoutReason.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/model/LogoutReason.kt @@ -38,5 +38,10 @@ enum class LogoutReason { /** * Session Expired. */ - SESSION_EXPIRED; + SESSION_EXPIRED, + + /** + * The migration to CC failed. + */ + MIGRATION_TO_CC_FAILED } diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConversationDAOTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConversationDAOTest.kt index 96e992c328c..a000851a0d6 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConversationDAOTest.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConversationDAOTest.kt @@ -886,21 +886,6 @@ class ConversationDAOTest : BaseDatabaseTest() { assertEquals(messageTimer, result?.messageTimer) } - @Test - fun givenSelfUserIsCreatorOfConversation_whenGettingConversationDetails_itReturnsCorrectDetails() = runTest(dispatcher) { - // given - conversationDAO.insertConversation(conversationEntity3.copy(creatorId = selfUserId.value)) - teamDAO.insertTeam(team) - userDAO.upsertUser(user2) - insertTeamUserAndMember(team, user2, conversationEntity3.id) - - // when - val result = conversationDAO.getConversationDetailsById(conversationEntity3.id) - - // then - assertEquals(1L, result?.isCreator) - } - @Test fun givenMixedConversation_whenGettingConversationProtocolInfo_itReturnsCorrectInfo() = runTest { // given @@ -2358,7 +2343,6 @@ class ConversationDAOTest : BaseDatabaseTest() { userDeleted = if (type == ConversationEntity.Type.ONE_ON_ONE) userEntity?.deleted else null, connectionStatus = if (type == ConversationEntity.Type.ONE_ON_ONE) userEntity?.connectionStatus else null, otherUserId = if (type == ConversationEntity.Type.ONE_ON_ONE) userEntity?.id else null, - isCreator = 0L, lastNotificationDate = lastNotificationDate, protocolInfo = protocolInfo, accessList = access, diff --git a/util/src/commonMain/kotlin/com.wire.kalium.util/serialization/SerializationUtils.kt b/util/src/commonMain/kotlin/com.wire.kalium.util/serialization/SerializationUtils.kt index 67f1ab54c8f..39c4def02b8 100644 --- a/util/src/commonMain/kotlin/com.wire.kalium.util/serialization/SerializationUtils.kt +++ b/util/src/commonMain/kotlin/com.wire.kalium.util/serialization/SerializationUtils.kt @@ -18,11 +18,20 @@ package com.wire.kalium.util.serialization +import kotlinx.serialization.KSerializer +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.doubleOrNull +import kotlinx.serialization.json.floatOrNull +import kotlinx.serialization.json.intOrNull +import kotlinx.serialization.json.longOrNull // See: https://github.com/Kotlin/kotlinx.serialization/issues/746#issuecomment-737000705 @@ -60,3 +69,29 @@ fun Map<*, *>.toJsonObject(): JsonObject { } return JsonObject(map) } + +private fun JsonElement.toAnyOrNull(): Any? { + return when (this) { + is JsonNull -> null + is JsonPrimitive -> toAnyValue() + is JsonObject -> this.map { it.key to it.value.toAnyOrNull() }.toMap() + is JsonArray -> this.map { it.toAnyOrNull() } + } +} + +private fun JsonPrimitive.toAnyValue(): Any? { + return this.booleanOrNull ?: this.intOrNull ?: this.longOrNull ?: this.floatOrNull ?: this.doubleOrNull ?: this.contentOrNull +} + +object AnyPrimitiveValueSerializer : KSerializer { + private val delegateSerializer = JsonElement.serializer() + override val descriptor = delegateSerializer.descriptor + override fun serialize(encoder: Encoder, value: Any) { + encoder.encodeSerializableValue(delegateSerializer, value.toJsonElement()) + } + + override fun deserialize(decoder: Decoder): Any { + val jsonPrimitive = decoder.decodeSerializableValue(delegateSerializer) + return requireNotNull(jsonPrimitive.toAnyOrNull()) { "value cannot be null" } + } +}