Skip to content
Snippets Groups Projects
Bindings.swift 19.7 KiB
Newer Older
Bruno Muniz's avatar
Bruno Muniz committed
import Shared
import Models
import Bindings
import XXModels
Bruno Muniz's avatar
Bruno Muniz committed
import Foundation
import DependencyInjection
Bruno Muniz's avatar
Bruno Muniz committed

public let evaluateNotification: NotificationEvaluation = BindingsNotificationsForMe

public protocol NotificationReportProtocol {
    func forMe() -> Bool
    func type() -> String
Ahmed Shehata's avatar
Ahmed Shehata committed
    func source() -> Data?
Bruno Muniz's avatar
Bruno Muniz committed
}

public protocol NotificationManyReportProtocol {
    func len() -> Int
    func get(index: Int) throws -> NotificationReportProtocol
}

extension BindingsNotificationForMeReport: NotificationReportProtocol {}

extension BindingsManyNotificationForMeReport: NotificationManyReportProtocol {
    public func get(index: Int) throws -> NotificationReportProtocol {
        try get(index)
    }
}

extension BindingsClient: BindingsInterface {
    public func removeContact(_ data: Data) throws {
        do {
            try deleteContact(data)
            log(string: "Deleted a contact", type: .info)
        } catch {
            log(string: "Failed to delete a contact: \(error.localizedDescription)", type: .error)
            throw error.friendly()
        }
    }

    func dumpThreads() {
        log(type: .crumbs)

        var error: NSError?
        let string = BindingsDumpStack(&error)

        if let error = error {
            log(string: error.localizedDescription, type: .error)
            return
        }

        log(string: string, type: .bindings)
    }

    public func resetSessionWith(_ recipient: Data) {
        var int: Int = 0

        do {
            try resetSession(recipient, meMarshaled: meMarshalled, message: "", ret0_: &int)
        } catch {
            print(">>> \(error.localizedDescription)")
        }
    }

Bruno Muniz's avatar
Bruno Muniz committed
    public func verify(marshaled: Data, verifiedMarshaled: Data) throws -> Bool {
        var bool: ObjCBool = false
        try verifyOwnership(marshaled, verifiedMarshaled: verifiedMarshaled, ret0_: &bool)
        log(string: "Onwership verification: \(bool.boolValue)", type: bool.boolValue ? .info : .error)
        return bool.boolValue
    }

    public func compress(
        image: Data,
        _ completion: @escaping(Result<Data, Error>) -> Void
    ) {
        var error: NSError?
        let compressed = BindingsCompressJpeg(image, &error)

        guard error == nil else {
            log(string: "Error when compressing jpeg: \(error!.localizedDescription)", type: .error)
            completion(.failure(error!.friendly()))
            return
        }

        guard let compressed = compressed else {
            completion(.failure(NSError.create("Image compression failed without error")))
            return
        }

        let compressionRate = String(format: "%.4f", Float(compressed.count)/Float(image.count))
        log(string: "Compressed image x\(compressionRate) (\(image.count) -> \(compressed.count))", type: .info)
        completion(.success(compressed))
    }

    public var hasRunningTasks: Bool {
        hasRunningProcessies()
    }

    public var myId: Data {
        guard let user = getUser(), let contact = user.getContact(), let id = contact.getID() else {
            fatalError("Couldn't get my ID")
        }

        return id
    }

    public var meMarshalled: Data {
        guard let user = getUser(), let contact = user.getContact(), let marshal = try? contact.marshal() else {
            fatalError("Couldn't get my own contact marshalled")
        }

        return marshal
    }

    public func getPreImages() -> String {
        getPreimages(receptionId)
    }

