Skip to content
Snippets Groups Projects
Session.swift 12.8 KiB
Newer Older
Bruno Muniz's avatar
Bruno Muniz committed
import Retry
Bruno Muniz's avatar
Bruno Muniz committed
import os.log
Bruno Muniz's avatar
Bruno Muniz committed
import Models
import Shared
import Combine
import Defaults
Bruno Muniz's avatar
Bruno Muniz committed
import XXModels
Bruno Muniz's avatar
Bruno Muniz committed
import Foundation
Bruno Muniz's avatar
Bruno Muniz committed
import ToastFeature
import BackupFeature
Bruno Muniz's avatar
Bruno Muniz committed
import NetworkMonitor
import DependencyInjection
Bruno Muniz's avatar
Bruno Muniz committed
import XXLegacyDatabaseMigrator
Ahmed Shehata's avatar
Ahmed Shehata committed

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"
    }
}

Bruno Muniz's avatar
Bruno Muniz committed
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
Bruno Muniz's avatar
Bruno Muniz committed
    @Dependency var networkMonitor: NetworkMonitoring

    public let client: Client
Bruno Muniz's avatar
Bruno Muniz committed
    public let dbManager: Database
Bruno Muniz's avatar
Bruno Muniz committed
    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()
    }

Ahmed Shehata's avatar
Ahmed Shehata committed
    public init(
        passphrase: String,
        backupFile: Data,
        ndf: String
    ) throws {
        let network = try! DependencyInjection.Container.shared.resolve() as XXNetworking
Ahmed Shehata's avatar
Ahmed Shehata committed

        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
Bruno Muniz's avatar
Bruno Muniz committed

        let oldPath = NSSearchPathForDirectoriesInDomains(
            .documentDirectory, .userDomainMask, true
        )[0].appending("/xxmessenger.sqlite")

        let newPath = FileManager.default
            .containerURL(forSecurityApplicationGroupIdentifier: "group.elixxir.messenger")!
            .appendingPathComponent("database")
            .appendingPathExtension("sqlite").path

        try Migrator.live()(
            try .init(path: oldPath),
            to: try .onDisk(path: newPath),
            myContactId: client.bindings.myId,
            meMarshaled: client.bindings.meMarshalled
        )

        dbManager = try Database.onDisk(path: newPath)

        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))
        }
    }

Bruno Muniz's avatar
Bruno Muniz committed
    public init(ndf: String) throws {
        let network = try! DependencyInjection.Container.shared.resolve() as XXNetworking
        self.client = try network.newClient(ndf: ndf)
Bruno Muniz's avatar
Bruno Muniz committed

        let oldPath = NSSearchPathForDirectoriesInDomains(
            .documentDirectory, .userDomainMask, true
        )[0].appending("/xxmessenger.sqlite")

        let newPath = FileManager.default
            .containerURL(forSecurityApplicationGroupIdentifier: "group.elixxir.messenger")!
            .appendingPathComponent("database")
            .appendingPathExtension("sqlite").path

        try Migrator.live()(
            try .init(path: oldPath),
            to: try .onDisk(path: newPath),
            myContactId: client.bindings.myId,
            meMarshaled: client.bindings.meMarshalled
        )

        dbManager = try Database.onDisk(path: newPath)

        try continueInitialization()
    }

    private func continueInitialization() throws {
Bruno Muniz's avatar
Bruno Muniz committed
        setupBindings()
        networkMonitor.start()

        networkMonitor.statusPublisher
            .filter { $0 == .available }.first()
            .sink { [unowned self] _ in client.bindings.replayRequests() }
            .store(in: &cancellables)

        registerUnfinishedTransfers()

        let query = Contact.Query(authStatus: [.verificationInProgress])
        _ = try? dbManager.bulkUpdateContacts(query, .init(authStatus: .verificationFailed))
Bruno Muniz's avatar
Bruno Muniz committed
    }

    public func setDummyTraffic(status: Bool) {
        client.dummyManager?.setStatus(status: status)
    }

    public func deleteMyself() throws {
        guard let username = username, let ud = client.userDiscovery else { return }
Bruno Muniz's avatar
Bruno Muniz committed

        try? unregisterNotifications()
Bruno Muniz's avatar
Bruno Muniz committed
        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("") }
Bruno Muniz's avatar
Bruno Muniz committed
        }.finalCatch { _ in fatalError("Couldn't delete account because network is not stopping") }

