diff --git a/App/client-ios.xcodeproj/project.pbxproj b/App/client-ios.xcodeproj/project.pbxproj index 3bafd36f61bebee028596893aab877e7775c4a7a..b1b9f433c468706ac682b6b291f72e0b61121bad 100644 --- a/App/client-ios.xcodeproj/project.pbxproj +++ b/App/client-ios.xcodeproj/project.pbxproj @@ -448,7 +448,7 @@ CODE_SIGN_ENTITLEMENTS = "client-ios/Resources/client-ios.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 232; + CURRENT_PROJECT_VERSION = 233; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = S6JDM2WW29; ENABLE_BITCODE = NO; @@ -487,7 +487,7 @@ CODE_SIGN_ENTITLEMENTS = "client-ios/Resources/client-ios.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 232; + CURRENT_PROJECT_VERSION = 233; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = S6JDM2WW29; ENABLE_BITCODE = NO; @@ -522,7 +522,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationExtension/NotificationExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 232; + CURRENT_PROJECT_VERSION = 233; DEVELOPMENT_TEAM = S6JDM2WW29; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -553,7 +553,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationExtension/NotificationExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 232; + CURRENT_PROJECT_VERSION = 233; DEVELOPMENT_TEAM = S6JDM2WW29; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/Sources/App/AppDelegate.swift b/Sources/App/AppDelegate.swift index 282e857220c9d718beb543c39ff4d988d6df94a9..f8113b1337336157dee853cb1ddd5fb6672c09a6 100644 --- a/Sources/App/AppDelegate.swift +++ b/Sources/App/AppDelegate.swift @@ -14,6 +14,7 @@ import CrashReporting import DependencyInjection import XXClient +import XXMessengerClient public class AppDelegate: UIResponder, UIApplicationDelegate { @Dependency private var pushRouter: PushRouter @@ -70,39 +71,38 @@ public class AppDelegate: UIResponder, UIApplicationDelegate { } public func applicationDidEnterBackground(_ application: UIApplication) { - if let cMix: CMix = try? DependencyInjection.Container.shared.resolve(), - let database: Database = try? DependencyInjection.Container.shared.resolve() { - let backgroundTask = application.beginBackgroundTask(withName: "xx.stop.network") {} - - // An option here would be: create async completion closure - - backgroundTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in - guard UIApplication.shared.backgroundTimeRemaining > 8 else { - if !self.calledStopNetwork { - self.calledStopNetwork = true - try! cMix.stopNetworkFollower() - } else { - if cMix.hasRunningProcesses() == false { - application.endBackgroundTask(backgroundTask) - timer.invalidate() + if let messenger = try? DependencyInjection.Container.shared.resolve() as Messenger, + let database = try? DependencyInjection.Container.shared.resolve() as Database, + let cMix = messenger.cMix.get() { + let backgroundTask = application.beginBackgroundTask(withName: "xx.stop.network") {} + + backgroundTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { timer in + guard UIApplication.shared.backgroundTimeRemaining > 8 else { + if !self.calledStopNetwork { + self.calledStopNetwork = true + try! cMix.stopNetworkFollower() + } else { + if cMix.hasRunningProcesses() == false { + application.endBackgroundTask(backgroundTask) + timer.invalidate() + } } + + return } - return - } + guard UIApplication.shared.backgroundTimeRemaining > 9 else { + if !self.forceFailedPendingMessages { + self.forceFailedPendingMessages = true - guard UIApplication.shared.backgroundTimeRemaining > 9 else { - if !self.forceFailedPendingMessages { - self.forceFailedPendingMessages = true + let query = Message.Query(status: [.sending]) + let assignment = Message.Assignments(status: .sendingFailed) + _ = try? database.bulkUpdateMessages(query, assignment) + } - let query = Message.Query(status: [.sending]) - let assignment = Message.Assignments(status: .sendingFailed) - _ = try? database.bulkUpdateMessages(query, assignment) + return } - - return - } - } + }) } } @@ -116,7 +116,8 @@ public class AppDelegate: UIResponder, UIApplicationDelegate { } public func applicationWillTerminate(_ application: UIApplication) { - if let cMix: CMix = try? DependencyInjection.Container.shared.resolve() { + if let messenger = try? DependencyInjection.Container.shared.resolve() as Messenger, + let cMix = messenger.cMix.get() { try? cMix.stopNetworkFollower() } } @@ -127,7 +128,8 @@ public class AppDelegate: UIResponder, UIApplicationDelegate { backgroundTimer = nil } - if let cMix: CMix = try? DependencyInjection.Container.shared.resolve() { + if let messenger = try? DependencyInjection.Container.shared.resolve() as Messenger, + let cMix = messenger.cMix.get() { guard self.calledStopNetwork == true else { return } try? cMix.startNetworkFollower(timeoutMS: 10_000) self.calledStopNetwork = false diff --git a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift index 2d7175c2c7f4f49f672793abf778d9fc70a5324b..9a2b59fcd87059f9e7f47d9cf39305aab608685b 100644 --- a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift +++ b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift @@ -172,20 +172,23 @@ final class SingleChatViewModel: NSObject { } func didSend(image: UIImage) { - guard let imageData = image.orientedUp().jpegData(compressionQuality: 1.0) else { return } + guard let imageData = image.orientedUp().jpegData(compressionQuality: 1.0) else { + return + } + hudRelay.send(.on) do { let _ = try transferManager.send( params: .init( payload: .init( - name: "", - type: "", + name: "abc", + type: "jpeg", preview: Data(), contents: imageData ), - recipientId: Data(), - retry: 1, + recipientId: contact.id, + retry: 10, period: "" ), callback: .init(handle: { @@ -197,6 +200,8 @@ final class SingleChatViewModel: NSObject { } }) ) + + hudRelay.send(.none) } catch { hudRelay.send(.error(.init(with: error))) } @@ -256,7 +261,7 @@ final class SingleChatViewModel: NSObject { }) ) - //message.roundURL = report.roundURL + message.roundURL = report.roundURL message.networkId = report.messageId if let timestamp = report.timestamp { message.date = Date.fromTimestamp(Int(timestamp)) diff --git a/Sources/Integration/Mocks/BindingsMock.swift b/Sources/Integration/Mocks/BindingsMock.swift deleted file mode 100644 index b4eb5c0bdb65e64130ef0f1c6e85e01993fb8bf4..0000000000000000000000000000000000000000 --- a/Sources/Integration/Mocks/BindingsMock.swift +++ /dev/null @@ -1,302 +0,0 @@ -import Models -import Combine -import XXModels -import Foundation - -public final class BindingsMock: BindingsInterface { - private var cancellables = Set<AnyCancellable>() - private let requestsSubject = PassthroughSubject<Contact, Never>() - private let groupRequestsSubject = PassthroughSubject<Group, Never>() - private let confirmationsSubject = PassthroughSubject<Contact, Never>() - - public var hasRunningTasks: Bool { - false - } - - public func replayRequests() {} - - public var myId: Data { - "MOCK_USER".data(using: .utf8)! - } - - public var receptionId: Data { - "RECEPTION_ID".data(using: .utf8)! - } - - public var meMarshalled: Data { - "MOCK_USER_MARSHALLED".data(using: .utf8)! - } - - public static var secret: (Int) -> Data? = { - "\($0)".data(using: .utf8)! - } - - public func verify(marshaled: Data, verifiedMarshaled: Data) throws -> Bool { - true - } - - public static let version: String = "MOCK" - - public static var new: ClientNew = { _,_,_,_,_ in true } - - public static var fromBackup: ClientFromBackup = { _,_,_,_,_,_ in Data() } - - public static var login: (String?, Data?, String?, NSErrorPointer) -> BindingsInterface? = { _,_,_,_ in BindingsMock() } - - public func meMarshalled(_: String, email: String?, phone: String?) -> Data { - meMarshalled - } - - public func startNetwork() {} - - public func stopNetwork() {} - - public static func listenLogs() {} - - public static func updateErrors() {} - - public func listenPreImageUpdates() {} - - public func getPreImages() -> String { "" } - - public func nodeRegistrationStatus() throws {} - - public func getId(from: Data) -> Data? { from } - - public func unregisterNotifications() throws {} - - public func registerNotifications(_: Data) throws {} - - public func compress(image: Data, _: @escaping(Result<Data, Error>) -> Void) {} - - public func generateUD() throws -> UserDiscoveryInterface { UserDiscoveryMock() } - - public func generateUDFromBackup( - email: String?, - phone: String? - ) throws -> UserDiscoveryInterface { UserDiscoveryMock() } - - public func generateTransferManager( - _: @escaping (Data, String?, String?, Data?) -> Void - ) throws -> TransferManagerInterface { - TransferManagerMock() - } - - public func listenEvents(_: @escaping (BackendEvent) -> Void) {} - - public func listenMessages(_: @escaping (Message) -> Void) throws {} - - - public func initializeBackup( - passphrase: String, - callback: @escaping (Data) -> Void - ) -> BackupInterface { BindingsBackupMock() } - - public func resumeBackup( - callback: @escaping (Data) -> Void - ) -> BackupInterface { BindingsBackupMock() } - - public func listenBackups(_: @escaping (Data) -> Void) -> BackupInterface { fatalError() } - - public func listenNetworkUpdates(_: @escaping (Bool) -> Void) {} - - public func confirm(_: Data, _ completion: @escaping (Result<Bool, Error>) -> Void) { - DispatchQueue.global().asyncAfter(deadline: .now() + 2) { - completion(.success(true)) - } - } - - public func listenRound(id: Int, _: @escaping (Result<Bool, Error>) -> Void) {} - - public func add(_ contact: Data, from: Data, _ completion: @escaping (Result<Bool, Error>) -> Void) { - DispatchQueue.global().asyncAfter(deadline: .now() + 2) { [weak self] in - if contact == Contact.georgeDiscovered.marshaled { - completion(.success(true)) - } else { - completion(.success(false)) - return - } - - self?.requestsSubject.send(.carlRequested) - self?.requestsSubject.send(.angelinaRequested) - self?.requestsSubject.send(.elonRequested) - - DispatchQueue.global().asyncAfter(deadline: .now() + 1) { [weak self] in - self?.confirmationsSubject.send(.georgeDiscovered) - } - } - } - - public func send( - _ payload: Data, - to recipient: Data - ) -> Result<E2ESendReportType, Error> { - .success(MockE2ESendReport()) - } - - public func listen( - report: Data, - _ completion: @escaping (Result<MessageDeliveryStatus, Error>) -> Void - ) { - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - completion(.success(.sent)) - } - } - - public func generateDummyTraficManager() throws -> DummyTrafficManaging { - MockDummyManager() - } - - public func removeContact(_ data: Data) throws {} - - public func resetSessionWith(_: Data) {} - - public func listenRequests( - _ requests: @escaping (Contact) -> Void, - _ confirmations: @escaping (Contact) -> Void, - _ resets: @escaping (Contact) -> Void - ) { - requestsSubject.sink(receiveValue: requests).store(in: &cancellables) - confirmationsSubject.sink(receiveValue: confirmations).store(in: &cancellables) - } - - public func listenGroupRequests( - _ groupRequests: @escaping (Group, [Data], String?) -> Void, - groupMessages: @escaping (Message) -> Void - ) throws -> GroupManagerInterface? { - groupRequestsSubject - .sink { groupRequests($0, [], nil) } - .store(in: &cancellables) - - return GroupManagerMock() - } - - public func restore( - ids: Data, - using ud: UserDiscoveryInterface, - lookupCallback: @escaping (Result<Contact, Error>) -> Void, - restoreCallback: @escaping (Int, Int, Int, String?) -> Void - ) -> RestoreReportType { - fatalError() - } - - public static func updateNDF(for: NetworkEnvironment, _ completion: @escaping (Result<Data?, Error>) -> Void) { - completion(.success(Data())) - } -} - -extension Group { - static let mockGroup = Group( - id: "mockGroup".data(using: .utf8)!, - name: "Bruno's birthday 6/1", - leaderId: "mockGroupLeader".data(using: .utf8)!, - createdAt: Date.distantPast, - authStatus: .pending, - serialized: "mockGroup".data(using: .utf8)! - ) -} - -extension Contact { - static func mock(_ count: Int = 1) -> [Contact] { - var mocks = [Contact]() - - for n in 0..<count { - mocks.append( - .init( - id: "brad\(n)".data(using: .utf8)!, - marshaled: "brad\(n)".data(using: .utf8)!, - username: "brad\(n)", - email: "brad\(n)@xx.io", - phone: "819820212\(n)5BR", - nickname: nil, - photo: nil, - authStatus: .verified, - isRecent: false, - createdAt: Date() - )) - } - - return mocks - } - - static let angelinaRequested = Contact( - id: "angelinajolie".data(using: .utf8)!, - marshaled: "angelinajolie".data(using: .utf8)!, - username: "angelinajolie", - email: nil, - phone: nil, - nickname: "Angelica Jolie", - photo: nil, - authStatus: .verificationInProgress, - isRecent: false, - createdAt: Date() - ) - - static let carlRequested = Contact( - id: "carlsagan".data(using: .utf8)!, - marshaled: "carlsagan".data(using: .utf8)!, - username: "carlsagan", - email: "carl@jpl.nasa", - phone: "81982022244BR", - nickname: "Carl Sagan", - photo: nil, - authStatus: .verified, - isRecent: false, - createdAt: Date.distantPast - ) - - static let elonRequested = Contact( - id: "elonmusk".data(using: .utf8)!, - marshaled: "elonmusk".data(using: .utf8)!, - username: "elonmusk", - email: "elon@tesla.com", - phone: nil, - nickname: "Elon Musk", - photo: nil, - authStatus: .verified, - isRecent: false, - createdAt: Date.distantPast - ) - - static let georgeDiscovered = Contact( - id: "georgebenson74".data(using: .utf8)!, - marshaled: "georgebenson74".data(using: .utf8)!, - username: "bruno_muniz74", - email: "george@xx.io", - phone: "81987022255BR", - nickname: "Bruno Muniz", - photo: nil, - authStatus: .stranger, - isRecent: false, - createdAt: Date() - ) -} - -public struct MockE2ESendReport: E2ESendReportType { - public var timestamp: Int64 { 1 } - public var marshalled: Data { Data() } - public var uniqueId: Data? { Data() } - public var roundURL: String { "https://www.google.com.br" } -} - -public struct MockDummyManager: DummyTrafficManaging { - public var status: Bool { true } - - public func setStatus(status: Bool) { - print("Dummy manager status set to \(status)") - } -} - -public struct BindingsBackupMock: BackupInterface { - public func stop() throws { - // TODO - } - - public func addJson(_: String?) { - // TODO - } - - public func isBackupRunning() -> Bool { - return true - } -} diff --git a/Sources/Integration/Session/Session+Contacts.swift b/Sources/Integration/Session/Session+Contacts.swift deleted file mode 100644 index d30f78c24e014cb411b0608638904d5664f626aa..0000000000000000000000000000000000000000 --- a/Sources/Integration/Session/Session+Contacts.swift +++ /dev/null @@ -1,258 +0,0 @@ -import Retry -import Models -import Shared -import XXModels -import Foundation - -extension Session { - public func getId(from data: Data) -> Data? { - client.bindings.getId(from: data) - } - - public func verify(contact: Contact) { - var contact = contact - contact.authStatus = .verificationInProgress - - do { - contact = try dbManager.saveContact(contact) - } catch { - log(string: "Failed to store contact request upon verification. Returning, request will be abandoned to not crash", type: .error) - } - - retry(max: 4, retryStrategy: .delay(seconds: 1)) { [weak self] in - if self?.networkMonitor.xxStatus != .available { - log(string: "Network is not available yet for ownership. Retrying in 1 second...", type: .error) - throw NSError.create("") - } - }.finalCatch { error in - log(string: "Failed to verify contact cause network wasn't available at all", type: .crumbs) - return - } - - let resultClosure: (Result<Contact, Error>) -> Void = { result in - switch result { - case .success(let mightBe): - guard try! self.client.bindings.verify(marshaled: contact.marshaled!, verifiedMarshaled: mightBe.marshaled!) else { - do { - try self.dbManager.deleteContact(contact) - } catch { - log(string: error.localizedDescription, type: .error) - } - - return - } - - contact.authStatus = .verified - - do { - try self.dbManager.saveContact(contact) - } catch { - log(string: error.localizedDescription, type: .error) - } - - case .failure: - contact.authStatus = .verificationFailed - - do { - try self.dbManager.saveContact(contact) - } catch { - log(string: error.localizedDescription, type: .error) - } - } - } - - let ud = client.userDiscovery! - - let hasEmail = contact.email != nil - let hasPhone = contact.phone != nil - - guard hasEmail || hasPhone else { - ud.lookup(forUserId: contact.id, resultClosure) - return - } - - var fact: String - - if hasEmail { - fact = "\(FactType.email.prefix)\(contact.email!)" - } else { - fact = "\(FactType.phone.prefix)\(contact.phone!)" - } - - do { - try ud.search(fact: fact, resultClosure) - } catch { - log(string: error.localizedDescription, type: .error) - contact.authStatus = .verificationFailed - - do { - try self.dbManager.saveContact(contact) - } catch { - log(string: error.localizedDescription, type: .error) - } - } - } - - public func retryRequest(_ contact: Contact) throws { - let name = (contact.nickname ?? contact.username) ?? "" - - client.bindings.add(contact.marshaled!, from: myQR) { [weak self, contact] in - var contact = contact - guard let self = self else { return } - - do { - switch $0 { - case .success: - contact.authStatus = .requested - - self.toastController.enqueueToast(model: .init( - title: Localized.Requests.Sent.Toast.resent(name), - leftImage: Asset.sharedSuccess.image - )) - - case .failure: - contact.createdAt = Date() - - self.toastController.enqueueToast(model: .init( - title: Localized.Requests.Sent.Toast.resentFailed(name), - color: Asset.accentDanger.color, - leftImage: Asset.requestFailedToaster.image - )) - } - - _ = try self.dbManager.saveContact(contact) - } catch { - log(string: error.localizedDescription, type: .error) - } - } - } - - public func add(_ contact: Contact) throws { - /// Make sure we are not adding ourselves - /// - guard contact.username != username else { - throw NSError.create("You can't add yourself") - } - - var contact = contact - - /// Check if this contact is actually - /// being requested/confirmed after failing - /// - if [.requestFailed, .confirmationFailed].contains(contact.authStatus) { - /// If it is being re-requested or - /// re-confirmed, no need to save again - /// - contact.createdAt = Date() - - if contact.authStatus == .confirmationFailed { - try confirm(contact) - return - } - } else { - /// If its not failed, lets make sure that - /// this is an actual new contact - /// - if let _ = try? dbManager.fetchContacts(.init(id: [contact.id])).first { - /// Found a user w/ that id already stored - /// - throw NSError.create("This user has already been requested") - } - - contact.authStatus = .requesting - } - - contact = try dbManager.saveContact(contact) - - let myself = client.bindings.meMarshalled( - username!, - email: isSharingEmail ? email : nil, - phone: isSharingPhone ? phone : nil - ) - - client.bindings.add(contact.marshaled!, from: myself) { [weak self, contact] in - guard let self = self else { return } - var contact = contact - - do { - switch $0 { - case .success(let success): - contact.authStatus = success ? .requested : .requestFailed - contact = try self.dbManager.saveContact(contact) - - let name = contact.nickname ?? contact.username - - self.toastController.enqueueToast(model: .init( - title: Localized.Requests.Sent.Toast.sent(name ?? ""), - leftImage: Asset.sharedSuccess.image - )) - - case .failure: - contact.createdAt = Date() - contact.authStatus = .requestFailed - contact = try self.dbManager.saveContact(contact) - - self.toastController.enqueueToast(model: .init( - title: Localized.Requests.Failed.toast(contact.nickname ?? contact.username!), - color: Asset.accentDanger.color, - leftImage: Asset.requestFailedToaster.image - )) - } - } catch { - print(error.localizedDescription) - } - } - } - - public func confirm(_ contact: Contact) throws { - var contact = contact - contact.authStatus = .confirming - contact = try dbManager.saveContact(contact) - - client.bindings.confirm(contact.marshaled!) { [weak self] in - switch $0 { - case .success(let confirmed): - contact.isRecent = true - contact.createdAt = Date() - contact.authStatus = confirmed ? .friend : .confirmationFailed - - case .failure: - contact.authStatus = .confirmationFailed - } - - _ = try? self?.dbManager.saveContact(contact) - } - } - - public func deleteContact(_ contact: Contact) throws { - if !(try dbManager.fetchFileTransfers(.init(contactId: contact.id))).isEmpty { - throw NSError.create("There is an ongoing file transfer with this contact as you are receiving or sending a file, please try again later once it’s done") - } - - try client.bindings.removeContact(contact.marshaled!) - - /// Currently this cascades into deleting - /// all messages w/ contact.id == senderId - /// But this shouldn't be the always the case - /// because if we have a group / this contact - /// the messages will be gone as well. - /// - /// Suggestion: If there's a group where this user belongs to - /// we will just cleanup the contact model stored on the db - /// leaving only username and id which are the equivalent to - /// .stranger contacts. - /// - //try dbManager.deleteContact(contact) - - var contact = contact - contact.email = nil - contact.phone = nil - contact.photo = nil - contact.isRecent = false - contact.marshaled = nil - contact.isBlocked = true - contact.authStatus = .stranger - contact.nickname = contact.username - _ = try! dbManager.saveContact(contact) - } -} diff --git a/Sources/Integration/Session/Session+Group.swift b/Sources/Integration/Session/Session+Group.swift deleted file mode 100644 index 47652ee94cc4ec7d870005ff69d2d21ea3839ce3..0000000000000000000000000000000000000000 --- a/Sources/Integration/Session/Session+Group.swift +++ /dev/null @@ -1,243 +0,0 @@ -import Models -import XXModels -import Foundation - -extension Session { - public func join(group: Group) throws { - guard let manager = client.groupManager else { fatalError("A group manager was not created") } - - try manager.join(group.serialized) - var group = group - group.authStatus = .participating - scanStrangers {} - try dbManager.saveGroup(group) - } - - public func leave(group: Group) throws { - guard let manager = client.groupManager else { fatalError("A group manager was not created") } - try manager.leave(group.id) - try dbManager.deleteGroup(group) - } - - public func createGroup( - name: String, - welcome: String?, - members: [Contact], - _ completion: @escaping (Result<GroupInfo, Error>) -> Void - ) { - guard let manager = client.groupManager else { - fatalError("A group manager was not created") - } - - manager.create( - me: myId, - name: name, - welcome: welcome, - with: members.map { $0.id }) { [weak self] result in - guard let self = self else { return } - - switch result { - case .success(let group): - try! self.dbManager.saveGroup(group) - - members - .map { GroupMember(groupId: group.id, contactId: $0.id) } - .forEach { try! self.dbManager.saveGroupMember($0) } - - // TODO: Add saveBulkGroupMembers to the database - - if let welcome = welcome { - let message = Message( - networkId: nil, - senderId: self.myId, - recipientId: nil, - groupId: group.id, - date: group.createdAt, - status: .sent, - isUnread: false, - text: welcome, - replyMessageId: nil, - roundURL: nil, - fileTransferId: nil - ) - - try! self.dbManager.saveMessage(message) - } - - let query = GroupInfo.Query(groupId: group.id) - let info = try! self.dbManager.fetchGroupInfos(query).first - completion(.success(info!)) - - case .failure(let error): - completion(.failure(error)) - } - } - } - - @discardableResult - func processGroupCreation(_ group: Group, memberIds: [Data], welcome: String?) -> GroupInfo { - /// Save the group - /// - _ = try! dbManager.saveGroup(group) - - /// Which of those members are not my friends? - /// - let friendsParticipating = try! dbManager.fetchContacts(Contact.Query(id: Set(memberIds))) - - /// Save the strangers as contacts - /// - let friendIds = friendsParticipating.map(\.id) - memberIds.forEach { - if !friendIds.contains($0) { - try! dbManager.saveContact(.init( - id: $0, - marshaled: nil, - username: nil, - email: nil, - phone: nil, - nickname: nil, - photo: nil, - authStatus: .stranger, - isRecent: false, - createdAt: Date() - )) - } - } - - /// Save group members relation - /// - memberIds.forEach { - try! dbManager.saveGroupMember(.init(groupId: group.id, contactId: $0)) - } - - /// Save the welcome message (if any) - /// - if let welcome = welcome { - _ = try! dbManager.saveMessage(.init( - networkId: nil, - senderId: group.leaderId, - recipientId: nil, - groupId: group.id, - date: group.createdAt, - status: .received, - isUnread: true, - text: welcome, - replyMessageId: nil, - roundURL: nil, - fileTransferId: nil - )) - } - - - if inappnotifications { - DeviceFeedback.sound(.contactAdded) - DeviceFeedback.shake(.notification) - } - - scanStrangers {} - - let info = try! dbManager.fetchGroupInfos(.init(groupId: group.id)).first - return info! - } -} - -// MARK: - GroupMessages - -extension Session { - public func send(_ payload: Payload, toGroup group: Group) { - var message = Message( - senderId: client.bindings.myId, - recipientId: nil, - groupId: group.id, - date: Date(), - status: .sending, - isUnread: false, - text: payload.text, - replyMessageId: payload.reply?.messageId, - roundURL: nil, - fileTransferId: nil - ) - - do { - message = try dbManager.saveMessage(message) - send(groupMessage: message) - } catch { - log(string: error.localizedDescription, type: .error) - } - } - - func send(groupMessage: Message) { - guard let manager = client.groupManager else { fatalError("A group manager was not created") } - var message = groupMessage - - var reply: Reply? - if let replyId = message.replyMessageId, - let replyMessage = try? dbManager.fetchMessages(Message.Query(networkId: replyId)).first { - reply = Reply(messageId: replyId, senderId: replyMessage.senderId) - } - - let payloadData = Payload(text: message.text, reply: reply).asData() - - DispatchQueue.global().async { [weak self] in - guard let self = self else { return } - - switch manager.send(payloadData, to: message.groupId!) { - case .success((let roundId, let uniqueId, let roundURL)): - message.roundURL = roundURL - - self.client.bindings.listenRound(id: Int(roundId)) { result in - switch result { - case .success(let succeeded): - message.networkId = uniqueId - message.status = succeeded ? .sent : .sendingFailed - case .failure: - message.status = .sendingFailed - } - - do { - try self.dbManager.saveMessage(message) - } catch { - log(string: error.localizedDescription, type: .error) - } - } - case .failure: - message.status = .sendingFailed - } - - do { - try self.dbManager.saveMessage(message) - } catch { - log(string: error.localizedDescription, type: .error) - } - } - } - - public func scanStrangers(_ completion: @escaping () -> Void) { - DispatchQueue.global().async { [weak self] in - guard let self = self, - let ud = self.client.userDiscovery, - let strangers = try? self.dbManager.fetchContacts(.init(username: .some(nil))), - !strangers.isEmpty else { return } - - ud.lookup(idList: strangers.map(\.id)) { result in - switch result { - case .success(let strangersWithUsernames): - let acquaintances = strangers.map { stranger -> Contact in - var exStranger = stranger - exStranger.username = strangersWithUsernames.first(where: { $0.id == stranger.id })?.username - return exStranger - } - - DispatchQueue.main.async { - acquaintances.forEach { _ = try? self.dbManager.saveContact($0) } - } - - completion() - case .failure(let error): - print(error.localizedDescription) - DispatchQueue.main.async { completion() } - } - } - } - } -} diff --git a/Sources/Integration/Session/Session.swift b/Sources/Integration/Session/Session.swift deleted file mode 100644 index 49e1c60e20b3be9765529620893c493d900b9cfd..0000000000000000000000000000000000000000 --- a/Sources/Integration/Session/Session.swift +++ /dev/null @@ -1,507 +0,0 @@ -import Retry -import os.log -import Models -import Shared -import Combine -import Defaults -import XXModels -import XXDatabase -import Foundation -import ToastFeature -import BackupFeature -import NetworkMonitor -import ReportingFeature -import DependencyInjection -import XXLegacyDatabaseMigrator - -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 reportingStatus: ReportingStatus - @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 - - 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 - - dbManager = try Database.onDisk(path: dbPath) - - if dbExistsInLegacyPath { - try Migrator.live()( - try .init(path: legacyPath), - to: dbManager, - myContactId: client.bindings.myId, - meMarshaled: client.bindings.meMarshalled - ) - - try FileManager.default.moveItem(atPath: legacyPath, toPath: legacyPath.appending("-backup")) - } - - 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 - - if let paramsPhone = params.phone, !paramsPhone.isEmpty { - phone = paramsPhone - } - - if let paramsEmail = params.email, !paramsEmail.isEmpty { - email = paramsEmail - } - } - - guard let username = username, username.isEmpty == false else { - fatalError("Trying to restore an account that has no username") - } - - 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) - - 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 - - dbManager = try Database.onDisk(path: dbPath) - - if dbExistsInLegacyPath { - try Migrator.live()( - try .init(path: legacyPath), - to: dbManager, - myContactId: client.bindings.myId, - meMarshaled: client.bindings.meMarshalled - ) - - try FileManager.default.moveItem(atPath: legacyPath, toPath: legacyPath.appending("-backup")) - } - - try continueInitialization() - } - - private func continueInitialization() throws { - var myContact = try self.myContact() - myContact.marshaled = client.bindings.meMarshalled - myContact.username = username - myContact.email = email - myContact.phone = phone - myContact.authStatus = .friend - myContact.isRecent = false - _ = try dbManager.saveContact(myContact) - - setupBindings() - networkMonitor.start() - - networkMonitor.statusPublisher - .filter { $0 == .available }.first() - .sink { [unowned self] _ in - client.bindings.replayRequests() - scanStrangers {} - } - .store(in: &cancellables) - - registerUnfinishedUploadTransfers() - registerUnfinishedDownloadTransfers() - - let query = Contact.Query(authStatus: [.verificationInProgress]) - _ = try? dbManager.bulkUpdateContacts(query, .init(authStatus: .verificationFailed)) - } - - 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") } - - try! 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 - } - - private func registerUnfinishedDownloadTransfers() { - guard let unfinishedReceivingMessages = try? dbManager.fetchMessages(.init(status: [.receiving])), - let unfinishedReceivingTransfers = try? dbManager.fetchFileTransfers(.init( - id: Set(unfinishedReceivingMessages - .filter { $0.fileTransferId != nil } - .compactMap(\.fileTransferId)))) - else { return } - - let pairs = unfinishedReceivingMessages.compactMap { message -> (Message, FileTransfer)? in - guard let transfer = unfinishedReceivingTransfers.first(where: { ft in - ft.id == message.fileTransferId - }) else { return nil } - - return (message, transfer) - } - - pairs.forEach { message, transfer in - var message = message - var transfer = transfer - - do { - try client.transferManager?.listenDownloadFromTransfer(with: transfer.id) { [weak self] completed, received, total, error in - guard let self = self else { return } - if completed { - transfer.progress = 1.0 - message.status = .received - - if let data = try? self.client.transferManager?.downloadFileFromTransfer(with: transfer.id), - let _ = try? FileManager.store(data: data, name: transfer.name, type: transfer.type) { - transfer.data = data - } - } else { - if error != nil { - message.status = .receivingFailed - } else { - transfer.progress = Float(received)/Float(total) - } - } - - _ = try? self.dbManager.saveFileTransfer(transfer) - _ = try? self.dbManager.saveMessage(message) - } - } catch { - message.status = .receivingFailed - _ = try? self.dbManager.saveMessage(message) - } - } - } - - private func registerUnfinishedUploadTransfers() { - 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 } - - let pairs = unfinishedSendingMessages.compactMap { message -> (Message, FileTransfer)? in - guard let transfer = unfinishedSendingTransfers.first(where: { ft in - ft.id == message.fileTransferId - }) else { return nil } - - return (message, transfer) - } - - pairs.forEach { message, transfer in - var message = message - var transfer = transfer - - do { - try client.transferManager?.listenUploadFromTransfer(with: transfer.id) { [weak self] completed, sent, arrived, total, error in - guard let self = self else { return } - - if completed { - transfer.progress = 1.0 - message.status = .sent - - try? self.client.transferManager?.endTransferUpload(with: transfer.id) - } 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) - } - } - } - - 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) - - guard username!.isEmpty == false else { - fatalError("Tried to build a backup with my username but an empty string was set to it") - } - - backupService.performBackupIfAutomaticIsEnabled() - } - - private func setupBindings() { - client.requests - .sink { [unowned self] contact in - let query = Contact.Query(id: [contact.id]) - - if let prexistent = try? dbManager.fetchContacts(query).first { - guard prexistent.authStatus == .stranger else { return } - } - - if self.inappnotifications { - DeviceFeedback.sound(.contactAdded) - DeviceFeedback.shake(.notification) - } - - verify(contact: contact) - }.store(in: &cancellables) - - client.requestsSent - .sink { [unowned self] in _ = try? dbManager.saveContact($0) } - .store(in: &cancellables) - - client.backup - .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. - /// - if var contact = try? dbManager.fetchContacts(.init(id: [$0.id])).first { - contact.authStatus = .friend - _ = try? dbManager.saveContact(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() - return - } - - client.initializeBackup(passphrase: passphrase) - backupService.passphrase = nil - updateFactsOnBackup() - } else { - backupService.passphrase = nil - client.stopListeningBackup() - } - } - .store(in: &cancellables) - - client.messages - .sink { [unowned self] in - if var contact = try? dbManager.fetchContacts(.init(id: [$0.senderId])).first { - guard contact.isBanned == false else { return } - contact.isRecent = false - _ = try? dbManager.saveContact(contact) - } - - _ = try? dbManager.saveMessage($0) - }.store(in: &cancellables) - - 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 - } - - if let contact = try! dbManager.fetchContacts(.init(id: [request.0.leaderId])).first { - if reportingStatus.isEnabled(), (contact.isBlocked || contact.isBanned) { - 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 = try? dbManager.fetchContacts(.init(id: [$0.id])).first { - contact.authStatus = .friend - 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 - )) - } - }.store(in: &cancellables) - - client.transfers - .sink { [unowned self] in - guard let transfer = try? dbManager.saveFileTransfer($0) else { return } - handle(incomingTransfer: transfer) - } - .store(in: &cancellables) - } - - func myContact() throws -> Contact { - if let contact = try dbManager.fetchContacts(.init(id: [client.bindings.myId])).first { - return contact - } else { - return try dbManager.saveContact(.init(id: client.bindings.myId)) - } - } -} diff --git a/Sources/LaunchFeature/LaunchViewModel.swift b/Sources/LaunchFeature/LaunchViewModel.swift index f31d75740093836148184020fa5f0500e50c7b75..06e9329d4a540f988511c5dea9ccb7940d392196 100644 --- a/Sources/LaunchFeature/LaunchViewModel.swift +++ b/Sources/LaunchFeature/LaunchViewModel.swift @@ -105,7 +105,7 @@ final class LaunchViewModel { do { try self.setupDatabase() - _ = try SetLogLevel.live(.fatal) + _ = try SetLogLevel.live(.info) RegisterLogWriter.live(.init(handle: { XXLogger.live().debug($0) @@ -161,9 +161,6 @@ final class LaunchViewModel { 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")") } @@ -241,7 +238,6 @@ final class LaunchViewModel { private func cleanUp() { // try? cMixManager.remove() // try? keychainHandler.clear() -// // dropboxService.unlink() } @@ -286,6 +282,8 @@ final class LaunchViewModel { } DependencyInjection.Container.shared.register(database) + + _ = try? database.bulkUpdateContacts(.init(authStatus: [.verificationInProgress]), .init(authStatus: .verificationFailed)) } func getContactWith(userId: Data) -> XXModels.Contact? { @@ -457,7 +455,7 @@ extension LaunchViewModel { guard let self = self else { return } self.handleGroupRequest(from: group) }), - groupChatProcessor: .init(handle: { print($0) }) // What is this? + groupChatProcessor: .init(handle: { print(String(data: $0.msg, encoding: .utf8)) }) ) DependencyInjection.Container.shared.register(manager) @@ -574,12 +572,16 @@ extension LaunchViewModel { return } + existentContact.isRecent = true existentContact.authStatus = .friend try! database.saveContact(existentContact) } - private func handleReset(from contact: XXClient.Contact) { - // TODO + private func handleReset(from user: XXClient.Contact) { + if var contact = try? database.fetchContacts(.init(id: [user.getId()])).first { + contact.authStatus = .friend + _ = try? database.saveContact(contact) + } } private func handleGroupRequest(from group: XXClient.Group) {