diff --git a/WireDomain/Sources/WireDomain/Components/ClientSessionComponent.swift b/WireDomain/Sources/WireDomain/Components/ClientSessionComponent.swift index aefe69845f3..0f73fb467c8 100644 --- a/WireDomain/Sources/WireDomain/Components/ClientSessionComponent.swift +++ b/WireDomain/Sources/WireDomain/Components/ClientSessionComponent.swift @@ -764,18 +764,6 @@ public final class ClientSessionComponent { public func createGroupConversationUseCase() -> some CreateGroupConversationUseCaseProtocol { CreateGroupConversationUseCase( - api: conversationsAPI, - store: conversationLocalStore, - mlsService: mlsService, - context: syncContext, - localDomain: backendMetadata.domain, - isFederationEnabled: backendMetadata.isFederationEnabled, - isMLSEnabled: isMLSEnabled - ) - } - - public func createChannelUseCase() -> some CreateChannelUseCaseProtocol { - CreateChannelUseCase( api: conversationsAPI, store: conversationLocalStore, mlsService: mlsService, diff --git a/WireDomain/Sources/WireDomain/UseCases/CreateChannelUseCase.swift b/WireDomain/Sources/WireDomain/UseCases/CreateChannelUseCase.swift deleted file mode 100644 index 8262663360d..00000000000 --- a/WireDomain/Sources/WireDomain/UseCases/CreateChannelUseCase.swift +++ /dev/null @@ -1,527 +0,0 @@ -// -// Wire -// Copyright (C) 2026 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/. -// - -import WireDataModel -import WireLogging -import WireNetwork - -// sourcery: AutoMockable -/// Creates and setup a channel. -/// Channels are MLS conversations which belong to a team and have a name. -public protocol CreateChannelUseCaseProtocol { - func invoke( - teamID: UUID, - name: String?, - historyDepth: String?, - cells: Bool?, - users: Set, - accessMode: Set, - accessRoles: Set, - enableReceipts: Bool - ) async throws -> ZMConversation -} - -/// Channels are MLS conversations which belong to a team and have a name. -public struct CreateChannelUseCase: CreateChannelUseCaseProtocol { - - public enum Failure: Error { - case missingSelfClientID - case missingConversationID - case conversationNotFound - case failedToCreateChannel(Error) - case missingLegalholdConsent - case nonFederatingDomains(Set) - case notConnected - case invalidOperation - } - - // MARK: - Properties - - private let api: any ConversationsAPI - private let store: any ConversationLocalStoreProtocol - private let mlsService: (any MLSServiceInterface)? - private let context: NSManagedObjectContext - private let localDomain: String? - private let isFederationEnabled: Bool - private let logger: WireLogger = .conversation - - // MARK: - Object lifecycle - - public init( - api: any ConversationsAPI, - store: any ConversationLocalStoreProtocol, - mlsService: (any MLSServiceInterface)?, - context: NSManagedObjectContext, - localDomain: String?, - isFederationEnabled: Bool - ) { - self.api = api - self.store = store - self.mlsService = mlsService - self.context = context - self.localDomain = localDomain - self.isFederationEnabled = isFederationEnabled - } - - public func invoke( - teamID: UUID, - name: String?, - historyDepth: String?, - cells: Bool?, - users: Set, - accessMode: Set, - accessRoles: Set, - enableReceipts: Bool - ) async throws -> ZMConversation { - do { - return try await createChannel( - teamID: teamID, - name: name, - historyDepth: historyDepth, - cells: cells, - users: users, - accessMode: accessMode, - accessRoles: accessRoles, - enableReceipts: enableReceipts - ) - - } catch let error as ConversationsAPIError { - switch error { - case .notConnected: - await context.perform { - users.forEach { $0.needsToBeUpdatedFromBackend = true } - context.enqueueDelayedSave() - } - - throw Failure.notConnected - case .missingLegalHoldConsent: - throw Failure.missingLegalholdConsent - case let .nonFederatingBackends(domains): - do { - return try await createChannelExcludingDomains( - domains, - teamID: teamID, - name: name, - accessMode: accessMode, - accessRoles: accessRoles, - historyDepth: historyDepth, - cells: cells, - enableReceipts: enableReceipts, - users: users - ) - } catch { - throw Failure.nonFederatingDomains(Set(domains)) - } - default: - throw Failure.failedToCreateChannel(error) - } - - } catch { - throw Failure.failedToCreateChannel(error) - } - } - - private func createChannel( - teamID: UUID, - name: String?, - historyDepth: String?, - cells: Bool?, - users: Set, - accessMode: Set, - accessRoles: Set, - enableReceipts: Bool - ) async throws -> ZMConversation { - let ( - selfClientID, - qualifiedUserIds, - unqualifiedUserIds - ) = try await context.perform { - let selfUser = ZMUser.selfUser(in: context) - - guard let selfClientID = selfUser.selfClient()?.remoteIdentifier else { - throw Failure.missingSelfClientID - } - - let usersExcludingSelfUser = users.filter { !$0.isSelfUser } - let qualifiedUserIDs: [WireNetwork.QualifiedID] - let unqualifiedUserIDs: [UUID] - - if let ids = usersExcludingSelfUser.qualifiedUserIDs { - qualifiedUserIDs = ids.toAPIModel() - unqualifiedUserIDs = [] - } else { - qualifiedUserIDs = [] - unqualifiedUserIDs = usersExcludingSelfUser.compactMap(\.remoteIdentifier) - } - - return ( - selfClientID, - qualifiedUserIDs, - unqualifiedUserIDs - ) - } - // TODO: [WPB-18347] - add history length parameter to body when API is ready - - let apiParameters = CreateGroupConversationParameters( - groupType: .channel, - messageProtocol: .mls, - creatorClientID: selfClientID, - qualifiedUserIDs: qualifiedUserIds, - unqualifiedUserIDs: unqualifiedUserIds, - name: name, - accessMode: accessMode, - accessRoles: accessRoles, - legacyAccessRole: nil, - teamID: teamID, - isReadReceiptsEnabled: enableReceipts, - cells: cells - ) - - let remoteConversation = try await api.createGroupConversation( - parameters: apiParameters - ) - - let localConversation = try await createConversationLocally( - remoteConversation - ) - - try await setupMLS( - for: localConversation, - with: users - ) - - return localConversation - } - - // MARK: - API error handling - - private func createChannelExcludingDomains( - _ excludedDomains: [String], - teamID: UUID, - name: String?, - accessMode: Set, - accessRoles: Set, - historyDepth: String?, - cells: Bool?, - enableReceipts: Bool, - users: Set - ) async throws -> ZMConversation { - let (unreachableUsers, reachableUsers) = await context.perform { - let unreachableUsers = users.belongingTo(domains: Set(excludedDomains)) - let reachableUsers = Set(users).subtracting(unreachableUsers) - - return (unreachableUsers, reachableUsers) - } - - // Retrying with reachable users (with federated domains) - let conversation = try await createChannel( - teamID: teamID, - name: name, - historyDepth: historyDepth, - cells: cells, - users: reachableUsers, - accessMode: accessMode, - accessRoles: accessRoles, - enableReceipts: enableReceipts - ) - - // Add system message for unreachable users (with non federated domains) - await appendFailedToAddUsersMessage( - in: conversation, - users: unreachableUsers - ) - - return conversation - } - - // MARK: - MLS - - private func setupMLS( - for conversation: ZMConversation, - with participants: Set - ) async throws { - let (mlsGroupID, isMLSConversation) = await context.perform { - ( - conversation.mlsGroupID, - conversation.messageProtocol == .mls - ) - } - - guard isMLSConversation, let mlsGroupID, let mlsService else { return } - - let ciphersuite = try await mlsService.createGroup( - for: mlsGroupID, - removalKeys: nil - ) - - await context.perform { - // Self user is creator, so we don't need to process a welcome message - conversation.mlsStatus = .ready - conversation.ciphersuite = ciphersuite - context.saveOrRollback() - } - - try await validate( - users: participants, - conversation: conversation - ) - - try await addMLSParticipants( - participants, - to: conversation - ) - } - - private func validate( - users: Set, - conversation: ZMConversation - ) async throws { - try await context.perform { - guard - conversation.conversationType == .group, - !users.isEmpty - else { - throw Failure.invalidOperation - } - } - } - - private func addMLSParticipants( - _ users: Set, - to conversation: ZMConversation - ) async throws { - guard let mlsService else { return } - - let (qualifiedID, groupID) = await context.perform { - (conversation.qualifiedID, conversation.mlsGroupID) - } - - WireLogger.mls.info( - "adding \(users.count) participants to conversation (\(String(describing: qualifiedID)))" - ) - - guard let groupID else { - WireLogger.mls.warn( - "failed to add participants to conversation (\(String(describing: qualifiedID))): missing group ID" - ) - throw Failure.invalidOperation - } - - let mlsUsers = await context.perform { - users.compactMap { - MLSUser(from: $0, localDomain: localDomain) - } - } - - do { - - try await mlsService.addMembersToConversation( - with: mlsUsers, - for: groupID - ) - - try await context.perform { - try context.save() - } - - } catch let MLSService.MLSAddMembersError.failedToClaimKeyPackages(failedMLSUsers) { - let failedUsers = await context.perform { - users.filter { - failedMLSUsers.contains(MLSUser(from: $0, localDomain: self.localDomain)) - } - } - - try await handleNotClaimedKeyPackages( - failedUsers: Set(failedUsers), - users: users, - conversation: conversation - ) - - } catch let SendMLSMessageFailure.nonFederatingDomains(domains: domains) { - - try await handleNonFederatingDomains( - domains, - users: users, - conversation: conversation - ) - - } catch let SendMLSMessageFailure.unreachableDomains(domains: domains) { - - try await handleUnreachableDomains( - domains, - users: users, - conversation: conversation - ) - - } catch { - WireLogger.mls.warn( - "failed to add members to conversation (\(String(describing: qualifiedID))): \(String(describing: error))" - ) - throw error - } - - } - - private func createConversationLocally( - _ conversation: WireNetwork.Conversation - ) async throws -> ZMConversation { - await store.storeConversation( - conversation.toDomainModel(), - timestamp: .now, - isFederationEnabled: isFederationEnabled, - isMLSEnabled: true - ) - - let qualifiedID = conversation.qualifiedID?.id - guard let conversationID = conversation.id ?? qualifiedID else { - throw Failure.missingConversationID - } - - let conversationDomain = conversation.qualifiedID?.domain - - let localConversation = await store.fetchConversation( - id: conversationID, - domain: conversationDomain - ) - - // Conversation should be stored locally - guard let localConversation else { - throw Failure.conversationNotFound - } - - await context.perform { - _ = context.saveOrRollback() - } - - return localConversation - } - - // MARK: - MLS error handling - - private func handleNotClaimedKeyPackages( - failedUsers: Set, - users: Set, - conversation: ZMConversation - ) async throws { - guard !failedUsers.isEmpty else { - return Flow.addParticipants.checkpoint( - description: "unexpected failedToClaimKeyPackages but no failed users" - ) - } - - let users = Set(users) - if failedUsers != users { - - // Operation was aborted because some users didn't have key packages - // We filter them out and retry once - Flow.addParticipants.checkpoint(description: "retrying failedUsers begin") - try await addMLSParticipants( - users.subtracting(failedUsers), - to: conversation - ) - Flow.addParticipants.checkpoint(description: "retrying failedUsers end") - } - - let failedUserIds = await context.perform { - failedUsers.map { $0.remoteIdentifier.transportString() } - } - - Flow.addParticipants.checkpoint( - description: "add FailedToAddUsersMessage for users: \(failedUserIds.joined(separator: ", "))" - ) - - await appendFailedToAddUsersMessage( - in: conversation, - users: failedUsers - ) - } - - private func handleUnreachableDomains( - _ domains: Set, - users: Set, - conversation: ZMConversation - ) async throws { - let unreachableUsers = await context.perform { users.belongingTo(domains: domains) } - - if unreachableUsers.isEmpty { - - /// Backend is not able to determine which users are unreachable. - /// We just insert a message and do not attempt to retry - - await appendFailedToAddUsersMessage( - in: conversation, - users: Set(users) - ) - } else { - try await retryAddingMLSParticipants( - users, - to: conversation, - excludingDomains: domains - ) - } - } - - private func handleNonFederatingDomains( - _ domains: Set, - users: Set, - conversation: ZMConversation - ) async throws { - try await retryAddingMLSParticipants( - users, - to: conversation, - excludingDomains: domains - ) - } - - private func retryAddingMLSParticipants( - _ users: Set, - to conversation: ZMConversation, - excludingDomains domains: Set - ) async throws { - let usersToExclude = await context.perform { users.belongingTo(domains: domains) } - let usersToAdd = Set(users).subtracting(usersToExclude) - - await appendFailedToAddUsersMessage( - in: conversation, - users: usersToExclude - ) - - guard !usersToAdd.isEmpty else { return } - - try await addMLSParticipants( - usersToAdd, - to: conversation - ) - } - - // MARK: - Helpers - - private func appendFailedToAddUsersMessage( - in conversation: ZMConversation, - users: Set - ) async { - await context.perform { - conversation.appendFailedToAddUsersSystemMessage( - users: users, - sender: conversation.creator, - at: conversation.lastServerTimeStamp ?? Date() - ) - context.enqueueDelayedSave() - } - } -} diff --git a/WireDomain/Sources/WireDomain/UseCases/CreateGroupConversationUseCase.swift b/WireDomain/Sources/WireDomain/UseCases/CreateGroupConversationUseCase.swift index bb9eb54d4e6..999fbeef3e8 100644 --- a/WireDomain/Sources/WireDomain/UseCases/CreateGroupConversationUseCase.swift +++ b/WireDomain/Sources/WireDomain/UseCases/CreateGroupConversationUseCase.swift @@ -21,29 +21,33 @@ import WireLogging import WireNetwork // sourcery: AutoMockable -/// Creates and setup a group conversation +/// Creates and sets up a group conversation or a channel. +/// +/// Channels are group conversations that belong to a team, have a name, and always use MLS. +/// Pass `.channel` as `groupType` for channels or `.group` for regular group conversations. public protocol CreateGroupConversationUseCaseProtocol { func invoke( + groupType: WireNetwork.ConversationGroupType, teamID: UUID?, messageProtocol: WireNetwork.ConversationMessageProtocol, name: String?, + historyDepth: String?, + cells: Bool?, users: Set, accessMode: Set, accessRoles: Set, enableReceipts: Bool, - cells: Bool?, isMLSEnabled: Bool ) async throws -> ZMConversation } -/// Channels are MLS conversations which belong to a team and have a name. public struct CreateGroupConversationUseCase: CreateGroupConversationUseCaseProtocol { public enum Failure: Error { case missingSelfClientID case missingConversationID case conversationNotFound - case failedToCreateGroup(Error) + case failedToCreate(Error) case missingLegalholdConsent case nonFederatingDomains(Set) case notConnected @@ -58,19 +62,17 @@ public struct CreateGroupConversationUseCase: CreateGroupConversationUseCaseProt private let context: NSManagedObjectContext private let localDomain: String? private let isFederationEnabled: Bool - private let isMLSEnabled: Bool private let logger: WireLogger = .conversation // MARK: - Object lifecycle public init( - api: ConversationsAPI, - store: ConversationLocalStoreProtocol, + api: any ConversationsAPI, + store: any ConversationLocalStoreProtocol, mlsService: (any MLSServiceInterface)?, context: NSManagedObjectContext, localDomain: String?, - isFederationEnabled: Bool, - isMLSEnabled: Bool + isFederationEnabled: Bool ) { self.api = api self.store = store @@ -78,30 +80,35 @@ public struct CreateGroupConversationUseCase: CreateGroupConversationUseCaseProt self.context = context self.localDomain = localDomain self.isFederationEnabled = isFederationEnabled - self.isMLSEnabled = isMLSEnabled } + // MARK: - Invoke + public func invoke( + groupType: WireNetwork.ConversationGroupType, teamID: UUID?, messageProtocol: WireNetwork.ConversationMessageProtocol, name: String?, + historyDepth: String?, + cells: Bool?, users: Set, accessMode: Set, accessRoles: Set, enableReceipts: Bool, - cells: Bool?, isMLSEnabled: Bool ) async throws -> ZMConversation { do { - return try await createGroup( + return try await createConversation( + groupType: groupType, teamID: teamID, messageProtocol: messageProtocol, name: name, + historyDepth: historyDepth, + cells: cells, users: users, accessMode: accessMode, accessRoles: accessRoles, enableReceipts: enableReceipts, - cells: cells, isMLSEnabled: isMLSEnabled ) } catch let error as ConversationsAPIError { @@ -111,44 +118,52 @@ public struct CreateGroupConversationUseCase: CreateGroupConversationUseCaseProt users.forEach { $0.needsToBeUpdatedFromBackend = true } context.enqueueDelayedSave() } - throw Failure.notConnected + case .missingLegalHoldConsent: throw Failure.missingLegalholdConsent + case let .nonFederatingBackends(domains): do { - return try await createGroupExcludingDomains( + return try await createConversationExcludingDomains( domains, + groupType: groupType, teamID: teamID, messageProtocol: messageProtocol, name: name, + historyDepth: historyDepth, + cells: cells, + users: users, accessMode: accessMode, accessRoles: accessRoles, enableReceipts: enableReceipts, - cells: cells, - users: users + isMLSEnabled: isMLSEnabled ) } catch { throw Failure.nonFederatingDomains(Set(domains)) } + default: - throw Failure.failedToCreateGroup(error) + throw Failure.failedToCreate(error) } } catch { - throw Failure.failedToCreateGroup(error) + throw Failure.failedToCreate(error) } - } - private func createGroup( + // MARK: - Core creation + + private func createConversation( + groupType: WireNetwork.ConversationGroupType, teamID: UUID?, messageProtocol: WireNetwork.ConversationMessageProtocol, name: String?, + historyDepth: String?, + cells: Bool?, users: Set, accessMode: Set, accessRoles: Set, enableReceipts: Bool, - cells: Bool?, isMLSEnabled: Bool ) async throws -> ZMConversation { let ( @@ -174,15 +189,13 @@ public struct CreateGroupConversationUseCase: CreateGroupConversationUseCaseProt unqualifiedUserIDs = usersExcludingSelfUser.compactMap(\.remoteIdentifier) } - return ( - selfClientID, - qualifiedUserIDs, - unqualifiedUserIDs - ) + return (selfClientID, qualifiedUserIDs, unqualifiedUserIDs) } + // TODO: [WPB-18347] - add historyDepth to body when API is ready + let apiParameters = CreateGroupConversationParameters( - groupType: .group, + groupType: groupType, messageProtocol: messageProtocol, creatorClientID: selfClientID, qualifiedUserIDs: qualifiedUserIds, @@ -201,19 +214,15 @@ public struct CreateGroupConversationUseCase: CreateGroupConversationUseCaseProt ) let localConversation = try await createConversationLocally( - remoteConversation + remoteConversation, + isMLSEnabled: isMLSEnabled ) - switch messageProtocol { - case .mls: - + if messageProtocol == .mls { try await setupMLS( for: localConversation, with: users ) - - case .mixed, .proteus: - break } return localConversation @@ -221,38 +230,39 @@ public struct CreateGroupConversationUseCase: CreateGroupConversationUseCaseProt // MARK: - API error handling - private func createGroupExcludingDomains( + private func createConversationExcludingDomains( _ excludedDomains: [String], + groupType: WireNetwork.ConversationGroupType, teamID: UUID?, messageProtocol: WireNetwork.ConversationMessageProtocol, name: String?, + historyDepth: String?, + cells: Bool?, + users: Set, accessMode: Set, accessRoles: Set, enableReceipts: Bool, - cells: Bool?, - users: Set + isMLSEnabled: Bool ) async throws -> ZMConversation { let (unreachableUsers, reachableUsers) = await context.perform { let unreachableUsers = users.belongingTo(domains: Set(excludedDomains)) - let reachableUsers = Set(users).subtracting(unreachableUsers) - - return (unreachableUsers, reachableUsers) + return (unreachableUsers, Set(users).subtracting(unreachableUsers)) } - // Retrying with reachable users (with federated domains) - let conversation = try await createGroup( + let conversation = try await createConversation( + groupType: groupType, teamID: teamID, messageProtocol: messageProtocol, name: name, + historyDepth: historyDepth, + cells: cells, users: reachableUsers, accessMode: accessMode, accessRoles: accessRoles, enableReceipts: enableReceipts, - cells: cells, isMLSEnabled: isMLSEnabled ) - // Add system message for unreachable users (with non federated domains) await appendFailedToAddUsersMessage( in: conversation, users: unreachableUsers @@ -261,6 +271,42 @@ public struct CreateGroupConversationUseCase: CreateGroupConversationUseCaseProt return conversation } + // MARK: - Local store + + private func createConversationLocally( + _ conversation: WireNetwork.Conversation, + isMLSEnabled: Bool + ) async throws -> ZMConversation { + await store.storeConversation( + conversation.toDomainModel(), + timestamp: .now, + isFederationEnabled: isFederationEnabled, + isMLSEnabled: isMLSEnabled + ) + + let qualifiedID = conversation.qualifiedID?.id + guard let conversationID = conversation.id ?? qualifiedID else { + throw Failure.missingConversationID + } + + let conversationDomain = conversation.qualifiedID?.domain + + let localConversation = await store.fetchConversation( + id: conversationID, + domain: conversationDomain + ) + + guard let localConversation else { + throw Failure.conversationNotFound + } + + await context.perform { + _ = context.saveOrRollback() + } + + return localConversation + } + // MARK: - MLS private func setupMLS( @@ -276,9 +322,15 @@ public struct CreateGroupConversationUseCase: CreateGroupConversationUseCaseProt guard isMLSConversation, let mlsGroupID, let mlsService else { return } - let ciphersuite = try await mlsService.createGroup( + try await validate( + users: participants, + conversation: conversation + ) + + let ciphersuite = try await establishMLSGroup( + participants, for: mlsGroupID, - removalKeys: nil + conversation: conversation ) await context.perform { @@ -287,16 +339,6 @@ public struct CreateGroupConversationUseCase: CreateGroupConversationUseCaseProt conversation.ciphersuite = ciphersuite context.saveOrRollback() } - - try await validate( - users: participants, - conversation: conversation - ) - - try await addMLSParticipants( - participants, - to: conversation - ) } private func validate( @@ -313,27 +355,24 @@ public struct CreateGroupConversationUseCase: CreateGroupConversationUseCaseProt } } - private func addMLSParticipants( + /// Creates an MLS group and adds participants atomically, with error handling and retries. + /// + /// On key package or federation failures, retries with the filtered user set. + /// Each retry creates a fresh group (the prior attempt was rolled back). + + private func establishMLSGroup( _ users: Set, - to conversation: ZMConversation - ) async throws { - guard let mlsService else { return } + for groupID: MLSGroupID, + conversation: ZMConversation + ) async throws -> WireDataModel.MLSCipherSuite { + guard let mlsService else { throw Failure.invalidOperation } - let (qualifiedID, groupID) = await context.perform { - (conversation.qualifiedID, conversation.mlsGroupID) - } + let qualifiedID = await context.perform { conversation.qualifiedID } WireLogger.mls.info( - "adding \(users.count) participants to conversation (\(String(describing: qualifiedID)))" + "establishing MLS group for conversation (\(String(describing: qualifiedID))) with \(users.count) participants" ) - guard let groupID else { - WireLogger.mls.warn( - "failed to add participants to conversation (\(String(describing: qualifiedID))): missing group ID" - ) - throw Failure.invalidOperation - } - let mlsUsers = await context.perform { users.compactMap { MLSUser(from: $0, localDomain: localDomain) @@ -341,86 +380,47 @@ public struct CreateGroupConversationUseCase: CreateGroupConversationUseCaseProt } do { - - try await mlsService.addMembersToConversation( + return try await mlsService.establishGroup( + for: groupID, with: mlsUsers, - for: groupID + removalKeys: nil ) - try await context.perform { - try context.save() - } - } catch let MLSService.MLSAddMembersError.failedToClaimKeyPackages(failedMLSUsers) { let failedUsers = await context.perform { users.filter { failedMLSUsers.contains(MLSUser(from: $0, localDomain: self.localDomain)) } } - - try await handleNotClaimedKeyPackages( + return try await handleNotClaimedKeyPackages( failedUsers: Set(failedUsers), users: users, + for: groupID, conversation: conversation ) } catch let SendMLSMessageFailure.nonFederatingDomains(domains: domains) { - - try await handleNonFederatingDomains( + return try await handleNonFederatingDomains( domains, users: users, + for: groupID, conversation: conversation ) } catch let SendMLSMessageFailure.unreachableDomains(domains: domains) { - - try await handleUnreachableDomains( + return try await handleUnreachableDomains( domains, users: users, + for: groupID, conversation: conversation ) } catch { WireLogger.mls.warn( - "failed to add members to conversation (\(String(describing: qualifiedID))): \(String(describing: error))" + "failed to establish MLS group for conversation (\(String(describing: qualifiedID))): \(String(describing: error))" ) throw error } - - } - - private func createConversationLocally( - _ conversation: WireNetwork.Conversation - ) async throws -> ZMConversation { - await store.storeConversation( - conversation.toDomainModel(), - timestamp: .now, - isFederationEnabled: isFederationEnabled, - isMLSEnabled: isMLSEnabled - ) - - let qualifiedID = conversation.qualifiedID?.id - guard let conversationID = conversation.id ?? qualifiedID else { - throw Failure.missingConversationID - } - - let conversationDomain = conversation.qualifiedID?.domain - - let localConversation = await store.fetchConversation( - id: conversationID, - domain: conversationDomain - ) - - // Conversation should be stored locally - guard let localConversation else { - throw Failure.conversationNotFound - } - - await context.perform { - _ = context.saveOrRollback() - } - - return localConversation } // MARK: - MLS error handling @@ -428,23 +428,27 @@ public struct CreateGroupConversationUseCase: CreateGroupConversationUseCaseProt private func handleNotClaimedKeyPackages( failedUsers: Set, users: Set, + for groupID: MLSGroupID, conversation: ZMConversation - ) async throws { + ) async throws -> WireDataModel.MLSCipherSuite { guard !failedUsers.isEmpty else { - return Flow.addParticipants.checkpoint( + Flow.addParticipants.checkpoint( description: "unexpected failedToClaimKeyPackages but no failed users" ) + throw Failure.invalidOperation } - let users = Set(users) - if failedUsers != users { + let allUsers = Set(users) + var ciphersuite: WireDataModel.MLSCipherSuite? - // Operation was aborted because some users didn't have key packages - // We filter them out and retry once + if failedUsers != allUsers { + // Filter out users without key packages and retry with the remainder. + // This creates a fresh group (the prior attempt was rolled back). Flow.addParticipants.checkpoint(description: "retrying failedUsers begin") - try await addMLSParticipants( - users.subtracting(failedUsers), - to: conversation + ciphersuite = try await establishMLSGroup( + allUsers.subtracting(failedUsers), + for: groupID, + conversation: conversation ) Flow.addParticipants.checkpoint(description: "retrying failedUsers end") } @@ -457,32 +461,37 @@ public struct CreateGroupConversationUseCase: CreateGroupConversationUseCaseProt description: "add FailedToAddUsersMessage for users: \(failedUserIds.joined(separator: ", "))" ) - await appendFailedToAddUsersMessage( - in: conversation, - users: failedUsers - ) + await appendFailedToAddUsersMessage(in: conversation, users: failedUsers) + + guard let ciphersuite else { + throw Failure.failedToCreate( + MLSService.MLSAddMembersError.failedToClaimKeyPackages(users: Array(failedUsers).compactMap { + MLSUser(from: $0, localDomain: localDomain) + }) + ) + } + + return ciphersuite } private func handleUnreachableDomains( _ domains: Set, users: Set, + for groupID: MLSGroupID, conversation: ZMConversation - ) async throws { + ) async throws -> WireDataModel.MLSCipherSuite { let unreachableUsers = await context.perform { users.belongingTo(domains: domains) } if unreachableUsers.isEmpty { - /// Backend is not able to determine which users are unreachable. - /// We just insert a message and do not attempt to retry - - await appendFailedToAddUsersMessage( - in: conversation, - users: Set(users) - ) + /// We just insert a message and do not attempt to retry. + await appendFailedToAddUsersMessage(in: conversation, users: Set(users)) + throw Failure.invalidOperation } else { - try await retryAddingMLSParticipants( + return try await retryEstablishingMLSGroup( users, - to: conversation, + for: groupID, + conversation: conversation, excludingDomains: domains ) } @@ -491,34 +500,33 @@ public struct CreateGroupConversationUseCase: CreateGroupConversationUseCaseProt private func handleNonFederatingDomains( _ domains: Set, users: Set, + for groupID: MLSGroupID, conversation: ZMConversation - ) async throws { - try await retryAddingMLSParticipants( + ) async throws -> WireDataModel.MLSCipherSuite { + try await retryEstablishingMLSGroup( users, - to: conversation, + for: groupID, + conversation: conversation, excludingDomains: domains ) } - private func retryAddingMLSParticipants( + private func retryEstablishingMLSGroup( _ users: Set, - to conversation: ZMConversation, + for groupID: MLSGroupID, + conversation: ZMConversation, excludingDomains domains: Set - ) async throws { + ) async throws -> WireDataModel.MLSCipherSuite { let usersToExclude = await context.perform { users.belongingTo(domains: domains) } let usersToAdd = Set(users).subtracting(usersToExclude) - await appendFailedToAddUsersMessage( - in: conversation, - users: usersToExclude - ) + await appendFailedToAddUsersMessage(in: conversation, users: usersToExclude) - guard !usersToAdd.isEmpty else { return } + guard !usersToAdd.isEmpty else { + throw Failure.invalidOperation + } - try await addMLSParticipants( - usersToAdd, - to: conversation - ) + return try await establishMLSGroup(usersToAdd, for: groupID, conversation: conversation) } // MARK: - Helpers diff --git a/wire-ios-data-model/Source/MLS/MLSActionExecutor.swift b/wire-ios-data-model/Source/MLS/MLSActionExecutor.swift index de8a70193d3..a66c0b2f961 100644 --- a/wire-ios-data-model/Source/MLS/MLSActionExecutor.swift +++ b/wire-ios-data-model/Source/MLS/MLSActionExecutor.swift @@ -40,14 +40,16 @@ public protocol MLSActionExecutorProtocol { /// - Parameters: /// - invitees: The key packages of the clients to add. /// - groupID: The group ID of the group to add members to. - /// - Returns: Update events returned by the backend. + /// - context: if provided, the operation will run within the existing transaction + /// and `performNonReentrant` will be skipped (ordering is guaranteed by the transaction). /// /// If any new CRL distribution points are found, they will be published. /// They can be observed with ``MLSActionExecutor/onNewCRLsDistributionPoints()`` func addMembers( _ invitees: [KeyPackage], - to groupID: MLSGroupID + to groupID: MLSGroupID, + context: CoreCryptoContextProtocol? ) async throws /// Creates and sends a commit bundle to remove clients from a group. @@ -64,10 +66,12 @@ public protocol MLSActionExecutorProtocol { /// Creates and sends a commit bundle to update the key material for a group. /// - /// - Parameter groupID: The group ID of the group to update key material for. - /// - Returns: Update events returned by the backend. + /// - Parameters: + /// - groupID: The group ID of the group to update key material for. + /// - context: if provided, the operation will run within the existing transaction + /// and `performNonReentrant` will be skipped (ordering is guaranteed by the transaction). - func updateKeyMaterial(for groupID: MLSGroupID) async throws + func updateKeyMaterial(for groupID: MLSGroupID, context: CoreCryptoContextProtocol?) async throws /// Creates and sends a commit bundle to commit the pending proposals for a group. /// @@ -235,33 +239,45 @@ public actor MLSActionExecutor: MLSActionExecutorProtocol { return MLSGroupID(welcomeBundle.id) } - public func addMembers(_ invitees: [WireDataModel.KeyPackage], to groupID: MLSGroupID) async throws { - try await performNonReentrant(groupID: groupID) { - do { - WireLogger.mls.info("adding members to group...", attributes: groupID.safeAttributes) - - let crlNewDistributionPoints = try await coreCrypto.extendedTransaction { - try await $0.addClientsToConversation( - conversationId: groupID.conversationId, - keyPackages: invitees.compactMap(\.coreCryptoKeyPackage) - ) - } - - if let newDistributionPoints = CRLsDistributionPoints( - from: crlNewDistributionPoints - ) { - onNewCRLsDistributionPointsSubject.send(newDistributionPoints) + public func addMembers( + _ invitees: [WireDataModel.KeyPackage], + to groupID: MLSGroupID, + context: CoreCryptoContextProtocol? + ) async throws { + if let context { + try await addMembersInternal(invitees, to: groupID, context: context) + } else { + try await performNonReentrant(groupID: groupID) { + try await coreCrypto.extendedTransaction { + try await self.addMembersInternal(invitees, to: groupID, context: $0) } + } + } + } - WireLogger.mls.info("success: adding members to group", attributes: groupID.safeAttributes) - } catch { - WireLogger.mls - .error( - "failed: adding members to group: \(String(describing: error))", - attributes: groupID.safeAttributes - ) - throw error + private func addMembersInternal( + _ invitees: [WireDataModel.KeyPackage], + to groupID: MLSGroupID, + context: CoreCryptoContextProtocol + ) async throws { + do { + WireLogger.mls.info("adding members to group...", attributes: groupID.safeAttributes) + + let crlNewDistributionPoints = try await context.addClientsToConversation( + conversationId: groupID.conversationId, + keyPackages: invitees.compactMap(\.coreCryptoKeyPackage) + ) + if let newDistributionPoints = CRLsDistributionPoints(from: crlNewDistributionPoints) { + onNewCRLsDistributionPointsSubject.send(newDistributionPoints) } + + WireLogger.mls.info("success: adding members to group", attributes: groupID.safeAttributes) + } catch { + WireLogger.mls.error( + "failed: adding members to group: \(String(describing: error))", + attributes: groupID.safeAttributes + ) + throw error } } @@ -286,23 +302,30 @@ public actor MLSActionExecutor: MLSActionExecutorProtocol { } } - public func updateKeyMaterial(for groupID: MLSGroupID) async throws { - try await performNonReentrant(groupID: groupID) { - do { - WireLogger.mls.info("updating key material for group...", attributes: groupID.safeAttributes) - return try await coreCrypto.extendedTransaction { - try await $0.updateKeyingMaterial(conversationId: groupID.conversationId) + public func updateKeyMaterial(for groupID: MLSGroupID, context: CoreCryptoContextProtocol?) async throws { + if let context { + try await updateKeyMaterialInternal(for: groupID, context: context) + } else { + try await performNonReentrant(groupID: groupID) { + try await coreCrypto.extendedTransaction { + try await self.updateKeyMaterialInternal(for: groupID, context: $0) } - } catch { - WireLogger.mls - .error( - "error: updating key material for group: \(String(describing: error))", - attributes: groupID.safeAttributes - ) - throw error } } } + + private func updateKeyMaterialInternal(for groupID: MLSGroupID, context: CoreCryptoContextProtocol) async throws { + do { + WireLogger.mls.info("updating key material for group...", attributes: groupID.safeAttributes) + try await context.updateKeyingMaterial(conversationId: groupID.conversationId) + } catch { + WireLogger.mls.error( + "error: updating key material for group: \(String(describing: error))", + attributes: groupID.safeAttributes + ) + throw error + } + } public func commitPendingProposals(in groupID: MLSGroupID) async throws { try await performNonReentrant(groupID: groupID) { diff --git a/wire-ios-data-model/Source/MLS/MLSService.swift b/wire-ios-data-model/Source/MLS/MLSService.swift index 8b57f9b0b9d..ca24ab87d85 100644 --- a/wire-ios-data-model/Source/MLS/MLSService.swift +++ b/wire-ios-data-model/Source/MLS/MLSService.swift @@ -345,7 +345,7 @@ public final class MLSService: MLSServiceInterface { private func internalUpdateKeyMaterial(for groupID: MLSGroupID) async throws { do { WireLogger.mls.info("updating key material for group (\(groupID.safeForLoggingDescription))") - try await mlsActionExecutor.updateKeyMaterial(for: groupID) + try await mlsActionExecutor.updateKeyMaterial(for: groupID, context: nil) staleKeyMaterialDetector.keyingMaterialUpdated(for: groupID) } catch { WireLogger.mls @@ -371,25 +371,137 @@ public final class MLSService: MLSServiceInterface { ) async throws -> MLSCipherSuite { guard let context else { throw MLSGroupCreationError.failedToCreateGroup } - do { - let ciphersuite = try await createGroup(for: groupID, removalKeys: removalKeys) - let mlsSelfUser = await context.perform { - let selfUser = ZMUser.selfUser(in: context) - return MLSUser(from: selfUser, localDomain: self.localDomain) + logger.info("establishing group atomically", attributes: groupID.safeAttributes) + + // Append self user last so that the other party's key packages are claimed first. + // This avoids depleting our own key packages if the other user has none + // (particularly relevant for 1:1). + let mlsSelfUser = await context.perform { + MLSUser(from: ZMUser.selfUser(in: context), localDomain: self.localDomain) + } + let usersWithSelf = users.filter { $0 != mlsSelfUser } + [mlsSelfUser] + + let mlsConfig = await featureRepository.fetchMLS().config + guard let ciphersuite = MLSCipherSuite(rawValue: mlsConfig.defaultCipherSuite.rawValue) else { + throw MLSGroupCreationError.invalidCiphersuite + } + + let externalSenders = try await fetchExternalSenders( + removalKeys: removalKeys, + ciphersuite: ciphersuite + ) + + try await retryOnCommitFailure( + for: groupID, + operation: { + try await self.claimAndEstablishGroup( + groupID: groupID, + users: users, + ciphersuite: ciphersuite, + externalSenders: externalSenders + ) + }, + groupOutOfSyncHandler: { missingUsers in + // The group was wiped on the prior failure — merge the missing users + // and re-establish from scratch. No further groupOutOfSync retry is + // applied (no handler passed), preventing infinite recursion. + let additionalUsers = missingUsers.map { MLSUser($0, selfClientID: nil) } + let mergedUsers = users + additionalUsers.filter { !users.contains($0) } + try await self.claimAndEstablishGroup( + groupID: groupID, + users: mergedUsers, + ciphersuite: ciphersuite, + externalSenders: externalSenders + ) } - // make sure we have the selfUser but only once - // add self in last position so - if the other user doesn't have key packages for 1-1 - we don't deplete our - // keypackages - // for nothing - let usersWithSelfUser = users.filter { $0 != mlsSelfUser } + [mlsSelfUser] - try await addMembersToConversation(with: usersWithSelfUser, for: groupID) - return ciphersuite + ) + + return ciphersuite + } + + /// Claims key packages and runs the atomic create+add transaction. + /// + /// This is the unit that gets retried on recoverable commit failures. Each retry + /// re-claims key packages (old ones may have been consumed) and re-creates the + /// group from scratch (it was wiped on the prior failure). + /// + private func claimAndEstablishGroup( + groupID: MLSGroupID, + users: [MLSUser], + ciphersuite: MLSCipherSuite, + externalSenders: [ExternalSenderKey] + ) async throws { + + // Claim key packages outside the transaction — this is a network call. + // failedToClaimKeyPackages propagates immediately without retry. + let keyPackages = try await claimKeyPackages(for: users, ciphersuite: ciphersuite) + + // Single CoreCrypto transaction: create the group and send the first commit atomically. + // createConversation is local-only; the commit (addMembers or updateKeyMaterial) is + // routed through MLSActionExecutor with the open context, so CRL point publishing + // and the executor's invariants are preserved. + // On failure of the commit step, wipe the group on the same open context so no + // epoch-0 orphan is left behind. + try await coreCrypto.extendedTransaction { context in + let e2eiIsEnabled = try await context.e2eiIsEnabled( + ciphersuite: ciphersuite.coreCryptoCipherSuite + ) + let config = ConversationConfiguration( + ciphersuite: ciphersuite.coreCryptoCipherSuite, + externalSenders: externalSenders, + custom: .init(keyRotationSpan: nil, wirePolicy: nil) + ) + + try await context.createConversation( + conversationId: groupID.conversationId, + creatorCredentialType: e2eiIsEnabled ? .x509 : .basic, + config: config + ) + + do { + if keyPackages.isEmpty { + // CoreCrypto does not accept an empty key-package list in addClients, + // but we still need to send a commit to the backend to advance the epoch. + try await self.mlsActionExecutor.updateKeyMaterial(for: groupID, context: context) + } else { + try await self.mlsActionExecutor.addMembers(keyPackages, to: groupID, context: context) + } + } catch { + self.logger.warn( + "failed to add clients to group, wiping to avoid orphaned epoch-0 group", + attributes: groupID.safeAttributes + ) + try? await context.wipeConversation(conversationId: groupID.conversationId) + throw error + } + } + + staleKeyMaterialDetector.keyingMaterialUpdated(for: groupID) + } + + private func fetchExternalSenders( + removalKeys: BackendMLSPublicKeys?, + ciphersuite: MLSCipherSuite + ) async throws -> [ExternalSenderKey] { + if let removalKeys { + return removalKeys.externalSenderKey(for: ciphersuite) + } + + logger.info("fetching backend public keys for external senders") + + do { + let backendPublicKeys = try await actionsProvider.fetchBackendPublicKeys( + in: notificationContext + ) + return backendPublicKeys.externalSenderKey(for: ciphersuite) } catch { - try await wipeGroup(groupID) - throw error + logger.warn("failed to fetch backend public keys: \(String(describing: error))") + throw MLSGroupCreationError.failedToGetExternalSenders } } + // MARK: - Group creation + public func createGroup( for groupID: MLSGroupID, parentGroupID: MLSGroupID @@ -423,25 +535,9 @@ public final class MLSService: MLSServiceInterface { } public func createSelfGroup(for groupID: MLSGroupID) async throws -> MLSCipherSuite { - do { - guard let context else { throw MLSAddMembersError.noManagedObjectContext } - let ciphersuite = try await createGroup(for: groupID) - let mlsSelfUser = await context.perform { - let selfUser = ZMUser.selfUser(in: context) - return MLSUser(from: selfUser, localDomain: self.localDomain) - } - - do { - try await addMembersToConversation(with: [mlsSelfUser], for: groupID) - } catch MLSAddMembersError.noInviteesToAdd { - logger.debug("createConversation noInviteesToAdd, updateKeyMaterial") - try await updateKeyMaterial(for: groupID) - } - return ciphersuite - } catch { - logger.error("create group for self conversation failed: \(error.localizedDescription)") - throw error - } + // Pass an empty user list — establishGroup appends the self user internally. + // The empty-key-packages case (no other devices) is handled by updateKeyMaterial. + try await establishGroup(for: groupID, with: []) } // MARK: - Add member @@ -479,9 +575,9 @@ public final class MLSService: MLSServiceInterface { // CC does not accept empty keypackages in addMembers, but // when creating a group we still need to send a commit to backend // to inform we are in the group - try await mlsActionExecutor.updateKeyMaterial(for: groupID) + try await mlsActionExecutor.updateKeyMaterial(for: groupID, context: nil) } else { - try await mlsActionExecutor.addMembers(keyPackages, to: groupID) + try await mlsActionExecutor.addMembers(keyPackages, to: groupID, context: nil) } } catch { logger @@ -1480,9 +1576,19 @@ public final class MLSService: MLSServiceInterface { } + /// Handles recoverable backend commit rejections for any commit-generating operation. + /// + /// - `retryAfterBackoff`: re-runs `operation` with exponential backoff. + /// - `retryAfterAddingMissingUsers`: if `groupOutOfSyncHandler` is provided, delegates to it + /// (used when the group state must be rebuilt, e.g. initial group creation). Otherwise uses + /// the default behaviour: adds the missing users to the existing group then retries `operation`. + /// - `resetBrokenMLSConversation`: notifies the delegate and returns. + /// - `giveUp`: throws `MLSRetryError.nonRecoverableError`. + private func retryOnCommitFailure( for groupID: MLSGroupID, operation: @escaping () async throws -> Void, + groupOutOfSyncHandler: ((_ missingUsers: Set) async throws -> Void)? = nil, retryCount: Int = 0 ) async throws { let logAttributes: LogAttributes = [ @@ -1509,36 +1615,45 @@ public final class MLSService: MLSServiceInterface { } case let .retryAfterAddingMissingUsers(missingUsers): - guard retryCount <= maxRetryAttempts else { - logger.error( - "failed to send commit due to missing users and reached max attempts", + if let handler = groupOutOfSyncHandler { + // Custom handler: used when the group cannot simply be patched in place + // (e.g. during initial group creation where the group was wiped on failure). + // The handler is responsible for any further recovery; no additional retry + // is applied here, preventing infinite recursion without a counter. + logger.warn( + "failed to send commit due to missing users, delegating to group-out-of-sync handler", attributes: logAttributes ) - throw MLSRetryError.retryLimitReached - } + try await handler(missingUsers) + } else { + // Default behaviour for existing groups: add the missing users to the group + // then retry the original operation. + guard retryCount <= maxRetryAttempts else { + logger.error( + "failed to send commit due to missing users and reached max attempts", + attributes: logAttributes + ) + throw MLSRetryError.retryLimitReached + } - logger.warn( - "failed to send commit due to missing users. Adding users then retrying operation - attempt: \(retryCount)...", - attributes: logAttributes - ) + logger.warn( + "failed to send commit due to missing users. Adding users then retrying operation - attempt: \(retryCount)...", + attributes: logAttributes + ) - let users = missingUsers.map { - MLSUser($0, selfClientID: nil) - } + let users = missingUsers.map { MLSUser($0, selfClientID: nil) } - // It's important to call the internal method because - // we don't want to re-enter the commit failure handling - // again for this action, otherwise we may end up in a loop. - try await internalAddMembersToConversation( - with: users, - for: groupID - ) + // Call the internal method directly to avoid re-entering the commit + // failure handler for this add, which would cause an infinite loop. + try await internalAddMembersToConversation(with: users, for: groupID) - try await retryOnCommitFailure( - for: groupID, - operation: operation, - retryCount: retryCount + 1 - ) + try await retryOnCommitFailure( + for: groupID, + operation: operation, + groupOutOfSyncHandler: nil, + retryCount: retryCount + 1 + ) + } case .resetBrokenMLSConversation: let feature = await featureRepository.fetchAllowedGlobalOperations() diff --git a/wire-ios-data-model/Source/MLS/MLSServiceInterface.swift b/wire-ios-data-model/Source/MLS/MLSServiceInterface.swift index f5b5e251d4b..96b33747084 100644 --- a/wire-ios-data-model/Source/MLS/MLSServiceInterface.swift +++ b/wire-ios-data-model/Source/MLS/MLSServiceInterface.swift @@ -80,17 +80,25 @@ public protocol MLSServiceInterface: MLSEncryptionServiceInterface, MLSDecryptio /// Creates a new group with the given ID and adds users to it /// - /// - Parameters: - /// - groupID: The ID of the group to create - /// - users: The users to add to the group - /// - removalKeys: External senders - /// - Returns: The ciphersuite used to create the group - /// - Throws: An error if the group creation fails or if adding users fails + /// Creates a new MLS group and adds members to it atomically within a single CoreCrypto transaction. /// - /// Calls ``MLSService/createGroup(for:parentGroupID:)`` to create the group, - /// then calls ``MLSService/addMembersToConversation(with:for:)`` to add the users and the self user. + /// Key packages are claimed before the transaction opens. Inside the transaction, + /// `createConversation` runs first, then `addClientsToConversation`. + /// If the add step fails, `wipeConversation` is called on the same context before + /// the transaction closes, leaving no orphaned epoch-0 group. /// - /// If group creation fails, it wipes the group using ``MLSService/wipeGroup(_:)`` + /// The self user is appended to the user list internally — callers should pass + /// only the invited users. + /// + /// Recoverable commit rejections (stale message, client mismatch) trigger an automatic + /// retry with backoff. Non-recoverable errors and `failedToClaimKeyPackages` propagate + /// immediately. + /// + /// - Parameters: + /// - groupID: The ID of the group to create. + /// - users: The users to add to the group (excluding self). + /// - removalKeys: External senders, if already available. + /// - Returns: The ciphersuite used to create the group. /// /// [confluence use case](https://wearezeta.atlassian.net/wiki/spaces/ENGINEERIN/pages/557220341/Use+case+create+a+group+conversation+MLS) diff --git a/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession.swift b/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession.swift index 0afdf6b2a77..0bb5ab413e3 100644 --- a/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession.swift +++ b/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession.swift @@ -393,10 +393,6 @@ public final class ZMUserSession: NSObject { clientSessionComponent?.createGroupConversationUseCase() } - public var createChannelUseCase: (some CreateChannelUseCaseProtocol)? { - clientSessionComponent?.createChannelUseCase() - } - private lazy var mlsClientManager = MLSClientManager( coreCryptoProvider: coreCryptoProvider, mlsService: mlsService diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Create/ConversationCreationController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Create/ConversationCreationController.swift index 73c1ed85a41..dafb5d77540 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Create/ConversationCreationController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Create/ConversationCreationController.swift @@ -403,14 +403,16 @@ extension ConversationCreationController: AddParticipantsConversationCreationDel do { let conversation = try await groupConversationUseCase.invoke( + groupType: .group, teamID: teamID, messageProtocol: conversationMessageProtocol, name: values.name, + historyDepth: nil, + cells: userSession.isWireDriveEnabled ? values.enableFileManagement : nil, users: Set(users), accessMode: Set(accessMode), accessRoles: Set(accessRoles), enableReceipts: values.enableReceipts, - cells: userSession.isWireDriveEnabled ? values.enableFileManagement : nil, isMLSEnabled: session.isBackendMLSEnabled ) diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Create/WireConversationChannelCreationFormViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Create/WireConversationChannelCreationFormViewController.swift index 403200b0618..1c5c35b93d4 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Create/WireConversationChannelCreationFormViewController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Create/WireConversationChannelCreationFormViewController.swift @@ -220,7 +220,7 @@ extension WireConversationChannelCreationFormViewController: AddParticipantsConv session: ZMUserSession, users: [ZMUser] ) async { - guard let channelUseCase = session.createChannelUseCase else { + guard let channelUseCase = session.createGroupConversationUseCase else { return } @@ -236,14 +236,17 @@ extension WireConversationChannelCreationFormViewController: AddParticipantsConv do { let conversation = try await channelUseCase.invoke( + groupType: .channel, teamID: teamID, + messageProtocol: .mls, name: values.name, historyDepth: channelHistoryDepth, cells: userSession.isWireDriveEnabled ? values.enableFileManagement : nil, users: Set(users), accessMode: Set(accessMode), accessRoles: Set(accessRoles), - enableReceipts: values.enableReceipts + enableReceipts: values.enableReceipts, + isMLSEnabled: true ) // Switching back to UI context @@ -258,7 +261,7 @@ extension WireConversationChannelCreationFormViewController: AddParticipantsConv didCreateConversation: syncedConversation ) - } catch let error as CreateChannelUseCase.Failure { + } catch let error as CreateGroupConversationUseCase.Failure { switch error { case .missingLegalholdConsent: