From b7128667a904ec8968c245525ae5f6cddfcd8d36 Mon Sep 17 00:00:00 2001 From: Mojtaba Chenani Date: Wed, 8 May 2024 17:34:11 +0200 Subject: [PATCH 1/3] Commit with unresolved merge conflicts --- .../conversation/ConversationGroupRepository.kt | 15 +++++++++++++++ .../conversation/MLSConversationRepository.kt | 3 ++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationGroupRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationGroupRepository.kt index 9b89cc90082..916193ca099 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationGroupRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationGroupRepository.kt @@ -138,20 +138,35 @@ internal class ConversationGroupRepositoryImpl( // edge case, in case backend goes 🍌 and returns non-matching domains if (failedUsers.isEmpty()) Either.Left(apiResult.value) +<<<<<<< HEAD createGroupConversation(name, validUsers, options, LastUsersAttempt.Failed(failedUsers, failType)) } } else { Either.Left(apiResult.value) } +======= + is Either.Right -> { + handleCreateConversationSuccess( + apiResult, + usersList, + failedUsersList, + selfTeamId + ) +>>>>>>> 8d2325bd2d (fix(mls): set correct CipherSuite when establishing new created group (#2749)) } is Either.Right -> handleGroupConversationCreated(apiResult.value, selfTeamId, usersList, lastUsersAttempt) } } +<<<<<<< HEAD private suspend fun handleGroupConversationCreated( conversationResponse: ConversationResponse, selfTeamId: TeamId?, +======= + private suspend fun handleCreateConversationSuccess( + apiResult: Either.Right, +>>>>>>> 8d2325bd2d (fix(mls): set correct CipherSuite when establishing new created group (#2749)) usersList: List, lastUsersAttempt: LastUsersAttempt, ): Either { diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/MLSConversationRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/MLSConversationRepository.kt index 4856daf905c..4511d57d10c 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/MLSConversationRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/MLSConversationRepository.kt @@ -613,8 +613,9 @@ internal class MLSConversationDataSource( } }.flatMap { additionResult -> wrapStorageRequest { - conversationDAO.updateConversationGroupState( + conversationDAO.updateMlsGroupStateAndCipherSuite( ConversationEntity.GroupState.ESTABLISHED, + ConversationEntity.CipherSuite.fromTag(mlsClient.getDefaultCipherSuite().toInt()), idMapper.toGroupIDEntity(groupID) ) }.map { additionResult } From b070fcb55cc5f0d4aab8da11d728a2e60f07acf2 Mon Sep 17 00:00:00 2001 From: Mojtaba Chenani Date: Fri, 24 May 2024 14:02:45 +0200 Subject: [PATCH 2/3] resolve conflicts --- .../ConversationGroupRepository.kt | 633 ++++++++++++++++++ 1 file changed, 633 insertions(+) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationGroupRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationGroupRepository.kt index e69de29bb2d..1f3cdda20f2 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationGroupRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationGroupRepository.kt @@ -0,0 +1,633 @@ +/* + * 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.conversation + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.MLSFailure +import com.wire.kalium.logic.NetworkFailure +import com.wire.kalium.logic.data.event.EventMapper +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.GroupID +import com.wire.kalium.logic.data.id.SelfTeamIdProvider +import com.wire.kalium.logic.data.id.TeamId +import com.wire.kalium.logic.data.id.toApi +import com.wire.kalium.logic.data.id.toDao +import com.wire.kalium.logic.data.id.toModel +import com.wire.kalium.logic.data.message.MessageContent.MemberChange.FailedToAdd +import com.wire.kalium.logic.data.service.ServiceId +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.data.user.UserRepository +import com.wire.kalium.logic.di.MapperProvider +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.functional.onSuccess +import com.wire.kalium.logic.sync.receiver.conversation.ConversationMessageTimerEventHandler +import com.wire.kalium.logic.sync.receiver.conversation.MemberJoinEventHandler +import com.wire.kalium.logic.sync.receiver.conversation.MemberLeaveEventHandler +import com.wire.kalium.logic.wrapApiRequest +import com.wire.kalium.logic.wrapNullableFlowStorageRequest +import com.wire.kalium.logic.wrapStorageRequest +import com.wire.kalium.network.api.base.authenticated.conversation.AddConversationMembersRequest +import com.wire.kalium.network.api.base.authenticated.conversation.AddServiceRequest +import com.wire.kalium.network.api.base.authenticated.conversation.ConversationApi +import com.wire.kalium.network.api.base.authenticated.conversation.ConversationMemberAddedResponse +import com.wire.kalium.network.api.base.authenticated.conversation.ConversationMemberRemovedResponse +import com.wire.kalium.network.api.base.authenticated.conversation.ConversationResponse +import com.wire.kalium.network.api.base.authenticated.conversation.model.ConversationCodeInfo +import com.wire.kalium.network.api.base.authenticated.notification.EventContentDTO +import com.wire.kalium.network.api.base.model.ServiceAddedResponse +import com.wire.kalium.network.exceptions.KaliumException +import com.wire.kalium.network.exceptions.isConversationHasNoCode +import com.wire.kalium.persistence.dao.conversation.ConversationDAO +import com.wire.kalium.persistence.dao.conversation.ConversationEntity +import com.wire.kalium.persistence.dao.message.LocalId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +interface ConversationGroupRepository { + suspend fun createGroupConversation( + name: String? = null, + usersList: List, + options: ConversationOptions = ConversationOptions(), + ): Either + + suspend fun addMembers(userIdList: List, conversationId: ConversationId): Either + suspend fun addService(serviceId: ServiceId, conversationId: ConversationId): Either + suspend fun deleteMember(userId: UserId, conversationId: ConversationId): Either + suspend fun joinViaInviteCode( + code: String, + key: String, + uri: String?, + password: String? + ): Either + + suspend fun fetchLimitedInfoViaInviteCode(code: String, key: String): Either + suspend fun generateGuestRoomLink( + conversationId: ConversationId, + password: String? + ): Either + + suspend fun revokeGuestRoomLink(conversationId: ConversationId): Either + suspend fun observeGuestRoomLink(conversationId: ConversationId): Flow> + suspend fun updateMessageTimer(conversationId: ConversationId, messageTimer: Long?): Either + suspend fun updateGuestRoomLink(conversationId: ConversationId, accountUrl: String): Either +} + +@Suppress("LongParameterList", "TooManyFunctions") +internal class ConversationGroupRepositoryImpl( + private val mlsConversationRepository: MLSConversationRepository, + private val joinExistingMLSConversation: JoinExistingMLSConversationUseCase, + private val memberJoinEventHandler: MemberJoinEventHandler, + private val memberLeaveEventHandler: MemberLeaveEventHandler, + private val conversationMessageTimerEventHandler: ConversationMessageTimerEventHandler, + private val conversationDAO: ConversationDAO, + private val conversationApi: ConversationApi, + private val newConversationMembersRepository: NewConversationMembersRepository, + private val userRepository: UserRepository, + private val newGroupConversationSystemMessagesCreator: Lazy, + private val selfUserId: UserId, + private val teamIdProvider: SelfTeamIdProvider, + private val conversationMapper: ConversationMapper = MapperProvider.conversationMapper(selfUserId), + private val eventMapper: EventMapper = MapperProvider.eventMapper(selfUserId), + private val protocolInfoMapper: ProtocolInfoMapper = MapperProvider.protocolInfoMapper(), +) : ConversationGroupRepository { + + override suspend fun createGroupConversation( + name: String?, + usersList: List, + options: ConversationOptions, + ): Either = createGroupConversation(name, usersList, options, LastUsersAttempt.None) + + private suspend fun createGroupConversation( + name: String?, + usersList: List, + options: ConversationOptions, + lastUsersAttempt: LastUsersAttempt, + ): Either = + teamIdProvider().flatMap { selfTeamId -> + val apiResult = wrapApiRequest { + conversationApi.createNewConversation( + conversationMapper.toApiModel(name, usersList, selfTeamId?.value, options) + ) + } + + when (apiResult) { + is Either.Left -> { + val canRetryOnce = apiResult.value.hasUnreachableDomainsError && lastUsersAttempt is LastUsersAttempt.None + if (canRetryOnce) { + extractValidUsersForRetryableError(apiResult.value, usersList) + .flatMap { (validUsers, failedUsers, failType) -> + // edge case, in case backend goes 🍌 and returns non-matching domains + if (failedUsers.isEmpty()) Either.Left(apiResult.value) + } + } + } + is Either.Right -> { + handleCreateConversationSuccess( + apiResult, + usersList, + failedUsersList, + selfTeamId + ) + } + + is Either.Right -> handleGroupConversationCreated(apiResult.value, selfTeamId, usersList, lastUsersAttempt) + } + } + + private suspend fun handleCreateConversationSuccess( + apiResult: Either.Right, + usersList: List, + lastUsersAttempt: LastUsersAttempt, + ): Either { + val conversationEntity = conversationMapper.fromApiModelToDaoModel( + conversationResponse, mlsGroupState = ConversationEntity.GroupState.PENDING_CREATION, selfTeamId + ) + val protocol = protocolInfoMapper.fromEntity(conversationEntity.protocolInfo) + + return wrapStorageRequest { + conversationDAO.insertConversation(conversationEntity) + }.flatMap { + newGroupConversationSystemMessagesCreator.value.conversationStarted(conversationEntity) + }.flatMap { + when (protocol) { + is Conversation.ProtocolInfo.Proteus -> Either.Right(setOf()) + is Conversation.ProtocolInfo.MLSCapable -> mlsConversationRepository.establishMLSGroup( + groupID = protocol.groupId, + members = usersList + selfUserId, + allowSkippingUsersWithoutKeyPackages = true + ).map { it.notAddedUsers } + } + }.flatMap { protocolSpecificAdditionFailures -> + newConversationMembersRepository.persistMembersAdditionToTheConversation( + conversationEntity.id, conversationResponse + ).flatMap { + if (protocolSpecificAdditionFailures.isEmpty()) { + Either.Right(Unit) + } else { + newGroupConversationSystemMessagesCreator.value.conversationFailedToAddMembers( + conversationEntity.id.toModel(), protocolSpecificAdditionFailures.toList(), FailedToAdd.Type.Unknown + ) + } + }.flatMap { + when (lastUsersAttempt) { + is LastUsersAttempt.None -> Either.Right(Unit) + is LastUsersAttempt.Failed -> + newGroupConversationSystemMessagesCreator.value.conversationFailedToAddMembers( + conversationEntity.id.toModel(), lastUsersAttempt.failedUsers, lastUsersAttempt.failType + ) + } + } + }.flatMap { + wrapStorageRequest { + newGroupConversationSystemMessagesCreator.value.conversationStartedUnverifiedWarning( + conversationEntity.id.toModel() + ) + } + }.flatMap { + wrapStorageRequest { + conversationDAO.getConversationByQualifiedID(conversationEntity.id)?.let { + conversationMapper.fromDaoModel(it) + } + } + } + } + + override suspend fun addMembers( + userIdList: List, + conversationId: ConversationId + ): Either = + wrapStorageRequest { conversationDAO.getConversationProtocolInfo(conversationId.toDao()) } + .flatMap { protocol -> + when (protocol) { + is ConversationEntity.ProtocolInfo.Proteus -> + tryAddMembersToCloudAndStorage(userIdList, conversationId, LastUsersAttempt.None) + + is ConversationEntity.ProtocolInfo.Mixed -> + tryAddMembersToCloudAndStorage(userIdList, conversationId, LastUsersAttempt.None) + .flatMap { + // best effort approach for migrated conversations, no retries + mlsConversationRepository.addMemberToMLSGroup(GroupID(protocol.groupId), userIdList) + } + + is ConversationEntity.ProtocolInfo.MLS -> { + tryAddMembersToMLSGroup(conversationId, protocol.groupId, userIdList, LastUsersAttempt.None) + } + } + } + + /** + * Handle the error cases and retry for claimPackages offline and out of packages. + * Handle error case and retry for sendingCommit unreachable or missing legal hold consent. + */ + private suspend fun tryAddMembersToMLSGroup( + conversationId: ConversationId, + groupId: String, + userIdList: List, + lastUsersAttempt: LastUsersAttempt, + remainingAttempts: Int = 2 + ): Either { + return when (val addingMemberResult = mlsConversationRepository.addMemberToMLSGroup(GroupID(groupId), userIdList)) { + is Either.Right -> handleMLSMembersNotAdded(conversationId, lastUsersAttempt) + is Either.Left -> { + addingMemberResult.value.handleMLSMembersFailed( + conversationId = conversationId, + groupId = groupId, + userIdList = userIdList, + lastUsersAttempt = lastUsersAttempt, + remainingAttempts = remainingAttempts, + ) + } + } + } + + private suspend fun CoreFailure.handleMLSMembersFailed( + conversationId: ConversationId, + groupId: String, + userIdList: List, + lastUsersAttempt: LastUsersAttempt, + remainingAttempts: Int, + ): Either { + return when { + // claiming key packages offline or out of packages + this is CoreFailure.MissingKeyPackages && remainingAttempts > 0 -> { + val (validUsers, failedUsers) = userIdList.partition { !this.failedUserIds.contains(it) } + tryAddMembersToMLSGroup( + conversationId = conversationId, + groupId = groupId, + userIdList = validUsers, + lastUsersAttempt = LastUsersAttempt.Failed( + failedUsers = lastUsersAttempt.failedUsers + failedUsers, + failType = FailedToAdd.Type.Federation, + ), + remainingAttempts = remainingAttempts - 1 + ) + } + + // sending commit unreachable + this is NetworkFailure.FederatedBackendFailure.RetryableFailure && remainingAttempts > 0 -> { + val (validUsers, failedUsers) = extractValidUsersForRetryableFederationError(userIdList, this) + tryAddMembersToMLSGroup( + conversationId = conversationId, + groupId = groupId, + userIdList = validUsers, + lastUsersAttempt = LastUsersAttempt.Failed( + failedUsers = lastUsersAttempt.failedUsers + failedUsers, + failType = FailedToAdd.Type.Federation, + ), + remainingAttempts = remainingAttempts - 1 + ) + } + + // missing legal hold consent + this.isMissingLegalHoldConsentError && remainingAttempts > 0 -> { + fetchAndExtractValidUsersForRetryableLegalHoldError(userIdList) + .flatMap { (validUsers, failedUsers) -> + tryAddMembersToMLSGroup( + conversationId = conversationId, + groupId = groupId, + userIdList = validUsers, + lastUsersAttempt = LastUsersAttempt.Failed( + failedUsers = lastUsersAttempt.failedUsers + failedUsers, + failType = FailedToAdd.Type.LegalHold, + ), + remainingAttempts = remainingAttempts - 1 + ) + } + } + + else -> { + newGroupConversationSystemMessagesCreator.value.conversationFailedToAddMembers( + conversationId = conversationId, + userIdList = (lastUsersAttempt.failedUsers + userIdList), + type = when { + this.isMissingLegalHoldConsentError -> FailedToAdd.Type.LegalHold + else -> FailedToAdd.Type.Federation + } + ).flatMap { + Either.Left(this) + } + } + } + } + + private suspend fun handleMLSMembersNotAdded( + conversationId: ConversationId, + lastUsersAttempt: LastUsersAttempt, + ): Either = + when (lastUsersAttempt) { + is LastUsersAttempt.Failed -> newGroupConversationSystemMessagesCreator.value.conversationFailedToAddMembers( + conversationId, lastUsersAttempt.failedUsers, lastUsersAttempt.failType + ) + + is LastUsersAttempt.None -> Either.Right(Unit) + } + + override suspend fun addService(serviceId: ServiceId, conversationId: ConversationId): Either = + wrapStorageRequest { conversationDAO.getConversationProtocolInfo(conversationId.toDao()) } + .flatMap { protocol -> + when (protocol) { + is ConversationEntity.ProtocolInfo.Proteus, is ConversationEntity.ProtocolInfo.Mixed -> { + wrapApiRequest { + conversationApi.addService( + AddServiceRequest(id = serviceId.id, provider = serviceId.provider), + conversationId.toApi() + ) + }.onSuccess { response -> + if (response is ServiceAddedResponse.Changed) { + memberJoinEventHandler.handle( + eventMapper.conversationMemberJoin( + LocalId.generate(), + response.event, + ) + ) + } + }.map { Unit } + } + + is ConversationEntity.ProtocolInfo.MLS -> { + val failure = MLSFailure.Generic( + UnsupportedOperationException("Adding service to MLS conversation is not supported") + ) + Either.Left(failure) + } + } + } + + private suspend fun tryAddMembersToCloudAndStorage( + userIdList: List, + conversationId: ConversationId, + lastUsersAttempt: LastUsersAttempt, + ): Either { + val apiResult = wrapApiRequest { + val users = userIdList.map { it.toApi() } + val addParticipantRequest = AddConversationMembersRequest(users, ConversationDataSource.DEFAULT_MEMBER_ROLE) + conversationApi.addMember(addParticipantRequest, conversationId.toApi()) + } + + return when (apiResult) { + is Either.Left -> handleAddingMembersFailure(apiResult, lastUsersAttempt, userIdList, conversationId) + is Either.Right -> handleAddingMembersSuccess(apiResult, lastUsersAttempt, conversationId) + } + } + + private suspend fun handleAddingMembersSuccess( + apiResult: Either.Right, + lastUsersAttempt: LastUsersAttempt, + conversationId: ConversationId + ) = if (apiResult.value is ConversationMemberAddedResponse.Changed) { + memberJoinEventHandler.handle( + eventMapper.conversationMemberJoin(LocalId.generate(), apiResult.value.event) + ).flatMap { + if (lastUsersAttempt is LastUsersAttempt.Failed && lastUsersAttempt.failedUsers.isNotEmpty()) { + newGroupConversationSystemMessagesCreator.value.conversationFailedToAddMembers( + conversationId, lastUsersAttempt.failedUsers, lastUsersAttempt.failType + ) + } + Either.Right(Unit) + } + } else { + Either.Right(Unit) + } + + private suspend fun handleAddingMembersFailure( + apiResult: Either.Left, + lastUsersAttempt: LastUsersAttempt, + userIdList: List, + conversationId: ConversationId + ): Either { + val canRetryOnce = apiResult.value.isRetryable && lastUsersAttempt is LastUsersAttempt.None + return if (canRetryOnce) { + extractValidUsersForRetryableError(apiResult.value, userIdList) + .flatMap { (validUsers, failedUsers, failType) -> + when (failedUsers.isNotEmpty()) { + true -> tryAddMembersToCloudAndStorage(validUsers, conversationId, LastUsersAttempt.Failed(failedUsers, failType)) + false -> { + newGroupConversationSystemMessagesCreator.value.conversationFailedToAddMembers( + conversationId, (validUsers + failedUsers), failType + ).flatMap { + Either.Left(apiResult.value) + } + } + } + } + } else { + val failType = (lastUsersAttempt as? LastUsersAttempt.Failed)?.failType ?: FailedToAdd.Type.Unknown + newGroupConversationSystemMessagesCreator.value.conversationFailedToAddMembers( + conversationId, userIdList + lastUsersAttempt.failedUsers, failType + ).flatMap { + Either.Left(apiResult.value) + } + } + } + + override suspend fun deleteMember( + userId: UserId, + conversationId: ConversationId + ): Either = + wrapStorageRequest { conversationDAO.getConversationProtocolInfo(conversationId.toDao()) } + .flatMap { protocol -> + when (protocol) { + is ConversationEntity.ProtocolInfo.Proteus -> + deleteMemberFromCloudAndStorage(userId, conversationId) + + is ConversationEntity.ProtocolInfo.Mixed -> + deleteMemberFromCloudAndStorage(userId, conversationId) + .flatMap { deleteMemberFromMlsGroup(userId, conversationId, protocol) } + + is ConversationEntity.ProtocolInfo.MLS -> { + deleteMemberFromMlsGroup(userId, conversationId, protocol) + } + } + } + + override suspend fun joinViaInviteCode( + code: String, + key: String, + uri: String?, + password: String? + ): Either = wrapApiRequest { + conversationApi.joinConversation(code, key, uri, password) + }.onSuccess { response -> + if (response is ConversationMemberAddedResponse.Changed) { + val conversationId = response.event.qualifiedConversation.toModel() + + memberJoinEventHandler.handle(eventMapper.conversationMemberJoin(LocalId.generate(), response.event)) + .flatMap { + wrapStorageRequest { conversationDAO.getConversationProtocolInfo(conversationId.toDao()) } + .flatMap { protocol -> + when (protocol) { + is ConversationEntity.ProtocolInfo.Proteus -> + Either.Right(Unit) + + is ConversationEntity.ProtocolInfo.MLSCapable -> { + joinExistingMLSConversation(conversationId).flatMap { + mlsConversationRepository.addMemberToMLSGroup(GroupID(protocol.groupId), listOf(selfUserId)) + } + } + } + } + } + } + } + + override suspend fun fetchLimitedInfoViaInviteCode( + code: String, + key: String + ): Either = + wrapApiRequest { conversationApi.fetchLimitedInformationViaCode(code, key) } + + private suspend fun deleteMemberFromMlsGroup( + userId: UserId, + conversationId: ConversationId, + protocol: ConversationEntity.ProtocolInfo.MLSCapable + ) = + if (userId == selfUserId) { + deleteMemberFromCloudAndStorage(userId, conversationId).flatMap { + mlsConversationRepository.leaveGroup(GroupID(protocol.groupId)) + } + } else { + // when removing a member from an MLS group, don't need to call the api + mlsConversationRepository.removeMembersFromMLSGroup(GroupID(protocol.groupId), listOf(userId)) + } + + private suspend fun deleteMemberFromCloudAndStorage(userId: UserId, conversationId: ConversationId) = + wrapApiRequest { + conversationApi.removeMember(userId.toApi(), conversationId.toApi()) + }.onSuccess { response -> + if (response is ConversationMemberRemovedResponse.Changed) { + memberLeaveEventHandler.handle( + eventMapper.conversationMemberLeave( + LocalId.generate(), + response.event, + ) + ) + } + }.map { } + + override suspend fun generateGuestRoomLink( + conversationId: ConversationId, + password: String? + ): Either = + wrapApiRequest { + conversationApi.generateGuestRoomLink(conversationId.toApi(), password) + } + + override suspend fun revokeGuestRoomLink(conversationId: ConversationId): Either = + wrapApiRequest { + conversationApi.revokeGuestRoomLink(conversationId.toApi()) + }.onSuccess { + wrapStorageRequest { + conversationDAO.deleteGuestRoomLink(conversationId.toDao()) + } + } + + override suspend fun observeGuestRoomLink(conversationId: ConversationId): Flow> = + wrapNullableFlowStorageRequest { + conversationDAO.observeGuestRoomLinkByConversationId(conversationId.toDao()) + .map { it?.let { ConversationGuestLink(it.link, it.isPasswordProtected) } } + } + + override suspend fun updateMessageTimer( + conversationId: ConversationId, + messageTimer: Long? + ): Either = + wrapApiRequest { conversationApi.updateMessageTimer(conversationId.toApi(), messageTimer) } + .onSuccess { + conversationMessageTimerEventHandler.handle( + eventMapper.conversationMessageTimerUpdate( + LocalId.generate(), + it, + ) + ) + } + .map { } + + override suspend fun updateGuestRoomLink(conversationId: ConversationId, accountUrl: String): Either = + wrapApiRequest { + conversationApi.guestLinkInfo(conversationId.toApi()) + }.fold({ + if (it is NetworkFailure.ServerMiscommunication && + it.kaliumException is KaliumException.InvalidRequestError && + it.kaliumException.isConversationHasNoCode() + ) { + wrapStorageRequest { + conversationDAO.deleteGuestRoomLink(conversationId.toDao()) + } + } else { + Either.Left(it) + } + }, { + wrapStorageRequest { + conversationDAO.updateGuestRoomLink(conversationId.toDao(), it.link(accountUrl), it.hasPassword) + } + }) + + /** + * Extract valid and invalid users lists from the given userIdList and a [FailedToAdd.Type] depending on a given [CoreFailure]. + * If the given [CoreFailure] is not retryable, the original userIdList is returned as valid users, invalid users list is empty + * and the type is [FailedToAdd.Type.Unknown]. + */ + private suspend fun extractValidUsersForRetryableError( + failure: CoreFailure, + userIdList: List, + ): Either = when { + failure is NetworkFailure.FederatedBackendFailure.RetryableFailure -> + Either.Right(extractValidUsersForRetryableFederationError(userIdList, failure)) + + failure.isMissingLegalHoldConsentError -> + fetchAndExtractValidUsersForRetryableLegalHoldError(userIdList) + + else -> + Either.Right(ValidToInvalidUsers(userIdList, emptyList(), FailedToAdd.Type.Unknown)) + } + + /** + * Filter the initial [userIdList] into valid and invalid users where valid users are only team members. + */ + private suspend fun fetchAndExtractValidUsersForRetryableLegalHoldError( + userIdList: List + ): Either = + userRepository.fetchUsersLegalHoldConsent(userIdList.toSet()).map { + ValidToInvalidUsers(it.usersWithConsent, it.usersWithoutConsent + it.usersFailed, FailedToAdd.Type.LegalHold) + } + + /** + * Extract from a [NetworkFailure.FederatedBackendFailure.RetryableFailure] the domains + * and filter the initial [userIdList] into valid and invalid users. + */ + private fun extractValidUsersForRetryableFederationError( + userIdList: List, + federatedDomainFailure: NetworkFailure.FederatedBackendFailure.RetryableFailure + ): ValidToInvalidUsers { + val (validUsers, failedUsers) = userIdList.partition { !federatedDomainFailure.domains.contains(it.domain) } + return ValidToInvalidUsers(validUsers, failedUsers, FailedToAdd.Type.Federation) + } + + private data class ValidToInvalidUsers(val validUsers: List, val failedUsers: List, val failType: FailedToAdd.Type) + + private sealed class LastUsersAttempt { + open val failedUsers: List = emptyList() + + data object None : LastUsersAttempt() + data class Failed(override val failedUsers: List, val failType: FailedToAdd.Type) : LastUsersAttempt() + } +} From 5cc7bd6012fc9a5c26b1cbeafaf958f51066106a Mon Sep 17 00:00:00 2001 From: Mojtaba Chenani Date: Fri, 24 May 2024 14:05:37 +0200 Subject: [PATCH 3/3] resolve conflicts --- .../ConversationGroupRepository.kt | 106 +++++++++++++----- 1 file changed, 77 insertions(+), 29 deletions(-) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationGroupRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationGroupRepository.kt index 1f3cdda20f2..0cb58e990fa 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationGroupRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationGroupRepository.kt @@ -30,6 +30,7 @@ import com.wire.kalium.logic.data.id.toApi import com.wire.kalium.logic.data.id.toDao import com.wire.kalium.logic.data.id.toModel import com.wire.kalium.logic.data.message.MessageContent.MemberChange.FailedToAdd +import com.wire.kalium.logic.data.mls.CipherSuite import com.wire.kalium.logic.data.service.ServiceId import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.UserRepository @@ -130,31 +131,21 @@ internal class ConversationGroupRepositoryImpl( } when (apiResult) { - is Either.Left -> { - val canRetryOnce = apiResult.value.hasUnreachableDomainsError && lastUsersAttempt is LastUsersAttempt.None - if (canRetryOnce) { - extractValidUsersForRetryableError(apiResult.value, usersList) - .flatMap { (validUsers, failedUsers, failType) -> - // edge case, in case backend goes 🍌 and returns non-matching domains - if (failedUsers.isEmpty()) Either.Left(apiResult.value) - } - } - } - is Either.Right -> { - handleCreateConversationSuccess( - apiResult, - usersList, - failedUsersList, - selfTeamId - ) - } + is Either.Left -> handleCreateConverstionFailure( + apiResult = apiResult, + usersList = usersList, + name = name, + options = options, + lastUsersAttempt = lastUsersAttempt + ) is Either.Right -> handleGroupConversationCreated(apiResult.value, selfTeamId, usersList, lastUsersAttempt) } } - private suspend fun handleCreateConversationSuccess( - apiResult: Either.Right, + private suspend fun handleGroupConversationCreated( + conversationResponse: ConversationResponse, + selfTeamId: TeamId?, usersList: List, lastUsersAttempt: LastUsersAttempt, ): Either { @@ -184,7 +175,9 @@ internal class ConversationGroupRepositoryImpl( Either.Right(Unit) } else { newGroupConversationSystemMessagesCreator.value.conversationFailedToAddMembers( - conversationEntity.id.toModel(), protocolSpecificAdditionFailures.toList(), FailedToAdd.Type.Unknown + conversationId = conversationEntity.id.toModel(), + userIdList = protocolSpecificAdditionFailures.toList(), + type = FailedToAdd.Type.Federation ) } }.flatMap { @@ -211,6 +204,27 @@ internal class ConversationGroupRepositoryImpl( } } + private suspend fun handleCreateConverstionFailure( + apiResult: Either.Left, + usersList: List, + name: String?, + options: ConversationOptions, + lastUsersAttempt: LastUsersAttempt + ): Either { + val canRetryOnce = apiResult.value.hasUnreachableDomainsError && lastUsersAttempt is LastUsersAttempt.None + return if (canRetryOnce) { + extractValidUsersForRetryableError(apiResult.value, usersList) + .flatMap { (validUsers, failedUsers, failType) -> + // edge case, in case backend goes 🍌 and returns non-matching domains + if (failedUsers.isEmpty()) Either.Left(apiResult.value) + + createGroupConversation(name, validUsers, options, LastUsersAttempt.Failed(failedUsers, failType)) + } + } else { + Either.Left(apiResult.value) + } + } + override suspend fun addMembers( userIdList: List, conversationId: ConversationId @@ -225,11 +239,21 @@ internal class ConversationGroupRepositoryImpl( tryAddMembersToCloudAndStorage(userIdList, conversationId, LastUsersAttempt.None) .flatMap { // best effort approach for migrated conversations, no retries - mlsConversationRepository.addMemberToMLSGroup(GroupID(protocol.groupId), userIdList) + mlsConversationRepository.addMemberToMLSGroup( + GroupID(protocol.groupId), + userIdList, + CipherSuite.fromTag(protocol.cipherSuite.cipherSuiteTag) + ) } is ConversationEntity.ProtocolInfo.MLS -> { - tryAddMembersToMLSGroup(conversationId, protocol.groupId, userIdList, LastUsersAttempt.None) + tryAddMembersToMLSGroup( + conversationId, + protocol.groupId, + userIdList, + LastUsersAttempt.None, + cipherSuite = CipherSuite.fromTag(protocol.cipherSuite.cipherSuiteTag) + ) } } } @@ -238,14 +262,22 @@ internal class ConversationGroupRepositoryImpl( * Handle the error cases and retry for claimPackages offline and out of packages. * Handle error case and retry for sendingCommit unreachable or missing legal hold consent. */ + @Suppress("LongMethod") private suspend fun tryAddMembersToMLSGroup( conversationId: ConversationId, groupId: String, userIdList: List, lastUsersAttempt: LastUsersAttempt, + cipherSuite: CipherSuite, remainingAttempts: Int = 2 ): Either { - return when (val addingMemberResult = mlsConversationRepository.addMemberToMLSGroup(GroupID(groupId), userIdList)) { + return when ( + val addingMemberResult = mlsConversationRepository.addMemberToMLSGroup( + GroupID(groupId), + userIdList, + cipherSuite + ) + ) { is Either.Right -> handleMLSMembersNotAdded(conversationId, lastUsersAttempt) is Either.Left -> { addingMemberResult.value.handleMLSMembersFailed( @@ -254,17 +286,20 @@ internal class ConversationGroupRepositoryImpl( userIdList = userIdList, lastUsersAttempt = lastUsersAttempt, remainingAttempts = remainingAttempts, + cipherSuite = cipherSuite ) } } } + @Suppress("LongMethod") private suspend fun CoreFailure.handleMLSMembersFailed( conversationId: ConversationId, groupId: String, userIdList: List, lastUsersAttempt: LastUsersAttempt, remainingAttempts: Int, + cipherSuite: CipherSuite ): Either { return when { // claiming key packages offline or out of packages @@ -278,7 +313,8 @@ internal class ConversationGroupRepositoryImpl( failedUsers = lastUsersAttempt.failedUsers + failedUsers, failType = FailedToAdd.Type.Federation, ), - remainingAttempts = remainingAttempts - 1 + remainingAttempts = remainingAttempts - 1, + cipherSuite = cipherSuite ) } @@ -293,7 +329,8 @@ internal class ConversationGroupRepositoryImpl( failedUsers = lastUsersAttempt.failedUsers + failedUsers, failType = FailedToAdd.Type.Federation, ), - remainingAttempts = remainingAttempts - 1 + remainingAttempts = remainingAttempts - 1, + cipherSuite = cipherSuite ) } @@ -309,7 +346,8 @@ internal class ConversationGroupRepositoryImpl( failedUsers = lastUsersAttempt.failedUsers + failedUsers, failType = FailedToAdd.Type.LegalHold, ), - remainingAttempts = remainingAttempts - 1 + remainingAttempts = remainingAttempts - 1, + cipherSuite = cipherSuite ) } } @@ -430,7 +468,7 @@ internal class ConversationGroupRepositoryImpl( } } } else { - val failType = (lastUsersAttempt as? LastUsersAttempt.Failed)?.failType ?: FailedToAdd.Type.Unknown + val failType = apiResult.value.toFailedToAddType() newGroupConversationSystemMessagesCreator.value.conversationFailedToAddMembers( conversationId, userIdList + lastUsersAttempt.failedUsers, failType ).flatMap { @@ -480,7 +518,11 @@ internal class ConversationGroupRepositoryImpl( is ConversationEntity.ProtocolInfo.MLSCapable -> { joinExistingMLSConversation(conversationId).flatMap { - mlsConversationRepository.addMemberToMLSGroup(GroupID(protocol.groupId), listOf(selfUserId)) + mlsConversationRepository.addMemberToMLSGroup( + GroupID(protocol.groupId), + listOf(selfUserId), + CipherSuite.fromTag(protocol.cipherSuite.cipherSuiteTag) + ) } } } @@ -600,6 +642,12 @@ internal class ConversationGroupRepositoryImpl( Either.Right(ValidToInvalidUsers(userIdList, emptyList(), FailedToAdd.Type.Unknown)) } + private fun CoreFailure.toFailedToAddType() = when { + this is NetworkFailure.FederatedBackendFailure -> FailedToAdd.Type.Federation + this.isMissingLegalHoldConsentError -> FailedToAdd.Type.LegalHold + else -> FailedToAdd.Type.Unknown + } + /** * Filter the initial [userIdList] into valid and invalid users where valid users are only team members. */