diff --git a/native/ios/Comm/AppDelegate.mm b/native/ios/Comm/AppDelegate.mm --- a/native/ios/Comm/AppDelegate.mm +++ b/native/ios/Comm/AppDelegate.mm @@ -40,6 +40,7 @@ #import "CommConstants.h" #import "CommCoreModule.h" +#import "CommIOSNotificationsBlobClient.h" #import "CommRustModule.h" #import "CommUtilsModule.h" #import "GlobalDBSingleton.h" @@ -95,7 +96,7 @@ didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { RCTAppSetupPrepareApp(application); - [self moveMessagesToDatabase:NO]; + [self processNSETemporaryStorage]; [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient error:nil]; @@ -400,6 +401,43 @@ CFNotificationSuspensionBehaviorDeliverImmediately); } +// NSE has limited time to process notifications. Therefore +// deferable and low priority networking such as fetched +// blob deletion from blob service should be handled by the +// main app on a low priority background thread. + +- (void)scheduleNSEBlobsDeletion { + dispatch_async( + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ + TemporaryMessageStorage *temporaryMessageStorage = + [[TemporaryMessageStorage alloc] initForBlobs]; + + NSArray *blobsData = + [temporaryMessageStorage readAndClearMessages]; + + if (!blobsData.count) { + return; + } + + [[CommIOSNotificationsBlobClient sharedInstance] + deleteBlobs:blobsData + withFailureHandler:^(NSString *_Nonnull blobData) { + // If blob deletion failed we write it back to the storage + // so that its deletion can be attampted again. + [temporaryMessageStorage writeMessage:blobData]; + }]; + }); +} + +- (void)applicationWillResignActive:(UIApplication *)application { + [[CommIOSNotificationsBlobClient sharedInstance] cancelOnGoingRequests]; +} + +- (void)processNSETemporaryStorage { + [self moveMessagesToDatabase:NO]; + [self scheduleNSEBlobsDeletion]; +} + // Copied from // ReactAndroid/src/main/java/com/facebook/hermes/reactexecutor/OnLoad.cpp static ::hermes::vm::RuntimeConfig diff --git a/native/ios/Comm/CommIOSNotifications/CommIOSNotificationsBlobClient.h b/native/ios/Comm/CommIOSNotifications/CommIOSNotificationsBlobClient.h --- a/native/ios/Comm/CommIOSNotifications/CommIOSNotificationsBlobClient.h +++ b/native/ios/Comm/CommIOSNotifications/CommIOSNotificationsBlobClient.h @@ -6,4 +6,7 @@ + (id)sharedInstance; - (void)getAndConsumeSync:(NSString *)blobHash withSuccessConsumer:(void (^)(NSData *))successConsumer; +- (void)deleteBlobs:(NSArray *_Nonnull)blobsData + withFailureHandler:(void (^_Nonnull)(NSString *_Nonnull))failureHandler; +- (void)cancelOnGoingRequests; @end diff --git a/native/ios/Comm/CommIOSNotifications/CommIOSNotificationsBlobClient.mm b/native/ios/Comm/CommIOSNotifications/CommIOSNotificationsBlobClient.mm --- a/native/ios/Comm/CommIOSNotifications/CommIOSNotificationsBlobClient.mm +++ b/native/ios/Comm/CommIOSNotifications/CommIOSNotificationsBlobClient.mm @@ -84,6 +84,61 @@ (int64_t)(blobServiceQueryTimeLimit * NSEC_PER_SEC))); } +- (void)deleteBlobs:(NSArray *)blobsData + withFailureHandler:(void (^_Nonnull)(NSString *_Nonnull))failureHandler { + NSError *authTokenError = nil; + NSString *authToken = + [CommIOSNotificationsBlobClient _getAuthToken:&authTokenError]; + + if (authTokenError) { + comm::Logger::log( + "Failed to create blob service auth token. Reason: " + + std::string([authTokenError.localizedDescription UTF8String])); + return; + } + + NSString *blobUrlStr = [blobServiceAddress stringByAppendingString:@"/blob"]; + NSURL *blobUrl = [NSURL URLWithString:blobUrlStr]; + + for (NSString *blobData in blobsData) { + NSMutableURLRequest *deleteRequest = + [NSMutableURLRequest requestWithURL:blobUrl]; + + [deleteRequest setValue:authToken forHTTPHeaderField:@"Authorization"]; + [deleteRequest setValue:@"application/json" + forHTTPHeaderField:@"content-type"]; + + deleteRequest.HTTPMethod = @"DELETE"; + deleteRequest.HTTPBody = [blobData dataUsingEncoding:NSUTF8StringEncoding]; + + NSURLSessionDataTask *task = [self.sharedBlobServiceSession + dataTaskWithRequest:deleteRequest + completionHandler:^( + NSData *_Nullable data, + NSURLResponse *_Nullable response, + NSError *_Nullable error) { + if (error) { + comm::Logger::log( + "COMM: Failed to delete blob from blob service. Reason: " + + std::string([error.localizedDescription UTF8String])); + failureHandler(blobData); + } + }]; + + [task resume]; + } +} + +- (void)cancelOnGoingRequests { + [self.sharedBlobServiceSession + getAllTasksWithCompletionHandler:^( + NSArray<__kindof NSURLSessionTask *> *_Nonnull tasks) { + for (NSURLSessionTask *task in tasks) { + [task cancel]; + } + }]; +} + + (NSString *)_getAuthToken:(NSError **)error { // Authentication data are retrieved on every request // since they might change while NSE process is running diff --git a/native/ios/Comm/TemporaryMessageStorage/TemporaryMessageStorage.h b/native/ios/Comm/TemporaryMessageStorage/TemporaryMessageStorage.h --- a/native/ios/Comm/TemporaryMessageStorage/TemporaryMessageStorage.h +++ b/native/ios/Comm/TemporaryMessageStorage/TemporaryMessageStorage.h @@ -8,6 +8,7 @@ @property(readonly) NSString *filePrefix; - (instancetype)initForRescinds; +- (instancetype)initForBlobs; - (void)writeMessage:(NSString *)message; - (NSArray *)readAndClearMessages; @end diff --git a/native/ios/Comm/TemporaryMessageStorage/TemporaryMessageStorage.mm b/native/ios/Comm/TemporaryMessageStorage/TemporaryMessageStorage.mm --- a/native/ios/Comm/TemporaryMessageStorage/TemporaryMessageStorage.mm +++ b/native/ios/Comm/TemporaryMessageStorage/TemporaryMessageStorage.mm @@ -24,6 +24,12 @@ return self; } +- (instancetype)initForBlobs { + self = [self _initAtDirectory:@"TemporaryMessageStorageBlobs" + withFilePrefix:@"blb"]; + return self; +} + - (instancetype)_initAtDirectory:(NSString *)directoryName withFilePrefix:(NSString *)filePrefix { self = [super init]; diff --git a/native/ios/NotificationService/NotificationService.mm b/native/ios/NotificationService/NotificationService.mm --- a/native/ios/NotificationService/NotificationService.mm +++ b/native/ios/NotificationService/NotificationService.mm @@ -9,6 +9,9 @@ NSString *const messageInfosKey = @"messageInfos"; NSString *const encryptedPayloadKey = @"encryptedPayload"; NSString *const encryptionFailureKey = @"encryptionFailure"; +NSString *const blobHashKey = @"blobHash"; +NSString *const requestBlobHashKey = @"blob_hash"; +NSString *const holderKey = @"holder"; const std::string callingProcessName = "NSE"; // The context for this constant can be found here: // https://linear.app/comm/issue/ENG-3074#comment-bd2f5e28 @@ -54,8 +57,8 @@ comm::Logger::log("NSE: Received erroneously unencrypted notitication."); } - if (self.bestAttemptContent.userInfo[@"blobHash"]) { - NSString *blobHash = self.bestAttemptContent.userInfo[@"blobHash"]; + if ([self isBlobNotification:self.bestAttemptContent.userInfo]) { + NSString *blobHash = self.bestAttemptContent.userInfo[blobHashKey]; NSData *encryptionKey = [[NSData alloc] initWithBase64EncodedString:self.bestAttemptContent .userInfo[@"encryptionKey"] @@ -77,6 +80,8 @@ } }]; + [NotificationService + persistBlobMetadataForDeletion:self.bestAttemptContent.userInfo]; self.contentHandler(self.bestAttemptContent); return; } @@ -191,6 +196,24 @@ [temporaryRescindsStorage writeMessage:serializedRescindPayload]; } ++ (void)persistBlobMetadataForDeletion:(NSDictionary *)payload { + NSString *blobHash = payload[blobHashKey]; + NSString *holder = payload[holderKey]; + + TemporaryMessageStorage *temporaryMessageStorageBlobs = + [[TemporaryMessageStorage alloc] initForBlobs]; + + [temporaryMessageStorageBlobs + writeMessage:[[NSString alloc] + initWithData:[NSJSONSerialization dataWithJSONObject:@{ + requestBlobHashKey : blobHash, + holderKey : holder + } + options:0 + error:nil] + encoding:NSUTF8StringEncoding]]; +} + + (BOOL)isRescind:(NSDictionary *)payload { return payload[backgroundNotificationTypeKey] && [payload[backgroundNotificationTypeKey] isEqualToString:@"CLEAR"]; @@ -202,6 +225,10 @@ return !payload[@"threadID"]; } +- (BOOL)isBlobNotification:(NSDictionary *)payload { + return payload[blobHashKey] && payload[holderKey]; +} + + (void)sendNewMessageInfosNotification { CFNotificationCenterPostNotification( CFNotificationCenterGetDarwinNotifyCenter(),