import Retry import Models import Shared import Combine import Defaults import Database import Foundation import NetworkMonitor import DependencyInjection 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 networkMonitor: NetworkMonitoring public let client: Client public let dbManager: DatabaseManager 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() } lazy public var groups: (Group.Request) -> AnyPublisher<[Group], Never> = { self.dbManager.publisher(fetch: Group.self, $0).catch { _ in Just([]) }.eraseToAnyPublisher() } lazy public var contacts: (Contact.Request) -> AnyPublisher<[Contact], Never> = { self.dbManager.publisher(fetch: Contact.self, $0).catch { _ in Just([]) }.eraseToAnyPublisher() } lazy public var singleMessages: (Contact) -> AnyPublisher<[Message], Never> = { self.dbManager.publisher(fetch: Message.self, .withContact($0.userId)).catch { _ in Just([]) }.eraseToAnyPublisher() } lazy public var groupMessages: (Group) -> AnyPublisher<[GroupMessage], Never> = { self.dbManager.publisher(fetch: GroupMessage.self, .fromGroup($0.groupId)).catch { _ in Just([]) }.eraseToAnyPublisher() } lazy public var groupChats: (GroupChatInfo.Request) -> AnyPublisher<[GroupChatInfo], Never> = { self.dbManager.publisher(fetch: GroupChatInfo.self, $0).catch { _ in Just([]) }.eraseToAnyPublisher() } lazy public var singleChats: (SingleChatInfo.Request) -> AnyPublisher<[SingleChatInfo], Never> = { _ in self.dbManager.publisher(fetch: Contact.self, .friends) .flatMap { [unowned self] contactList -> AnyPublisher<[SingleChatInfo], Error> in let contactIds = contactList.map { $0.userId } let messagesPublisher: AnyPublisher<[Message], Error> = dbManager .publisher(fetch: .latestOnesFromContactIds(contactIds)) .map { $0.sorted(by: { $0.timestamp > $1.timestamp }) } .eraseToAnyPublisher() return messagesPublisher.map { messages -> [SingleChatInfo] in contactList.map { contact -> SingleChatInfo in SingleChatInfo(contact: contact, lastMessage: messages.first { $0.sender == contact.userId || $0.receiver == contact.userId }) } } .eraseToAnyPublisher() } .catch { _ in Just([]) } .map { $0.filter { $0.lastMessage != nil }} .map { $0.sorted(by: { $0.lastMessage!.timestamp > $1.lastMessage!.timestamp })} .eraseToAnyPublisher() } public init(ndf: String) throws { let network = try! DependencyInjection.Container.shared.resolve() as XXNetworking self.client = try network.newClient(ndf: ndf) dbManager = GRDBDatabaseManager() try dbManager.setup() setupBindings() networkMonitor.start() networkMonitor.statusPublisher .filter { $0 == .available }.first() .sink { [unowned self] _ in client.bindings.replayRequests() } .store(in: &cancellables) registerUnfinishedTransfers() if let pendingVerificationUsers: [Contact] = try? dbManager.fetch(.verificationInProgress) { pendingVerificationUsers.forEach { var contact = $0 contact.status = .verificationFailed do { _ = try dbManager.save(contact) } catch { log(string: error.localizedDescription, type: .error) } } } } public func setDummyTraffic(status: Bool) { client.dummyManager?.setStatus(status: status) } public func deleteMyself() throws { log(string: "Will start deleting account process", type: .crumbs) guard let username = username, let ud = client.userDiscovery else { log(string: "Failed deleting account. No username or UD", type: .error) return } do { try unregisterNotifications() } catch { log(string: "Failed to unregister for notifications", type: .error) } try ud.deleteMyself(username) log(string: "Deleted myself from User Discovery", type: .info) stop() log(string: "Requested network stop", type: .crumbs) cleanUp() } private func cleanUp() { retry(max: 10, retryStrategy: .delay(seconds: 1)) { [unowned self] in guard self.hasRunningTasks == false else { let string = "Tried to clean up database and defaults but network hasn't stopped yet. Sleeping for a second..." log(string: string, type: .error) throw NSError.create("") } }.finalCatch { _ in fatalError("Couldn't delete account because network is not stopping") } dbManager.drop() log(string: "Dropped database", type: .info) FileManager.xxCleanup() log(string: "Wiped disk", type: .info) 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 log(string: "Wiped defaults", type: .info) } public func forceFailMessages() { if let pendingE2E: [Message] = try? dbManager.fetch(.sending) { pendingE2E.forEach { var message = $0 message.status = .failedToSend do { try dbManager.save(message) } catch { log(string: error.localizedDescription, type: .error) } } } if let pendingGroupMessages: [GroupMessage] = try? dbManager.fetch(.sending) { pendingGroupMessages.forEach { var message = $0 message.status = .failed do { try dbManager.save(message) } catch { log(string: error.localizedDescription, type: .error) } } } } private func registerUnfinishedTransfers() { guard let unfinisheds: [Message] = try? dbManager.fetch(.sendingAttachment), !unfinisheds.isEmpty else { return } log(string: "There are unfinished transfers from the last session. Re-registering their upload progress", type: .crumbs) for var message in unfinisheds { guard let tid = message.payload.attachment?.transferId else { log(string: "Impossible to resume a transfer that had no TID", type: .error) return } do { try client.transferManager?.listenUploadFromTransfer(with: tid) { completed, sent, arrived, total, error in if completed { message.status = .sent message.payload.attachment?.progress = 1.0 log(string: "FT Up finished", type: .info) if let transfer: FileTransfer = try? self.dbManager.fetch(.withTID(tid)).first { do { try self.dbManager.delete(transfer) } catch { log(string: error.localizedDescription, type: .error) } } } else { if let error = error { log(string: error.localizedDescription, type: .error) message.status = .failedToSend } else { let progress = Float(arrived)/Float(total) message.payload.attachment?.progress = progress log(string: "FT Up: \(progress)", type: .crumbs) return } } do { _ = try self.dbManager.save(message) } catch { log(string: error.localizedDescription, type: .error) } } } catch { log(string: "An error occurred when trying to register unfinished FT: \(error.localizedDescription). Switching it to 'sent'", type: .error) message.status = .sent do { _ = try self.dbManager.save(message) } catch { log(string: error.localizedDescription, type: .error) } } } } private func setupBindings() { client.requests .sink { [unowned self] request in if let _: Contact = try? dbManager.fetch(.withUserId(request.userId)).first { return } if self.inappnotifications { DeviceFeedback.sound(.contactAdded) DeviceFeedback.shake(.notification) } verify(contact: request) } .store(in: &cancellables) client.groupMessages .sink { [unowned self] in do { _ = try dbManager.save($0) } catch { log(string: "Failed to save an incoming group message: \(error.localizedDescription)", type: .error) } }.store(in: &cancellables) client.messages .sink { [unowned self] in do { _ = try dbManager.save($0) } catch { log(string: "Failed to save an incoming direct message: \(error.localizedDescription)", type: .error) } }.store(in: &cancellables) client.network .sink { [unowned self] in networkMonitor.update($0) } .store(in: &cancellables) client.incomingTransfers .sink { [unowned self] in handle(incomingTransfer: $0) } .store(in: &cancellables) client.groupRequests .sink { [unowned self] request in if let _: Group = try? dbManager.fetch(.withGroupId(request.0.groupId)).first { return } DispatchQueue.global().async { [weak self] in self?.processGroupCreation(request.0, memberIds: request.1, welcome: request.2) } }.store(in: &cancellables) client.confirmations .sink { [unowned self] in guard var contact: Contact = try? dbManager.fetch(.withUserId($0.userId)).first else { return } contact.status = .friend do { try dbManager.save(contact) } catch { log(string: error.localizedDescription, type: .error) } }.store(in: &cancellables) } public func getTextFromMessage(messageId: Data) -> String? { guard let message: Message = try? dbManager.fetch(.withUniqueId(messageId)).first else { return nil } return message.payload.text } public func getTextFromGroupMessage(messageId: Data) -> String? { guard let message: GroupMessage = try? dbManager.fetch(.withUniqueId(messageId)).first else { return nil } return message.payload.text } }