From 53af41fcc61971c38ac159c98ca4f501e9204ffa Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 4 Apr 2018 16:26:41 -0400 Subject: [PATCH] Reusable UploadOperation based on extracted OWSOperation // FREEBIE --- .../Cells/AttachmentUploadView.m | 6 +- Signal/src/views/QuotedReplyPreview.swift | 19 +- .../OWS106EnsureProfileComplete.swift | 3 +- .../Messages/Interactions/TSOutgoingMessage.m | 74 ++-- .../Messages/Interactions/TSQuotedMessage.h | 18 +- .../Messages/Interactions/TSQuotedMessage.m | 42 +- .../src/Messages/OWSMessageSender.h | 9 - .../src/Messages/OWSMessageSender.m | 416 ++++++++---------- .../src/Network/API/OWSUploadOperation.h | 27 ++ .../src/Network/API/OWSUploadOperation.m | 190 ++++++++ .../src/Network/API/OWSUploadingService.h | 29 -- .../src/Network/API/OWSUploadingService.m | 181 -------- .../src/Network/OWSCensorshipConfiguration.m | 9 +- .../src/Util/NSError+MessageSending.h | 15 + .../src/Util/NSError+MessageSending.m | 62 +++ SignalServiceKit/src/Util/OWSError.h | 3 +- SignalServiceKit/src/Util/OWSError.m | 7 +- SignalServiceKit/src/Util/OWSOperation.h | 63 +++ SignalServiceKit/src/Util/OWSOperation.m | 181 ++++++++ 19 files changed, 840 insertions(+), 514 deletions(-) create mode 100644 SignalServiceKit/src/Network/API/OWSUploadOperation.h create mode 100644 SignalServiceKit/src/Network/API/OWSUploadOperation.m delete mode 100644 SignalServiceKit/src/Network/API/OWSUploadingService.h delete mode 100644 SignalServiceKit/src/Network/API/OWSUploadingService.m create mode 100644 SignalServiceKit/src/Util/NSError+MessageSending.h create mode 100644 SignalServiceKit/src/Util/NSError+MessageSending.m create mode 100644 SignalServiceKit/src/Util/OWSOperation.h create mode 100644 SignalServiceKit/src/Util/OWSOperation.m diff --git a/Signal/src/ViewControllers/ConversationView/Cells/AttachmentUploadView.m b/Signal/src/ViewControllers/ConversationView/Cells/AttachmentUploadView.m index 16254cd6d..dcb6a9da3 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/AttachmentUploadView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/AttachmentUploadView.m @@ -5,9 +5,9 @@ #import "AttachmentUploadView.h" #import "OWSBezierPathView.h" #import "OWSProgressView.h" -#import "OWSUploadingService.h" -#import "TSAttachmentStream.h" -#import "UIView+OWS.h" +#import +#import +#import NS_ASSUME_NONNULL_BEGIN diff --git a/Signal/src/views/QuotedReplyPreview.swift b/Signal/src/views/QuotedReplyPreview.swift index 865eff4d2..fea4ff21e 100644 --- a/Signal/src/views/QuotedReplyPreview.swift +++ b/Signal/src/views/QuotedReplyPreview.swift @@ -52,15 +52,16 @@ class QuotedReplyPreview: UIView { }() let thumbnailView: UIView? = { - if let image = quotedMessage.thumbnailImage() { - let imageView = UIImageView(image: image) - imageView.contentMode = .scaleAspectFill - imageView.autoPinToSquareAspectRatio() - imageView.layer.cornerRadius = 3.0 - imageView.clipsToBounds = true - - return imageView - } + // FIXME TODO +// if let image = quotedMessage.thumbnailImage() { +// let imageView = UIImageView(image: image) +// imageView.contentMode = .scaleAspectFill +// imageView.autoPinToSquareAspectRatio() +// imageView.layer.cornerRadius = 3.0 +// imageView.clipsToBounds = true +// +// return imageView +// } return nil }() diff --git a/SignalMessaging/environment/migrations/OWS106EnsureProfileComplete.swift b/SignalMessaging/environment/migrations/OWS106EnsureProfileComplete.swift index 5e663255f..bf0d0a860 100644 --- a/SignalMessaging/environment/migrations/OWS106EnsureProfileComplete.swift +++ b/SignalMessaging/environment/migrations/OWS106EnsureProfileComplete.swift @@ -92,8 +92,7 @@ public class OWS106EnsureProfileComplete: OWSDatabaseMigration { let (promise, fulfill, reject) = Promise.pending() guard let networkManager = Environment.current().networkManager else { - owsFail("\(TAG) network manager was unexpectedly not set") - return Promise(error: OWSErrorMakeAssertionError()) + return Promise(error: OWSErrorMakeAssertionError("\(TAG) network manager was unexpectedly not set")) } ProfileFetcherJob(networkManager: networkManager).getProfile(recipientId: localRecipientId).then { _ -> Void in diff --git a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m index 67ed26ecf..0255297fa 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m +++ b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m @@ -394,9 +394,14 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec - (OWSSignalServiceProtosDataMessageBuilder *)dataMessageBuilder { TSThread *thread = self.thread; + OWSAssert(thread); + OWSSignalServiceProtosDataMessageBuilder *builder = [OWSSignalServiceProtosDataMessageBuilder new]; [builder setTimestamp:self.timestamp]; [builder setBody:self.body]; + [builder setExpireTimer:self.expiresInSeconds]; + + // Group Messages BOOL attachmentWasGroupAvatar = NO; if ([thread isKindOfClass:[TSGroupThread class]]) { TSGroupThread *gThread = (TSGroupThread *)thread; @@ -426,66 +431,59 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec [groupBuilder setId:gThread.groupModel.groupId]; [builder setGroup:groupBuilder.build]; } + + // Message Attachments if (!attachmentWasGroupAvatar) { NSMutableArray *attachments = [NSMutableArray new]; for (NSString *attachmentId in self.attachmentIds) { - NSString *sourceFilename = self.attachmentFilenameMap[attachmentId]; + NSString *_Nullable sourceFilename = self.attachmentFilenameMap[attachmentId]; [attachments addObject:[self buildAttachmentProtoForAttachmentId:attachmentId filename:sourceFilename]]; } [builder setAttachmentsArray:attachments]; } - [builder setExpireTimer:self.expiresInSeconds]; - return builder; -} - -// recipientId is nil when building "sent" sync messages for messages -// sent to groups. -- (OWSSignalServiceProtosDataMessage *)buildDataMessage:(NSString *_Nullable)recipientId -{ - OWSAssert(self.thread); - - OWSSignalServiceProtosDataMessageBuilder *builder = [self dataMessageBuilder]; - [builder setTimestamp:self.timestamp]; - [builder addLocalProfileKeyIfNecessary:self.thread recipientId:recipientId]; - - if (self.quotedMessage) { - OWSSignalServiceProtosDataMessageQuoteBuilder *quoteBuilder = - [OWSSignalServiceProtosDataMessageQuoteBuilder new]; - [quoteBuilder setId:self.quotedMessage.timestamp]; - [quoteBuilder setAuthor:self.quotedMessage.authorId]; - + + // Quoted Attachment + TSQuotedMessage *quotedMessage = self.quotedMessage; + if (quotedMessage) { + OWSSignalServiceProtosDataMessageQuoteBuilder *quoteBuilder = [OWSSignalServiceProtosDataMessageQuoteBuilder new]; + [quoteBuilder setId:quotedMessage.timestamp]; + [quoteBuilder setAuthor:quotedMessage.authorId]; + BOOL hasQuotedText = NO; BOOL hasQuotedAttachment = NO; if (self.quotedMessage.body.length > 0) { - [quoteBuilder setText:self.quotedMessage.body]; - hasQuotedText = YES; + [quoteBuilder setText:quotedMessage.body]; } - if (self.quotedMessage.contentType.length > 0) { - - OWSSignalServiceProtosAttachmentPointerBuilder *attachmentBuilder = - [OWSSignalServiceProtosAttachmentPointerBuilder new]; - if (self.quotedMessage.thumbnailData.length > 0) { - [attachmentBuilder setThumbnail:self.quotedMessage.thumbnailData]; + if (quotedMessage.thumbnailAttachmentIds.count > 0) { + NSMutableArray *thumbnailAttachments = [NSMutableArray new]; + for (NSString *attachmentId in quotedMessage.thumbnailAttachmentIds) { + hasQuotedAttachment = YES; + NSString *_Nullable sourceFilename = quotedMessage.thumbnailAttachmentFilenameMap[attachmentId]; + [thumbnailAttachments addObject:[self buildAttachmentProtoForAttachmentId:attachmentId filename:sourceFilename]]; } - if (self.quotedMessage.sourceFilename.length > 0) { - [attachmentBuilder setFileName:self.quotedMessage.sourceFilename]; - } - [attachmentBuilder setContentType:self.quotedMessage.contentType]; - [quoteBuilder.attachments addObject:[attachmentBuilder build]]; - - hasQuotedAttachment = YES; + [quoteBuilder setAttachmentsArray:thumbnailAttachments]; } - + if (hasQuotedText || hasQuotedAttachment) { [builder setQuoteBuilder:quoteBuilder]; } else { OWSFail(@"%@ Invalid quoted message data.", self.logTag); } } + + return builder; +} - return [builder build]; +// recipientId is nil when building "sent" sync messages for messages sent to groups. +- (OWSSignalServiceProtosDataMessage *)buildDataMessage:(NSString *_Nullable)recipientId +{ + OWSAssert(self.thread); + OWSSignalServiceProtosDataMessageBuilder *builder = [self dataMessageBuilder]; + [builder addLocalProfileKeyIfNecessary:self.thread recipientId:recipientId]; + + return [[self dataMessageBuilder] build]; } - (NSData *)buildPlainTextData:(SignalRecipient *)recipient diff --git a/SignalServiceKit/src/Messages/Interactions/TSQuotedMessage.h b/SignalServiceKit/src/Messages/Interactions/TSQuotedMessage.h index a971c81be..d24c0ecb3 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSQuotedMessage.h +++ b/SignalServiceKit/src/Messages/Interactions/TSQuotedMessage.h @@ -6,6 +6,8 @@ NS_ASSUME_NONNULL_BEGIN +@class TSAttachment; + @interface TSQuotedMessage : TSYapDatabaseObject @property (nonatomic, readonly) uint64_t timestamp; @@ -15,10 +17,9 @@ NS_ASSUME_NONNULL_BEGIN // or attachment with caption. @property (nullable, nonatomic, readonly) NSString *body; -// This property should be set IFF we are quoting an attachment message. -@property (nullable, nonatomic, readonly) NSString *sourceFilename; -// This property can be set IFF we are quoting an attachment message, but it is optional. -@property (nullable, nonatomic, readonly) NSData *thumbnailData; +//// This property can be set IFF we are quoting an attachment message, but it is optional. +//@property (nullable, nonatomic, readonly) NSData *thumbnailData; + // This is a MIME type. // // This property should be set IFF we are quoting an attachment message. @@ -33,7 +34,14 @@ NS_ASSUME_NONNULL_BEGIN thumbnailData:(NSData *_Nullable)thumbnailData contentType:(NSString *_Nullable)contentType; -- (nullable UIImage *)thumbnailImage; +#pragma mark - Attachments + +@property (nonatomic, readonly) NSArray *thumbnailAttachmentIds; +// A map of attachment id-to-"source" filename. +@property (nonatomic, readonly) NSMutableDictionary *thumbnailAttachmentFilenameMap; + +- (BOOL)hasThumbnailAttachments; +- (nullable TSAttachment *)firstThumbnailAttachmentWithTransaction:(YapDatabaseReadTransaction *)transaction; @end diff --git a/SignalServiceKit/src/Messages/Interactions/TSQuotedMessage.m b/SignalServiceKit/src/Messages/Interactions/TSQuotedMessage.m index 1d99d58aa..8b154cde0 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSQuotedMessage.m +++ b/SignalServiceKit/src/Messages/Interactions/TSQuotedMessage.m @@ -3,6 +3,7 @@ // #import "TSQuotedMessage.h" +#import "TSAttachment.h" NS_ASSUME_NONNULL_BEGIN @@ -26,23 +27,50 @@ NS_ASSUME_NONNULL_BEGIN _timestamp = timestamp; _authorId = authorId; _body = body; - _sourceFilename = sourceFilename; - _thumbnailData = thumbnailData; + // TODO get source filename from attachment +// _sourceFilename = sourceFilename; +// _thumbnailData = thumbnailData; _contentType = contentType; return self; } +// TODO maybe this should live closer to the view - (nullable UIImage *)thumbnailImage { - if (self.thumbnailData.length == 0) { - return nil; - } +// if (self.thumbnailData.length == 0) { +// return nil; +// } +// +// // PERF TODO cache +// return [UIImage imageWithData:self.thumbnailData]; + return nil; +} + +//- (void)setThumbnailAttachmentId:(NSString *)thumbnailAttachmentId +//{ +// _thumbnailAttachmentId = thumbnailAttachmentId; +//} +// +//- (BOOL)hasThumbnailAttachment +//{ +// return self.thumbnailAttachmentId.length > 0; +//} +// - // PERF TODO cache - return [UIImage imageWithData:self.thumbnailData]; +- (BOOL)hasThumbnailAttachments +{ + return self.thumbnailAttachmentIds.count > 0; } +- (nullable TSAttachment *)firstThumbnailAttachmentWithTransaction:(YapDatabaseReadTransaction *)transaction; +{ + if (!self.hasThumbnailAttachments) { + return nil; + } + + return [TSAttachment fetchObjectWithUniqueID:self.thumbnailAttachmentIds.firstObject transaction:transaction]; +} @end diff --git a/SignalServiceKit/src/Messages/OWSMessageSender.h b/SignalServiceKit/src/Messages/OWSMessageSender.h index f702c89ba..4539b9ff0 100644 --- a/SignalServiceKit/src/Messages/OWSMessageSender.h +++ b/SignalServiceKit/src/Messages/OWSMessageSender.h @@ -32,15 +32,6 @@ typedef void (^RetryableFailureHandler)(NSError *_Nonnull error); // // For example, If one member of a group deletes their account, the group should // ignore errors when trying to send messages to this ex-member. -@interface NSError (OWSMessageSender) - -- (BOOL)isRetryable; -- (void)setIsRetryable:(BOOL)value; - -- (BOOL)shouldBeIgnoredForGroups; -- (void)setShouldBeIgnoredForGroups:(BOOL)value; - -@end #pragma mark - diff --git a/SignalServiceKit/src/Messages/OWSMessageSender.m b/SignalServiceKit/src/Messages/OWSMessageSender.m index 60ebdea8e..f32005f7a 100644 --- a/SignalServiceKit/src/Messages/OWSMessageSender.m +++ b/SignalServiceKit/src/Messages/OWSMessageSender.m @@ -5,8 +5,12 @@ #import "OWSMessageSender.h" #import "AppContext.h" #import "ContactsUpdater.h" +#import "Cryptography.h" +#import "MIMETypeUtil.h" #import "NSData+keyVersionByte.h" #import "NSData+messagePadding.h" +#import "NSError+MessageSending.h" +#import "NSNotificationCenter+OWS.h" #import "OWSBackgroundTask.h" #import "OWSBlockingManager.h" #import "OWSDevice.h" @@ -14,6 +18,7 @@ #import "OWSError.h" #import "OWSIdentityManager.h" #import "OWSMessageServiceParams.h" +#import "OWSOperation.h" #import "OWSOutgoingSentMessageTranscript.h" #import "OWSOutgoingSyncMessage.h" #import "OWSPrimaryStorage+PreKeyStore.h" @@ -21,7 +26,7 @@ #import "OWSPrimaryStorage+sessionStore.h" #import "OWSPrimaryStorage.h" #import "OWSRequestFactory.h" -#import "OWSUploadingService.h" +#import "OWSUploadOperation.h" #import "PreKeyBundle+jsonDict.h" #import "SignalRecipient.h" #import "TSAccountManager.h" @@ -34,7 +39,9 @@ #import "TSNetworkManager.h" #import "TSOutgoingMessage.h" #import "TSPreKeyManager.h" +#import "TSQuotedMessage.h" #import "TSThread.h" +#import "TextSecureKitEnv.h" #import "Threading.h" #import #import @@ -42,7 +49,6 @@ #import #import #import -#import NS_ASSUME_NONNULL_BEGIN @@ -57,58 +63,6 @@ void AssertIsOnSendingQueue() #endif } -static void *kNSError_MessageSender_IsRetryable = &kNSError_MessageSender_IsRetryable; -static void *kNSError_MessageSender_ShouldBeIgnoredForGroups = &kNSError_MessageSender_ShouldBeIgnoredForGroups; -static void *kNSError_MessageSender_IsFatal = &kNSError_MessageSender_IsFatal; - -// isRetryable and isFatal are opposites but not redundant. -// -// If a group message send fails, the send will be retried if any of the errors were retryable UNLESS -// any of the errors were fatal. Fatal errors trump retryable errors. -@implementation NSError (OWSMessageSender) - -- (BOOL)isRetryable -{ - NSNumber *value = objc_getAssociatedObject(self, kNSError_MessageSender_IsRetryable); - // This value should always be set for all errors by the time OWSSendMessageOperation - // queries it's value. If not, default to retrying in production. - OWSAssert(value); - return value ? [value boolValue] : YES; -} - -- (void)setIsRetryable:(BOOL)value -{ - objc_setAssociatedObject(self, kNSError_MessageSender_IsRetryable, @(value), OBJC_ASSOCIATION_COPY); -} - -- (BOOL)shouldBeIgnoredForGroups -{ - NSNumber *value = objc_getAssociatedObject(self, kNSError_MessageSender_ShouldBeIgnoredForGroups); - // This value will NOT always be set for all errors by the time we query it's value. - // Default to NOT ignoring. - return value ? [value boolValue] : NO; -} - -- (void)setShouldBeIgnoredForGroups:(BOOL)value -{ - objc_setAssociatedObject(self, kNSError_MessageSender_ShouldBeIgnoredForGroups, @(value), OBJC_ASSOCIATION_COPY); -} - -- (BOOL)isFatal -{ - NSNumber *value = objc_getAssociatedObject(self, kNSError_MessageSender_IsFatal); - // This value will NOT always be set for all errors by the time we query it's value. - // Default to NOT fatal. - return value ? [value boolValue] : NO; -} - -- (void)setIsFatal:(BOOL)value -{ - objc_setAssociatedObject(self, kNSError_MessageSender_IsFatal, @(value), OBJC_ASSOCIATION_COPY); -} - -@end - #pragma mark - /** @@ -118,27 +72,22 @@ static void *kNSError_MessageSender_IsFatal = &kNSError_MessageSender_IsFatal; * Used by `OWSMessageSender` to serialize message sending, ensuring that messages are emitted in the order they * were sent. */ -@interface OWSSendMessageOperation : NSOperation +@interface OWSSendMessageOperation : OWSOperation - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithMessage:(TSOutgoingMessage *)message messageSender:(OWSMessageSender *)messageSender - success:(void (^)(void))successHandler - failure:(void (^)(NSError *_Nonnull error))failureHandler NS_DESIGNATED_INITIALIZER; + dbConnection:(YapDatabaseConnection *)dbConnection + success:(void (^)(void))aSuccessHandler + failure:(void (^)(NSError *_Nonnull error))aFailureHandler NS_DESIGNATED_INITIALIZER; @end #pragma mark - -typedef NS_ENUM(NSInteger, OWSSendMessageOperationState) { - OWSSendMessageOperationStateNew, - OWSSendMessageOperationStateExecuting, - OWSSendMessageOperationStateFinished -}; - @interface OWSMessageSender (OWSSendMessageOperation) -- (void)attemptToSendMessage:(TSOutgoingMessage *)message +- (void)sendMessageToService:(TSOutgoingMessage *)message success:(void (^)(void))successHandler failure:(RetryableFailureHandler)failureHandler; @@ -146,19 +95,13 @@ typedef NS_ENUM(NSInteger, OWSSendMessageOperationState) { #pragma mark - -NSString *const OWSSendMessageOperationKeyIsExecuting = @"isExecuting"; -NSString *const OWSSendMessageOperationKeyIsFinished = @"isFinished"; - -NSUInteger const OWSSendMessageOperationMaxRetries = 4; - @interface OWSSendMessageOperation () @property (nonatomic, readonly) TSOutgoingMessage *message; @property (nonatomic, readonly) OWSMessageSender *messageSender; +@property (nonatomic, readonly) YapDatabaseConnection *dbConnection; @property (nonatomic, readonly) void (^successHandler)(void); @property (nonatomic, readonly) void (^failureHandler)(NSError *_Nonnull error); -@property (nonatomic) OWSSendMessageOperationState operationState; -@property (nonatomic) OWSBackgroundTask *backgroundTask; @end @@ -168,144 +111,98 @@ NSUInteger const OWSSendMessageOperationMaxRetries = 4; - (instancetype)initWithMessage:(TSOutgoingMessage *)message messageSender:(OWSMessageSender *)messageSender - success:(void (^)(void))aSuccessHandler - failure:(void (^)(NSError *_Nonnull error))aFailureHandler + dbConnection:(YapDatabaseConnection *)dbConnection + success:(void (^)(void))successHandler + failure:(void (^)(NSError *_Nonnull error))failureHandler { self = [super init]; if (!self) { return self; } - _operationState = OWSSendMessageOperationStateNew; - self.backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__]; - + self.remainingRetries = 6; _message = message; _messageSender = messageSender; - - __weak typeof(self) weakSelf = self; - _successHandler = ^{ - typeof(self) strongSelf = weakSelf; - if (!strongSelf) { - OWSProdCFail([OWSAnalyticsEvents messageSenderErrorSendOperationDidNotComplete]); - return; - } - - [message updateWithMessageState:TSOutgoingMessageStateSentToService]; - - aSuccessHandler(); - - [strongSelf markAsComplete]; - }; - - _failureHandler = ^(NSError *_Nonnull error) { - typeof(self) strongSelf = weakSelf; - if (!strongSelf) { - OWSProdCFail([OWSAnalyticsEvents messageSenderErrorSendOperationDidNotComplete]); - return; - } - - [strongSelf.message updateWithSendingError:error]; - - DDLogDebug(@"%@ failed with error.", strongSelf.logTag); - aFailureHandler(error); - - [strongSelf markAsComplete]; - }; + _dbConnection = dbConnection; + _successHandler = successHandler; + _failureHandler = failureHandler; return self; } -#pragma mark - NSOperation overrides +#pragma mark - OWSOperation overrides -- (BOOL)isExecuting +- (nullable NSError *)checkForPreconditionError { - return self.operationState == OWSSendMessageOperationStateExecuting; -} + for (NSOperation *dependency in self.dependencies) { + if (![dependency isKindOfClass:[OWSOperation class]]) { + NSString *errorDescription = + [NSString stringWithFormat:@"%@ unknown dependency: %@", self.logTag, dependency.class]; + NSError *assertionError = OWSErrorMakeAssertionError(errorDescription); + return assertionError; + } -- (BOOL)isFinished -{ - return self.operationState == OWSSendMessageOperationStateFinished; -} + OWSOperation *upload = (OWSOperation *)dependency; -- (void)start -{ - [self willChangeValueForKey:OWSSendMessageOperationKeyIsExecuting]; - self.operationState = OWSSendMessageOperationStateExecuting; - [self didChangeValueForKey:OWSSendMessageOperationKeyIsExecuting]; - [self main]; -} + // Cannot proceed if dependency failed - surface the dependency's error. + NSError *_Nullable dependencyError = upload.failingError; + if (dependencyError) { + return dependencyError; + } + } -- (void)main -{ - [self tryWithRemainingRetries:OWSSendMessageOperationMaxRetries]; -} + // Sanity check preconditions + if (self.message.hasAttachments) { + [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) { + TSAttachmentStream *attachmentStream + = (TSAttachmentStream *)[self.message attachmentWithTransaction:transaction]; + OWSAssert(attachmentStream); + OWSAssert([attachmentStream isKindOfClass:[TSAttachmentStream class]]); + OWSAssert(attachmentStream.serverId); + OWSAssert(attachmentStream.isUploaded); + }]; + } -#pragma mark - methods + return nil; +} -- (void)tryWithRemainingRetries:(NSUInteger)remainingRetries +- (void)run { // If the message has been deleted, abort send. if (self.message.shouldBeSaved && ![TSOutgoingMessage fetchObjectWithUniqueID:self.message.uniqueId]) { DDLogInfo(@"%@ aborting message send; message deleted.", self.logTag); NSError *error = OWSErrorWithCodeDescription( OWSErrorCodeMessageDeletedBeforeSent, @"Message was deleted before it could be sent."); - self.failureHandler(error); + error.isFatal = YES; + [self reportError:error]; return; } - // Use this flag to ensure a given operation only succeeds or fails once. - __block BOOL onceFlag = NO; - RetryableFailureHandler retryableFailureHandler = ^(NSError *_Nonnull error) { - DDLogInfo(@"%@ Sending failed. Remaining retries: %lu", self.logTag, (unsigned long)remainingRetries); - - OWSAssert(!onceFlag); - onceFlag = YES; - - if (![error isRetryable] || [error isFatal]) { - DDLogInfo(@"%@ Skipping retry due to terminal error.", self.logTag); - self.failureHandler(error); - return; - } - - if (remainingRetries > 0) { - [self tryWithRemainingRetries:remainingRetries - 1]; - } else { - DDLogWarn(@"%@ Too many failures. Giving up sending.", self.logTag); - - self.failureHandler(error); + [self.messageSender sendMessageToService:self.message + success:^{ + [self reportSuccess]; } - }; - - [self.messageSender attemptToSendMessage:self.message - success:^{ - OWSAssert(!onceFlag); - onceFlag = YES; - - self.successHandler(); - } - failure:retryableFailureHandler]; + failure:^(NSError *error) { + [self reportError:error]; + }]; } -- (void)markAsComplete +- (void)didSucceed { - [self willChangeValueForKey:OWSSendMessageOperationKeyIsExecuting]; - [self willChangeValueForKey:OWSSendMessageOperationKeyIsFinished]; - - // Ensure we call the success or failure handler exactly once. - @synchronized(self) - { - OWSAssert(self.operationState != OWSSendMessageOperationStateFinished); - - self.operationState = OWSSendMessageOperationStateFinished; - } + [self.message updateWithMessageState:TSOutgoingMessageStateSentToService]; + self.successHandler(); +} - [self didChangeValueForKey:OWSSendMessageOperationKeyIsExecuting]; - [self didChangeValueForKey:OWSSendMessageOperationKeyIsFinished]; +- (void)didFailWithError:(NSError *)error +{ + [self.message updateWithSendingError:error]; + + DDLogDebug(@"%@ failed with error: %@", self.logTag, error); + self.failureHandler(error); } @end - int const OWSMessageSenderRetryAttempts = 3; NSString *const OWSMessageSenderInvalidDeviceException = @"InvalidDeviceException"; NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; @@ -315,7 +212,6 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; @property (nonatomic, readonly) TSNetworkManager *networkManager; @property (nonatomic, readonly) OWSPrimaryStorage *primaryStorage; @property (nonatomic, readonly) OWSBlockingManager *blockingManager; -@property (nonatomic, readonly) OWSUploadingService *uploadingService; @property (nonatomic, readonly) YapDatabaseConnection *dbConnection; @property (nonatomic, readonly) id contactsManager; @property (nonatomic, readonly) ContactsUpdater *contactsUpdater; @@ -340,8 +236,6 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; _contactsManager = contactsManager; _contactsUpdater = contactsUpdater; _sendingQueueMap = [NSMutableDictionary new]; - - _uploadingService = [[OWSUploadingService alloc] initWithNetworkManager:networkManager]; _dbConnection = primaryStorage.newDatabaseConnection; OWSSingletonAssert(); @@ -361,10 +255,16 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; { OWSAssert(message); + NSString *kDefaultQueueKey = @"kDefaultQueueKey"; NSString *queueKey = message.uniqueThreadId ?: kDefaultQueueKey; OWSAssert(queueKey.length > 0); + if ([kDefaultQueueKey isEqualToString:queueKey]) { + // when do we get here? + DDLogDebug(@"%@ using default message queue", self.logTag); + } + @synchronized(self) { NSOperationQueue *sendingQueue = self.sendingQueueMap[queueKey]; @@ -409,64 +309,134 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; [message updateWithMessageState:TSOutgoingMessageStateAttemptingOut transaction:transaction]; }]; + NSOperationQueue *sendingQueue = [self sendingQueueForMessage:message]; OWSSendMessageOperation *sendMessageOperation = [[OWSSendMessageOperation alloc] initWithMessage:message messageSender:self + dbConnection:self.dbConnection success:successHandler failure:failureHandler]; - - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - NSOperationQueue *sendingQueue = [self sendingQueueForMessage:message]; - [sendingQueue addOperation:sendMessageOperation]; - }); - }); -} - -- (void)attemptToSendMessage:(TSOutgoingMessage *)message - success:(void (^)(void))successHandler - failure:(RetryableFailureHandler)failureHandler -{ - [self ensureAnyAttachmentsUploaded:message - success:^() { - [self sendMessageToService:message - success:successHandler - failure:^(NSError *error) { - DDLogDebug( - @"%@ Message send attempt failed: %@", self.logTag, message.debugDescription); - failureHandler(error); - }]; + if (message.hasAttachments) { + OWSUploadOperation *uploadAttachmentOperation = + [[OWSUploadOperation alloc] initWithAttachmentId:message.attachmentIds.firstObject + message:message + dbConnection:self.dbConnection]; + [sendMessageOperation addDependency:uploadAttachmentOperation]; + [sendingQueue addOperation:uploadAttachmentOperation]; } - failure:^(NSError *error) { - DDLogDebug(@"%@ Attachment upload attempt failed: %@", self.logTag, message.debugDescription); - failureHandler(error); - }]; -} -- (void)ensureAnyAttachmentsUploaded:(TSOutgoingMessage *)message - success:(void (^)(void))successHandler - failure:(RetryableFailureHandler)failureHandler -{ - if (!message.hasAttachments) { - return successHandler(); - } - - TSAttachmentStream *attachmentStream = - [TSAttachmentStream fetchObjectWithUniqueID:message.attachmentIds.firstObject]; - - if (!attachmentStream) { - OWSProdError([OWSAnalyticsEvents messageSenderErrorCouldNotLoadAttachment]); - NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError(); - // Not finding local attachment is a terminal failure. - [error setIsRetryable:NO]; - return failureHandler(error); - } + // if (message.quotedMessage.hasThumbnailAttachments) { + // OWSUploadOperation *uploadQuoteThumbnailOperation = [[OWSUploadOperation alloc] + // initWithAttachmentId:message.attachmentIds.firstObject + // message:message + // dbConnection:self.dbConnection]; + // [sendMessageOperation addDependency:uploadAttachmentOperation]; + // [sendingQueue addOperation:uploadQuoteThumbnailOperation]; + // } - [self.uploadingService uploadAttachmentStream:attachmentStream - message:message - success:successHandler - failure:failureHandler]; + [sendingQueue addOperation:sendMessageOperation]; + }); } +//- (void)attemptToSendMessage:(TSOutgoingMessage *)message +// success:(void (^)(void))successHandler +// failure:(RetryableFailureHandler)failureHandler +//{ +// [self ensureAnyAttachmentsUploaded:message +// success:^() { +// [self sendMessageToService:message +// success:successHandler +// failure:^(NSError *error) { +// DDLogDebug( +// @"%@ Message send attempt failed: %@", self.logTag, message.debugDescription); +// failureHandler(error); +// }]; +// } +// failure:^(NSError *error) { +// DDLogDebug(@"%@ Attachment upload attempt failed: %@", self.logTag, message.debugDescription); +// failureHandler(error); +// }]; +//} +//- (void)ensureAnyAttachmentsUploaded:(TSOutgoingMessage *)message +// success:(void (^)(void))successHandler +// failure:(RetryableFailureHandler)failureHandler +//{ +// if (!message.hasAttachments) { +// return successHandler(); +// } +// +// TSAttachmentStream *attachmentStream = +// [TSAttachmentStream fetchObjectWithUniqueID:message.attachmentIds.firstObject]; +// +// if (!attachmentStream) { +// OWSProdError([OWSAnalyticsEvents messageSenderErrorCouldNotLoadAttachment]); +// NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError(); +// // Not finding local attachment is a terminal failure. +// [error setIsRetryable:NO]; +// return failureHandler(error); +// } +// +// [OWSUploadingService uploadAttachmentStream:attachmentStream +// message:message +// networkManager:self.networkManager +// success:successHandler +// failure:failureHandler]; +//} + +//- (void)ensureAnyQuotedThumbnailUploaded:(TSOutgoingMessage *)message +// success:(void (^)(void))successHandler +// failure:(RetryableFailureHandler)failureHandler +//{ +// if (!message.hasAttachments) { +// return successHandler(); +// } +// +// TSAttachmentStream *attachmentStream = +// [TSAttachmentStream fetchObjectWithUniqueID:message.attachmentIds.firstObject]; +// +// if (!attachmentStream) { +// OWSProdError([OWSAnalyticsEvents messageSenderErrorCouldNotLoadAttachment]); +// NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError(); +// // Not finding local attachment is a terminal failure. +// [error setIsRetryable:NO]; +// return failureHandler(error); +// } +// +// if (message.quotedMessage.hasThumbnailAttachment) { +// DDLogDebug(@"%@ uploading thumbnail for message: %llu", self.logTag, message.timestamp); +// +// __block TSAttachmentStream *thumbnailAttachmentStream; +// [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) { +// thumbnailAttachmentStream = [message.quotedMessage thumbnailAttachmentWithTransaction:transaction]; +// }]; +// +// if (!thumbnailAttachmentStream) { +// OWSProdError([OWSAnalyticsEvents messageSenderErrorCouldNotLoadAttachment]); +// NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError(); +// // Not finding local attachment is a terminal failure. +// [error setIsRetryable:NO]; +// return failureHandler(error); +// } +// +// [self.uploadingService uploadAttachmentStream:attachmentStream +// message:message +// success:^() { +// [self.uploadingService uploadAttachmentStream:attachmentStream +// message:message +// success:successHandler +// failure:failureHandler]; +// } +// failure:failureHandler]; +// +// } +// [self.uploadingService uploadAttachmentStream:attachmentStream +// message:message +// success:successHandler +// failure:failureHandler]; +// +// +//} + - (void)enqueueTemporaryAttachment:(DataSource *)dataSource contentType:(NSString *)contentType inMessage:(TSOutgoingMessage *)message diff --git a/SignalServiceKit/src/Network/API/OWSUploadOperation.h b/SignalServiceKit/src/Network/API/OWSUploadOperation.h new file mode 100644 index 000000000..d102ec285 --- /dev/null +++ b/SignalServiceKit/src/Network/API/OWSUploadOperation.h @@ -0,0 +1,27 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSOperation.h" + +NS_ASSUME_NONNULL_BEGIN + +@class TSOutgoingMessage; +@class YapDatabaseConnection; + +extern NSString *const kAttachmentUploadProgressNotification; +extern NSString *const kAttachmentUploadProgressKey; +extern NSString *const kAttachmentUploadAttachmentIDKey; + +@interface OWSUploadOperation : OWSOperation + +@property (nullable, readonly) NSError *lastError; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithAttachmentId:(NSString *)attachmentId + message:(TSOutgoingMessage *)outgoingMessage + dbConnection:(YapDatabaseConnection *)dbConnection NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Network/API/OWSUploadOperation.m b/SignalServiceKit/src/Network/API/OWSUploadOperation.m new file mode 100644 index 000000000..c30162c2c --- /dev/null +++ b/SignalServiceKit/src/Network/API/OWSUploadOperation.m @@ -0,0 +1,190 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSUploadOperation.h" +#import "Cryptography.h" +#import "MIMETypeUtil.h" +#import "NSError+MessageSending.h" +#import "NSNotificationCenter+OWS.h" +#import "OWSError.h" +#import "OWSOperation.h" +#import "OWSRequestFactory.h" +#import "TSAttachmentStream.h" +#import "TSNetworkManager.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +NSString *const kAttachmentUploadProgressNotification = @"kAttachmentUploadProgressNotification"; +NSString *const kAttachmentUploadProgressKey = @"kAttachmentUploadProgressKey"; +NSString *const kAttachmentUploadAttachmentIDKey = @"kAttachmentUploadAttachmentIDKey"; + +// Use a slightly non-zero value to ensure that the progress +// indicator shows up as quickly as possible. +static const CGFloat kAttachmentUploadProgressTheta = 0.001f; + +@interface OWSUploadOperation () + +@property (readonly, nonatomic) NSString *attachmentId; +@property (readonly, nonatomic) TSOutgoingMessage *outgoingMessage; +@property (readonly, nonatomic) YapDatabaseConnection *dbConnection; + +@end + +@implementation OWSUploadOperation + +- (instancetype)initWithAttachmentId:(NSString *)attachmentId + message:(TSOutgoingMessage *)outgoingMessage + dbConnection:(YapDatabaseConnection *)dbConnection +{ + self = [super init]; + if (!self) { + return self; + } + + self.remainingRetries = 4; + _attachmentId = attachmentId; + _outgoingMessage = outgoingMessage; + _dbConnection = dbConnection; + + return self; +} + +- (TSNetworkManager *)networkManager +{ + return [TSNetworkManager sharedManager]; +} + +- (void)run +{ + __block TSAttachmentStream *attachmentStream; + [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) { + attachmentStream = [TSAttachmentStream fetchObjectWithUniqueID:self.attachmentId transaction:transaction]; + }]; + + if (!attachmentStream) { + OWSProdError([OWSAnalyticsEvents messageSenderErrorCouldNotLoadAttachment]); + NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError(); + // Not finding local attachment is a terminal failure. + error.isRetryable = NO; + [self reportError:error]; + return; + } + + if (attachmentStream.isUploaded) { + DDLogDebug(@"%@ Attachment previously uploaded.", self.logTag); + [self reportSuccess]; + return; + } + + [self fireNotificationWithProgress:0]; + + DDLogDebug(@"%@ alloc attachment: %@", self.logTag, self.attachmentId); + TSRequest *request = [OWSRequestFactory allocAttachmentRequest]; + [self.networkManager makeRequest:request + success:^(NSURLSessionDataTask *task, id responseObject) { + if (![responseObject isKindOfClass:[NSDictionary class]]) { + DDLogError(@"%@ unexpected response from server: %@", self.logTag, responseObject); + NSError *error = OWSErrorMakeUnableToProcessServerResponseError(); + error.isRetryable = YES; + [self reportError:error]; + return; + } + + NSDictionary *responseDict = (NSDictionary *)responseObject; + UInt64 serverId = ((NSDecimalNumber *)[responseDict objectForKey:@"id"]).unsignedLongLongValue; + NSString *location = [responseDict objectForKey:@"location"]; + + dispatch_async([OWSDispatch attachmentsQueue], ^{ + [self uploadWithServerId:serverId location:location attachmentStream:attachmentStream]; + }); + } + failure:^(NSURLSessionDataTask *task, NSError *error) { + DDLogError(@"%@ Failed to allocate attachment with error: %@", self.logTag, error); + error.isRetryable = YES; + [self reportError:error]; + }]; +} + +- (void)uploadWithServerId:(UInt64)serverId + location:(NSString *)location + attachmentStream:(TSAttachmentStream *)attachmentStream +{ + DDLogDebug(@"%@ started uploading data for attachment: %@", self.logTag, self.attachmentId); + NSError *error; + NSData *attachmentData = [attachmentStream readDataFromFileWithError:&error]; + if (error) { + DDLogError(@"%@ Failed to read attachment data with error: %@", self.logTag, error); + error.isRetryable = YES; + [self reportError:error]; + return; + } + + NSData *encryptionKey; + NSData *digest; + NSData *encryptedAttachmentData = + [Cryptography encryptAttachmentData:attachmentData outKey:&encryptionKey outDigest:&digest]; + + attachmentStream.encryptionKey = encryptionKey; + attachmentStream.digest = digest; + + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:location]]; + request.HTTPMethod = @"PUT"; + [request setValue:OWSMimeTypeApplicationOctetStream forHTTPHeaderField:@"Content-Type"]; + + AFURLSessionManager *manager = [[AFURLSessionManager alloc] + initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]]; + + NSURLSessionUploadTask *uploadTask; + uploadTask = [manager uploadTaskWithRequest:request + fromData:encryptedAttachmentData + progress:^(NSProgress *_Nonnull uploadProgress) { + [self fireNotificationWithProgress:uploadProgress.fractionCompleted]; + } + completionHandler:^(NSURLResponse *_Nonnull response, id _Nullable responseObject, NSError *_Nullable error) { + OWSAssertIsOnMainThread(); + if (error) { + error.isRetryable = YES; + [self reportError:error]; + return; + } + + NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode; + BOOL isValidResponse = (statusCode >= 200) && (statusCode < 400); + if (!isValidResponse) { + DDLogError(@"%@ Unexpected server response: %d", self.logTag, (int)statusCode); + NSError *invalidResponseError = OWSErrorMakeUnableToProcessServerResponseError(); + invalidResponseError.isRetryable = YES; + [self reportError:invalidResponseError]; + return; + } + + DDLogInfo(@"%@ Uploaded attachment: %p.", self.logTag, attachmentStream.uniqueId); + attachmentStream.serverId = serverId; + attachmentStream.isUploaded = YES; + [attachmentStream saveAsyncWithCompletionBlock:^{ + [self reportSuccess]; + }]; + }]; + + [uploadTask resume]; +} + +- (void)fireNotificationWithProgress:(CGFloat)aProgress +{ + NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; + + CGFloat progress = MAX(kAttachmentUploadProgressTheta, aProgress); + [notificationCenter postNotificationNameAsync:kAttachmentUploadProgressNotification + object:nil + userInfo:@{ + kAttachmentUploadProgressKey : @(progress), + kAttachmentUploadAttachmentIDKey : self.attachmentId + }]; +} + + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Network/API/OWSUploadingService.h b/SignalServiceKit/src/Network/API/OWSUploadingService.h deleted file mode 100644 index 3d098a0b7..000000000 --- a/SignalServiceKit/src/Network/API/OWSUploadingService.h +++ /dev/null @@ -1,29 +0,0 @@ -// -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. -// - -#import "OWSMessageSender.h" - -NS_ASSUME_NONNULL_BEGIN - -@class TSAttachmentStream; -@class TSNetworkManager; -@class TSOutgoingMessage; - -extern NSString *const kAttachmentUploadProgressNotification; -extern NSString *const kAttachmentUploadProgressKey; -extern NSString *const kAttachmentUploadAttachmentIDKey; - -@interface OWSUploadingService : NSObject - -- (instancetype)init NS_UNAVAILABLE; -- (instancetype)initWithNetworkManager:(TSNetworkManager *)networkManager NS_DESIGNATED_INITIALIZER; - -- (void)uploadAttachmentStream:(TSAttachmentStream *)attachmentStream - message:(TSOutgoingMessage *)outgoingMessage - success:(void (^)(void))successHandler - failure:(RetryableFailureHandler)failureHandler; - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Network/API/OWSUploadingService.m b/SignalServiceKit/src/Network/API/OWSUploadingService.m deleted file mode 100644 index 8cd4336a2..000000000 --- a/SignalServiceKit/src/Network/API/OWSUploadingService.m +++ /dev/null @@ -1,181 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -#import "OWSUploadingService.h" -#import "Cryptography.h" -#import "MIMETypeUtil.h" -#import "NSNotificationCenter+OWS.h" -#import "OWSError.h" -#import "OWSMessageSender.h" -#import "OWSRequestFactory.h" -#import "TSAttachmentStream.h" -#import "TSNetworkManager.h" -#import "TSOutgoingMessage.h" - -NS_ASSUME_NONNULL_BEGIN - -NSString *const kAttachmentUploadProgressNotification = @"kAttachmentUploadProgressNotification"; -NSString *const kAttachmentUploadProgressKey = @"kAttachmentUploadProgressKey"; -NSString *const kAttachmentUploadAttachmentIDKey = @"kAttachmentUploadAttachmentIDKey"; - -// Use a slightly non-zero value to ensure that the progress -// indicator shows up as quickly as possible. -static const CGFloat kAttachmentUploadProgressTheta = 0.001f; - -@interface OWSUploadingService () - -@property (nonatomic, readonly) TSNetworkManager *networkManager; - -@end - -@implementation OWSUploadingService - -- (instancetype)initWithNetworkManager:(TSNetworkManager *)networkManager -{ - self = [super init]; - if (!self) { - return self; - } - - _networkManager = networkManager; - - return self; -} - -- (void)uploadAttachmentStream:(TSAttachmentStream *)attachmentStream - message:(TSOutgoingMessage *)outgoingMessage - success:(void (^)(void))successHandler - failure:(RetryableFailureHandler)failureHandler -{ - void (^successHandlerWrapper)(void) = ^{ - [self fireProgressNotification:1 attachmentId:attachmentStream.uniqueId]; - - successHandler(); - }; - - RetryableFailureHandler failureHandlerWrapper = ^(NSError *_Nonnull error) { - [self fireProgressNotification:0 attachmentId:attachmentStream.uniqueId]; - - failureHandler(error); - }; - - if (attachmentStream.serverId) { - DDLogDebug(@"%@ Attachment previously uploaded.", self.logTag); - successHandlerWrapper(); - return; - } - - [self fireProgressNotification:kAttachmentUploadProgressTheta attachmentId:attachmentStream.uniqueId]; - - TSRequest *request = [OWSRequestFactory allocAttachmentRequest]; - [self.networkManager makeRequest:request - success:^(NSURLSessionDataTask *task, id responseObject) { - dispatch_async([OWSDispatch attachmentsQueue], ^{ // TODO can we move this queue specification up a level? - if (![responseObject isKindOfClass:[NSDictionary class]]) { - DDLogError(@"%@ unexpected response from server: %@", self.logTag, responseObject); - NSError *error = OWSErrorMakeUnableToProcessServerResponseError(); - [error setIsRetryable:YES]; - return failureHandlerWrapper(error); - } - - NSDictionary *responseDict = (NSDictionary *)responseObject; - UInt64 serverId = ((NSDecimalNumber *)[responseDict objectForKey:@"id"]).unsignedLongLongValue; - NSString *location = [responseDict objectForKey:@"location"]; - - NSError *error; - NSData *attachmentData = [attachmentStream readDataFromFileWithError:&error]; - if (error) { - DDLogError(@"%@ Failed to read attachment data with error:%@", self.logTag, error); - [error setIsRetryable:YES]; - return failureHandlerWrapper(error); - } - - NSData *encryptionKey; - NSData *digest; - NSData *encryptedAttachmentData = - [Cryptography encryptAttachmentData:attachmentData outKey:&encryptionKey outDigest:&digest]; - - attachmentStream.encryptionKey = encryptionKey; - attachmentStream.digest = digest; - - [self uploadDataWithProgress:encryptedAttachmentData - location:location - attachmentId:attachmentStream.uniqueId - success:^{ - OWSAssertIsOnMainThread(); - - DDLogInfo(@"%@ Uploaded attachment: %p.", self.logTag, attachmentStream); - attachmentStream.serverId = serverId; - attachmentStream.isUploaded = YES; - [attachmentStream saveAsyncWithCompletionBlock:successHandlerWrapper]; - } - failure:failureHandlerWrapper]; - - }); - } - failure:^(NSURLSessionDataTask *task, NSError *error) { - DDLogError(@"%@ Failed to allocate attachment with error: %@", self.logTag, error); - [error setIsRetryable:YES]; - failureHandlerWrapper(error); - }]; -} - - -- (void)uploadDataWithProgress:(NSData *)cipherText - location:(NSString *)location - attachmentId:(NSString *)attachmentId - success:(void (^)(void))successHandler - failure:(RetryableFailureHandler)failureHandler -{ - NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:location]]; - request.HTTPMethod = @"PUT"; - request.HTTPBody = cipherText; - [request setValue:OWSMimeTypeApplicationOctetStream forHTTPHeaderField:@"Content-Type"]; - - AFURLSessionManager *manager = [[AFURLSessionManager alloc] - initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]]; - - NSURLSessionUploadTask *uploadTask; - uploadTask = [manager uploadTaskWithRequest:request - fromData:cipherText - progress:^(NSProgress *_Nonnull uploadProgress) { - [self fireProgressNotification:MAX(kAttachmentUploadProgressTheta, uploadProgress.fractionCompleted) - attachmentId:attachmentId]; - } - completionHandler:^(NSURLResponse *_Nonnull response, id _Nullable responseObject, NSError *_Nullable error) { - OWSAssertIsOnMainThread(); - if (error) { - [error setIsRetryable:YES]; - return failureHandler(error); - } - - NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode; - BOOL isValidResponse = (statusCode >= 200) && (statusCode < 400); - if (!isValidResponse) { - DDLogError(@"%@ Unexpected server response: %d", self.logTag, (int)statusCode); - NSError *invalidResponseError = OWSErrorMakeUnableToProcessServerResponseError(); - [invalidResponseError setIsRetryable:YES]; - return failureHandler(invalidResponseError); - } - - successHandler(); - }]; - - [uploadTask resume]; -} - -- (void)fireProgressNotification:(CGFloat)progress attachmentId:(NSString *)attachmentId -{ - NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; - [notificationCenter postNotificationNameAsync:kAttachmentUploadProgressNotification - object:nil - userInfo:@{ - kAttachmentUploadProgressKey : @(progress), - kAttachmentUploadAttachmentIDKey : attachmentId - }]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Network/OWSCensorshipConfiguration.m b/SignalServiceKit/src/Network/OWSCensorshipConfiguration.m index deb9277db..5c3275a29 100644 --- a/SignalServiceKit/src/Network/OWSCensorshipConfiguration.m +++ b/SignalServiceKit/src/Network/OWSCensorshipConfiguration.m @@ -169,16 +169,17 @@ NSString *const OWSCensorshipConfiguration_DefaultFrontingHost = OWSCensorshipCo + (nullable NSData *)certificateDataWithName:(NSString *)name error:(NSError **)error { if (!name.length) { - OWSFail(@"%@ expected name with length > 0", self.logTag); - *error = OWSErrorMakeAssertionError(); + NSString *failureDescription = [NSString stringWithFormat:@"%@ expected name with length > 0", self.logTag]; + *error = OWSErrorMakeAssertionError(failureDescription); return nil; } NSBundle *bundle = [NSBundle bundleForClass:self.class]; NSString *path = [bundle pathForResource:name ofType:@"crt"]; if (![[NSFileManager defaultManager] fileExistsAtPath:path]) { - OWSFail(@"%@ Missing certificate for name: %@", self.logTag, name); - *error = OWSErrorMakeAssertionError(); + NSString *failureDescription = + [NSString stringWithFormat:@"%@ Missing certificate for name: %@", self.logTag, name]; + *error = OWSErrorMakeAssertionError(failureDescription); return nil; } diff --git a/SignalServiceKit/src/Util/NSError+MessageSending.h b/SignalServiceKit/src/Util/NSError+MessageSending.h new file mode 100644 index 000000000..18c738917 --- /dev/null +++ b/SignalServiceKit/src/Util/NSError+MessageSending.h @@ -0,0 +1,15 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +NS_ASSUME_NONNULL_BEGIN + +@interface NSError (MessageSending) + +@property (nonatomic) BOOL isRetryable; +@property (nonatomic) BOOL isFatal; +@property (nonatomic) BOOL shouldBeIgnoredForGroups; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Util/NSError+MessageSending.m b/SignalServiceKit/src/Util/NSError+MessageSending.m new file mode 100644 index 000000000..f34db8089 --- /dev/null +++ b/SignalServiceKit/src/Util/NSError+MessageSending.m @@ -0,0 +1,62 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "NSError+MessageSending.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +static void *kNSError_MessageSender_IsRetryable = &kNSError_MessageSender_IsRetryable; +static void *kNSError_MessageSender_ShouldBeIgnoredForGroups = &kNSError_MessageSender_ShouldBeIgnoredForGroups; +static void *kNSError_MessageSender_IsFatal = &kNSError_MessageSender_IsFatal; + +// isRetryable and isFatal are opposites but not redundant. +// +// If a group message send fails, the send will be retried if any of the errors were retryable UNLESS +// any of the errors were fatal. Fatal errors trump retryable errors. +@implementation NSError (MessageSending) + +- (BOOL)isRetryable +{ + NSNumber *value = objc_getAssociatedObject(self, kNSError_MessageSender_IsRetryable); + // This value should always be set for all errors by the time OWSSendMessageOperation + // queries it's value. If not, default to retrying in production. + OWSAssert(value); + return value ? [value boolValue] : YES; +} + +- (void)setIsRetryable:(BOOL)value +{ + objc_setAssociatedObject(self, kNSError_MessageSender_IsRetryable, @(value), OBJC_ASSOCIATION_COPY); +} + +- (BOOL)shouldBeIgnoredForGroups +{ + NSNumber *value = objc_getAssociatedObject(self, kNSError_MessageSender_ShouldBeIgnoredForGroups); + // This value will NOT always be set for all errors by the time we query it's value. + // Default to NOT ignoring. + return value ? [value boolValue] : NO; +} + +- (void)setShouldBeIgnoredForGroups:(BOOL)value +{ + objc_setAssociatedObject(self, kNSError_MessageSender_ShouldBeIgnoredForGroups, @(value), OBJC_ASSOCIATION_COPY); +} + +- (BOOL)isFatal +{ + NSNumber *value = objc_getAssociatedObject(self, kNSError_MessageSender_IsFatal); + // This value will NOT always be set for all errors by the time we query it's value. + // Default to NOT fatal. + return value ? [value boolValue] : NO; +} + +- (void)setIsFatal:(BOOL)value +{ + objc_setAssociatedObject(self, kNSError_MessageSender_IsFatal, @(value), OBJC_ASSOCIATION_COPY); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Util/OWSError.h b/SignalServiceKit/src/Util/OWSError.h index b5c326a9f..ed6c219e9 100644 --- a/SignalServiceKit/src/Util/OWSError.h +++ b/SignalServiceKit/src/Util/OWSError.h @@ -15,6 +15,7 @@ typedef NS_ENUM(NSInteger, OWSErrorCode) { OWSErrorCodePrivacyVerificationFailure = 20, OWSErrorCodeUntrustedIdentity = 25, OWSErrorCodeFailedToSendOutgoingMessage = 30, + OWSErrorCodeAssertionFailure = 31, OWSErrorCodeFailedToDecryptMessage = 100, OWSErrorCodeFailedToEncryptMessage = 110, OWSErrorCodeSignalServiceFailure = 1001, @@ -51,7 +52,7 @@ extern NSError *OWSErrorMakeUntrustedIdentityError(NSString *description, NSStri extern NSError *OWSErrorMakeUnableToProcessServerResponseError(void); extern NSError *OWSErrorMakeFailedToSendOutgoingMessageError(void); extern NSError *OWSErrorMakeNoSuchSignalRecipientError(void); -extern NSError *OWSErrorMakeAssertionError(void); +extern NSError *OWSErrorMakeAssertionError(NSString *description); extern NSError *OWSErrorMakeMessageSendDisabledDueToPreKeyUpdateFailuresError(void); extern NSError *OWSErrorMakeMessageSendFailedToBlockListError(void); extern NSError *OWSErrorMakeWriteAttachmentDataError(void); diff --git a/SignalServiceKit/src/Util/OWSError.m b/SignalServiceKit/src/Util/OWSError.m index 5d05f686b..870958f53 100644 --- a/SignalServiceKit/src/Util/OWSError.m +++ b/SignalServiceKit/src/Util/OWSError.m @@ -35,10 +35,11 @@ NSError *OWSErrorMakeNoSuchSignalRecipientError() @"ERROR_DESCRIPTION_UNREGISTERED_RECIPIENT", @"Error message when attempting to send message")); } -NSError *OWSErrorMakeAssertionError() +NSError *OWSErrorMakeAssertionError(NSString *description) { - return OWSErrorWithCodeDescription(OWSErrorCodeFailedToSendOutgoingMessage, - NSLocalizedString(@"ERROR_DESCRIPTION_UNKNOWN_ERROR", @"Worst case generic error message")); + OWSCFail(@"Assertion failed: %@", description); + return OWSErrorWithCodeDescription(OWSErrorCodeAssertionFailure, + NSLocalizedString(@"ERROR_DESCRIPTION_UNKNOWN_ERROR", @"Worst case generic error message")); } NSError *OWSErrorMakeUntrustedIdentityError(NSString *description, NSString *recipientId) diff --git a/SignalServiceKit/src/Util/OWSOperation.h b/SignalServiceKit/src/Util/OWSOperation.h new file mode 100644 index 000000000..66b3dcaf6 --- /dev/null +++ b/SignalServiceKit/src/Util/OWSOperation.h @@ -0,0 +1,63 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSInteger, OWSOperationState) { + OWSOperationStateNew, + OWSOperationStateExecuting, + OWSOperationStateFinished +}; + +// A base class for implementing retryable operations. +// To utilize the retryable behavior: +// Set remainingRetries to something greater than 0, and when you're reporting an error, +// set `error.isRetryable = YES`. +// If the failure is one that will not succeed upon retry, set `error.isFatal = YES`. +// +// isRetryable and isFatal are opposites but not redundant. +// +// If a group message send fails, the send will be retried if any of the errors were retryable UNLESS +// any of the errors were fatal. Fatal errors trump retryable errors. +@interface OWSOperation : NSOperation + +@property (nullable) NSError *failingError; +@property NSUInteger remainingRetries; + +#pragma mark - Subclass Overrides + +// Called one time only +- (nullable NSError *)checkForPreconditionError; + +// Called every retry, this is where the bulk of the operation's work should go. +- (void)run; + +// Called at most one time. +- (void)didSucceed; + +// Called at most one time, once retry is no longer possible. +- (void)didFailWithError:(NSError *)error; + +#pragma mark - Success/Error - Do Not Override + +// Complete the operation successfully. +// Should be called at most once per operation instance. +- (void)reportSuccess; + +// To avoid retry, report an error with `error.isFatal = YES` +// otherwise the operation will retry if possible. +// Should be called at most once per `run`, and you should +// ensure that `run` cannot succeed after calling `reportError` +// e.g. generally: +// +// [self reportError:someError]; +// return; +// +- (void)reportError:(NSError *)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Util/OWSOperation.m b/SignalServiceKit/src/Util/OWSOperation.m new file mode 100644 index 000000000..b3883e39e --- /dev/null +++ b/SignalServiceKit/src/Util/OWSOperation.m @@ -0,0 +1,181 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSOperation.h" +#import "NSError+MessageSending.h" +#import "OWSBackgroundTask.h" + +NS_ASSUME_NONNULL_BEGIN + +NSString *const OWSOperationKeyIsExecuting = @"isExecuting"; +NSString *const OWSOperationKeyIsFinished = @"isFinished"; + +@interface OWSOperation () + +@property (nonatomic) OWSOperationState operationState; +@property (nonatomic) OWSBackgroundTask *backgroundTask; + +@end + +@implementation OWSOperation + +- (instancetype)init +{ + self = [super init]; + if (!self) { + return self; + } + + _operationState = OWSOperationStateNew; + _backgroundTask = [OWSBackgroundTask backgroundTaskWithLabel:self.logTag]; + + // Operations are not retryable by default. + _remainingRetries = 0; + + return self; +} + +- (void)dealloc +{ + DDLogDebug(@"%@ in dealloc", self.logTag); +} + +#pragma mark - Subclass Overrides + +// Called one time only +- (nullable NSError *)checkForPreconditionError +{ + // no-op + // Override in subclass if necessary + return nil; +} + +// Called every retry, this is where the bulk of the operation's work should go. +- (void)run +{ + OWSFail(@"%@ Abstract method", self.logTag); +} + +// Called at most one time. +- (void)didSucceed +{ + // no-op + // Override in subclass if necessary +} + +// Called at most one time, once retry is no longer possible. +- (void)didFailWithError:(NSError *)error +{ + // no-op + // Override in subclass if necessary +} + +#pragma mark - NSOperation overrides + +// Do not override this method in a subclass instead, override `run` +- (void)main +{ + DDLogDebug(@"%@ started.", self.logTag); + NSError *_Nullable preconditionError = [self checkForPreconditionError]; + if (preconditionError) { + [self failOperationWithError:preconditionError]; + return; + } + + [self run]; +} + +#pragma mark - Public Methods + +// These methods are not intended to be subclassed +- (void)reportSuccess +{ + DDLogDebug(@"%@ succeeded.", self.logTag); + [self didSucceed]; + [self markAsComplete]; +} + +- (void)reportError:(NSError *)error +{ + DDLogDebug(@"%@ reportError: %@, fatal?: %d, retryable?: %d, remainingRetries: %d", + self.logTag, + error, + error.isFatal, + error.isRetryable, + self.remainingRetries); + + if (error.isFatal) { + [self failOperationWithError:error]; + return; + } + + if (!error.isRetryable) { + [self failOperationWithError:error]; + return; + } + + if (self.remainingRetries == 0) { + [self failOperationWithError:error]; + return; + } + + self.remainingRetries--; + + // TODO Do we want some kind of exponential backoff? + // I'm not sure that there is a one-size-fits all backoff approach + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self run]; + }); +} + +#pragma mark - Life Cycle + +- (void)failOperationWithError:(NSError *)error +{ + DDLogDebug(@"%@ failed terminally.", self.logTag); + self.failingError = error; + + [self didFailWithError:error]; + [self markAsComplete]; +} + +- (BOOL)isExecuting +{ + return self.operationState == OWSOperationStateExecuting; +} + +- (BOOL)isFinished +{ + return self.operationState == OWSOperationStateFinished; +} + +- (void)start +{ + [self willChangeValueForKey:OWSOperationKeyIsExecuting]; + self.operationState = OWSOperationStateExecuting; + [self didChangeValueForKey:OWSOperationKeyIsExecuting]; + + [self main]; +} + +- (void)markAsComplete +{ + [self willChangeValueForKey:OWSOperationKeyIsExecuting]; + [self willChangeValueForKey:OWSOperationKeyIsFinished]; + + // Ensure we call the success or failure handler exactly once. + @synchronized(self) + { + OWSAssert(self.operationState != OWSOperationStateFinished); + + self.operationState = OWSOperationStateFinished; + } + + [self didChangeValueForKey:OWSOperationKeyIsExecuting]; + [self didChangeValueForKey:OWSOperationKeyIsFinished]; +} + +@end + +NS_ASSUME_NONNULL_END