diff --git a/native/ios/NotificationService/NotificationService.mm b/native/ios/NotificationService/NotificationService.mm index be7a02838..ee4d98af3 100644 --- a/native/ios/NotificationService/NotificationService.mm +++ b/native/ios/NotificationService/NotificationService.mm @@ -1,263 +1,356 @@ #import "NotificationService.h" #import "Logger.h" #import "NotificationsCryptoModule.h" +#import "StaffUtils.h" #import "TemporaryMessageStorage.h" NSString *const backgroundNotificationTypeKey = @"backgroundNotifType"; NSString *const messageInfosKey = @"messageInfos"; NSString *const encryptedPayloadKey = @"encryptedPayload"; NSString *const encryptionFailureKey = @"encryptionFailure"; const std::string callingProcessName = "NSE"; // The context for this constant can be found here: // https://linear.app/comm/issue/ENG-3074#comment-bd2f5e28 int64_t const notificationRemovalDelay = (int64_t)(0.1 * NSEC_PER_SEC); CFStringRef newMessageInfosDarwinNotification = CFSTR("app.comm.darwin_new_message_infos"); @interface NotificationService () @property(nonatomic, strong) void (^contentHandler) (UNNotificationContent *contentToDeliver); @property(nonatomic, strong) UNMutableNotificationContent *bestAttemptContent; @end @implementation NotificationService - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler: (void (^)(UNNotificationContent *_Nonnull)) contentHandler { self.contentHandler = contentHandler; self.bestAttemptContent = [request.content mutableCopy]; + UNNotificationContent *publicUserContent = self.bestAttemptContent; + + // Step 1: notification decryption. if ([self shouldBeDecrypted:self.bestAttemptContent.userInfo]) { - @try { - [self decryptBestAttemptContent]; - } @catch (NSException *e) { - comm::Logger::log( - "NSE: Received exception: " + std::string([e.name UTF8String]) + - " with reason: " + std::string([e.reason UTF8String]) + - " during notification decryption"); - self.contentHandler([[UNNotificationContent alloc] init]); + std::string decryptErrorMessage; + try { + @try { + [self decryptBestAttemptContent]; + } @catch (NSException *e) { + decryptErrorMessage = "NSE: Received Obj-C exception: " + + std::string([e.name UTF8String]) + + " during notification decryption."; + } + } catch (const std::exception &e) { + decryptErrorMessage = + "NSE: Received C++ exception: " + std::string(e.what()) + + " during notification decryption."; + } + + if (decryptErrorMessage.size()) { + NSString *errorMessage = + [NSString stringWithUTF8String:decryptErrorMessage.c_str()]; + [self callContentHandlerOnErrorMessage:errorMessage + withPublicUserContent:[[UNNotificationContent alloc] + init]]; return; } } else if ([self shouldAlertUnencryptedNotification:self.bestAttemptContent .userInfo]) { // In future this will be replaced by notification content // modification for DEV environment and staff members comm::Logger::log("NSE: Received erroneously unencrypted notitication."); } - [self persistMessagePayload:self.bestAttemptContent.userInfo]; + NSMutableArray *errorMessages = [[NSMutableArray alloc] init]; + + // Step 2: notification persistence in a temporary storage + std::string persistErrorMessage; + try { + @try { + [self persistMessagePayload:self.bestAttemptContent.userInfo]; + } @catch (NSException *e) { + persistErrorMessage = + "Obj-C exception: " + std::string([e.name UTF8String]) + + " during notification persistence."; + } + } catch (const std::exception &e) { + persistErrorMessage = "C++ exception: " + std::string(e.what()) + + " during notification persistence."; + } + + if (persistErrorMessage.size()) { + [errorMessages + addObject:[NSString stringWithUTF8String:persistErrorMessage.c_str()]]; + } + + // Step 3: (optional) rescind read notifications + // Message payload persistence is a higher priority task, so it has // to happen prior to potential notification center clearing. if ([self isRescind:self.bestAttemptContent.userInfo]) { - [self removeNotificationWithIdentifier:self.bestAttemptContent - .userInfo[@"notificationId"]]; - self.contentHandler([[UNNotificationContent alloc] init]); - return; + std::string rescindErrorMessage; + try { + @try { + [self + removeNotificationWithIdentifier:self.bestAttemptContent + .userInfo[@"notificationId"]]; + } @catch (NSException *e) { + rescindErrorMessage = + "Obj-C exception: " + std::string([e.name UTF8String]) + + " during notification rescind."; + } + } catch (const std::exception &e) { + rescindErrorMessage = "C++ exception: " + std::string(e.what()) + + " during notification rescind."; + } + + if (rescindErrorMessage.size()) { + [errorMessages + addObject:[NSString + stringWithUTF8String:persistErrorMessage.c_str()]]; + } + + publicUserContent = [[UNNotificationContent alloc] init]; } if ([self isBadgeOnly:self.bestAttemptContent.userInfo]) { UNMutableNotificationContent *badgeOnlyContent = [[UNMutableNotificationContent alloc] init]; badgeOnlyContent.badge = self.bestAttemptContent.badge; - self.contentHandler(badgeOnlyContent); - return; + self.bestAttemptContent = badgeOnlyContent; + publicUserContent = badgeOnlyContent; } [self sendNewMessageInfosNotification]; // TODO modify self.bestAttemptContent here + if (errorMessages.count) { + NSString *cumulatedErrorMessage = [@"NSE: Received " + stringByAppendingString:[errorMessages componentsJoinedByString:@" "]]; + [self callContentHandlerOnErrorMessage:cumulatedErrorMessage + withPublicUserContent:publicUserContent]; + return; + } self.contentHandler(self.bestAttemptContent); } - (void)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 ([self isRescind:self.bestAttemptContent.userInfo]) { // If we get to this place it means we were unable to // remove relevant notification from notification center in // in time given to NSE to process notification. // It is an extremely unlikely to happen. - comm::Logger::log("NSE: Exceeded time limit to rescind a notification."); - self.contentHandler([[UNNotificationContent alloc] init]); + NSString *errorMessage = + @"NSE: Exceeded time limit to rescind a notification."; + [self + callContentHandlerOnErrorMessage:errorMessage + withPublicUserContent:[[UNNotificationContent alloc] init]]; return; } if ([self shouldBeDecrypted:self.bestAttemptContent.userInfo] && !self.bestAttemptContent.userInfo[@"succesfullyDecrypted"]) { // If we get to this place it means we were unable to // decrypt encrypted notification content in time // given to NSE to process notification. - comm::Logger::log("NSE: Exceeded time limit to decrypt a notification."); - self.contentHandler([[UNNotificationContent alloc] init]); + NSString *errorMessage = + @"NSE: Exceeded time limit to decrypt a notification."; + [self + callContentHandlerOnErrorMessage:errorMessage + withPublicUserContent:[[UNNotificationContent alloc] init]]; return; } self.contentHandler(self.bestAttemptContent); } - (void)removeNotificationWithIdentifier:(NSString *)identifier { dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); void (^delayedSemaphorePostCallback)() = ^() { dispatch_time_t timeToPostSemaphore = dispatch_time(DISPATCH_TIME_NOW, notificationRemovalDelay); dispatch_after(timeToPostSemaphore, dispatch_get_main_queue(), ^{ dispatch_semaphore_signal(semaphore); }); }; [UNUserNotificationCenter.currentNotificationCenter getDeliveredNotificationsWithCompletionHandler:^( NSArray *_Nonnull notifications) { for (UNNotification *notif in notifications) { if ([identifier isEqual:notif.request.content.userInfo[@"id"]]) { [UNUserNotificationCenter.currentNotificationCenter removeDeliveredNotificationsWithIdentifiers:@[ notif.request.identifier ]]; } } delayedSemaphorePostCallback(); }]; dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); } - (void)persistMessagePayload:(NSDictionary *)payload { if (payload[messageInfosKey]) { TemporaryMessageStorage *temporaryStorage = [[TemporaryMessageStorage alloc] init]; [temporaryStorage writeMessage:payload[messageInfosKey]]; return; } if (![self isRescind:payload]) { return; } NSError *jsonError = nil; NSData *binarySerializedRescindPayload = [NSJSONSerialization dataWithJSONObject:payload options:0 error:&jsonError]; if (jsonError) { comm::Logger::log( "NSE: Failed to serialize rescind payload. Details: " + std::string([jsonError.localizedDescription UTF8String])); return; } NSString *serializedRescindPayload = [[NSString alloc] initWithData:binarySerializedRescindPayload encoding:NSUTF8StringEncoding]; TemporaryMessageStorage *temporaryRescindsStorage = [[TemporaryMessageStorage alloc] initForRescinds]; [temporaryRescindsStorage writeMessage:serializedRescindPayload]; } - (BOOL)isRescind:(NSDictionary *)payload { return payload[backgroundNotificationTypeKey] && [payload[backgroundNotificationTypeKey] isEqualToString:@"CLEAR"]; } - (BOOL)isBadgeOnly:(NSDictionary *)payload { // TODO: refactor this check by introducing // badgeOnly property in iOS notification payload return !payload[@"threadID"]; } - (void)sendNewMessageInfosNotification { CFNotificationCenterPostNotification( CFNotificationCenterGetDarwinNotifyCenter(), newMessageInfosDarwinNotification, (__bridge const void *)(self), nil, TRUE); } - (BOOL)shouldBeDecrypted:(NSDictionary *)payload { return payload[encryptedPayloadKey]; } - (BOOL)shouldAlertUnencryptedNotification:(NSDictionary *)payload { return payload[encryptionFailureKey] && [payload[encryptionFailureKey] isEqualToNumber:@(1)]; } - (NSString *)singleDecrypt:(NSString *)data { std::string encryptedData = std::string([data UTF8String]); return [NSString stringWithUTF8String: (comm::NotificationsCryptoModule::decrypt( encryptedData, comm::NotificationsCryptoModule::olmEncryptedTypeMessage, callingProcessName)) .c_str()]; } - (void)decryptBestAttemptContent { NSString *decryptedSerializedPayload = [self singleDecrypt:self.bestAttemptContent.userInfo[encryptedPayloadKey]]; NSDictionary *decryptedPayload = [NSJSONSerialization JSONObjectWithData:[decryptedSerializedPayload dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil]; NSMutableDictionary *mutableUserInfo = [self.bestAttemptContent.userInfo mutableCopy]; NSMutableDictionary *mutableAps = nil; if (mutableUserInfo[@"aps"]) { mutableAps = [mutableUserInfo[@"aps"] mutableCopy]; } NSString *body = decryptedPayload[@"merged"]; if (body) { self.bestAttemptContent.body = body; if (mutableAps && mutableAps[@"alert"]) { mutableAps[@"alert"] = body; } } NSString *threadID = decryptedPayload[@"threadID"]; if (threadID) { self.bestAttemptContent.threadIdentifier = threadID; mutableUserInfo[@"threadID"] = threadID; if (mutableAps) { mutableAps[@"thread-id"] = threadID; } } NSString *badgeStr = decryptedPayload[@"badge"]; if (badgeStr) { NSNumber *badge = @([badgeStr intValue]); self.bestAttemptContent.badge = badge; if (mutableAps) { mutableAps[@"badge"] = badge; } } // The rest have been already decrypted and handled. static NSArray *handledKeys = @[ @"merged", @"badge", @"threadID" ]; for (NSString *payloadKey in decryptedPayload) { if ([handledKeys containsObject:payloadKey]) { continue; } mutableUserInfo[payloadKey] = decryptedPayload[payloadKey]; } if (mutableAps) { mutableUserInfo[@"aps"] = mutableAps; } [mutableUserInfo removeObjectForKey:encryptedPayloadKey]; self.bestAttemptContent.userInfo = mutableUserInfo; } +- (UNNotificationContent *)buildContentForError:(NSString *)error { + UNMutableNotificationContent *content = + [[UNMutableNotificationContent alloc] init]; + content.body = error; + return content; +} + +- (void)callContentHandlerOnErrorMessage:(NSString *)errorMessage + withPublicUserContent: + (UNNotificationContent *)publicUserContent { + comm::Logger::log(std::string([errorMessage UTF8String])); + + if (!comm::StaffUtils::isStaffRelease()) { + self.contentHandler(publicUserContent); + return; + } + + UNNotificationContent *content = [self buildContentForError:errorMessage]; + self.contentHandler(content); +} + @end