import HUD import Shared import Models import Combine import Defaults import XXModels import XXLogger import Keychain import Foundation import Permissions import ToastFeature import DropboxFeature import VersionChecking import ReportingFeature import CombineSchedulers import DependencyInjection import XXClient import struct XXClient.FileTransfer import class XXClient.Cancellable import XXDatabase import XXLegacyDatabaseMigrator import XXMessengerClient import NetworkMonitor struct Update { let content: String let urlString: String let positiveActionTitle: String let negativeActionTitle: String? let actionStyle: CapsuleButtonStyle } enum LaunchRoute { case chats case update(Update) case onboarding } final class LaunchViewModel { @Dependency var database: Database @Dependency var versionChecker: VersionChecker @Dependency var dropboxService: DropboxInterface @Dependency var fetchBannedList: FetchBannedList @Dependency var reportingStatus: ReportingStatus @Dependency var toastController: ToastController @Dependency var keychainHandler: KeychainHandling @Dependency var networkMonitor: NetworkMonitoring @Dependency var processBannedList: ProcessBannedList @Dependency var permissionHandler: PermissionHandling @KeyObject(.username, defaultValue: nil) var username: String? @KeyObject(.biometrics, defaultValue: false) var isBiometricsOn: Bool @KeyObject(.dummyTrafficOn, defaultValue: false) var dummyTrafficOn: Bool var hudPublisher: AnyPublisher<HUDStatus, Never> { hudSubject.eraseToAnyPublisher() } var authCallbacksCancellable: Cancellable? var routePublisher: AnyPublisher<LaunchRoute, Never> { routeSubject.eraseToAnyPublisher() } var mainScheduler: AnySchedulerOf<DispatchQueue> = { DispatchQueue.main.eraseToAnyScheduler() }() var backgroundScheduler: AnySchedulerOf<DispatchQueue> = { DispatchQueue.global().eraseToAnyScheduler() }() private var cancellables = Set<AnyCancellable>() private let routeSubject = PassthroughSubject<LaunchRoute, Never>() private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none) func viewDidAppear() { mainScheduler.schedule(after: .init(.now() + 1)) { [weak self] in guard let self = self else { return } self.hudSubject.send(.on) self.versionChecker().sink { [unowned self] in switch $0 { case .upToDate: self.updateBannedList { self.updateErrors { self.continueWithInitialization() } } case .failure(let error): self.versionFailed(error: error) case .updateRequired(let info): self.versionUpdateRequired(info) case .updateRecommended(let info): self.versionUpdateRecommended(info) } }.store(in: &self.cancellables) } } func continueWithInitialization() { do { try self.setupDatabase() _ = try SetLogLevel.live(.trace) RegisterLogWriter.live(.init(handle: { XXLogger.live().debug($0) })) guard let certPath = Bundle.module.path(forResource: "cmix.rip", ofType: "crt"), let contactFilePath = Bundle.module.path(forResource: "udContact", ofType: "bin") else { fatalError("Couldn't retrieve alternative UD credentials") } let address = "46.101.98.49:18001" let cert = try Data(contentsOf: URL(fileURLWithPath: certPath)) let contactFile = try Data(contentsOf: URL(fileURLWithPath: contactFilePath)) var environment: MessengerEnvironment = .live() environment.udCert = cert environment.udAddress = address environment.udContact = contactFile environment.ndfEnvironment = .mainnet let messenger = Messenger.live(environment) DependencyInjection.Container.shared.register(messenger) if messenger.isLoaded() == false { if messenger.isCreated() == false { try messenger.create() } try messenger.load() } try messenger.start() authCallbacksCancellable = messenger.registerAuthCallbacks( AuthCallbacks(handle: { switch $0 { case .confirm(contact: let contact, receptionId: _, ephemeralId: _, roundId: _): self.handleConfirm(from: contact) case .request(contact: let contact, receptionId: _, ephemeralId: _, roundId: _): self.handleDirectRequest(from: contact) case .reset(contact: let contact, receptionId: _, ephemeralId: _, roundId: _): self.handleReset(from: contact) } }) ) if messenger.isConnected() == false { try messenger.connect() } try messenger.e2e.get()?.registerListener( senderId: nil, messageType: 2, callback: .init(handle: { // let roundId = $0.roundId guard let payload = try? Payload(with: $0.payload) else { fatalError("Couldn't decode payload: \(String(data: $0.payload, encoding: .utf8) ?? "nil")") } try! self.database.saveMessage(.init( networkId: $0.id, senderId: $0.sender, recipientId: messenger.e2e.get()!.getContact().getId(), groupId: nil, date: Date.fromTimestamp($0.timestamp), status: .received, isUnread: true, text: payload.text, replyMessageId: payload.reply?.messageId, roundURL: "https://www.google.com.br", fileTransferId: nil )) if var contact = try? self.database.fetchContacts(.init(id: [$0.sender])).first { contact.isRecent = false try! self.database.saveContact(contact) } }) ) try generateGroupManager(messenger: messenger) try generateTrafficManager(messenger: messenger) try generateTransferManager(messenger: messenger) networkMonitor.start() if messenger.isLoggedIn() == false { if try messenger.isRegistered() == false { hudSubject.send(.none) routeSubject.send(.onboarding) } else { hudSubject.send(.none) checkBiometrics { [weak self] bioResult in switch bioResult { case .success(let granted): if granted { try! messenger.logIn() self?.routeSubject.send(.chats) } else { // WHAT SHOULD HAPPEN HERE? } case .failure(let error): print(">>> Bio auth failed: \(error.localizedDescription)") } } } } else { hudSubject.send(.none) checkBiometrics { [weak self] bioResult in switch bioResult { case .success(let granted): if granted { self?.routeSubject.send(.chats) } else { // WHAT SHOULD HAPPEN HERE? } case .failure(let error): print(">>> Bio auth failed: \(error.localizedDescription)") } } } } catch { let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) hudSubject.send(.error(.init(content: xxError))) } } private func cleanUp() { // try? cMixManager.remove() // try? keychainHandler.clear() // // dropboxService.unlink() } private func presentOnboardingFlow() { hudSubject.send(.none) routeSubject.send(.onboarding) } private func setupDatabase() throws { let legacyOldPath = NSSearchPathForDirectoriesInDomains( .documentDirectory, .userDomainMask, true )[0].appending("/xxmessenger.sqlite") let legacyPath = FileManager.default .containerURL(forSecurityApplicationGroupIdentifier: "group.elixxir.messenger")! .appendingPathComponent("database") .appendingPathExtension("sqlite").path let dbExistsInLegacyOldPath = FileManager.default.fileExists(atPath: legacyOldPath) let dbExistsInLegacyPath = FileManager.default.fileExists(atPath: legacyPath) if dbExistsInLegacyOldPath && !dbExistsInLegacyPath { try? FileManager.default.moveItem(atPath: legacyOldPath, toPath: legacyPath) } let dbPath = FileManager.default .containerURL(forSecurityApplicationGroupIdentifier: "group.elixxir.messenger")! .appendingPathComponent("xxm_database") .appendingPathExtension("sqlite").path let database = try Database.onDisk(path: dbPath) if dbExistsInLegacyPath { try Migrator.live()( try .init(path: legacyPath), to: database, myContactId: Data(), //client.bindings.myId, meMarshaled: Data() //client.bindings.meMarshalled ) try FileManager.default.moveItem(atPath: legacyPath, toPath: legacyPath.appending("-backup")) } DependencyInjection.Container.shared.register(database) } func getContactWith(userId: Data) -> XXModels.Contact? { let query = Contact.Query( id: [userId], isBlocked: reportingStatus.isEnabled() ? false : nil, isBanned: reportingStatus.isEnabled() ? false : nil ) guard let database: Database = try? DependencyInjection.Container.shared.resolve(), let contact = try? database.fetchContacts(query).first else { return nil } return contact } func getGroupInfoWith(groupId: Data) -> GroupInfo? { let query = GroupInfo.Query(groupId: groupId) guard let database: Database = try? DependencyInjection.Container.shared.resolve(), let info = try? database.fetchGroupInfos(query).first else { return nil } return info } private func versionFailed(error: Error) { let title = Localized.Launch.Version.failed let content = error.localizedDescription let hudError = HUDError(content: content, title: title, dismissable: false) hudSubject.send(.error(hudError)) } private func versionUpdateRequired(_ info: DappVersionInformation) { hudSubject.send(.none) let model = Update( content: info.minimumMessage, urlString: info.appUrl, positiveActionTitle: Localized.Launch.Version.Required.positive, negativeActionTitle: nil, actionStyle: .brandColored ) routeSubject.send(.update(model)) } private func versionUpdateRecommended(_ info: DappVersionInformation) { hudSubject.send(.none) let model = Update( content: Localized.Launch.Version.Recommended.title, urlString: info.appUrl, positiveActionTitle: Localized.Launch.Version.Recommended.positive, negativeActionTitle: Localized.Launch.Version.Recommended.negative, actionStyle: .simplestColoredRed ) routeSubject.send(.update(model)) } private func checkBiometrics(completion: @escaping (Result<Bool, Error>) -> Void) { if permissionHandler.isBiometricsAvailable && isBiometricsOn { permissionHandler.requestBiometrics { switch $0 { case .success(let granted): completion(.success(granted)) case .failure(let error): completion(.failure(error)) } } } else { completion(.success(true)) } } private func updateErrors(completion: @escaping () -> Void) { let errorsURLString = "https://git.xx.network/elixxir/client-error-database/-/raw/main/clientErrors.json" URLSession.shared.dataTask(with: URL(string: errorsURLString)!) { [weak self] data, _, error in guard let self = self else { return } guard error == nil else { print(">>> Issue when trying to download errors json: \(error!.localizedDescription)") self.updateErrors(completion: completion) return } guard let data = data, let json = String(data: data, encoding: .utf8) else { print(">>> Issue when trying to unwrap errors json") return } do { try UpdateCommonErrors.live(jsonFile: json) completion() } catch { print(">>> Issue when trying to update common errors: \(error.localizedDescription)") } }.resume() } private func updateBannedList(completion: @escaping () -> Void) { fetchBannedList { result in switch result { case .failure(_): DispatchQueue.main.asyncAfter(deadline: .now() + 2) { self.updateBannedList(completion: completion) } case .success(let data): self.processBannedList(data, completion: completion) } } } private func processBannedList(_ data: Data, completion: @escaping () -> Void) { processBannedList( data: data, forEach: { result in switch result { case .success(let userId): let query = Contact.Query(id: [userId]) if var contact = try! database.fetchContacts(query).first { if contact.isBanned == false { contact.isBanned = true try! database.saveContact(contact) self.enqueueBanWarning(contact: contact) } } else { try! database.saveContact(.init(id: userId, isBanned: true)) } case .failure(_): break } }, completion: { result in switch result { case .failure(_): DispatchQueue.main.asyncAfter(deadline: .now() + 2) { self.updateBannedList(completion: completion) } case .success(_): completion() } } ) } private func enqueueBanWarning(contact: XXModels.Contact) { let name = (contact.nickname ?? contact.username) ?? "One of your contacts" toastController.enqueueToast(model: .init( title: "\(name) has been banned for offensive content.", leftImage: Asset.requestSentToaster.image )) } } extension LaunchViewModel { private func generateGroupManager(messenger: Messenger) throws { let manager = try NewGroupChat.live( e2eId: messenger.e2e()!.getId(), groupRequest: .init(handle: { [weak self] group in guard let self = self else { return } self.handleGroupRequest(from: group) }), groupChatProcessor: .init(handle: { print($0) }) // What is this? ) DependencyInjection.Container.shared.register(manager) } private func generateTransferManager(messenger: Messenger) throws { let manager = try InitFileTransfer.live( e2eId: messenger.e2e()!.getId(), callback: .init(handle: { switch $0 { case .success(let receivedFile): print(receivedFile.name) case .failure(let error): print(error.localizedDescription) } }) ) DependencyInjection.Container.shared.register(manager) } private func generateTrafficManager(messenger: Messenger) throws { let manager = try NewDummyTrafficManager.live( cMixId: messenger.e2e()!.getId(), maxNumMessages: 1, avgSendDeltaMS: 1, randomRangeMS: 1 ) DependencyInjection.Container.shared.register(manager) try! manager.setStatus(dummyTrafficOn) } } extension LaunchViewModel { private func handleDirectRequest(from contact: XXClient.Contact) { guard let id = try? contact.getId() else { fatalError("Couldn't extract ID from contact request arrived.") } if let _ = try? database.fetchContacts(.init(id: [id])).first { print(">>> Tried to handle request from pre-existing contact.") return } let facts = try? contact.getFacts() let email = facts?.first(where: { $0.type == FactType.email.rawValue })?.fact let phone = facts?.first(where: { $0.type == FactType.phone.rawValue })?.fact let username = facts?.first(where: { $0.type == FactType.username.rawValue })?.fact var model = try! database.saveContact(.init( id: id, marshaled: contact.data, username: username, email: email, phone: phone, nickname: nil, photo: nil, authStatus: .verificationInProgress, isRecent: true, createdAt: Date() )) do { if email == nil, phone == nil { try performLookup(on: contact) { [weak self] in guard let self = self else { return } switch $0 { case .success(let lookedUpContact): if try! self.verifyOwnership(contact, lookedUpContact) { // How could this ever throw? model.authStatus = .verified try! self.database.saveContact(model) } else { try! self.database.deleteContact(model) } case .failure(let error): model.authStatus = .verificationFailed print(">>> Error \(#file):\(#line): \(error.localizedDescription)") try! self.database.saveContact(model) } } } else { try performSearch(on: contact) { [weak self] in guard let self = self else { return } switch $0 { case .success(let searchedContact): if try! self.verifyOwnership(contact, searchedContact) { // How could this ever throw? model.authStatus = .verified try! self.database.saveContact(model) } else { try! self.database.deleteContact(model) } case .failure(let error): model.authStatus = .verificationFailed print(">>> Error \(#file):\(#line): \(error.localizedDescription)") try! self.database.saveContact(model) } } } } catch { print(">>> Error \(#file):\(#line): \(error.localizedDescription)") } } private func handleConfirm(from contact: XXClient.Contact) { guard let id = try? contact.getId() else { fatalError("Couldn't extract ID from contact confirmation arrived.") } guard var existentContact = try? database.fetchContacts(.init(id: [id])).first else { print(">>> Tried to handle a confirmation from someone that is not a contact yet") return } existentContact.authStatus = .friend try! database.saveContact(existentContact) } private func handleReset(from contact: XXClient.Contact) { // TODO } private func handleGroupRequest(from group: XXClient.Group) { if let _ = try? database.fetchGroups(.init(id: [group.getId()])).first { print(">>> Tried to handle a group request that is already handled") return } guard let members = try? group.getMembership(), let leader = members.first else { fatalError("Failed to get group membership/leader") } try! database.saveGroup(.init( id: group.getId(), name: String(data: group.getName(), encoding: .utf8)!, leaderId: leader.id, createdAt: Date.fromTimestamp(Int(group.getCreatedMS())), authStatus: .pending, serialized: group.serialize() )) if let initialMessage = String(data: group.getInitMessage(), encoding: .utf8) { try! database.saveMessage(.init( senderId: leader.id, recipientId: nil, groupId: group.getId(), date: Date.fromTimestamp(Int(group.getCreatedMS())), status: .received, isUnread: true, text: initialMessage )) } // TODO: // All other members should be added to the database as GroupMembers } private func performLookup( on contact: XXClient.Contact, completion: @escaping (Result<XXClient.Contact, Error>) -> Void ) throws { guard let messenger = try? DependencyInjection.Container.shared.resolve() as Messenger else { fatalError(">>> Tried to lookup, but there's no messenger instance on DI container") } print(">>> Performing Lookup") let _ = try LookupUD.live( e2eId: messenger.e2e.get()!.getId(), udContact: try messenger.ud.get()!.getContact(), lookupId: contact.getId(), callback: .init(handle: { switch $0 { case .success(let otherContact): print(">>> Lookup succeeded") completion(.success(otherContact)) case .failure(let error): print(">>> Lookup failed: \(error.localizedDescription)") completion(.failure(error)) } }) ) } private func performSearch( on contact: XXClient.Contact, completion: @escaping (Result<XXClient.Contact, Error>) -> Void ) throws { guard let messenger = try? DependencyInjection.Container.shared.resolve() as Messenger else { fatalError(">>> Tried to search, but there's no messenger instance on DI container") } print(">>> Performing Search") let _ = try SearchUD.live( e2eId: messenger.e2e.get()!.getId(), udContact: try messenger.ud.get()!.getContact(), facts: contact.getFacts(), callback: .init(handle: { switch $0 { case .success(let otherContact): print(">>> Search succeeded") completion(.success(otherContact.first!)) case .failure(let error): print(">>> Search failed: \(error.localizedDescription)") completion(.failure(error)) } }) ) } private func verifyOwnership( _ lhs: XXClient.Contact, _ rhs: XXClient.Contact ) throws -> Bool { guard let messenger = try? DependencyInjection.Container.shared.resolve() as Messenger else { fatalError(">>> Tried to verify ownership, but there's no messenger instance on DI container") } let e2e = messenger.e2e.get()! return try e2e.verifyOwnership(received: lhs, verified: rhs, e2eId: e2e.getId()) } }