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 "OWSBezierPathView.h"
#import "OWSProgressView.h"
#import "OWSUploadingService.h"
#import "TSAttachmentStream.h"
#import "UIView+OWS.h"
#import <SignalMessaging/UIView+OWS.h>
#import <SignalServiceKit/OWSUploadOperation.h>
#import <SignalServiceKit/TSAttachmentStream.h>
NS_ASSUME_NONNULL_BEGIN

@ -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
}()

@ -92,8 +92,7 @@ public class OWS106EnsureProfileComplete: OWSDatabaseMigration {
let (promise, fulfill, reject) = Promise<Void>.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

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

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

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

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

@ -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 <AxolotlKit/AxolotlExceptions.h>
#import <AxolotlKit/CipherMessage.h>
@ -42,7 +49,6 @@
#import <AxolotlKit/SessionBuilder.h>
#import <AxolotlKit/SessionCipher.h>
#import <TwistedOakCollapsingFutures/CollapsingFutures.h>
#import <objc/runtime.h>
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<ContactsManagerProtocol> 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

@ -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
{
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;
}

@ -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,
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);

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

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