From 75b184c0b90f8f490a11219413136afe55800fc4 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Tue, 1 Dec 2020 19:45:42 +1100 Subject: [PATCH] Use new message receiving pipeline in PN extension --- Session/Signal/AppDelegate.m | 11 - .../MessageReceiver+Handling.swift | 31 +-- .../Sending & Receiving/MessageReceiver.swift | 10 +- ...ionPushNotificationExtension.entitlements} | 0 .../NotificationServiceExtension.swift | 226 ++++++------------ Signal.xcodeproj/project.pbxproj | 8 +- 6 files changed, 98 insertions(+), 188 deletions(-) rename SessionPushNotificationExtension/Meta/{LokiPushNotificationService.entitlements => SessionPushNotificationExtension.entitlements} (100%) diff --git a/Session/Signal/AppDelegate.m b/Session/Signal/AppDelegate.m index 09bc074f7..421579e60 100644 --- a/Session/Signal/AppDelegate.m +++ b/Session/Signal/AppDelegate.m @@ -511,17 +511,6 @@ static NSTimeInterval launchStartedAt; [Environment.shared.preferences setHasGeneratedThumbnails:YES]; }]; } - -#ifdef DEBUG - // A bug in orphan cleanup could be disastrous so let's only - // run it in DEBUG builds for a few releases. - // - // TODO: Release to production once we have analytics. - // TODO: Orphan cleanup is somewhat expensive - not least in doing a bunch - // TODO: of disk access. We might want to only run it "once per version" - // TODO: or something like that in production. - [OWSOrphanDataCleaner auditOnLaunchIfNecessary]; -#endif [self.readReceiptManager prepareCachedValues]; diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift index 1647ff4c7..85f3626b7 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -7,7 +7,7 @@ extension MessageReceiver { return SSKEnvironment.shared.blockingManager.isRecipientIdBlocked(publicKey) } - internal static func handle(_ message: Message, associatedWithProto proto: SNProtoContent, openGroupID: String?, using transaction: Any) throws { + public static func handle(_ message: Message, associatedWithProto proto: SNProtoContent, openGroupID: String?, using transaction: Any) throws { switch message { case let message as ReadReceipt: handleReadReceipt(message, using: transaction) case let message as TypingIndicator: handleTypingIndicator(message, using: transaction) @@ -135,7 +135,8 @@ extension MessageReceiver { SSKEnvironment.shared.disappearingMessagesJob.startIfNecessary() } - private static func handleVisibleMessage(_ message: VisibleMessage, associatedWithProto proto: SNProtoContent, openGroupID: String?, using transaction: Any) throws { + @discardableResult + public static func handleVisibleMessage(_ message: VisibleMessage, associatedWithProto proto: SNProtoContent, openGroupID: String?, using transaction: Any) throws -> String { let storage = Configuration.shared.storage let transaction = transaction as! YapDatabaseReadWriteTransaction // Parse & persist attachments @@ -188,21 +189,23 @@ extension MessageReceiver { groupPublicKey: message.groupPublicKey, openGroupID: openGroupID, using: transaction) else { throw Error.noThread } message.threadID = threadID // Start attachment downloads if needed - storage.withAsync({ transaction in - attachmentsToDownload.forEach { attachmentID in - let downloadJob = AttachmentDownloadJob(attachmentID: attachmentID, tsIncomingMessageID: tsIncomingMessageID) - if CurrentAppContext().isMainAppAndActive { // This has to be called from the main thread - JobQueue.shared.add(downloadJob, using: transaction) - } else { - JobQueue.shared.addWithoutExecuting(downloadJob, using: transaction) - } + attachmentsToDownload.forEach { attachmentID in + let downloadJob = AttachmentDownloadJob(attachmentID: attachmentID, tsIncomingMessageID: tsIncomingMessageID) + if CurrentAppContext().isMainAppAndActive { + JobQueue.shared.add(downloadJob, using: transaction) + } else { + JobQueue.shared.addWithoutExecuting(downloadJob, using: transaction) } - }, completion: { }) - // Cancel any typing indicators - cancelTypingIndicatorsIfNeeded(for: message.sender!) + } + // Cancel any typing indicators if needed + if CurrentAppContext().isMainAppAndActive { + cancelTypingIndicatorsIfNeeded(for: message.sender!) + } // Notify the user if needed - guard let tsIncomingMessage = TSIncomingMessage.fetch(uniqueId: tsIncomingMessageID, transaction: transaction), let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction) else { return } + guard CurrentAppContext().isMainAppAndActive, let tsIncomingMessage = TSIncomingMessage.fetch(uniqueId: tsIncomingMessageID, transaction: transaction), + let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction) else { return tsIncomingMessageID } SSKEnvironment.shared.notificationsManager!.notifyUser(for: tsIncomingMessage, in: thread, transaction: transaction) + return tsIncomingMessageID } private static func handleClosedGroupUpdate(_ message: ClosedGroupUpdate, using transaction: Any) { diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift index d24b42839..e31b34707 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift @@ -1,8 +1,8 @@ import SessionUtilitiesKit -internal enum MessageReceiver { +public enum MessageReceiver { - internal enum Error : LocalizedError { + public enum Error : LocalizedError { case duplicateMessage case invalidMessage case unknownMessage @@ -17,14 +17,14 @@ internal enum MessageReceiver { case noGroupPrivateKey case sharedSecretGenerationFailed - internal var isRetryable: Bool { + public var isRetryable: Bool { switch self { case .duplicateMessage, .invalidMessage, .unknownMessage, .unknownEnvelopeType, .noData, .senderBlocked, .selfSend: return false default: return true } } - internal var errorDescription: String? { + public var errorDescription: String? { switch self { case .duplicateMessage: return "Duplicate message." case .invalidMessage: return "Invalid message." @@ -43,7 +43,7 @@ internal enum MessageReceiver { } } - internal static func parse(_ data: Data, openGroupMessageServerID: UInt64?, using transaction: Any) throws -> (Message, SNProtoContent) { + public static func parse(_ data: Data, openGroupMessageServerID: UInt64?, using transaction: Any) throws -> (Message, SNProtoContent) { let userPublicKey = Configuration.shared.storage.getUserPublicKey() let isOpenGroupMessage = (openGroupMessageServerID != nil) // Parse the envelope diff --git a/SessionPushNotificationExtension/Meta/LokiPushNotificationService.entitlements b/SessionPushNotificationExtension/Meta/SessionPushNotificationExtension.entitlements similarity index 100% rename from SessionPushNotificationExtension/Meta/LokiPushNotificationService.entitlements rename to SessionPushNotificationExtension/Meta/SessionPushNotificationExtension.entitlements diff --git a/SessionPushNotificationExtension/NotificationServiceExtension.swift b/SessionPushNotificationExtension/NotificationServiceExtension.swift index 509e7adb7..93a561056 100644 --- a/SessionPushNotificationExtension/NotificationServiceExtension.swift +++ b/SessionPushNotificationExtension/NotificationServiceExtension.swift @@ -1,158 +1,75 @@ import UserNotifications +import SessionMessagingKit import SignalUtilitiesKit +// TODO: Group notifications + final class NotificationServiceExtension : UNNotificationServiceExtension { - static let isFromRemoteKey = "remote" - static let threadIdKey = "Signal.AppNotificationsUserInfoKey.threadId" - private var didPerformSetup = false + private var areVersionMigrationsComplete = false + private var contentHandler: ((UNNotificationContent) -> Void)? + private var notificationContent: UNMutableNotificationContent? - var areVersionMigrationsComplete = false - var contentHandler: ((UNNotificationContent) -> Void)? - var notificationContent: UNMutableNotificationContent? + private static let isFromRemoteKey = "remote" + private static let threadIdKey = "Signal.AppNotificationsUserInfoKey.threadId" override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler - notificationContent = request.content.mutableCopy() as? UNMutableNotificationContent - + self.notificationContent = request.content.mutableCopy() as? UNMutableNotificationContent + + // Abort if the main app is running var isMainAppActive = false if let sharedUserDefaults = UserDefaults(suiteName: "group.com.loki-project.loki-messenger") { isMainAppActive = sharedUserDefaults.bool(forKey: "isMainAppActive") } - // If the main app is running, skip the whole process - guard !isMainAppActive else { return self.completeWithFailure(content: notificationContent!) } - - // The code using DispatchQueue.main.async { self.setUpIfNecessary() { Modify the notification content } } will somehow cause a freeze when a second PN comes - - DispatchQueue.main.sync { self.setUpIfNecessary() {} } - + guard !isMainAppActive else { return self.handleFailure(for: notificationContent!) } + + // Perform main setup + DispatchQueue.main.sync { self.setUpIfNecessary() { } } + + // Handle the push notification AppReadiness.runNowOrWhenAppDidBecomeReady { - if let notificationContent = self.notificationContent { - // Modify the notification content here... - let base64EncodedData = notificationContent.userInfo["ENCRYPTED_DATA"] as! String - let data = Data(base64Encoded: base64EncodedData)! - if let envelope = try? MessageWrapper.unwrap(data: data), let data = try? envelope.serializedData() { - // TODO TODO TODO - - /* - decrypter.decryptEnvelope(envelope, - envelopeData: data, - successBlock: { result, transaction in - if let envelope = try? SNProtoEnvelope.parseData(result.envelopeData) { - messageManager.throws_processEnvelope(envelope, plaintextData: result.plaintextData, wasReceivedByUD: wasReceivedByUD, transaction: transaction, serverID: 0) - self.handleDecryptionResult(result: result, notificationContent: notificationContent, transaction: transaction) - } else { - self.completeWithFailure(content: notificationContent) - } - }, - failureBlock: { - self.completeWithFailure(content: notificationContent) - } - ) - */ - } else { - self.completeWithFailure(content: notificationContent) + let notificationContent = self.notificationContent! + guard let base64EncodedData = notificationContent.userInfo["ENCRYPTED_DATA"] as! String?, let data = Data(base64Encoded: base64EncodedData), + let envelope = try? MessageWrapper.unwrap(data: data), let envelopeAsData = try? envelope.serializedData() else { + return self.handleFailure(for: notificationContent) + } + Storage.write { transaction in // Intentionally capture self + do { + let (message, proto) = try MessageReceiver.parse(envelopeAsData, openGroupMessageServerID: nil, using: transaction) + guard let visibleMessage = message as? VisibleMessage else { + return self.handleFailure(for: notificationContent) + } + let tsIncomingMessageID = try MessageReceiver.handleVisibleMessage(visibleMessage, associatedWithProto: proto, openGroupID: nil, using: transaction) + guard let tsIncomingMessage = TSIncomingMessage.fetch(uniqueId: tsIncomingMessageID, transaction: transaction) else { + return self.handleFailure(for: notificationContent) + } + let snippet = tsIncomingMessage.previewText(with: transaction).filterForDisplay + let userInfo: [String:Any] = [ NotificationServiceExtension.threadIdKey : tsIncomingMessage.thread.uniqueId!, NotificationServiceExtension.isFromRemoteKey : true ] + let senderPublicKey = message.sender! + let senderDisplayName = OWSProfileManager.shared().profileNameForRecipient(withID: senderPublicKey, avoidWriteTransaction: true) ?? senderPublicKey + notificationContent.userInfo = userInfo + notificationContent.badge = 1 + let notificationsPreference = Environment.shared.preferences!.notificationPreviewType() + switch notificationsPreference { + case .namePreview: + notificationContent.title = senderDisplayName + notificationContent.body = snippet! + case .nameNoPreview: + notificationContent.title = senderDisplayName + notificationContent.body = "New Message" + case .noNameNoPreview: + notificationContent.title = "Session" + notificationContent.body = "New Message" + default: break + } + self.handleSuccess(for: notificationContent) + } catch { + self.handleFailure(for: notificationContent) } } } } - - /* - func handleDecryptionResult(result: OWSMessageDecryptResult, notificationContent: UNMutableNotificationContent, transaction: YapDatabaseReadWriteTransaction) { - let contentProto = try? SNProtoContent.parseData(result.plaintextData!) - var thread: TSThread - var newNotificationBody = "" - let masterPublicKey = OWSPrimaryStorage.shared().getMasterHexEncodedPublicKey(for: result.source, in: transaction) ?? result.source - var displayName = OWSUserProfile.fetch(uniqueId: masterPublicKey, transaction: transaction)?.profileName ?? SSKEnvironment.shared.contactsManager.displayName(forPhoneIdentifier: masterPublicKey) - if let groupID = contentProto?.dataMessage?.group?.id { - thread = TSGroupThread.getOrCreateThread(withGroupId: groupID, groupType: .closedGroup, transaction: transaction) - var groupName = thread.name() - if groupName.count < 1 { - groupName = MessageStrings.newGroupDefaultTitle - } - let senderName = OWSUserProfile.fetch(uniqueId: masterPublicKey, transaction: transaction)?.profileName ?? SSKEnvironment.shared.contactsManager.displayName(forPhoneIdentifier: masterPublicKey) - displayName = String(format: NotificationStrings.incomingGroupMessageTitleFormat, senderName, groupName) - let group: SNProtoGroupContext = contentProto!.dataMessage!.group! - let oldGroupModel = (thread as! TSGroupThread).groupModel - let removedMembers = NSMutableSet(array: oldGroupModel.groupMemberIds) - let newGroupModel = TSGroupModel.init(title: group.name, - memberIds:group.members, - image: oldGroupModel.groupImage, - groupId: group.id, - groupType: oldGroupModel.groupType, - adminIds: group.admins) - removedMembers.minus(Set(newGroupModel.groupMemberIds)) - newGroupModel.removedMembers = removedMembers - switch contentProto?.dataMessage?.group?.type { - case .update: - newNotificationBody = oldGroupModel.getInfoStringAboutUpdate(to: newGroupModel, contactsManager: SSKEnvironment.shared.contactsManager) - break - case .quit: - let nameString = SSKEnvironment.shared.contactsManager.displayName(forPhoneIdentifier: masterPublicKey, transaction: transaction) - newNotificationBody = NSLocalizedString("GROUP_MEMBER_LEFT", comment: nameString) - break - default: - break - } - } else { - thread = TSContactThread.getOrCreateThread(withContactId: result.source, transaction: transaction) - } - let userInfo: [String:Any] = [ NotificationServiceExtension.threadIdKey : thread.uniqueId!, NotificationServiceExtension.isFromRemoteKey : true ] - notificationContent.title = displayName - notificationContent.userInfo = userInfo - notificationContent.badge = 1 - if let attachment = contentProto?.dataMessage?.attachments.last { - newNotificationBody = TSAttachment.emoji(forMimeType: attachment.contentType!) + "Attachment" - if let rawMessageBody = contentProto?.dataMessage?.body, rawMessageBody.count > 0 { - newNotificationBody += ": \(rawMessageBody)" - } - } - if newNotificationBody.count < 1 { - newNotificationBody = contentProto?.dataMessage?.body ?? "You've got a new message" - } - newNotificationBody = handleMentionIfNecessary(rawMessageBody: newNotificationBody, threadID: thread.uniqueId!, transaction: transaction) - - let notificationPreference = Environment.shared.preferences - if let notificationType = notificationPreference?.notificationPreviewType() { - switch notificationType { - case .nameNoPreview: - notificationContent.body = "New Message" - case .noNameNoPreview: - notificationContent.title = "" - notificationContent.body = "New Message" - default: - notificationContent.body = newNotificationBody - } - } else { - notificationContent.body = newNotificationBody - } - - if notificationContent.body.count < 1 { - self.completeWithFailure(content: notificationContent) - } else { - self.contentHandler!(notificationContent) - } - } - */ - - func handleMentionIfNecessary(rawMessageBody: String, threadID: String, transaction: YapDatabaseReadWriteTransaction) -> String { - var string = rawMessageBody - let regex = try! NSRegularExpression(pattern: "@[0-9a-fA-F]*", options: []) - var outerMatch = regex.firstMatch(in: string, options: .withoutAnchoringBounds, range: NSRange(location: 0, length: string.utf16.count)) - while let match = outerMatch { - let publicKey = String((string as NSString).substring(with: match.range).dropFirst()) // Drop the @ - let matchEnd: Int - let displayName: String? = OWSProfileManager.shared().profileNameForRecipient(withID: publicKey, transaction: transaction) - if let displayName = displayName { - string = (string as NSString).replacingCharacters(in: match.range, with: "@\(displayName)") - matchEnd = match.range.location + displayName.utf16.count - } else { - matchEnd = match.range.location + match.range.length - } - outerMatch = regex.firstMatch(in: string, options: .withoutAnchoringBounds, range: NSRange(location: matchEnd, length: string.utf16.count - matchEnd)) - } - return string - } func setUpIfNecessary(completion: @escaping () -> Void) { AssertIsOnMainThread() @@ -193,22 +110,19 @@ final class NotificationServiceExtension : UNNotificationServiceExtension { } ) - NotificationCenter.default.addObserver(self, - selector: #selector(storageIsReady), - name: .StorageIsReady, - object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(storageIsReady), name: .StorageIsReady, object: nil) } override func serviceExtensionTimeWillExpire() { // Called just before the extension will be terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. - if let contentHandler = contentHandler, let notificationContent = notificationContent { - contentHandler(notificationContent) - } - } - - func wasReceivedByUD(envelope: SNProtoEnvelope) -> Bool { - return (envelope.type == .unidentifiedSender && (!envelope.hasSource || envelope.source!.count < 1)) + let userInfo: [String:Any] = [ NotificationServiceExtension.isFromRemoteKey : true ] + let notificationContent = self.notificationContent! + notificationContent.userInfo = userInfo + notificationContent.badge = 1 + notificationContent.title = "Session" + notificationContent.body = "New Message" + handleSuccess(for: notificationContent) } @objc @@ -242,14 +156,18 @@ final class NotificationServiceExtension : UNNotificationServiceExtension { } func completeSilenty() { - contentHandler?(.init()) + contentHandler!(.init()) } - - func completeWithFailure(content: UNMutableNotificationContent) { - content.body = "You've got a new message" + + func handleSuccess(for content: UNMutableNotificationContent) { + contentHandler!(content) + } + + func handleFailure(for content: UNMutableNotificationContent) { + content.body = "New Message" content.title = "Session" - let userInfo: [String:Any] = [NotificationServiceExtension.isFromRemoteKey : true] + let userInfo: [String:Any] = [ NotificationServiceExtension.isFromRemoteKey : true ] content.userInfo = userInfo - contentHandler?(content) + contentHandler!(content) } } diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 41dd955a6..ed70acbbb 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -1282,7 +1282,7 @@ 7BC01A3B241F40AB00BC7C55 /* SessionPushNotificationExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SessionPushNotificationExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtension.swift; sourceTree = ""; }; 7BC01A3F241F40AB00BC7C55 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 7BDCFC0424206E7300641C39 /* LokiPushNotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = LokiPushNotificationService.entitlements; sourceTree = ""; }; + 7BDCFC0424206E7300641C39 /* SessionPushNotificationExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SessionPushNotificationExtension.entitlements; sourceTree = ""; }; 7BDCFC07242186E700641C39 /* NotificationServiceExtensionContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtensionContext.swift; sourceTree = ""; }; 7DD180F770F8518B4E8796F2 /* Pods-SessionUtilitiesKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionUtilitiesKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionUtilitiesKit/Pods-SessionUtilitiesKit.app store release.xcconfig"; sourceTree = ""; }; 8981C8F64D94D3C52EB67A2C /* Pods-SignalTests.test.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalTests.test.xcconfig"; path = "Pods/Target Support Files/Pods-SignalTests/Pods-SignalTests.test.xcconfig"; sourceTree = ""; }; @@ -2687,7 +2687,7 @@ isa = PBXGroup; children = ( 7BC01A3F241F40AB00BC7C55 /* Info.plist */, - 7BDCFC0424206E7300641C39 /* LokiPushNotificationService.entitlements */, + 7BDCFC0424206E7300641C39 /* SessionPushNotificationExtension.entitlements */, ); path = Meta; sourceTree = ""; @@ -5748,7 +5748,7 @@ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_ENTITLEMENTS = SessionPushNotificationExtension/Meta/LokiPushNotificationService.entitlements; + CODE_SIGN_ENTITLEMENTS = SessionPushNotificationExtension/Meta/SessionPushNotificationExtension.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; @@ -5818,7 +5818,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_ENTITLEMENTS = SessionPushNotificationExtension/Meta/LokiPushNotificationService.entitlements; + CODE_SIGN_ENTITLEMENTS = SessionPushNotificationExtension/Meta/SessionPushNotificationExtension.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic;