Reusable UploadOperation based on extracted OWSOperation

// FREEBIE
pull/1/head
Michael Kirk 7 years ago
parent f3c511b78b
commit 53af41fcc6

@ -5,9 +5,9 @@
#import "AttachmentUploadView.h" #import "AttachmentUploadView.h"
#import "OWSBezierPathView.h" #import "OWSBezierPathView.h"
#import "OWSProgressView.h" #import "OWSProgressView.h"
#import "OWSUploadingService.h" #import <SignalMessaging/UIView+OWS.h>
#import "TSAttachmentStream.h" #import <SignalServiceKit/OWSUploadOperation.h>
#import "UIView+OWS.h" #import <SignalServiceKit/TSAttachmentStream.h>
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN

@ -52,15 +52,16 @@ class QuotedReplyPreview: UIView {
}() }()
let thumbnailView: UIView? = { let thumbnailView: UIView? = {
if let image = quotedMessage.thumbnailImage() { // FIXME TODO
let imageView = UIImageView(image: image) // if let image = quotedMessage.thumbnailImage() {
imageView.contentMode = .scaleAspectFill // let imageView = UIImageView(image: image)
imageView.autoPinToSquareAspectRatio() // imageView.contentMode = .scaleAspectFill
imageView.layer.cornerRadius = 3.0 // imageView.autoPinToSquareAspectRatio()
imageView.clipsToBounds = true // imageView.layer.cornerRadius = 3.0
// imageView.clipsToBounds = true
return imageView //
} // return imageView
// }
return nil return nil
}() }()

@ -92,8 +92,7 @@ public class OWS106EnsureProfileComplete: OWSDatabaseMigration {
let (promise, fulfill, reject) = Promise<Void>.pending() let (promise, fulfill, reject) = Promise<Void>.pending()
guard let networkManager = Environment.current().networkManager else { guard let networkManager = Environment.current().networkManager else {
owsFail("\(TAG) network manager was unexpectedly not set") return Promise(error: OWSErrorMakeAssertionError("\(TAG) network manager was unexpectedly not set"))
return Promise(error: OWSErrorMakeAssertionError())
} }
ProfileFetcherJob(networkManager: networkManager).getProfile(recipientId: localRecipientId).then { _ -> Void in ProfileFetcherJob(networkManager: networkManager).getProfile(recipientId: localRecipientId).then { _ -> Void in

@ -394,9 +394,14 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec
- (OWSSignalServiceProtosDataMessageBuilder *)dataMessageBuilder - (OWSSignalServiceProtosDataMessageBuilder *)dataMessageBuilder
{ {
TSThread *thread = self.thread; TSThread *thread = self.thread;
OWSAssert(thread);
OWSSignalServiceProtosDataMessageBuilder *builder = [OWSSignalServiceProtosDataMessageBuilder new]; OWSSignalServiceProtosDataMessageBuilder *builder = [OWSSignalServiceProtosDataMessageBuilder new];
[builder setTimestamp:self.timestamp]; [builder setTimestamp:self.timestamp];
[builder setBody:self.body]; [builder setBody:self.body];
[builder setExpireTimer:self.expiresInSeconds];
// Group Messages
BOOL attachmentWasGroupAvatar = NO; BOOL attachmentWasGroupAvatar = NO;
if ([thread isKindOfClass:[TSGroupThread class]]) { if ([thread isKindOfClass:[TSGroupThread class]]) {
TSGroupThread *gThread = (TSGroupThread *)thread; TSGroupThread *gThread = (TSGroupThread *)thread;
@ -426,56 +431,39 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec
[groupBuilder setId:gThread.groupModel.groupId]; [groupBuilder setId:gThread.groupModel.groupId];
[builder setGroup:groupBuilder.build]; [builder setGroup:groupBuilder.build];
} }
// Message Attachments
if (!attachmentWasGroupAvatar) { if (!attachmentWasGroupAvatar) {
NSMutableArray *attachments = [NSMutableArray new]; NSMutableArray *attachments = [NSMutableArray new];
for (NSString *attachmentId in self.attachmentIds) { for (NSString *attachmentId in self.attachmentIds) {
NSString *sourceFilename = self.attachmentFilenameMap[attachmentId]; NSString *_Nullable sourceFilename = self.attachmentFilenameMap[attachmentId];
[attachments addObject:[self buildAttachmentProtoForAttachmentId:attachmentId filename:sourceFilename]]; [attachments addObject:[self buildAttachmentProtoForAttachmentId:attachmentId filename:sourceFilename]];
} }
[builder setAttachmentsArray:attachments]; [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) { // Quoted Attachment
OWSSignalServiceProtosDataMessageQuoteBuilder *quoteBuilder = TSQuotedMessage *quotedMessage = self.quotedMessage;
[OWSSignalServiceProtosDataMessageQuoteBuilder new]; if (quotedMessage) {
[quoteBuilder setId:self.quotedMessage.timestamp]; OWSSignalServiceProtosDataMessageQuoteBuilder *quoteBuilder = [OWSSignalServiceProtosDataMessageQuoteBuilder new];
[quoteBuilder setAuthor:self.quotedMessage.authorId]; [quoteBuilder setId:quotedMessage.timestamp];
[quoteBuilder setAuthor:quotedMessage.authorId];
BOOL hasQuotedText = NO; BOOL hasQuotedText = NO;
BOOL hasQuotedAttachment = NO; BOOL hasQuotedAttachment = NO;
if (self.quotedMessage.body.length > 0) { if (self.quotedMessage.body.length > 0) {
[quoteBuilder setText:self.quotedMessage.body];
hasQuotedText = YES; hasQuotedText = YES;
[quoteBuilder setText:quotedMessage.body];
} }
if (self.quotedMessage.contentType.length > 0) { if (quotedMessage.thumbnailAttachmentIds.count > 0) {
NSMutableArray *thumbnailAttachments = [NSMutableArray new];
OWSSignalServiceProtosAttachmentPointerBuilder *attachmentBuilder = for (NSString *attachmentId in quotedMessage.thumbnailAttachmentIds) {
[OWSSignalServiceProtosAttachmentPointerBuilder new];
if (self.quotedMessage.thumbnailData.length > 0) {
[attachmentBuilder setThumbnail:self.quotedMessage.thumbnailData];
}
if (self.quotedMessage.sourceFilename.length > 0) {
[attachmentBuilder setFileName:self.quotedMessage.sourceFilename];
}
[attachmentBuilder setContentType:self.quotedMessage.contentType];
[quoteBuilder.attachments addObject:[attachmentBuilder build]];
hasQuotedAttachment = YES; hasQuotedAttachment = YES;
NSString *_Nullable sourceFilename = quotedMessage.thumbnailAttachmentFilenameMap[attachmentId];
[thumbnailAttachments addObject:[self buildAttachmentProtoForAttachmentId:attachmentId filename:sourceFilename]];
}
[quoteBuilder setAttachmentsArray:thumbnailAttachments];
} }
if (hasQuotedText || hasQuotedAttachment) { if (hasQuotedText || hasQuotedAttachment) {
@ -485,7 +473,17 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec
} }
} }
return [builder build]; 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 addLocalProfileKeyIfNecessary:self.thread recipientId:recipientId];
return [[self dataMessageBuilder] build];
} }
- (NSData *)buildPlainTextData:(SignalRecipient *)recipient - (NSData *)buildPlainTextData:(SignalRecipient *)recipient

@ -6,6 +6,8 @@
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
@class TSAttachment;
@interface TSQuotedMessage : TSYapDatabaseObject @interface TSQuotedMessage : TSYapDatabaseObject
@property (nonatomic, readonly) uint64_t timestamp; @property (nonatomic, readonly) uint64_t timestamp;
@ -15,10 +17,9 @@ NS_ASSUME_NONNULL_BEGIN
// or attachment with caption. // or attachment with caption.
@property (nullable, nonatomic, readonly) NSString *body; @property (nullable, nonatomic, readonly) NSString *body;
// This property should be set IFF we are quoting an attachment message. //// This property can be set IFF we are quoting an attachment message, but it is optional.
@property (nullable, nonatomic, readonly) NSString *sourceFilename; //@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 is a MIME type.
// //
// This property should be set IFF we are quoting an attachment message. // This property should be set IFF we are quoting an attachment message.
@ -33,7 +34,14 @@ NS_ASSUME_NONNULL_BEGIN
thumbnailData:(NSData *_Nullable)thumbnailData thumbnailData:(NSData *_Nullable)thumbnailData
contentType:(NSString *_Nullable)contentType; contentType:(NSString *_Nullable)contentType;
- (nullable UIImage *)thumbnailImage; #pragma mark - Attachments
@property (nonatomic, readonly) NSArray<NSString *> *thumbnailAttachmentIds;
// A map of attachment id-to-"source" filename.
@property (nonatomic, readonly) NSMutableDictionary<NSString *, NSString *> *thumbnailAttachmentFilenameMap;
- (BOOL)hasThumbnailAttachments;
- (nullable TSAttachment *)firstThumbnailAttachmentWithTransaction:(YapDatabaseReadTransaction *)transaction;
@end @end

@ -3,6 +3,7 @@
// //
#import "TSQuotedMessage.h" #import "TSQuotedMessage.h"
#import "TSAttachment.h"
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
@ -26,23 +27,50 @@ NS_ASSUME_NONNULL_BEGIN
_timestamp = timestamp; _timestamp = timestamp;
_authorId = authorId; _authorId = authorId;
_body = body; _body = body;
_sourceFilename = sourceFilename; // TODO get source filename from attachment
_thumbnailData = thumbnailData; // _sourceFilename = sourceFilename;
// _thumbnailData = thumbnailData;
_contentType = contentType; _contentType = contentType;
return self; return self;
} }
// TODO maybe this should live closer to the view
- (nullable UIImage *)thumbnailImage - (nullable UIImage *)thumbnailImage
{ {
if (self.thumbnailData.length == 0) { // if (self.thumbnailData.length == 0) {
// return nil;
// }
//
// // PERF TODO cache
// return [UIImage imageWithData:self.thumbnailData];
return nil; return nil;
} }
//- (void)setThumbnailAttachmentId:(NSString *)thumbnailAttachmentId
//{
// _thumbnailAttachmentId = thumbnailAttachmentId;
//}
//
//- (BOOL)hasThumbnailAttachment
//{
// return self.thumbnailAttachmentId.length > 0;
//}
//
// PERF TODO cache - (BOOL)hasThumbnailAttachments
return [UIImage imageWithData:self.thumbnailData]; {
return self.thumbnailAttachmentIds.count > 0;
} }
- (nullable TSAttachment *)firstThumbnailAttachmentWithTransaction:(YapDatabaseReadTransaction *)transaction;
{
if (!self.hasThumbnailAttachments) {
return nil;
}
return [TSAttachment fetchObjectWithUniqueID:self.thumbnailAttachmentIds.firstObject transaction:transaction];
}
@end @end

@ -32,15 +32,6 @@ typedef void (^RetryableFailureHandler)(NSError *_Nonnull error);
// //
// For example, If one member of a group deletes their account, the group should // 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. // 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 - #pragma mark -

@ -5,8 +5,12 @@
#import "OWSMessageSender.h" #import "OWSMessageSender.h"
#import "AppContext.h" #import "AppContext.h"
#import "ContactsUpdater.h" #import "ContactsUpdater.h"
#import "Cryptography.h"
#import "MIMETypeUtil.h"
#import "NSData+keyVersionByte.h" #import "NSData+keyVersionByte.h"
#import "NSData+messagePadding.h" #import "NSData+messagePadding.h"
#import "NSError+MessageSending.h"
#import "NSNotificationCenter+OWS.h"
#import "OWSBackgroundTask.h" #import "OWSBackgroundTask.h"
#import "OWSBlockingManager.h" #import "OWSBlockingManager.h"
#import "OWSDevice.h" #import "OWSDevice.h"
@ -14,6 +18,7 @@
#import "OWSError.h" #import "OWSError.h"
#import "OWSIdentityManager.h" #import "OWSIdentityManager.h"
#import "OWSMessageServiceParams.h" #import "OWSMessageServiceParams.h"
#import "OWSOperation.h"
#import "OWSOutgoingSentMessageTranscript.h" #import "OWSOutgoingSentMessageTranscript.h"
#import "OWSOutgoingSyncMessage.h" #import "OWSOutgoingSyncMessage.h"
#import "OWSPrimaryStorage+PreKeyStore.h" #import "OWSPrimaryStorage+PreKeyStore.h"
@ -21,7 +26,7 @@
#import "OWSPrimaryStorage+sessionStore.h" #import "OWSPrimaryStorage+sessionStore.h"
#import "OWSPrimaryStorage.h" #import "OWSPrimaryStorage.h"
#import "OWSRequestFactory.h" #import "OWSRequestFactory.h"
#import "OWSUploadingService.h" #import "OWSUploadOperation.h"
#import "PreKeyBundle+jsonDict.h" #import "PreKeyBundle+jsonDict.h"
#import "SignalRecipient.h" #import "SignalRecipient.h"
#import "TSAccountManager.h" #import "TSAccountManager.h"
@ -34,7 +39,9 @@
#import "TSNetworkManager.h" #import "TSNetworkManager.h"
#import "TSOutgoingMessage.h" #import "TSOutgoingMessage.h"
#import "TSPreKeyManager.h" #import "TSPreKeyManager.h"
#import "TSQuotedMessage.h"
#import "TSThread.h" #import "TSThread.h"
#import "TextSecureKitEnv.h"
#import "Threading.h" #import "Threading.h"
#import <AxolotlKit/AxolotlExceptions.h> #import <AxolotlKit/AxolotlExceptions.h>
#import <AxolotlKit/CipherMessage.h> #import <AxolotlKit/CipherMessage.h>
@ -42,7 +49,6 @@
#import <AxolotlKit/SessionBuilder.h> #import <AxolotlKit/SessionBuilder.h>
#import <AxolotlKit/SessionCipher.h> #import <AxolotlKit/SessionCipher.h>
#import <TwistedOakCollapsingFutures/CollapsingFutures.h> #import <TwistedOakCollapsingFutures/CollapsingFutures.h>
#import <objc/runtime.h>
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
@ -57,58 +63,6 @@ void AssertIsOnSendingQueue()
#endif #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 - #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 * Used by `OWSMessageSender` to serialize message sending, ensuring that messages are emitted in the order they
* were sent. * were sent.
*/ */
@interface OWSSendMessageOperation : NSOperation @interface OWSSendMessageOperation : OWSOperation
- (instancetype)init NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithMessage:(TSOutgoingMessage *)message - (instancetype)initWithMessage:(TSOutgoingMessage *)message
messageSender:(OWSMessageSender *)messageSender messageSender:(OWSMessageSender *)messageSender
success:(void (^)(void))successHandler dbConnection:(YapDatabaseConnection *)dbConnection
failure:(void (^)(NSError *_Nonnull error))failureHandler NS_DESIGNATED_INITIALIZER; success:(void (^)(void))aSuccessHandler
failure:(void (^)(NSError *_Nonnull error))aFailureHandler NS_DESIGNATED_INITIALIZER;
@end @end
#pragma mark - #pragma mark -
typedef NS_ENUM(NSInteger, OWSSendMessageOperationState) {
OWSSendMessageOperationStateNew,
OWSSendMessageOperationStateExecuting,
OWSSendMessageOperationStateFinished
};
@interface OWSMessageSender (OWSSendMessageOperation) @interface OWSMessageSender (OWSSendMessageOperation)
- (void)attemptToSendMessage:(TSOutgoingMessage *)message - (void)sendMessageToService:(TSOutgoingMessage *)message
success:(void (^)(void))successHandler success:(void (^)(void))successHandler
failure:(RetryableFailureHandler)failureHandler; failure:(RetryableFailureHandler)failureHandler;
@ -146,19 +95,13 @@ typedef NS_ENUM(NSInteger, OWSSendMessageOperationState) {
#pragma mark - #pragma mark -
NSString *const OWSSendMessageOperationKeyIsExecuting = @"isExecuting";
NSString *const OWSSendMessageOperationKeyIsFinished = @"isFinished";
NSUInteger const OWSSendMessageOperationMaxRetries = 4;
@interface OWSSendMessageOperation () @interface OWSSendMessageOperation ()
@property (nonatomic, readonly) TSOutgoingMessage *message; @property (nonatomic, readonly) TSOutgoingMessage *message;
@property (nonatomic, readonly) OWSMessageSender *messageSender; @property (nonatomic, readonly) OWSMessageSender *messageSender;
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
@property (nonatomic, readonly) void (^successHandler)(void); @property (nonatomic, readonly) void (^successHandler)(void);
@property (nonatomic, readonly) void (^failureHandler)(NSError *_Nonnull error); @property (nonatomic, readonly) void (^failureHandler)(NSError *_Nonnull error);
@property (nonatomic) OWSSendMessageOperationState operationState;
@property (nonatomic) OWSBackgroundTask *backgroundTask;
@end @end
@ -168,144 +111,98 @@ NSUInteger const OWSSendMessageOperationMaxRetries = 4;
- (instancetype)initWithMessage:(TSOutgoingMessage *)message - (instancetype)initWithMessage:(TSOutgoingMessage *)message
messageSender:(OWSMessageSender *)messageSender messageSender:(OWSMessageSender *)messageSender
success:(void (^)(void))aSuccessHandler dbConnection:(YapDatabaseConnection *)dbConnection
failure:(void (^)(NSError *_Nonnull error))aFailureHandler success:(void (^)(void))successHandler
failure:(void (^)(NSError *_Nonnull error))failureHandler
{ {
self = [super init]; self = [super init];
if (!self) { if (!self) {
return self; return self;
} }
_operationState = OWSSendMessageOperationStateNew; self.remainingRetries = 6;
self.backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
_message = message; _message = message;
_messageSender = messageSender; _messageSender = messageSender;
_dbConnection = dbConnection;
__weak typeof(self) weakSelf = self; _successHandler = successHandler;
_successHandler = ^{ _failureHandler = failureHandler;
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];
};
return self; 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 OWSOperation *upload = (OWSOperation *)dependency;
{
return self.operationState == OWSSendMessageOperationStateFinished;
}
- (void)start // Cannot proceed if dependency failed - surface the dependency's error.
{ NSError *_Nullable dependencyError = upload.failingError;
[self willChangeValueForKey:OWSSendMessageOperationKeyIsExecuting]; if (dependencyError) {
self.operationState = OWSSendMessageOperationStateExecuting; return dependencyError;
[self didChangeValueForKey:OWSSendMessageOperationKeyIsExecuting]; }
[self main]; }
}
- (void)main // Sanity check preconditions
{ if (self.message.hasAttachments) {
[self tryWithRemainingRetries:OWSSendMessageOperationMaxRetries]; [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 the message has been deleted, abort send.
if (self.message.shouldBeSaved && ![TSOutgoingMessage fetchObjectWithUniqueID:self.message.uniqueId]) { if (self.message.shouldBeSaved && ![TSOutgoingMessage fetchObjectWithUniqueID:self.message.uniqueId]) {
DDLogInfo(@"%@ aborting message send; message deleted.", self.logTag); DDLogInfo(@"%@ aborting message send; message deleted.", self.logTag);
NSError *error = OWSErrorWithCodeDescription( NSError *error = OWSErrorWithCodeDescription(
OWSErrorCodeMessageDeletedBeforeSent, @"Message was deleted before it could be sent."); OWSErrorCodeMessageDeletedBeforeSent, @"Message was deleted before it could be sent.");
self.failureHandler(error); error.isFatal = YES;
[self reportError:error];
return; return;
} }
// Use this flag to ensure a given operation only succeeds or fails once. [self.messageSender sendMessageToService:self.message
__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 attemptToSendMessage:self.message
success:^{ success:^{
OWSAssert(!onceFlag); [self reportSuccess];
onceFlag = YES;
self.successHandler();
} }
failure:retryableFailureHandler]; failure:^(NSError *error) {
[self reportError:error];
}];
} }
- (void)markAsComplete - (void)didSucceed
{ {
[self willChangeValueForKey:OWSSendMessageOperationKeyIsExecuting]; [self.message updateWithMessageState:TSOutgoingMessageStateSentToService];
[self willChangeValueForKey:OWSSendMessageOperationKeyIsFinished]; self.successHandler();
}
// Ensure we call the success or failure handler exactly once.
@synchronized(self)
{
OWSAssert(self.operationState != OWSSendMessageOperationStateFinished);
self.operationState = OWSSendMessageOperationStateFinished; - (void)didFailWithError:(NSError *)error
} {
[self.message updateWithSendingError:error];
[self didChangeValueForKey:OWSSendMessageOperationKeyIsExecuting]; DDLogDebug(@"%@ failed with error: %@", self.logTag, error);
[self didChangeValueForKey:OWSSendMessageOperationKeyIsFinished]; self.failureHandler(error);
} }
@end @end
int const OWSMessageSenderRetryAttempts = 3; int const OWSMessageSenderRetryAttempts = 3;
NSString *const OWSMessageSenderInvalidDeviceException = @"InvalidDeviceException"; NSString *const OWSMessageSenderInvalidDeviceException = @"InvalidDeviceException";
NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
@ -315,7 +212,6 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
@property (nonatomic, readonly) TSNetworkManager *networkManager; @property (nonatomic, readonly) TSNetworkManager *networkManager;
@property (nonatomic, readonly) OWSPrimaryStorage *primaryStorage; @property (nonatomic, readonly) OWSPrimaryStorage *primaryStorage;
@property (nonatomic, readonly) OWSBlockingManager *blockingManager; @property (nonatomic, readonly) OWSBlockingManager *blockingManager;
@property (nonatomic, readonly) OWSUploadingService *uploadingService;
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection; @property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
@property (nonatomic, readonly) id<ContactsManagerProtocol> contactsManager; @property (nonatomic, readonly) id<ContactsManagerProtocol> contactsManager;
@property (nonatomic, readonly) ContactsUpdater *contactsUpdater; @property (nonatomic, readonly) ContactsUpdater *contactsUpdater;
@ -340,8 +236,6 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
_contactsManager = contactsManager; _contactsManager = contactsManager;
_contactsUpdater = contactsUpdater; _contactsUpdater = contactsUpdater;
_sendingQueueMap = [NSMutableDictionary new]; _sendingQueueMap = [NSMutableDictionary new];
_uploadingService = [[OWSUploadingService alloc] initWithNetworkManager:networkManager];
_dbConnection = primaryStorage.newDatabaseConnection; _dbConnection = primaryStorage.newDatabaseConnection;
OWSSingletonAssert(); OWSSingletonAssert();
@ -361,10 +255,16 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
{ {
OWSAssert(message); OWSAssert(message);
NSString *kDefaultQueueKey = @"kDefaultQueueKey"; NSString *kDefaultQueueKey = @"kDefaultQueueKey";
NSString *queueKey = message.uniqueThreadId ?: kDefaultQueueKey; NSString *queueKey = message.uniqueThreadId ?: kDefaultQueueKey;
OWSAssert(queueKey.length > 0); OWSAssert(queueKey.length > 0);
if ([kDefaultQueueKey isEqualToString:queueKey]) {
// when do we get here?
DDLogDebug(@"%@ using default message queue", self.logTag);
}
@synchronized(self) @synchronized(self)
{ {
NSOperationQueue *sendingQueue = self.sendingQueueMap[queueKey]; NSOperationQueue *sendingQueue = self.sendingQueueMap[queueKey];
@ -409,63 +309,133 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
[message updateWithMessageState:TSOutgoingMessageStateAttemptingOut transaction:transaction]; [message updateWithMessageState:TSOutgoingMessageStateAttemptingOut transaction:transaction];
}]; }];
NSOperationQueue *sendingQueue = [self sendingQueueForMessage:message];
OWSSendMessageOperation *sendMessageOperation = OWSSendMessageOperation *sendMessageOperation =
[[OWSSendMessageOperation alloc] initWithMessage:message [[OWSSendMessageOperation alloc] initWithMessage:message
messageSender:self messageSender:self
dbConnection:self.dbConnection
success:successHandler success:successHandler
failure:failureHandler]; failure:failureHandler];
if (message.hasAttachments) {
OWSUploadOperation *uploadAttachmentOperation =
[[OWSUploadOperation alloc] initWithAttachmentId:message.attachmentIds.firstObject
message:message
dbConnection:self.dbConnection];
[sendMessageOperation addDependency:uploadAttachmentOperation];
[sendingQueue addOperation:uploadAttachmentOperation];
}
// if (message.quotedMessage.hasThumbnailAttachments) {
// OWSUploadOperation *uploadQuoteThumbnailOperation = [[OWSUploadOperation alloc]
// initWithAttachmentId:message.attachmentIds.firstObject
// message:message
// dbConnection:self.dbConnection];
// [sendMessageOperation addDependency:uploadAttachmentOperation];
// [sendingQueue addOperation:uploadQuoteThumbnailOperation];
// }
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSOperationQueue *sendingQueue = [self sendingQueueForMessage:message];
[sendingQueue addOperation:sendMessageOperation]; [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 //- (void)attemptToSendMessage:(TSOutgoingMessage *)message
success:(void (^)(void))successHandler // success:(void (^)(void))successHandler
failure:(RetryableFailureHandler)failureHandler // failure:(RetryableFailureHandler)failureHandler
{ //{
if (!message.hasAttachments) { // [self ensureAnyAttachmentsUploaded:message
return successHandler(); // success:^() {
} // [self sendMessageToService:message
// success:successHandler
TSAttachmentStream *attachmentStream = // failure:^(NSError *error) {
[TSAttachmentStream fetchObjectWithUniqueID:message.attachmentIds.firstObject]; // DDLogDebug(
// @"%@ Message send attempt failed: %@", self.logTag, message.debugDescription);
if (!attachmentStream) { // failureHandler(error);
OWSProdError([OWSAnalyticsEvents messageSenderErrorCouldNotLoadAttachment]); // }];
NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError(); // }
// Not finding local attachment is a terminal failure. // failure:^(NSError *error) {
[error setIsRetryable:NO]; // DDLogDebug(@"%@ Attachment upload attempt failed: %@", self.logTag, message.debugDescription);
return failureHandler(error); // failureHandler(error);
} // }];
//}
[self.uploadingService uploadAttachmentStream:attachmentStream //- (void)ensureAnyAttachmentsUploaded:(TSOutgoingMessage *)message
message:message // success:(void (^)(void))successHandler
success:successHandler // failure:(RetryableFailureHandler)failureHandler
failure: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 - (void)enqueueTemporaryAttachment:(DataSource *)dataSource
contentType:(NSString *)contentType contentType:(NSString *)contentType

@ -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

@ -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 <YapDatabase/YapDatabaseConnection.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 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

@ -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

@ -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

@ -169,16 +169,17 @@ NSString *const OWSCensorshipConfiguration_DefaultFrontingHost = OWSCensorshipCo
+ (nullable NSData *)certificateDataWithName:(NSString *)name error:(NSError **)error + (nullable NSData *)certificateDataWithName:(NSString *)name error:(NSError **)error
{ {
if (!name.length) { if (!name.length) {
OWSFail(@"%@ expected name with length > 0", self.logTag); NSString *failureDescription = [NSString stringWithFormat:@"%@ expected name with length > 0", self.logTag];
*error = OWSErrorMakeAssertionError(); *error = OWSErrorMakeAssertionError(failureDescription);
return nil; return nil;
} }
NSBundle *bundle = [NSBundle bundleForClass:self.class]; NSBundle *bundle = [NSBundle bundleForClass:self.class];
NSString *path = [bundle pathForResource:name ofType:@"crt"]; NSString *path = [bundle pathForResource:name ofType:@"crt"];
if (![[NSFileManager defaultManager] fileExistsAtPath:path]) { if (![[NSFileManager defaultManager] fileExistsAtPath:path]) {
OWSFail(@"%@ Missing certificate for name: %@", self.logTag, name); NSString *failureDescription =
*error = OWSErrorMakeAssertionError(); [NSString stringWithFormat:@"%@ Missing certificate for name: %@", self.logTag, name];
*error = OWSErrorMakeAssertionError(failureDescription);
return nil; return nil;
} }

@ -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

@ -0,0 +1,62 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "NSError+MessageSending.h"
#import <objc/runtime.h>
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

@ -15,6 +15,7 @@ typedef NS_ENUM(NSInteger, OWSErrorCode) {
OWSErrorCodePrivacyVerificationFailure = 20, OWSErrorCodePrivacyVerificationFailure = 20,
OWSErrorCodeUntrustedIdentity = 25, OWSErrorCodeUntrustedIdentity = 25,
OWSErrorCodeFailedToSendOutgoingMessage = 30, OWSErrorCodeFailedToSendOutgoingMessage = 30,
OWSErrorCodeAssertionFailure = 31,
OWSErrorCodeFailedToDecryptMessage = 100, OWSErrorCodeFailedToDecryptMessage = 100,
OWSErrorCodeFailedToEncryptMessage = 110, OWSErrorCodeFailedToEncryptMessage = 110,
OWSErrorCodeSignalServiceFailure = 1001, OWSErrorCodeSignalServiceFailure = 1001,
@ -51,7 +52,7 @@ extern NSError *OWSErrorMakeUntrustedIdentityError(NSString *description, NSStri
extern NSError *OWSErrorMakeUnableToProcessServerResponseError(void); extern NSError *OWSErrorMakeUnableToProcessServerResponseError(void);
extern NSError *OWSErrorMakeFailedToSendOutgoingMessageError(void); extern NSError *OWSErrorMakeFailedToSendOutgoingMessageError(void);
extern NSError *OWSErrorMakeNoSuchSignalRecipientError(void); extern NSError *OWSErrorMakeNoSuchSignalRecipientError(void);
extern NSError *OWSErrorMakeAssertionError(void); extern NSError *OWSErrorMakeAssertionError(NSString *description);
extern NSError *OWSErrorMakeMessageSendDisabledDueToPreKeyUpdateFailuresError(void); extern NSError *OWSErrorMakeMessageSendDisabledDueToPreKeyUpdateFailuresError(void);
extern NSError *OWSErrorMakeMessageSendFailedToBlockListError(void); extern NSError *OWSErrorMakeMessageSendFailedToBlockListError(void);
extern NSError *OWSErrorMakeWriteAttachmentDataError(void); extern NSError *OWSErrorMakeWriteAttachmentDataError(void);

@ -35,9 +35,10 @@ NSError *OWSErrorMakeNoSuchSignalRecipientError()
@"ERROR_DESCRIPTION_UNREGISTERED_RECIPIENT", @"Error message when attempting to send message")); @"ERROR_DESCRIPTION_UNREGISTERED_RECIPIENT", @"Error message when attempting to send message"));
} }
NSError *OWSErrorMakeAssertionError() NSError *OWSErrorMakeAssertionError(NSString *description)
{ {
return OWSErrorWithCodeDescription(OWSErrorCodeFailedToSendOutgoingMessage, OWSCFail(@"Assertion failed: %@", description);
return OWSErrorWithCodeDescription(OWSErrorCodeAssertionFailure,
NSLocalizedString(@"ERROR_DESCRIPTION_UNKNOWN_ERROR", @"Worst case generic error message")); NSLocalizedString(@"ERROR_DESCRIPTION_UNKNOWN_ERROR", @"Worst case generic error message"));
} }

@ -0,0 +1,63 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import <Foundation/Foundation.h>
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

@ -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
Loading…
Cancel
Save