mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 01:20:12 +00:00
Use the ClientFactory pattern everywhere replacing raw ClientBuilders.
# Conflicts: # ElementX/Sources/Services/UserSession/UserSessionStore.swift # NSE/Sources/NSEUserSession.swift
This commit is contained in:
@@ -1,63 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
// sourcery: AutoMockable
|
||||
protocol AuthenticationClientFactoryProtocol {
|
||||
func makeClient(homeserverAddress: String,
|
||||
sessionDirectories: SessionDirectories,
|
||||
passphrase: String,
|
||||
clientSessionDelegate: ClientSessionDelegate,
|
||||
appSettings: AppSettings,
|
||||
appHooks: AppHooks) async throws -> ClientProtocol
|
||||
|
||||
func makeInMemoryClient(homeserverAddress: String,
|
||||
clientSessionDelegate: ClientSessionDelegate,
|
||||
appSettings: AppSettings,
|
||||
appHooks: AppHooks) async throws -> ClientProtocol
|
||||
}
|
||||
|
||||
/// A wrapper around `ClientBuilder` to allow for mocked clients to be injected into authentication tests.
|
||||
struct AuthenticationClientFactory: AuthenticationClientFactoryProtocol {
|
||||
func makeClient(homeserverAddress: String,
|
||||
sessionDirectories: SessionDirectories,
|
||||
passphrase: String,
|
||||
clientSessionDelegate: ClientSessionDelegate,
|
||||
appSettings: AppSettings,
|
||||
appHooks: AppHooks) async throws -> ClientProtocol {
|
||||
try await ClientBuilder
|
||||
.baseBuilder(httpProxy: appSettings.websiteURL.globalProxy,
|
||||
slidingSync: .discover,
|
||||
sessionDelegate: clientSessionDelegate,
|
||||
appHooks: appHooks,
|
||||
enableOnlySignedDeviceIsolationMode: appSettings.enableOnlySignedDeviceIsolationMode,
|
||||
threadsEnabled: appSettings.threadsEnabled)
|
||||
.sqliteStore(config: .init(dataPath: sessionDirectories.dataPath, cachePath: sessionDirectories.cachePath)
|
||||
.passphrase(passphrase: passphrase))
|
||||
.serverNameOrHomeserverUrl(serverNameOrUrl: homeserverAddress)
|
||||
.build()
|
||||
}
|
||||
|
||||
func makeInMemoryClient(homeserverAddress: String,
|
||||
clientSessionDelegate: ClientSessionDelegate,
|
||||
appSettings: AppSettings,
|
||||
appHooks: AppHooks) async throws -> ClientProtocol {
|
||||
try await ClientBuilder
|
||||
.baseBuilder(httpProxy: appSettings.websiteURL.globalProxy,
|
||||
slidingSync: .discover,
|
||||
sessionDelegate: clientSessionDelegate,
|
||||
appHooks: appHooks,
|
||||
enableOnlySignedDeviceIsolationMode: appSettings.enableOnlySignedDeviceIsolationMode,
|
||||
threadsEnabled: appSettings.threadsEnabled)
|
||||
.inMemoryStore()
|
||||
.serverNameOrHomeserverUrl(serverNameOrUrl: homeserverAddress)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
||||
|
||||
private let userSessionStore: UserSessionStoreProtocol
|
||||
private let classicAppManager: ClassicAppManagerProtocol?
|
||||
private let clientFactory: AuthenticationClientFactoryProtocol
|
||||
private let clientFactory: ClientFactoryProtocol
|
||||
private let appSettings: AppSettings
|
||||
private let appHooks: AppHooks
|
||||
|
||||
@@ -33,7 +33,7 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
||||
init(userSessionStore: UserSessionStoreProtocol,
|
||||
encryptionKeyProvider: EncryptionKeyProviderProtocol,
|
||||
classicAppManager: ClassicAppManagerProtocol?,
|
||||
clientFactory: AuthenticationClientFactoryProtocol = AuthenticationClientFactory(),
|
||||
clientFactory: ClientFactoryProtocol = ClientFactory(),
|
||||
appSettings: AppSettings,
|
||||
appHooks: AppHooks) {
|
||||
sessionDirectories = .init()
|
||||
@@ -252,15 +252,15 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
||||
// so that caches (e.g. server versions) are always fresh for the new server.
|
||||
rotateSessionDirectory()
|
||||
|
||||
let client = try await clientFactory.makeClient(homeserverAddress: homeserverAddress,
|
||||
sessionDirectories: sessionDirectories,
|
||||
passphrase: passphrase,
|
||||
clientSessionDelegate: userSessionStore.clientSessionDelegate,
|
||||
appSettings: appSettings,
|
||||
appHooks: appHooks)
|
||||
let client = try await clientFactory.makeAuthenticationClient(homeserverAddress: homeserverAddress,
|
||||
sessionDirectories: sessionDirectories,
|
||||
passphrase: passphrase,
|
||||
clientSessionDelegate: userSessionStore.clientSessionDelegate,
|
||||
appSettings: appSettings,
|
||||
appHooks: appHooks)
|
||||
try await appHooks.remoteSettingsHook.initializeCache(using: client, applyingTo: appSettings).get()
|
||||
|
||||
return client
|
||||
return appHooks.clientFactoryHook.updateAuthenticationClient(client)
|
||||
}
|
||||
|
||||
private func rotateSessionDirectory() {
|
||||
@@ -394,7 +394,7 @@ extension AuthenticationService {
|
||||
AuthenticationService(userSessionStore: UserSessionStoreMock(.init()),
|
||||
encryptionKeyProvider: EncryptionKeyProvider(),
|
||||
classicAppManager: classicAppManager,
|
||||
clientFactory: AuthenticationClientFactoryMock(.init()),
|
||||
clientFactory: ClientFactoryMock(.init()),
|
||||
appSettings: .volatile(),
|
||||
appHooks: AppHooks())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
//
|
||||
// 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(homeserverAddress: String,
|
||||
sessionDirectories: SessionDirectories,
|
||||
passphrase: 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)
|
||||
.sqliteStore(config: .init(dataPath: sessionDirectories.dataPath, cachePath: sessionDirectories.cachePath)
|
||||
.passphrase(passphrase: passphrase))
|
||||
.serverNameOrHomeserverUrl(serverNameOrUrl: homeserverAddress)
|
||||
|
||||
return try await build(builder, for: .authentication, appHooks: appHooks)
|
||||
}
|
||||
|
||||
func makeInMemoryClient(homeserverAddress: 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: homeserverAddress)
|
||||
|
||||
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)
|
||||
.sqliteStore(config: .init(dataPath: credentials.restorationToken.sessionDirectories.dataPath,
|
||||
cachePath: credentials.restorationToken.sessionDirectories.cachePath)
|
||||
.passphrase(passphrase: credentials.restorationToken.passphrase))
|
||||
.withSearchIndexStore(path: credentials.restorationToken.sessionDirectories.dataPath,
|
||||
password: credentials.restorationToken.passphrase)
|
||||
.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,
|
||||
appSettings: CommonSettingsProtocol,
|
||||
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: appSettings.enableOnlySignedDeviceIsolationMode,
|
||||
requestTimeout: 15000,
|
||||
maxRequestRetryTime: 5000,
|
||||
threadsEnabled: appSettings.threadsEnabled)
|
||||
.systemIsMemoryConstrained()
|
||||
.sqliteStore(config: .init(dataPath: credentials.restorationToken.sessionDirectories.dataPath,
|
||||
cachePath: credentials.restorationToken.sessionDirectories.cachePath)
|
||||
.passphrase(passphrase: credentials.restorationToken.passphrase))
|
||||
.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 {
|
||||
var 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// Copyright 2026 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import MatrixRustSDK
|
||||
|
||||
// sourcery: AutoMockable
|
||||
nonisolated protocol ClientFactoryProtocol {
|
||||
// MARK: Authentication
|
||||
|
||||
#if IS_MAIN_APP
|
||||
func makeAuthenticationClient(homeserverAddress: String,
|
||||
sessionDirectories: SessionDirectories,
|
||||
passphrase: String,
|
||||
clientSessionDelegate: ClientSessionDelegate,
|
||||
appSettings: AppSettings,
|
||||
appHooks: AppHooks) async throws -> ClientProtocol
|
||||
|
||||
func makeInMemoryClient(homeserverAddress: String,
|
||||
clientSessionDelegate: ClientSessionDelegate,
|
||||
appSettings: AppSettings,
|
||||
appHooks: AppHooks) async throws -> ClientProtocol
|
||||
|
||||
// MARK: Restoration
|
||||
|
||||
func makeAppClient(credentials: KeychainCredentials,
|
||||
clientSessionDelegate: ClientSessionDelegate,
|
||||
appSettings: AppSettings,
|
||||
appHooks: AppHooks) async throws -> ClientProtocol
|
||||
#endif
|
||||
|
||||
func makeNSEClient(credentials: KeychainCredentials,
|
||||
roomID: String,
|
||||
clientSessionDelegate: ClientSessionDelegate,
|
||||
appSettings: CommonSettingsProtocol,
|
||||
appHooks: AppHooks) async throws -> ClientProtocol
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import MatrixRustSDK
|
||||
|
||||
class UserSessionStore: UserSessionStoreProtocol {
|
||||
private let keychainController: KeychainControllerProtocol
|
||||
private let clientFactory: ClientFactoryProtocol
|
||||
private let appSettings: AppSettings
|
||||
private let analyticsService: AnalyticsServiceProtocol
|
||||
private let networkMonitor: NetworkMonitorProtocol
|
||||
@@ -31,11 +32,13 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
}
|
||||
|
||||
init(keychainController: KeychainControllerProtocol,
|
||||
clientFactory: ClientFactoryProtocol = ClientFactory(),
|
||||
appSettings: AppSettings,
|
||||
analyticsService: AnalyticsServiceProtocol,
|
||||
appHooks: AppHooks,
|
||||
networkMonitor: NetworkMonitorProtocol) {
|
||||
self.keychainController = keychainController
|
||||
self.clientFactory = clientFactory
|
||||
self.appSettings = appSettings
|
||||
self.analyticsService = analyticsService
|
||||
self.appHooks = appHooks
|
||||
@@ -127,26 +130,13 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
return .failure(.failedRestoringLogin)
|
||||
}
|
||||
|
||||
let homeserverURL = credentials.restorationToken.session.homeserverUrl
|
||||
appHooks.remoteSettingsHook.loadCache(forHomeserver: homeserverURL, applyingTo: appSettings)
|
||||
|
||||
let builder = ClientBuilder
|
||||
.baseBuilder(httpProxy: URL(string: homeserverURL)?.globalProxy,
|
||||
slidingSync: .restored,
|
||||
sessionDelegate: keychainController,
|
||||
appHooks: appHooks,
|
||||
enableOnlySignedDeviceIsolationMode: appSettings.enableOnlySignedDeviceIsolationMode,
|
||||
threadsEnabled: appSettings.threadsEnabled)
|
||||
.sqliteStore(config: .init(dataPath: credentials.restorationToken.sessionDirectories.dataPath,
|
||||
cachePath: credentials.restorationToken.sessionDirectories.cachePath)
|
||||
.passphrase(passphrase: credentials.restorationToken.passphrase))
|
||||
.withSearchIndexStore(path: credentials.restorationToken.sessionDirectories.dataPath,
|
||||
password: credentials.restorationToken.passphrase)
|
||||
.homeserverUrl(url: homeserverURL)
|
||||
appHooks.remoteSettingsHook.loadCache(forHomeserver: credentials.restorationToken.session.homeserverUrl, applyingTo: appSettings)
|
||||
|
||||
do {
|
||||
let client = try await builder.build()
|
||||
try await client.restoreSession(session: credentials.restorationToken.session)
|
||||
let client = try await clientFactory.makeAppClient(credentials: credentials,
|
||||
clientSessionDelegate: keychainController,
|
||||
appSettings: appSettings,
|
||||
appHooks: appHooks)
|
||||
|
||||
MXLog.info("Set up session for user \(credentials.userID) at: \(credentials.restorationToken.sessionDirectories)")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user