Files
element-x-ios/ElementX/Sources/Services/Client/ClientFactory.swift
T
2026-10-13 19:00:43 +03:00

191 lines
10 KiB
Swift

//
// Copyright 2025 Element Creations Ltd.
// Copyright 2024-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Foundation
import MatrixRustSDK
/// A wrapper around `ClientBuilder` to allow for mocked clients to be injected into tests.
nonisolated struct ClientFactory: ClientFactoryProtocol {
// MARK: Authentication
#if IS_MAIN_APP
func makeAuthenticationClient(serverNameOrBaseURL: String,
sessionDirectories: SessionDirectories,
passphrase: Data,
clientSessionDelegate: ClientSessionDelegate,
appSettings: AppSettings,
appHooks: AppHooks) async throws -> ClientProtocol {
let builder = makeBaseBuilder(httpProxy: appSettings.websiteURL.globalProxy,
discoverSlidingSync: true,
sessionDelegate: clientSessionDelegate,
appHooks: appHooks,
enableOnlySignedDeviceIsolationMode: appSettings.enableOnlySignedDeviceIsolationMode,
threadsEnabled: appSettings.threadsEnabled)
.enableAutomaticBackPagination(enableAutomaticBackPagination: true)
.sqliteStore(config: .init(dataPath: sessionDirectories.dataPath, cachePath: sessionDirectories.cachePath)
.highEntropyPassphrase(passphrase: passphrase, base64Variant: .padded))
.withSearchIndexStore(path: sessionDirectories.dataPath,
key: passphrase.base64EncodedString())
.serverNameOrHomeserverUrl(serverNameOrUrl: serverNameOrBaseURL)
return try await build(builder, for: .authentication, appHooks: appHooks)
}
func makeInMemoryClient(serverNameOrBaseURL: String,
clientSessionDelegate: ClientSessionDelegate,
appSettings: AppSettings,
appHooks: AppHooks) async throws -> ClientProtocol {
let builder = makeBaseBuilder(httpProxy: appSettings.websiteURL.globalProxy,
discoverSlidingSync: true,
sessionDelegate: clientSessionDelegate,
appHooks: appHooks,
enableOnlySignedDeviceIsolationMode: appSettings.enableOnlySignedDeviceIsolationMode,
threadsEnabled: appSettings.threadsEnabled)
.inMemoryStore()
.serverNameOrHomeserverUrl(serverNameOrUrl: serverNameOrBaseURL)
return try await build(builder, for: .classicAppAccount, appHooks: appHooks)
}
// MARK: Restoration
func makeAppClient(credentials: KeychainCredentials,
clientSessionDelegate: ClientSessionDelegate,
appSettings: AppSettings,
appHooks: AppHooks) async throws -> ClientProtocol {
let homeserverURL = credentials.restorationToken.session.homeserverUrl
let builder = makeBaseBuilder(httpProxy: URL(string: homeserverURL)?.globalProxy,
discoverSlidingSync: false,
sessionDelegate: clientSessionDelegate,
appHooks: appHooks,
enableOnlySignedDeviceIsolationMode: appSettings.enableOnlySignedDeviceIsolationMode,
threadsEnabled: appSettings.threadsEnabled)
.enableAutomaticBackPagination(enableAutomaticBackPagination: true)
.sqliteStore(config: .init(dataPath: credentials.restorationToken.sessionDirectories.dataPath,
cachePath: credentials.restorationToken.sessionDirectories.cachePath)
.highEntropyPassphrase(passphrase: credentials.restorationToken.passphrase, base64Variant: .padded))
.withSearchIndexStore(path: credentials.restorationToken.sessionDirectories.dataPath,
key: credentials.restorationToken.passphrase.base64EncodedString())
.homeserverUrl(url: homeserverURL)
return try await build(builder, for: .restoration(credentials.restorationToken.session, .all), appHooks: appHooks)
}
#endif
func makeNSEClient(credentials: KeychainCredentials,
roomID: String,
clientSessionDelegate: ClientSessionDelegate,
userSettings: UserSettings,
appHooks: AppHooks) async throws -> ClientProtocol {
let homeserverURL = credentials.restorationToken.session.homeserverUrl
let builder = makeBaseBuilder(setupEncryption: false,
httpProxy: URL(string: homeserverURL)?.globalProxy,
discoverSlidingSync: false,
sessionDelegate: clientSessionDelegate,
appHooks: appHooks,
enableOnlySignedDeviceIsolationMode: userSettings.app.enableOnlySignedDeviceIsolationMode,
requestTimeout: 15000,
maxRequestRetryTime: 5000,
threadsEnabled: userSettings.app.threadsEnabled)
.systemIsMemoryConstrained()
.sqliteStore(config: .init(dataPath: credentials.restorationToken.sessionDirectories.dataPath,
cachePath: credentials.restorationToken.sessionDirectories.cachePath)
.highEntropyPassphrase(passphrase: credentials.restorationToken.passphrase, base64Variant: .padded))
.homeserverUrl(url: homeserverURL)
return try await build(builder, for: .restoration(credentials.restorationToken.session, .one(roomId: roomID)), appHooks: appHooks)
}
// MARK: - Helpers
/// A helper method that applies the common builder modifiers needed for the app.
private func makeBaseBuilder(setupEncryption: Bool = true,
httpProxy: String? = nil,
discoverSlidingSync: Bool,
sessionDelegate: ClientSessionDelegate,
appHooks: AppHooks,
enableOnlySignedDeviceIsolationMode: Bool,
requestTimeout: UInt64? = 30000,
maxRequestRetryTime: UInt64? = nil,
threadsEnabled: Bool) -> ClientBuilder {
var builder = ClientBuilder()
.crossProcessLockConfig(crossProcessLockConfig: .multiProcess(holderName: InfoPlistReader.main.bundleIdentifier))
.setSessionDelegate(sessionDelegate: sessionDelegate)
.userAgent(userAgent: UserAgentBuilder.makeASCIIUserAgent())
.threadsEnabled(enabled: threadsEnabled, threadSubscriptions: threadsEnabled)
.requestConfig(config: .init(retryLimit: 3, // Must be non-zero for the SDK to retry API calls when rate-limited.
timeout: requestTimeout,
maxConcurrentRequests: nil,
maxRetryTime: maxRequestRetryTime))
.dmRoomDefinition(dmRoomDefinition: .twoMembers)
if discoverSlidingSync {
builder = builder.slidingSyncVersionBuilder(versionBuilder: .discoverNative)
}
if setupEncryption {
builder = builder
.autoEnableCrossSigning(autoEnableCrossSigning: true)
.backupDownloadStrategy(backupDownloadStrategy: .afterDecryptionFailure)
.enableShareHistoryOnInvite(enableShareHistoryOnInvite: true)
.autoEnableBackups(autoEnableBackups: true)
}
// Set recipient strategy and trust requirement even if `setupEncryption` is false to ensure messages
// from insecure devices aren't displayed in push notifications.
// See https://github.com/element-hq/element-x-ios/issues/4702.
if enableOnlySignedDeviceIsolationMode {
builder = builder
.roomKeyRecipientStrategy(strategy: .identityBasedStrategy)
.decryptionSettings(decryptionSettings: .init(senderDeviceTrustRequirement: .crossSignedOrLegacy))
} else {
builder = builder
.roomKeyRecipientStrategy(strategy: .errorOnVerifiedUserProblem)
.decryptionSettings(decryptionSettings: .init(senderDeviceTrustRequirement: .untrusted))
}
if let httpProxy {
builder = builder.proxy(url: httpProxy)
}
return builder
}
private func build(_ builder: ClientBuilder,
for mode: BuildMode,
appHooks: AppHooks) async throws -> ClientProtocol {
let client: ClientProtocol = try await appHooks.clientFactoryHook.configure(builder, toRestore: mode.session).build()
switch mode {
case .authentication:
break
case .restoration(let session, let roomLoadSettings):
try await client.restoreSessionWith(session: session, roomLoadSettings: roomLoadSettings)
case .classicAppAccount:
break
}
return client
}
private enum BuildMode {
case authentication
case restoration(Session, RoomLoadSettings)
case classicAppAccount
var session: Session? {
switch self {
case .authentication, .classicAppAccount: nil
case .restoration(let session, _): session
}
}
}
}