Bruno Muniz's avatar
Bruno Muniz committed
        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
    }

    private func registerUnfinishedTransfers() {
        guard let unfinishedSendingMessages = try? dbManager.fetchMessages(.init(status: [.sending])),
              let unfinishedSendingTransfers = try? dbManager.fetchFileTransfers(.init(
                id: Set(unfinishedSendingMessages
                    .filter { $0.fileTransferId != nil }
                    .compactMap(\.fileTransferId))))
        else { return }

        // What would be a good way to do this?

        let pairs = unfinishedSendingMessages.map { message -> (Message, FileTransfer) in
            let transfer = unfinishedSendingTransfers.first { ft in
                ft.id == message.fileTransferId
            }

            return (message, transfer!)
        }

        pairs.forEach { message, transfer in
            var message = message
            var transfer = transfer

            do {
                try client.transferManager?.listenUploadFromTransfer(with: transfer.id) { completed, sent, arrived, total, error in
                    if completed {
                        transfer.progress = 1.0
                        message.status = .sent

                    } else {
                        if error != nil {
                            message.status = .sendingFailed
                        } else {
                            transfer.progress = Float(arrived)/Float(total)
                        }
                    }

                    _ = try? self.dbManager.saveFileTransfer(transfer)
                    _ = try? self.dbManager.saveMessage(message)
                }
            } catch {
                message.status = .sendingFailed
                _ = try? self.dbManager.saveMessage(message)
            }
        }
Bruno Muniz's avatar
Bruno Muniz committed

    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!
Bruno Muniz's avatar
Bruno Muniz committed
            }
        }

        let params = BackupParameters(
            email: email,
            phone: phone,
            username: username!
        ).jsonFormat

        client.addJson(params)
        backupService.performBackupIfAutomaticIsEnabled()
Bruno Muniz's avatar
Bruno Muniz committed
    }

    private func setupBindings() {
        client.requests
            .sink { [unowned self] in
                if let _ = try? dbManager.fetchContacts(.init(id: [$0.id])).first {
                    return
                }
Bruno Muniz's avatar
Bruno Muniz committed

                if self.inappnotifications {
                    DeviceFeedback.sound(.contactAdded)
                    DeviceFeedback.shake(.notification)
                }

                verify(contact: $0)
            }.store(in: &cancellables)

        client.requestsSent
            .sink { [unowned self] in _ = try? dbManager.saveContact($0) }
Bruno Muniz's avatar
Bruno Muniz committed
            .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
Bruno Muniz's avatar
Bruno Muniz committed
            .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.authStatus = .friend
                _ = try? dbManager.saveContact(contact)
Bruno Muniz's avatar
Bruno Muniz committed
            }.store(in: &cancellables)

        backupService.settingsPublisher
            .map { $0.enabledService != nil }
            .removeDuplicates()
Bruno Muniz's avatar
Bruno Muniz committed
            .sink { [unowned self] in
                if $0 == true {
Ahmed Shehata's avatar
Ahmed Shehata committed
                    guard let passphrase = backupService.passphrase else {
                        client.resumeBackup()
                        updateFactsOnBackup()
                        return
                    }

                    client.initializeBackup(passphrase: passphrase)
                    backupService.passphrase = nil
                    updateFactsOnBackup()
                } else {
Ahmed Shehata's avatar
Ahmed Shehata committed
                    backupService.passphrase = nil
                    client.stopListeningBackup()
Bruno Muniz's avatar
Bruno Muniz committed
                }
            }
            .store(in: &cancellables)

        networkMonitor.statusPublisher
            .sink { print($0) }
            .store(in: &cancellables)

        client.messages
Ahmed Shehata's avatar
Ahmed Shehata committed
            .sink { [unowned self] in
                if var contact = try? dbManager.fetchContacts(.init(id: [$0.senderId])).first {
Ahmed Shehata's avatar
Ahmed Shehata committed
                    contact.isRecent = false
                    _ = try? dbManager.saveContact(contact)
Ahmed Shehata's avatar
Ahmed Shehata committed
                }

                _ = try? dbManager.saveMessage($0)
Ahmed Shehata's avatar
Ahmed Shehata committed
            }.store(in: &cancellables)
Bruno Muniz's avatar
Bruno Muniz committed

        client.network
            .sink { [unowned self] in networkMonitor.update($0) }
            .store(in: &cancellables)

        client.groupRequests
            .sink { [unowned self] request in
                if let _ = try? dbManager.fetchGroups(.init(id: [request.0.id])).first {
                    return
                }
Bruno Muniz's avatar
Bruno Muniz committed

                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 = try? dbManager.fetchContacts(.init(id: [$0.id])).first {
                    contact.authStatus = .friend
Ahmed Shehata's avatar
Ahmed Shehata committed
                    contact.isRecent = true
                    contact.createdAt = Date()
                    _ = try? dbManager.saveContact(contact)

                    toastController.enqueueToast(model: .init(
                        title: contact.nickname ?? contact.username!,
                        subtitle: Localized.Requests.Confirmations.toaster,
                        leftImage: Asset.sharedSuccess.image
                    ))
Bruno Muniz's avatar
Bruno Muniz committed
                }
            }.store(in: &cancellables)
    }
}