    public func meMarshalled(_ username: String, email: String?, phone: String?) -> Data {
        guard let user = getUser(),
              let contact = user.getContact(),
              let factList = contact.getFactList() else { fatalError() }
Bruno Muniz's avatar
Bruno Muniz committed

        try! factList.add(username, factType: FactType.username.rawValue)
Bruno Muniz's avatar
Bruno Muniz committed

        if let email = email {
            try! factList.add(email, factType: FactType.email.rawValue)
Bruno Muniz's avatar
Bruno Muniz committed
        }

        if let phone = phone {
            try! factList.add(phone, factType: FactType.phone.rawValue)
        return try! contact.marshal()
Bruno Muniz's avatar
Bruno Muniz committed
    }

    public var receptionId: Data {
        guard let user = getUser(), let recId = user.getReceptionID() else { fatalError() }
Bruno Muniz's avatar
Bruno Muniz committed
        return recId
    }

    public static let version: String = {
        return BindingsGetVersion()
    }()

    public static let new: ClientNew = BindingsNewClient

    public static let fromBackup: ClientFromBackup = BindingsNewClientFromBackup

Bruno Muniz's avatar
Bruno Muniz committed
    public static let secret: (Int) -> Data? = BindingsGenerateSecret

    public static let login: (String?, Data?, String?, NSErrorPointer) -> BindingsInterface? = BindingsLogin

    public static func updateNDF(
        for env: NetworkEnvironment,
        _ completion: @escaping (Result<Data?, Error>) -> Void
    ) {
        log(type: .crumbs)

        var error: NSError?
        let ndf = BindingsDownloadAndVerifySignedNdfWithUrl(env.url, env.cert, &error)

        if let error = error {
            log(string: error.localizedDescription, type: .error)
            completion(.failure(error))
        } else {
            completion(.success(ndf))
        }
    }

    /// Fetches a JSON with up-to-date error descriptions
    /// then passes it to the bindings that will emit cleaner
    /// errors
    ///
    /// - ToDo: Request status codes for errors
    ///
    public static func updateErrors() {
        log(type: .crumbs)

        var error: NSError?
        if let dbErrors = BindingsDownloadErrorDB(&error) {
            var otherError: NSError?
            BindingsUpdateCommonErrors(String(data: dbErrors, encoding: .utf8), &otherError)

            if let otherError = otherError {
                log(string: otherError.localizedDescription, type: .error)
            }
        }

        if let error = error {
            log(string: error.localizedDescription, type: .error)
        }
    }

    /// Starts the network
    ///
    /// If network status is != 0 it means the network is
    /// not ready yet or the device is not ready. A recursion was applied
    /// as a temporary solution in order to retry indefinitely
    ///
    /// - ToDo: Split function into smaller functions
    ///
    public func startNetwork() {
        log(type: .crumbs)

        var error: NSError?
        let status = networkFollowerStatus()

        BindingsLogLevel(1, &error)
        registerErrorCallback(BindingsError())

        guard status == 0 else {
            log(string: ">>> Network is not ready yet. Let's give it a second...", type: .error)
Bruno Muniz's avatar
Bruno Muniz committed
            sleep(1)
            startNetwork()
            return
        }

        try! startNetworkFollower(10000)
        log(string: ">>> Starting the network...", type: .info)
Bruno Muniz's avatar
Bruno Muniz committed
    }

    /// (Tries) to stop the network
    ///
    /// - Warning: This function tries to stop several
    ///            threads and it may take some time.
    ///            That's why we register a background
    ///            task on AppDelegate.swift
    ///
    public func stopNetwork() {
        log(type: .crumbs)

        try! stopNetworkFollower()
        log(string: "Stopping the network...", type: .info)
    }

    /// Extracts *user id* from a contact
    ///
    /// - Parameters:
    ///   - from: Byte array containing contact object
    ///
    /// - Returns: Optional byte array, if *user id* could be retrieved
    ///
    public func getId(from marshaled: Data) -> Data? {
        log(type: .crumbs)

        var error: NSError?
        let contact = BindingsUnmarshalContact(marshaled, &error)

        if let error = error {
            log(string: error.localizedDescription, type: .error)
            return nil
        }

        return contact?.getID()
    }

    public func add(_ contact: Data, from me: Data, _ completion: @escaping (Result<Bool, Error>) -> Void) {
        log(type: .crumbs)

        do {
            var roundId = Int()
            try requestAuthenticatedChannel(contact, meMarshaled: me, message: nil, ret0_: &roundId)
            completion(.success(true))
        } catch {
            log(string: error.localizedDescription, type: .error)
            completion(.failure(error.friendly()))
        }
    }

    /// Confirms a contact request
    ///
    /// - Parameters:
    ///   - contact: Byte array containing *contact object*
    ///   - completion: Result callback with associated
    ///                 values *boolean* = success &&
    ///                 !timedOut or *Error* upon throwing
    ///
    public func confirm(_ contact: Data, _ completion: @escaping (Result<Bool, Error>) -> Void) {
        log(type: .crumbs)

        do {
            var roundId = Int()
            try confirmAuthenticatedChannel(contact, ret0_: &roundId)
            completion(.success(true))
        } catch {
            log(string: error.localizedDescription, type: .error)
            completion(.failure(error.friendly()))
        }
    }

    /// Sends a message over CMIX
    ///
    /// - Parameters:
    ///   - recipient: Byte array containing *user id*
    ///   - payload: Byte array containing *message payload*
    ///
    /// - Returns: Result w/ associated values
    ///            byte array containing *SentReport*
    ///            or *Error* upon throwing
    ///
    public func send(_ payload: Data, to recipient: Data) -> Result<E2ESendReportType, Error> {
        log(type: .crumbs)

        do {
            let report = try sendE2E(recipient, payload: payload, messageType: 2, parameters: nil)

            var roundIds = [Int]()

            if let roundList = report.getRoundList(), let payloadUnwrapped = try? Payload(with: payload) {
                let length = roundList.len()
                for index in 0..<length {
                    var integer: Int = 0
                    do {
                        try roundList.get(index, ret0_: &integer)
                        roundIds.append(integer)
                    } catch {
                        log(string: "Error trying to inspect round list: \(error.localizedDescription)", type: .error)
                    }
                }

                log(string: "Round ids for \(payloadUnwrapped.text.prefix(5))... = \(roundIds)", type: .info)
            }

            return .success(report)
        } catch {
            log(string: error.localizedDescription, type: .error)
            return .failure(error)
        }
    }

    /// Listens to the delivery of a message through a report
    ///
    /// - Note: Delivery actually refers to the
    ///         gateway, not necessarily the other end
    ///         received/read this message yet.
    ///
    /// - Parameters:
    ///   - report: SentReport marshalled
    ///   - completion: Result callback w/ associated
    ///                 values *completed* or *Error*
    ///                 upon throwing
    ///
    public func listen(report: Data, _ completion: @escaping (Result<MessageDeliveryStatus, Error>) -> Void) {
Bruno Muniz's avatar
Bruno Muniz committed
        do {
            try listenDelivery(of: report) { msgId, delivered, timedOut, roundResults in
                let status: MessageDeliveryStatus

Bruno Muniz's avatar
Bruno Muniz committed
                if delivered == false {
                    let extendedLogs =
                    """
                    Round delivery callback from wait(forMessageDelivery:)
                    - timedOut = \(timedOut)
                    - delivered = \(delivered)
                    """
                    log(string: extendedLogs, type: .error)
                    log(string: extendedLogs, type: .error)

                    if timedOut == true {
                        status = .timedout
                    } else {
                        status = .failed
                    }
                } else {
                    status = .sent
                completion(.success(status))
Bruno Muniz's avatar
Bruno Muniz committed
            }
        } catch {
            completion(.failure(error))
        }
    }

Ahmed Shehata's avatar
Ahmed Shehata committed
    public func registerNotifications(_ token: Data) throws {
        let tokenString = token.map { String(format: "%02hhx", $0) }.joined()
Bruno Muniz's avatar
Bruno Muniz committed

        do {
Ahmed Shehata's avatar
Ahmed Shehata committed
            try register(forNotifications: tokenString)
Bruno Muniz's avatar
Bruno Muniz committed
        } catch {
            throw error.friendly()
        }
    }

    /// Unregisters device token on backend
    ///
    /// - Throws: If when trying to unregister
    ///           some exception come up such as
    ///           timing out or user is not registered
    ///
    public func unregisterNotifications() throws {
        log(type: .crumbs)

        do {
            try unregisterForNotifications()
            log(string: "Unregistered notifications", type: .info)
        } catch {
            log(string: error.localizedDescription, type: .error)
            throw error.friendly()
        }
    }

    /// Checks if number of nodes already registered is enough
    ///
    /// Whenever the user wants to do an operation that involves
    /// *User Discovery*, the app should make sure that a minimum
    /// amount of nodes already know about this user
    ///
    /// - Throws: `NodeRegistrationError.amountIsTooLow` if
    ///            the ratio is below minimum (currently 85%).
    ///            `NodeRegistrationError.networkIsNotHealthyYet`
    ///            when trying to fetch registration status and
    ///            network is not healthy yet
    ///
    public func nodeRegistrationStatus() throws {
        log(type: .crumbs)

        enum NodeRegistrationError: Error {
            case amountIsTooLow
        }

        var shortRatio: String?

        do {
            let status = try getNodeRegistrationStatus()
            let registered = Float(status.getRegistered())
            let total = Float(status.getTotal())
            let ratio = Float(registered/total)

            let nf = NumberFormatter()
            nf.roundingMode = .down
            nf.maximumFractionDigits = 2
            nf.numberStyle = .percent
            shortRatio = nf.string(from: NSNumber(value: ratio))

            guard ratio >= 0.85 else { throw NodeRegistrationError.amountIsTooLow }
            log(string: "Node registration rate: \(shortRatio ?? "")", type: .info)
        } catch NodeRegistrationError.amountIsTooLow {

            let string = "Node registration rate is still below 85% (\(shortRatio ?? ""))"
            log(string: string, type: .error)

            let userError = "We are still establishing a secure registration with the decentralized network. Please try again in a few seconds."

            throw NSError.create(userError)
        } catch {
            log(string: error.localizedDescription, type: .error)
            throw error
        }
    }

    /// Instantiates a transfer manager
    ///
    /// - Returns: An instance of *BindingsFileTransfer (TransferManager)*
    ///
    /// - Throws: `FTError.noInstance` if no error was thrown
    ///            but also no instance was created
    ///
    public func generateTransferManager(
        _ callback: @escaping (Data, String?, String?, Data?) -> Void
    ) throws -> TransferManagerInterface {
        log(type: .crumbs)

        let incomingTransferCallback = IncomingTransferCallback { tid, name, type, sender, size, preview in
            guard let tid = tid else { fatalError("An incoming transfer has no TID?") }

            callback(tid, name, type, sender)
        }

        var error: NSError?
        let manager = BindingsNewFileTransferManager(self, incomingTransferCallback, "", &error)

        guard let error = error else { return manager! }
        throw error.friendly()
    }

    public func generateDummyTraficManager() throws -> DummyTrafficManaging {
        var error: NSError?
        let manager = BindingsNewDummyTrafficManager(self, 5, 30000, 25000, &error)

        guard let error = error else { return manager! }
        throw error.friendly()
    }

    public func generateUDFromBackup(email: String?, phone: String?) throws -> UserDiscoveryInterface {
        var error: NSError?

        let paramEmail = email != nil ? "E\(email!)" : nil
        let paramPhone = phone != nil ? "P\(phone!)" : nil

        let udb = BindingsNewUserDiscoveryFromBackup(self, paramEmail, paramPhone, &error)

        /// Alternate udb

        guard let certPath = Bundle.module.path(forResource: "ud.elixxir.io", ofType: "crt") else {
            fatalError("Couldn't retrieve cert.")
        }

        guard let contactFilePath = Bundle.module.path(forResource: "udContact-test", ofType: "bin") else {
            fatalError("Couldn't retrieve cert.")
        }

Bruno Muniz's avatar
Bruno Muniz committed
//        try! udb!.setAlternative(
//            "18.198.117.203:11420".data(using: .utf8),
//            cert: try! Data(contentsOf: URL(fileURLWithPath: certPath)),
//            contactFile: try! Data(contentsOf: URL(fileURLWithPath: contactFilePath))
//        )

        guard let error = error else { return udb! }
        throw error.friendly()
    }

Bruno Muniz's avatar
Bruno Muniz committed
    public func generateUD() throws -> UserDiscoveryInterface {
        log(type: .crumbs)

        var error: NSError?
        let udb = BindingsNewUserDiscovery(self, &error)

        /// Alternate udb

        guard let certPath = Bundle.module.path(forResource: "ud.elixxir.io", ofType: "crt") else {
            fatalError("Couldn't retrieve cert.")
        }

        guard let contactFilePath = Bundle.module.path(forResource: "udContact-test", ofType: "bin") else {
            fatalError("Couldn't retrieve cert.")
        }

Bruno Muniz's avatar
Bruno Muniz committed
//        try! udb!.setAlternative(
//            "18.198.117.203:11420".data(using: .utf8),
//            cert: try! Data(contentsOf: URL(fileURLWithPath: certPath)),
//            contactFile: try! Data(contentsOf: URL(fileURLWithPath: contactFilePath))
//        )
Bruno Muniz's avatar
Bruno Muniz committed
        guard let error = error else { return udb! }
        throw error.friendly()
    }

    public func restore(
        ids: Data,
        using ud: UserDiscoveryInterface,
        lookupCallback: @escaping (Result<Contact, Error>) -> Void,
        restoreCallback: @escaping (Int, Int, Int, String?) -> Void
    ) -> RestoreReportType {
        let restoreCb = RestoreContactsCallback(restoreCallback)

        let lookupCb = LookupCallback {
            switch $0 {
            case .success(let contact):
                lookupCallback(.success(.init(with: contact, status: .stranger)))
            case .failure(let error):
                lookupCallback(.failure(error))
            }
        }

        return BindingsRestoreContactsFromBackup(ids, self, ud as? BindingsUserDiscovery, lookupCb, restoreCb)!
    }
Bruno Muniz's avatar
Bruno Muniz committed
}

extension BindingsContact {

    /// Scans the contact instance for a specified fact
    ///
    /// - Parameters:
    ///   - fact: enum defined in ```FactType```
    ///           that specifies the type we're
    ///           searching
    ///
    /// - Note: Since GoLang does not support collections
    ///         We need to do this workaround *length* and
    ///         *get* instead of subscripting as in Swift.
    ///
    /// - Returns: Optional string in case we find the the fact
    ///
    /// - ToDo: Return a struct that contains all possible facts (?)
    ///
    func retrieve(fact: FactType) -> String? {
        log(type: .crumbs)

        guard let factList = getFactList() else { return nil }
        for index in 0..<factList.num() {
            if let actualFact = factList.get(index) {
                if actualFact.type() == fact.rawValue {
                    return String(actualFact.stringify().dropFirst())
                }
            }
        }
        return nil
    }
}

extension BindingsSendReport: E2ESendReportType {
    public var marshalled: Data { try! marshal() }
    public var timestamp: Int64 { getTimestampNano() }
    public var uniqueId: Data? { getMessageID() }
    public var roundURL: String { getRoundURL() }
}

public protocol DummyTrafficManaging {
    var status: Bool { get }
    func setStatus(status: Bool)
}

extension BindingsDummyTraffic: DummyTrafficManaging {
    public var status: Bool {
        getStatus()
    }

    public func setStatus(status: Bool) {
        try? setStatus(status)
    }
}

extension BindingsBackup: BackupInterface {}

extension BindingsRestoreContactsReport: RestoreReportType {}