import Retry import Models import Shared import Combine import Defaults import XXModels import Foundation import BackupFeature import NetworkMonitor import DependencyInjection import os.log import ToastFeature let logHandler = OSLog(subsystem: "xx.network", category: "Performance debugging") struct BackupParameters: Codable { var email: String? var phone: String? var username: String } struct BackupReport: Codable { var contactIds: [String] var parameters: String private enum CodingKeys: String, CodingKey { case parameters = "Params" case contactIds = "RestoredContacts" } } public final class Session: SessionType { @KeyObject(.theme, defaultValue: nil) var theme: String? @KeyObject(.email, defaultValue: nil) var email: String? @KeyObject(.phone, defaultValue: nil) var phone: String? @KeyObject(.avatar, defaultValue: nil) var avatar: Data? @KeyObject(.username, defaultValue: nil) var username: String? @KeyObject(.biometrics, defaultValue: false) var biometrics: Bool @KeyObject(.hideAppList, defaultValue: false) var hideAppList: Bool @KeyObject(.requestCounter, defaultValue: 0) var requestCounter: Int @KeyObject(.sharingEmail, defaultValue: false) var isSharingEmail: Bool @KeyObject(.sharingPhone, defaultValue: false) var isSharingPhone: Bool @KeyObject(.recordingLogs, defaultValue: true) var recordingLogs: Bool @KeyObject(.crashReporting, defaultValue: true) var crashReporting: Bool @KeyObject(.icognitoKeyboard, defaultValue: false) var icognitoKeyboard: Bool @KeyObject(.pushNotifications, defaultValue: false) var pushNotifications: Bool @KeyObject(.inappnotifications, defaultValue: true) var inappnotifications: Bool @Dependency var backupService: BackupService @Dependency var toastController: ToastController @Dependency var networkMonitor: NetworkMonitoring public let client: Client public let dbManager: Database private var cancellables = Set<AnyCancellable>() public var myId: Data { client.bindings.myId } public var version: String { type(of: client.bindings).version } public var myQR: Data { client .bindings .meMarshalled( username!, email: isSharingEmail ? email : nil, phone: isSharingPhone ? phone : nil ) } public var hasRunningTasks: Bool { client.bindings.hasRunningTasks } public var isOnline: AnyPublisher<Bool, Never> { networkMonitor.statusPublisher.map { $0 == .available }.eraseToAnyPublisher() } public init( passphrase: String, backupFile: Data, ndf: String ) throws { let network = try! DependencyInjection.Container.shared.resolve() as XXNetworking os_signpost(.begin, log: logHandler, name: "Decrypting", "Calling newClientFromBackup") let (client, backupData) = try network.newClientFromBackup(passphrase: passphrase, data: backupFile, ndf: ndf) os_signpost(.end, log: logHandler, name: "Decrypting", "Finished newClientFromBackup") self.client = client dbManager = GRDBDatabaseManager() let report = try! JSONDecoder().decode(BackupReport.self, from: backupData!) if !report.parameters.isEmpty { let params = try! JSONDecoder().decode(BackupParameters.self, from: Data(report.parameters.utf8)) username = params.username phone = params.phone email = params.email } try continueInitialization() if !report.contactIds.isEmpty { client.restoreContacts(fromBackup: try! JSONSerialization.data(withJSONObject: report.contactIds)) } } public init(ndf: String) throws { let network = try! DependencyInjection.Container.shared.resolve() as XXNetworking self.client = try network.newClient(ndf: ndf) dbManager = GRDBDatabaseManager() try continueInitialization() } private func continueInitialization() throws { try dbManager.setup() setupBindings() networkMonitor.start() networkMonitor.statusPublisher .filter { $0 == .available }.first() .sink { [unowned self] _ in client.bindings.replayRequests() } .store(in: &cancellables) registerUnfinishedTransfers() if let pendingVerificationUsers: [Contact] = try? dbManager.fetch(.verificationInProgress) { pendingVerificationUsers.forEach { var contact = $0 contact.status = .verificationFailed _ = try? dbManager.save(contact) } } } public func setDummyTraffic(status: Bool) { client.dummyManager?.setStatus(status: status) } public func deleteMyself() throws { guard let username = username, let ud = client.userDiscovery else { return } try? unregisterNotifications() try ud.deleteMyself(username) stop() cleanUp() } private func cleanUp() { retry(max: 10, retryStrategy: .delay(seconds: 1)) { [unowned self] in guard self.hasRunningTasks == false else { throw NSError.create("") } }.finalCatch { _ in fatalError("Couldn't delete account because network is not stopping") } dbManager.drop() FileManager.xxCleanup() email = nil phone = nil theme = nil avatar = nil self.username = nil isSharingEmail = false isSharingPhone = false requestCounter = 0 biometrics = false hideAppList = false recordingLogs = true crashReporting = true icognitoKeyboard = false pushNotifications = false inappnotifications = true } public func hideRequestOf(group: Group) { var group = group group.status = .hidden _ = try? dbManager.save(group) } public func hideRequestOf(contact: Contact) { var contact = contact contact.status = .hidden _ = try? dbManager.save(contact) } public func forceFailMessages() { if let pendingE2E: [Message] = try? dbManager.fetch(.sending) { pendingE2E.forEach { var message = $0 message.status = .failedToSend _ = try? dbManager.save(message) } } if let pendingGroupMessages: [GroupMessage] = try? dbManager.fetch(.sending) { pendingGroupMessages.forEach { var message = $0 message.status = .failed _ = try? dbManager.save(message) } } } private func registerUnfinishedTransfers() { guard let unfinisheds: [Message] = try? dbManager.fetch(.sendingAttachment), !unfinisheds.isEmpty else { return } for var message in unfinisheds { guard let tid = message.payload.attachment?.transferId else { return } do { try client.transferManager?.listenUploadFromTransfer(with: tid) { completed, sent, arrived, total, error in if completed { message.status = .sent message.payload.attachment?.progress = 1.0 if let transfer: FileTransfer = try? self.dbManager.fetch(.withTID(tid)).first { try? self.dbManager.delete(transfer) } } else { if let error = error { log(string: error.localizedDescription, type: .error) message.status = .failedToSend } else { let progress = Float(arrived)/Float(total) message.payload.attachment?.progress = progress return } } _ = try? self.dbManager.save(message) } } catch { message.status = .sent _ = try? self.dbManager.save(message) } } } func updateFactsOnBackup() { struct BackupParameters: Codable { var email: String? var phone: String? var username: String var jsonFormat: String { let data = try! JSONEncoder().encode(self) let json = String(data: data, encoding: .utf8) return json! } } let params = BackupParameters( email: email, phone: phone, username: username! ).jsonFormat client.addJson(params) backupService.performBackupIfAutomaticIsEnabled() } private func setupBindings() { client.requests .sink { [unowned self] request in if let _: Contact = try? dbManager.fetch(.withUserId(request.userId)).first { return } if self.inappnotifications { DeviceFeedback.sound(.contactAdded) DeviceFeedback.shake(.notification) } verify(contact: request) }.store(in: &cancellables) client.requestsSent .sink { [unowned self] in _ = try? dbManager.save($0) } .store(in: &cancellables) client.backup .throttle(for: .seconds(5), scheduler: DispatchQueue.main, latest: true) .sink { [unowned self] in backupService.updateBackup(data: $0) } .store(in: &cancellables) client.resets .sink { [unowned self] in /// This will get called when my contact restore its contact. /// TODO: Hold a record on the chat that this contact restored. /// var contact = $0 contact.status = .friend _ = try? dbManager.save(contact) }.store(in: &cancellables) backupService.settingsPublisher .map { $0.enabledService != nil } .removeDuplicates() .sink { [unowned self] in if $0 == true { guard let passphrase = backupService.passphrase else { client.resumeBackup() updateFactsOnBackup() return } client.initializeBackup(passphrase: passphrase) backupService.passphrase = nil updateFactsOnBackup() } else { backupService.passphrase = nil client.stopListeningBackup() } } .store(in: &cancellables) networkMonitor.statusPublisher .sink { print($0) } .store(in: &cancellables) client.groupMessages .sink { [unowned self] in _ = try? dbManager.save($0) } .store(in: &cancellables) client.messages .sink { [unowned self] in if var contact: Contact = try? dbManager.fetch(.withUserId($0.sender)).first { contact.isRecent = false _ = try? dbManager.save(contact) } _ = try? dbManager.save($0) }.store(in: &cancellables) client.network .sink { [unowned self] in networkMonitor.update($0) } .store(in: &cancellables) client.incomingTransfers .sink { [unowned self] in handle(incomingTransfer: $0) } .store(in: &cancellables) client.groupRequests .sink { [unowned self] request in if let _: Group = try? dbManager.fetch(.withGroupId(request.0.groupId)).first { return } DispatchQueue.global().async { [weak self] in self?.processGroupCreation(request.0, memberIds: request.1, welcome: request.2) } }.store(in: &cancellables) client.confirmations .sink { [unowned self] in if var contact: Contact = try? dbManager.fetch(.withUserId($0.userId)).first { contact.status = .friend contact.isRecent = true contact.createdAt = Date() _ = try? dbManager.save(contact) toastController.enqueueToast(model: .init( title: contact.nickname ?? contact.username, subtitle: Localized.Requests.Confirmations.toaster, leftImage: Asset.sharedSuccess.image )) } }.store(in: &cancellables) } public func getTextFromMessage(messageId: Data) -> String? { guard let message: Message = try? dbManager.fetch(.withUniqueId(messageId)).first else { return nil } return message.payload.text } public func getTextFromGroupMessage(messageId: Data) -> String? { guard let message: GroupMessage = try? dbManager.fetch(.withUniqueId(messageId)).first else { return nil } return message.payload.text } public func getContactWith(userId: Data) -> Contact? { let contact: Contact? = try? dbManager.fetch(.withUserId(userId)).first return contact } public func getGroupChatInfoWith(groupId: Data) -> GroupChatInfo? { let info: GroupChatInfo? = try? dbManager.fetch(.fromGroup(groupId)).first return info } }