// _____ _ // |_ _| |_ _ _ ___ ___ _ __ __ _ // | | | ' \| '_/ -_) -_) ' \/ _` |_ // |_| |_||_|_| \___\___|_|_|_\__,_(_) // // Threema iOS Client // Copyright (c) 2019-2020 Threema GmbH // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License, version 3, // as published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import Foundation import CocoaLumberjackSwift @objc class WCSessionManager: NSObject { @objc static let shared = WCSessionManager() private var sessions: [Data: WCSession] = [Data: WCSession]() private(set) var running: [Data] = [Data]() private var runningSessionsQueue:DispatchQueue = DispatchQueue(label: "ch.threema.runningSessionsQueue", attributes: []) private var observerAlreadySet: Bool = false private override init() { super.init() loadSessionsFromArchive() } public class func isWebHostAllowed(scannedHostName: String, whiteList: String) -> Bool { if whiteList.count == 0 { return false } let arr = whiteList.components(separatedBy: ",") for host in arr { let pattern = host.trimmingCharacters(in: .whitespaces) if pattern == scannedHostName { return true } let slicedPattern = String(pattern.dropFirst()) if pattern.hasPrefix("*") && scannedHostName.hasSuffix(slicedPattern) { return true; } } return false } } // MARK: Private functions extension WCSessionManager { /** Add observers * batteryLevelDidChange * batteryStateDidChange * profileNicknameChanged * profilePictureChanged * blackListChanged */ private func addObservers() { if observerAlreadySet == false { UIDevice.current.isBatteryMonitoringEnabled = true NotificationCenter.default.addObserver(self, selector: #selector(self.batteryLevelDidChange), name: UIDevice.batteryLevelDidChangeNotification , object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.batteryStateDidChange), name: UIDevice.batteryStateDidChangeNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.profileNicknameChanged), name: NSNotification.Name(rawValue: kNotificationProfileNicknameChanged), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.profilePictureChanged), name: NSNotification.Name(rawValue: kNotificationProfilePictureChanged), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.blackListChanged), name: NSNotification.Name(rawValue: kNotificationBlockedContact), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.managedObjectContextDidChange), name: .NSManagedObjectContextObjectsDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.refreshDirtyObjects), name: NSNotification.Name(rawValue: kNotificationDBRefreshedDirtyObject), object: nil) observerAlreadySet = true } } private func removeObservers() { UIDevice.current.isBatteryMonitoringEnabled = false NotificationCenter.default.removeObserver(self) observerAlreadySet = false } private func allSessionsSavePath() -> String { let documentDir = DocumentManager.documentsDirectory() return documentDir!.appendingPathComponent("AllWCSessions").path } private func runningSessionsSavePath() -> String { let documentDir = DocumentManager.documentsDirectory() return documentDir!.appendingPathComponent("RunningWCSessions").path } } // MARK: Public functions extension WCSessionManager { @objc public func saveSessionsToArchive() { let allSessionsSavePath = self.allSessionsSavePath() let runningSessionsSavePath = self.runningSessionsSavePath() do { try FileManager.default.removeItem(atPath: allSessionsSavePath) } catch { } NSKeyedArchiver.archiveRootObject(self.sessions, toFile: allSessionsSavePath) do { try FileManager.default.removeItem(atPath: runningSessionsSavePath) } catch { } NSKeyedArchiver.archiveRootObject(self.running, toFile: runningSessionsSavePath) } private func loadSessionsFromArchive() { let allSessionsSavePath = self.allSessionsSavePath() let runningSessionsSavePath = self.runningSessionsSavePath() if FileManager.default.fileExists(atPath: allSessionsSavePath) { if let allSessions = NSKeyedUnarchiver.unarchiveObject(withFile: allSessionsSavePath) as? [Data: WCSession] { self.sessions = allSessions do { try FileManager.default.removeItem(atPath: allSessionsSavePath) } catch { } } } if FileManager.default.fileExists(atPath: runningSessionsSavePath) { if let runningSessions = NSKeyedUnarchiver.unarchiveObject(withFile: runningSessionsSavePath) as? [Data] { self.running = runningSessions do { try FileManager.default.removeItem(atPath: runningSessionsSavePath) } catch { } } } } /** Connect a old or new session. Search for correct session or create a new one */ @objc public func connect(authToken: Data?, wca: String?, publicKeyHash: String) { canConnectToWebClient(completionHandler: { (isValid) in if isValid == true { if let webClientSession = WebClientSessionStore.shared.webClientSessionForHash(publicKeyHash) { var session: WCSession? = self.sessions[webClientSession.initiatorPermanentPublicKey!] if LicenseStore.requiresLicenseKey() == true { let mdmSetup = MDMSetup(setup: false)! if let webHosts = mdmSetup.webHosts() { if WCSessionManager.isWebHostAllowed(scannedHostName: webClientSession.saltyRTCHost!, whiteList: webHosts) == false { ValidationLogger.shared().logString("Threema Web: Scanned qr code host is not white listed") if AppDelegate.shared().isAppInBackground() { Utils.sendErrorLocalNotification(NSLocalizedString("webClient_scan_error_mdm_host_title", comment: ""), body: NSLocalizedString("webClient_scan_error_mdm_host_message", comment: ""), userInfo: nil) } else { let rootVC = UIApplication.shared.keyWindow?.rootViewController UIAlertTemplate.showAlert(owner: rootVC!, title: BundleUtil.localizedString(forKey: "webClient_scan_error_mdm_host_title"), message: BundleUtil.localizedString(forKey: "webClient_scan_error_mdm_host_message")) } return } } } if session != nil && wca != nil { if let connectionWca = session?.connectionWca() { if wca!.elementsEqual(connectionWca) { // same wca, ignore this request ValidationLogger.shared()?.logString("Threema Web: Ignore connect, because it's the same wca") return } } } if session == nil { session = WCSession.init(webClientSession: webClientSession) self.sessions[webClientSession.initiatorPermanentPublicKey!] = session } if wca != nil { session!.setWcaForConnection(wca: wca!) } self.addWCSessionToRunning(webClientSession: webClientSession) self.addObservers() ServerConnector.shared().sendPushOverrideTimeout() session!.connect(authToken: authToken) } else { // session not found } } }) } @objc public func connect(authToken: Data?, wca: String?, webClientSession: WebClientSession) { canConnectToWebClient(completionHandler: { (isValid) in if isValid == true { var session: WCSession? = self.sessions[webClientSession.initiatorPermanentPublicKey!] if session != nil && wca != nil { if let connectionWca = session?.connectionWca() { if wca!.elementsEqual(connectionWca) { // same wca, ignore this request ValidationLogger.shared()?.logString("Threema Web: Ignore connect, because it's the same wca") return } } } if session == nil { session = WCSession.init(webClientSession: webClientSession) self.sessions[webClientSession.initiatorPermanentPublicKey!] = session } if LicenseStore.requiresLicenseKey() == true { let mdmSetup = MDMSetup(setup: false)! if let webHosts = mdmSetup.webHosts() { if WCSessionManager.isWebHostAllowed(scannedHostName: webClientSession.saltyRTCHost!, whiteList: webHosts) == false { ValidationLogger.shared().logString("Threema Web: Scanned qr code host is not white listed") if AppDelegate.shared().isAppInBackground() { Utils.sendErrorLocalNotification(NSLocalizedString("webClient_scan_error_mdm_host_title", comment: ""), body: NSLocalizedString("webClient_scan_error_mdm_host_message", comment: ""), userInfo: nil) } else { let rootVC = UIApplication.shared.keyWindow?.rootViewController UIAlertTemplate.showAlert(owner: rootVC!, title: BundleUtil.localizedString(forKey: "webClient_scan_error_mdm_host_title"), message: BundleUtil.localizedString(forKey: "webClient_scan_error_mdm_host_message")) } return } } } if wca != nil { session!.setWcaForConnection(wca: wca!) } self.addWCSessionToRunning(webClientSession: webClientSession) self.addObservers() ServerConnector.shared().sendPushOverrideTimeout() session!.connect(authToken: authToken) } }) } @objc public func connectAllRunningSessions() { if running.count == 0 { ValidationLogger.shared()?.logString("Threema Web: There is no active session") WebClientSessionStore.shared.setAllWebClientSessionsInactive() return } addObservers() ServerConnector.shared().sendPushOverrideTimeout() ValidationLogger.shared()?.logString("Threema Web: Connect active sessions (\(running.count))") for publicKey in running { if let session = sessions[publicKey] { if session.connectionStatus() == .disconnected { ValidationLogger.shared()?.logString("Threema Web: Connect active session") session.connect(authToken: nil) } else { if let connectionStatus = session.connectionStatus() { ValidationLogger.shared()?.logString("Threema Web: Can't connect active session, wrong state \(connectionStatus)") } else { ValidationLogger.shared()?.logString("Threema Web: Can't connect active session, connectionStatus is nil!") } } } } } /** Stop all running sessions. Clears the list of running sessions */ @objc public func stopAllSessions() { // disconnect all active sessions and set all sessions on core data to inactive ValidationLogger.shared().logString("Threema Web: Stop all active sessions") for publicKey in running { if let session = sessions[publicKey] { session.stop(close: true, forget: false, sendDisconnect: true, reason: .stop) } } removeObservers() } /** Stop and delete all running sessions. Clears the list of running sessions */ @objc public func stopAndForgetAllSessions() { // disconnect all active sessions and set all sessions on core data to inactive ValidationLogger.shared().logString("Threema Web: Stop and forget all active sessions") WebClientSessionStore.shared.setAllWebClientSessionsInactive() for publicKey in running { if let session = sessions[publicKey] { session.stop(close: true, forget: true, sendDisconnect: true, reason: .stop) } } } /** Stop specific session. */ public func stopSession(_ webClientSession: WebClientSession) { ValidationLogger.shared().logString("Threema Web: Stop session") if let session: WCSession = sessions[webClientSession.initiatorPermanentPublicKey!] { session.stop(close: true, forget: false, sendDisconnect: true, reason: .stop) } } /** Stop and delete specific session. */ public func stopAndDeleteSession(_ webClientSession: WebClientSession) { ValidationLogger.shared().logString("Threema Web: Stop and delete all active sessions") let publicKey = webClientSession.initiatorPermanentPublicKey! if let session: WCSession = sessions[publicKey] { session.stop(close: true, forget: true, sendDisconnect: true, reason: .delete) sessions.removeValue(forKey: publicKey) } WebClientSessionStore.shared.deleteWebClientSession(webClientSession) } /** Remove WebClientSession from running list. */ public func removeWCSessionFromRunning(_ session: WCSession) { if let publicKey = session.webClientSession?.initiatorPermanentPublicKey { runningSessionsQueue.sync { if let index = running.firstIndex(of: publicKey) { running.remove(at: index) } WebClientSessionStore.shared.updateWebClientSession(session: session.webClientSession!, active: false) } } if running.count == 0 { ServerConnector.shared().resetPushOverrideTimeout() } } /** Remove all not permanent sessions. */ public func removeAllNotPermanentSessions() { //Remove all not permanent sessions WebClientSessionStore.shared.removeAllNotPermanentSessions() } @objc public func isRunningWCSession() -> Bool { return running.count > 0 } public func pauseAllRunningSessions() { ValidationLogger.shared().logString("Threema Web: Pause all sessions") sendConnectionAckToAllActiveSessions() for publicKey in running { if let session = sessions[publicKey] { session.stop(close: false, forget: false, sendDisconnect: false, reason: .pause) } } saveSessionsToArchive() } @objc public func updateConversationPushSetting(conversation: Conversation) { for publicKey in running { if let session = sessions[publicKey] { let conversationResponse = WebConversationUpdate.init(conversation: conversation, objectMode: .modified, session: session) session.messageQueue.enqueue(data: conversationResponse.messagePack(), blackListed: false) } } } } extension WCSessionManager { // MARK: Private functions private func addWCSessionToRunning(webClientSession: WebClientSession) { runningSessionsQueue.sync { if !running.contains(webClientSession.initiatorPermanentPublicKey!) { running.append(webClientSession.initiatorPermanentPublicKey!) WebClientSessionStore.shared.updateWebClientSession(session: webClientSession, active: true) } } } private func canConnectToWebClient(completionHandler: @escaping ((_ isValid: Bool) -> Void)) { if UserSettings.shared().threemaWeb { if LicenseStore.shared().isValid() == true { completionHandler(true) } else { LicenseStore.shared()?.performLicenseCheck(completion: { (success) in if success == true { completionHandler(true) } else { ValidationLogger.shared()?.logString("Threema Web: LicenseStore is invalid, stop all sessions") self.stopAllSessions() completionHandler(false) } }) } } else { ValidationLogger.shared()?.logString("Threema Web: LicenseStore is invalid, stop all sessions") self.stopAllSessions() completionHandler(false) } } private func sendConnectionAckToAllActiveSessions() { for publicKey in running { if let session = sessions[publicKey] { if let context = session.connectionContext() { context.sendConnectionAck() } } } } private func sendMessagePackToAllActiveSessions(messagePack: Data, blackListed: Bool) { for publicKey in running { if let session = sessions[publicKey] { session.sendMessageToWeb(blacklisted: blackListed, msgpack: messagePack) } } } private func sendMessagePackToAllActiveSessions(with requestedConversationId: String, messagePack: Data, blackListed: Bool) { for publicKey in running { if let session = sessions[publicKey] { if session.requestedConversations(contains: requestedConversationId) == true { session.sendMessageToWeb(blacklisted: blackListed, msgpack: messagePack) } } } } private func sendMessagePackToRequestedSession(with requestId: String, messagePack: Data, blackListed: Bool) { for publicKey in running { if let session = sessions[publicKey] { if (session.requestMessage(for: requestId) != nil) { session.sendMessageToWeb(blacklisted: blackListed, msgpack: messagePack) } } } } private func responseUpdateContact(contact: Contact, objectMode: WebReceiverUpdate.ObjectMode) { let receiverUpdate = WebReceiverUpdate.init(updatedContact: contact, objectMode: objectMode) DDLogVerbose("Threema Web: MessagePack -> Send update/receiver") sendMessagePackToAllActiveSessions(messagePack: receiverUpdate.messagePack(), blackListed: false) } private func responseUpdateAvatar(contact: Contact?, groupProxy: GroupProxy?) { if contact != nil { let avatarUpdate = WebAvatarUpdate.init(contact: contact!) DDLogVerbose("Threema Web: MessagePack -> Send update/avatar") sendMessagePackToAllActiveSessions(messagePack: avatarUpdate.messagePack(), blackListed: false) } else if groupProxy != nil { let avatarUpdate = WebAvatarUpdate.init(groupProxy: groupProxy!) DDLogVerbose("Threema Web: MessagePack -> Send update/avatar") sendMessagePackToAllActiveSessions(messagePack: avatarUpdate.messagePack(), blackListed: false) } } private func responseUpdateMessage(with requestedConversationId: String, message: BaseMessage, conversation: Conversation, objectMode: WebMessagesUpdate.ObjectMode, exclude requestId: String) { for publicKey in running { if let session = sessions[publicKey] { if session.requestedConversations(contains: requestedConversationId) == true { if (session.requestMessage(for: requestId) == nil) { sendResponseUpdateMessage(message: message, conversation: conversation, objectMode: objectMode, session: session) } } } } } private func responseUpdateMessage(with requestedConversationId: String, message: BaseMessage, conversation: Conversation, objectMode: WebMessagesUpdate.ObjectMode) { for publicKey in running { if let session = sessions[publicKey] { if session.requestedConversations(contains: requestedConversationId) == true { sendResponseUpdateMessage(message: message, conversation: conversation, objectMode: objectMode, session: session) } } } } private func sendResponseUpdateMessage(message: BaseMessage, conversation: Conversation, objectMode: WebMessagesUpdate.ObjectMode, session: WCSession) { if objectMode == .removed { let messageUpdate: WebMessagesUpdate = WebMessagesUpdate.init(baseMessage: message, conversation: conversation, objectMode: objectMode, session: session) DDLogVerbose("Threema Web: MessagePack -> Send update/messages") session.sendMessageToWeb(blacklisted: false, msgpack: messageUpdate.messagePack()) } else { let messageUpdate: WebMessagesUpdate = WebMessagesUpdate.init(baseMessage: message, conversation: conversation, objectMode: objectMode, session: session) DDLogVerbose("Threema Web: MessagePack -> Send update/messages") session.sendMessageToWeb(blacklisted: false, msgpack: messageUpdate.messagePack()) } } private func responseUpdateConversation(conversation: Conversation, objectMode: WebConversationUpdate.ObjectMode) { for publicKey in running { if let session = sessions[publicKey] { let conversationResponse = WebConversationUpdate.init(conversation: conversation, objectMode: objectMode, session: session) DDLogVerbose("Threema Web: MessagePack -> Send update/conversation") session.sendMessageToWeb(blacklisted: false, msgpack: conversationResponse.messagePack()) } } } private func responseUpdateGroup(group: GroupProxy, objectMode: WebReceiverUpdate.ObjectMode) { let receiverUpdate = WebReceiverUpdate.init(updatedGroup: group, objectMode: objectMode) DDLogVerbose("Threema Web: MessagePack -> Send update/receiver") sendMessagePackToAllActiveSessions(messagePack: receiverUpdate.messagePack(), blackListed: false) } private func responseUpdateTyping(identity: String, isTyping: Bool) { let typingUpdate = WebTypingUpdate.init(identity: identity, typing: isTyping) DDLogVerbose("Threema Web: MessagePack -> Send update/typing") sendMessagePackToAllActiveSessions(messagePack: typingUpdate.messagePack(), blackListed: true) } private func responseUpdateDeletedConversation(conversation: Conversation, contact: Contact?, objectMode: WebConversationUpdate.ObjectMode) { let conversationResponse = WebConversationUpdate.init(conversation: conversation, contact: contact, objectMode: objectMode) DDLogVerbose("Threema Web: MessagePack -> Send update/conversation") sendMessagePackToAllActiveSessions(messagePack: conversationResponse.messagePack(), blackListed: false) } private func webRequestMessage(for requestId: String) -> WebAbstractMessage? { for publicKey in running { if let session = sessions[publicKey] { if let webAbstractMessage = session.requestMessage(for: requestId) { return webAbstractMessage } } } return nil } private func removeWebRequestMessage(with requestId: String) { for publicKey in running { if let session = sessions[publicKey] { if (session.requestMessage(for: requestId) != nil) { session.removeRequestCreateMessage(requestId: requestId) } } } } } extension WCSessionManager { // MARK: Database Observer @objc func managedObjectContextDidChange(notification: NSNotification) { let managedObjectContext = notification.object as! NSManagedObjectContext // MARK: update self.handleUpdatedObjects(updatedObjects: managedObjectContext.updatedObjects) // MARK: inserted self.handleInsertedObjects(insertedObjects: managedObjectContext.insertedObjects) // MARK: deleted self.handleDeletedObjects(deletedObjects: managedObjectContext.deletedObjects) } private func handleUpdatedObjects(updatedObjects: Set) { for managedObject in updatedObjects { switch managedObject { case is Contact: self.updateContact(managedObject as! Contact) case is BaseMessage: self.updateBaseMessage(managedObject as! BaseMessage) case is Conversation: self.updateConversation(managedObject as! Conversation) default: break } } } private func handleInsertedObjects(insertedObjects: Set) { let backgroundKey = kAppAckBackgroundTask + SwiftUtils.pseudoRandomString(length: 10) BackgroundTaskManager.shared.newBackgroundTask(key: backgroundKey, timeout: Int(kAppCoreDataProcessMessageBackgroundTaskTime)) { for managedObject in insertedObjects { switch managedObject { case is Contact: let contact = managedObject as! Contact if contact.identity != nil && contact.publicKey != nil { let objectMode: WebReceiverUpdate.ObjectMode = .new self.responseUpdateContact(contact: contact, objectMode: objectMode) } case is BaseMessage: self.insertBaseMessage(managedObject as! BaseMessage) case is Conversation: self.insertConversation(managedObject as! Conversation) default: break } } BackgroundTaskManager.shared.cancelBackgroundTask(key: backgroundKey) } } private func updateContact(_ contact: Contact) { let changedValues = contact.changedValues() let backgroundKey = kAppAckBackgroundTask + SwiftUtils.pseudoRandomString(length: 10) BackgroundTaskManager.shared.newBackgroundTask(key: backgroundKey, timeout: Int(kAppCoreDataProcessMessageBackgroundTaskTime)) { if (contact.identity != nil) && (changedValues.keys.contains("firstName") || changedValues.keys.contains("lastName") || changedValues.keys.contains("displayName") || changedValues.keys.contains("publicNickname") || changedValues.keys.contains("verificationLevel") || changedValues.keys.contains("state") || changedValues.keys.contains("featureLevel") || changedValues.keys.contains("workContact") || changedValues.keys.contains("hidden")) { let objectMode: WebReceiverUpdate.ObjectMode = .modified self.responseUpdateContact(contact: contact, objectMode: objectMode) } if contact.changedValues().keys.contains("contactImage") || contact.changedValues().keys.contains("imageData") { self.responseUpdateAvatar(contact: contact, groupProxy: nil) } BackgroundTaskManager.shared.cancelBackgroundTask(key: backgroundKey) } } private func updateBaseMessage(_ baseMessage: BaseMessage) { let changedValues = baseMessage.changedValuesForCurrentEvent() let backgroundKey = kAppAckBackgroundTask + SwiftUtils.pseudoRandomString(length: 10) BackgroundTaskManager.shared.newBackgroundTask(key: backgroundKey, timeout: Int(kAppCoreDataProcessMessageBackgroundTaskTime)) { guard let conversation = baseMessage.conversation else { return } let identity = conversation.isGroup() ? conversation.groupId.hexEncodedString() : self.baseMessageIdentity(baseMessage) self.processBaseMessageUpdate(baseMessage: baseMessage, changedValues: changedValues, identity: identity) if let lastMessage = conversation.lastMessage, lastMessage.id == baseMessage.id { if self.shouldSendUpdate(changedValues: changedValues) { let objectMode: WebConversationUpdate.ObjectMode = .modified self.responseUpdateConversation(conversation: conversation, objectMode: objectMode) // background task to send ack to server let backgroundKey = kAppAckBackgroundTask + baseMessage.id.hexEncodedString() BackgroundTaskManager.shared.newBackgroundTask(key: backgroundKey, timeout: Int(kAppAckBackgroundTaskTime), completionHandler: nil) } } BackgroundTaskManager.shared.cancelBackgroundTask(key: backgroundKey) } } private func processBaseMessageUpdate(baseMessage: BaseMessage, changedValues: [String : Any], identity: String) { if shouldSendUpdate(changedValues: changedValues) { let objectMode: WebMessagesUpdate.ObjectMode = .modified self.responseUpdateMessage(with: identity, message: baseMessage, conversation: baseMessage.conversation, objectMode: objectMode) // background task to send ack to server let backgroundKey = kAppAckBackgroundTask + baseMessage.id.hexEncodedString() BackgroundTaskManager.shared.newBackgroundTask(key: backgroundKey, timeout: Int(kAppAckBackgroundTaskTime), completionHandler:nil) } } private func shouldSendUpdate(changedValues: [String: Any]) -> Bool { if changedValues.count == 1 { let changedValueDict = changedValues.first if changedValueDict?.key == "progress" { return false } } else if changedValues.count == 0 { return false } return true } private func updateConversation(_ conversation: Conversation) { let changedValues = conversation.changedValues() let changedValuesForCurrentEvent = conversation.changedValuesForCurrentEvent() let backgroundKey = kAppAckBackgroundTask + SwiftUtils.pseudoRandomString(length: 10) BackgroundTaskManager.shared.newBackgroundTask(key: backgroundKey, timeout: Int(kAppCoreDataProcessMessageBackgroundTaskTime)) { if conversation.isGroup() { if changedValues.keys.contains("groupName") || changedValues.keys.contains("members") { let objectMode: WebReceiverUpdate.ObjectMode = .modified let groupProxy = GroupProxy.init(for: conversation) self.responseUpdateGroup(group: groupProxy!, objectMode: objectMode) } else if changedValues.keys.contains("groupImage") { let groupProxy = GroupProxy.init(for: conversation) self.responseUpdateAvatar(contact: nil, groupProxy: groupProxy) } } else if changedValuesForCurrentEvent.keys.contains("typing") { self.responseUpdateTyping(identity: conversation.contact.identity, isTyping: conversation.typing.boolValue) } if (changedValues.keys.contains("lastMessage") || changedValues.keys.contains("marked") || changedValues.keys.contains("unreadMessageCount")) && conversation.lastMessage != nil { let objectMode: WebConversationUpdate.ObjectMode = .modified self.responseUpdateConversation(conversation: conversation, objectMode: objectMode) } BackgroundTaskManager.shared.cancelBackgroundTask(key: backgroundKey) } } private func insertConversation(_ conversation: Conversation) { if conversation.isGroup() { let receiverObjectMode: WebReceiverUpdate.ObjectMode = .new let groupProxy = GroupProxy.init(for: conversation) self.responseUpdateGroup(group: groupProxy!, objectMode: receiverObjectMode) } let objectMode: WebConversationUpdate.ObjectMode = .new self.responseUpdateConversation(conversation: conversation, objectMode: objectMode) } private func insertBaseMessage(_ baseMessage: BaseMessage) { if let conversation = baseMessage.conversation { var id: String if conversation.isGroup() { id = conversation.groupId.hexEncodedString() } else { if let sender = baseMessage.sender { id = sender.identity } else { if let contact = conversation.contact { id = contact.identity } else { id = MyIdentityStore.shared().identity } } } switch baseMessage { case is TextMessage: self.processTextMessageResponse(baseMessage, id) case is SystemMessage: self.processSystemMessageResponse(baseMessage, id) case is FileMessage, is ImageMessage, is VideoMessage, is AudioMessage: self.processFileMessageResponse(baseMessage, id) default: break } } } private func processTextMessageResponse(_ baseMessage: BaseMessage, _ id: String) { var createTextMessageResponse: WebCreateTextMessageResponse? if baseMessage.webRequestId != nil { if let createTextMessageRequest = self.webRequestMessage(for: baseMessage.webRequestId) as? WebCreateTextMessageRequest { createTextMessageResponse = WebCreateTextMessageResponse.init(message: baseMessage, request: createTextMessageRequest) } } let objectMode: WebMessagesUpdate.ObjectMode = .new if createTextMessageResponse != nil && baseMessage.webRequestId != nil { DDLogVerbose("Threema Web: MessagePack -> Send create/textMessage") self.sendMessagePackToRequestedSession(with: baseMessage.webRequestId!, messagePack: createTextMessageResponse!.messagePack(), blackListed: false) self.responseUpdateMessage(with: id, message: baseMessage, conversation: baseMessage.conversation, objectMode: objectMode, exclude: baseMessage.webRequestId!) self.removeWebRequestMessage(with: baseMessage.webRequestId!) } else { self.responseUpdateMessage(with: id, message: baseMessage, conversation: baseMessage.conversation, objectMode: objectMode) } // background task to send ack to server let backgroundKey = kAppAckBackgroundTask + baseMessage.id.hexEncodedString() BackgroundTaskManager.shared.newBackgroundTask(key: backgroundKey, timeout: Int(kAppAckBackgroundTaskTime), completionHandler: nil) } private func processSystemMessageResponse(_ baseMessage: BaseMessage, _ id: String) { let objectMode: WebMessagesUpdate.ObjectMode = .new self.responseUpdateMessage(with: id, message: baseMessage, conversation: baseMessage.conversation, objectMode: objectMode) // background task to send ack to server let backgroundKey = kAppAckBackgroundTask + baseMessage.id.hexEncodedString() BackgroundTaskManager.shared.newBackgroundTask(key: backgroundKey, timeout: Int(kAppAckBackgroundTaskTime), completionHandler: nil) } private func processFileMessageResponse(_ baseMessage: BaseMessage, _ id: String) { var createFileMessageResponse: WebCreateFileMessageResponse? var backgroundIdentifier: String? if baseMessage.webRequestId != nil { if let createFileMessageRequest = self.webRequestMessage(for: baseMessage.webRequestId) as? WebCreateFileMessageRequest { createFileMessageRequest.ack = WebAbstractMessageAcknowledgement.init(baseMessage.webRequestId, true, nil) createFileMessageResponse = WebCreateFileMessageResponse.init(message: baseMessage, request: createFileMessageRequest) if let bgIdentifier = createFileMessageRequest.backgroundIdentifier { backgroundIdentifier = bgIdentifier } } } let objectMode: WebMessagesUpdate.ObjectMode = .new if createFileMessageResponse != nil && baseMessage.webRequestId != nil { DDLogVerbose("Threema Web: MessagePack -> Send create/fileMessage") self.sendMessagePackToRequestedSession(with: baseMessage.webRequestId!, messagePack: createFileMessageResponse!.messagePack(), blackListed: false) self.responseUpdateMessage(with: id, message: baseMessage, conversation: baseMessage.conversation, objectMode: objectMode, exclude: baseMessage.webRequestId!) self.removeWebRequestMessage(with: baseMessage.webRequestId!) } else { self.responseUpdateMessage(with: id, message: baseMessage, conversation: baseMessage.conversation, objectMode: objectMode) } // background task to send ack to server let backgroundKey = kAppAckBackgroundTask + baseMessage.id.hexEncodedString() BackgroundTaskManager.shared.newBackgroundTask(key: backgroundKey, timeout: Int(kAppAckBackgroundTaskTime), completionHandler: nil) if backgroundIdentifier != nil { BackgroundTaskManager.shared.cancelBackgroundTask(key: backgroundIdentifier!) } } private func handleDeletedObjects(deletedObjects: Set) { let backgroundKey = kAppAckBackgroundTask + SwiftUtils.pseudoRandomString(length: 10) BackgroundTaskManager.shared.newBackgroundTask(key: backgroundKey, timeout: Int(kAppCoreDataProcessMessageBackgroundTaskTime)) { for managedObject in deletedObjects { if managedObject is Conversation { let conversation = managedObject as! Conversation let objectMode: WebConversationUpdate.ObjectMode = .removed let contact: Contact? = conversation.changedValuesForCurrentEvent()["contact"] as? Contact self.responseUpdateDeletedConversation(conversation: conversation, contact: contact, objectMode: objectMode) } else if managedObject is Contact { let contact = managedObject as! Contact if contact.identity != nil && contact.publicKey != nil { let objectMode: WebReceiverUpdate.ObjectMode = .removed self.responseUpdateContact(contact: contact, objectMode: objectMode) } } else if managedObject is BaseMessage { let baseMessage = managedObject as! BaseMessage let identity: String? var conversation: Conversation? = nil if baseMessage.changedValues().keys.contains("conversation") && baseMessage.changedValuesForCurrentEvent()["conversation"] != nil { conversation = baseMessage.changedValuesForCurrentEvent()["conversation"] as? Conversation } if baseMessage.conversation != nil { conversation = baseMessage.conversation } if conversation != nil { if baseMessage.sender != nil { identity = baseMessage.sender.identity } else { if conversation!.contact != nil { identity = conversation!.contact.identity } else { identity = MyIdentityStore.shared().identity } } if conversation!.isGroup() { let objectMode: WebMessagesUpdate.ObjectMode = .removed self.responseUpdateMessage(with: conversation!.groupId.hexEncodedString(), message: baseMessage, conversation: conversation!, objectMode: objectMode) } if identity != nil { if !conversation!.isGroup() { let objectMode: WebMessagesUpdate.ObjectMode = .removed self.responseUpdateMessage(with: identity!, message: baseMessage, conversation: conversation!, objectMode: objectMode) } } } } } BackgroundTaskManager.shared.cancelBackgroundTask(key: backgroundKey) } } private func baseMessageIdentity(_ baseMessage: BaseMessage) -> String { if let sender = baseMessage.sender { return sender.identity } if let contact = baseMessage.conversation.contact { return contact.identity } return MyIdentityStore.shared().identity } } extension WCSessionManager { // MARK: BatteryNotifications @objc func batteryLevelDidChange(_ notification: Notification) { let batteryResponse = WebBatteryStatusUpdate.init() DDLogVerbose("Threema Web: MessagePack -> Send update/batteryStatus") sendMessagePackToAllActiveSessions(messagePack: batteryResponse.messagePack(), blackListed: true) } @objc func batteryStateDidChange(_ notification: Notification) { let batteryResponse = WebBatteryStatusUpdate.init() DDLogVerbose("Threema Web: MessagePack -> Send update/batteryStatus") sendMessagePackToAllActiveSessions(messagePack: batteryResponse.messagePack(), blackListed: true) } // MARK: ProfileNotifications @objc func profileNicknameChanged(_ notification: Notification) { let profileUpdate = WebProfileUpdate.init(nicknameChanged: true, newNickname: MyIdentityStore.shared().pushFromName, newAvatar: nil, deleteAvatar: false) DDLogVerbose("Threema Web: MessagePack -> Send update/profile") sendMessagePackToAllActiveSessions(messagePack: profileUpdate.messagePack(), blackListed: false) } @objc func profilePictureChanged(_ notification: Notification) { var profileUpdate: WebProfileUpdate? if let profileDict = MyIdentityStore.shared().profilePicture as? [AnyHashable: Any] { if let pictureData = profileDict["ProfilePicture"] { profileUpdate = WebProfileUpdate.init(nicknameChanged: false, newNickname: nil, newAvatar: pictureData as? Data, deleteAvatar: false) } else { profileUpdate = WebProfileUpdate.init(nicknameChanged: false, newNickname: nil, newAvatar: nil, deleteAvatar: true) } } else { profileUpdate = WebProfileUpdate.init(nicknameChanged: false, newNickname: nil, newAvatar: nil, deleteAvatar: true) } DDLogVerbose("Threema Web: MessagePack -> Send update/profile") sendMessagePackToAllActiveSessions(messagePack: profileUpdate!.messagePack(), blackListed: false) } @objc func blackListChanged(_ notification: Notification) { let identity = notification.object as! String var contact: Contact? let entityManager = EntityManager() contact = entityManager.entityFetcher.contact(forId: identity) if contact != nil { if contact!.identity != nil && contact!.publicKey != nil { let objectMode: WebReceiverUpdate.ObjectMode = .modified self.responseUpdateContact(contact: contact!, objectMode: objectMode) } } } @objc func refreshDirtyObjects(_ notification: Notification) { if let objectID = notification.userInfo?[kKeyObjectID] as? NSManagedObjectID { let entityManager = EntityManager() if let managedObject = entityManager.entityFetcher.getManagedObject(by: objectID) as? NSManagedObject { var insertedObjects = Set() insertedObjects.insert(managedObject) self.handleInsertedObjects(insertedObjects: insertedObjects) } } } }