Merge branch 'charlesmchen/outgoingMessageState'

pull/1/head
Matthew Chen 9 years ago
commit bb24ffc917

@ -1,5 +1,6 @@
// Created by Michael Kirk on 9/23/16.
// Copyright © 2016 Open Whisper Systems. All rights reserved.
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
#import "OWSRecordTranscriptJob.h"
#import "OWSAttachmentsProcessor.h"
@ -85,8 +86,8 @@ NS_ASSUME_NONNULL_BEGIN
attachmentIds:[NSMutableArray new]
expiresInSeconds:transcript.expirationDuration
expireStartedAt:transcript.expirationStartedAt];
textMessage.messageState = TSOutgoingMessageStateDelivered;
[textMessage save];
// Since updateWithWasDelivered is a new message, updateWithWasDelivered will save it.
[textMessage updateWithWasDelivered];
}
}

@ -19,7 +19,7 @@ typedef NS_ENUM(int32_t, TSErrorMessageType) {
TSErrorMessageUnknownContactBlockOffer,
};
-(instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithTimestamp:(uint64_t)timestamp
inThread:(TSThread *)thread
failedMessageType:(TSErrorMessageType)errorMessageType NS_DESIGNATED_INITIALIZER;

@ -19,8 +19,8 @@ typedef NS_ENUM(NSInteger, TSInfoMessageType) {
+ (instancetype)userNotRegisteredMessageInThread:(TSThread *)thread;
@property TSInfoMessageType messageType;
@property NSString *customMessage;
@property (atomic, readonly) TSInfoMessageType messageType;
@property (atomic, readonly) NSString *customMessage;
- (instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;

@ -104,7 +104,6 @@
TSThread *fetchedThread = [TSThread fetchObjectWithUniqueID:self.uniqueThreadId transaction:transaction];
[fetchedThread updateWithLastMessage:self transaction:transaction];
}

@ -12,19 +12,10 @@ NS_ASSUME_NONNULL_BEGIN
@class TSAttachmentPointer;
typedef NS_ENUM(NSInteger, TSGroupMetaMessage) {
TSGroupMessageNone,
TSGroupMessageNew,
TSGroupMessageUpdate,
TSGroupMessageDeliver,
TSGroupMessageQuit
};
@interface TSMessage : TSInteraction
@property (nonatomic, readonly) NSMutableArray<NSString *> *attachmentIds;
@property (nullable, nonatomic) NSString *body;
@property (nonatomic) TSGroupMetaMessage groupMetaMessage;
@property (nonatomic) uint32_t expiresInSeconds;
@property (nonatomic) uint64_t expireStartedAt;
@property (nonatomic, readonly) uint64_t expiresAt;

@ -8,6 +8,7 @@
#import "TSAttachment.h"
#import "TSAttachmentPointer.h"
#import "TSThread.h"
#import <YapDatabase/YapDatabase.h>
#import <YapDatabase/YapDatabaseTransaction.h>
NS_ASSUME_NONNULL_BEGIN
@ -37,6 +38,8 @@ static const NSUInteger OWSMessageSchemaVersion = 3;
@end
#pragma mark -
@implementation TSMessage
- (instancetype)initWithTimestamp:(uint64_t)timestamp

@ -6,11 +6,6 @@
NS_ASSUME_NONNULL_BEGIN
@class OWSSignalServiceProtosAttachmentPointer;
@class OWSSignalServiceProtosDataMessageBuilder;
@interface TSOutgoingMessage : TSMessage
typedef NS_ENUM(NSInteger, TSOutgoingMessageState) {
// The message is either:
// a) Enqueued for sending.
@ -19,20 +14,38 @@ typedef NS_ENUM(NSInteger, TSOutgoingMessageState) {
TSOutgoingMessageStateAttemptingOut,
// The failure state.
TSOutgoingMessageStateUnsent,
// The message has been sent to the service, but not received by any recipient client.
TSOutgoingMessageStateSent,
// The message has been sent to the service and received by at least one recipient client.
// A recipient may have more than one client, and group message may have more than one recipient.
TSOutgoingMessageStateDelivered
// These two enum values have been combined into TSOutgoingMessageStateSentToService.
TSOutgoingMessageStateSent_OBSOLETE,
TSOutgoingMessageStateDelivered_OBSOLETE,
// The message has been sent to the service.
TSOutgoingMessageStateSentToService,
};
typedef NS_ENUM(NSInteger, TSGroupMetaMessage) {
TSGroupMessageNone,
TSGroupMessageNew,
TSGroupMessageUpdate,
TSGroupMessageDeliver,
TSGroupMessageQuit
};
@class OWSSignalServiceProtosAttachmentPointer;
@class OWSSignalServiceProtosDataMessageBuilder;
@interface TSOutgoingMessage : TSMessage
- (instancetype)initWithTimestamp:(uint64_t)timestamp;
- (instancetype)initWithTimestamp:(uint64_t)timestamp inThread:(nullable TSThread *)thread;
- (instancetype)initWithTimestamp:(uint64_t)timestamp
inThread:(nullable TSThread *)thread
messageBody:(nullable NSString *)body;
- (instancetype)initWithTimestamp:(uint64_t)timestamp
inThread:(nullable TSThread *)thread
groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage;
- (instancetype)initWithTimestamp:(uint64_t)timestamp
inThread:(nullable TSThread *)thread
messageBody:(nullable NSString *)body
@ -49,17 +62,32 @@ typedef NS_ENUM(NSInteger, TSOutgoingMessageState) {
messageBody:(nullable NSString *)body
attachmentIds:(NSMutableArray<NSString *> *)attachmentIds
expiresInSeconds:(uint32_t)expiresInSeconds
expireStartedAt:(uint64_t)expireStartedAt NS_DESIGNATED_INITIALIZER;
expireStartedAt:(uint64_t)expireStartedAt;
- (instancetype)initWithTimestamp:(uint64_t)timestamp
inThread:(nullable TSThread *)thread
messageBody:(nullable NSString *)body
attachmentIds:(NSMutableArray<NSString *> *)attachmentIds
expiresInSeconds:(uint32_t)expiresInSeconds
expireStartedAt:(uint64_t)expireStartedAt
groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;
@property (nonatomic) TSOutgoingMessageState messageState;
@property BOOL hasSyncedTranscript;
@property NSString *customMessage;
@property (atomic, readonly) TSOutgoingMessageState messageState;
// The message has been sent to the service and received by at least one recipient client.
// A recipient may have more than one client, and group message may have more than one recipient.
@property (atomic, readonly) BOOL wasDelivered;
@property (atomic, readonly) BOOL hasSyncedTranscript;
@property (atomic, readonly) NSString *customMessage;
@property (atomic, readonly) NSString *mostRecentFailureText;
// A map of attachment id-to-filename.
@property (nonatomic, readonly) NSMutableDictionary<NSString *, NSString *> *attachmentFilenameMap;
@property (atomic, readonly) TSGroupMetaMessage groupMetaMessage;
/**
* Whether the message should be serialized as a modern aka Content, or the old style legacy message.
* Sync and Call messsages must be sent as Content, but other old style DataMessage payloads should be
@ -67,8 +95,6 @@ typedef NS_ENUM(NSInteger, TSOutgoingMessageState) {
*/
@property (nonatomic, readonly) BOOL isLegacyMessage;
- (void)setSendingError:(NSError *)error;
/**
* Signal Identifier (e.g. e164 number) or nil if in a group thread.
*/
@ -105,6 +131,42 @@ typedef NS_ENUM(NSInteger, TSOutgoingMessageState) {
- (OWSSignalServiceProtosAttachmentPointer *)buildAttachmentProtoForAttachmentId:(NSString *)attachmentId
filename:(nullable NSString *)filename;
// TSOutgoingMessage are updated from many threads. We don't want to save
// our local copy (this instance) since it may be out of date. Instead, we
// use these "updateWith..." methods to:
//
// a) Update a property of this instance.
// b) Load an up-to-date instance of this model from from the data store.
// c) Update and save that fresh instance.
// d) If this message hasn't yet been saved, save this local instance.
//
// After "updateWith...":
//
// a) An updated copy of this message will always have been saved in the
// data store.
// b) The local property on this instance will always have been updated.
// c) Other properties on this instance may be out of date.
//
// All mutable properties of this class have been made read-only to
// prevent accidentally modifying them directly.
//
// This isn't a perfect arrangement, but in practice this will prevent
// data loss and will resolve all known issues.
- (void)updateWithMessageState:(TSOutgoingMessageState)messageState;
- (void)updateWithSendingError:(NSError *)error;
- (void)updateWithHasSyncedTranscript:(BOOL)hasSyncedTranscript;
- (void)updateWithCustomMessage:(NSString *)customMessage transaction:(YapDatabaseReadWriteTransaction *)transaction;
- (void)updateWithCustomMessage:(NSString *)customMessage;
- (void)updateWithWasDeliveredWithTransaction:(YapDatabaseReadWriteTransaction *)transaction;
- (void)updateWithWasDelivered;
#pragma mark - Sent Recipients
- (NSUInteger)sentRecipientsCount;
- (BOOL)wasSentToRecipient:(NSString *)contactId;
- (void)updateWithSentRecipient:(NSString *)contactId transaction:(YapDatabaseReadWriteTransaction *)transaction;
- (void)updateWithSentRecipient:(NSString *)contactId;
@end
NS_ASSUME_NONNULL_END

@ -8,19 +8,58 @@
#import "TSAttachmentStream.h"
#import "TSContactThread.h"
#import "TSGroupThread.h"
#import <YapDatabase/YapDatabase.h>
#import <YapDatabase/YapDatabaseTransaction.h>
NS_ASSUME_NONNULL_BEGIN
NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRecipientAll";
@interface TSOutgoingMessage ()
@property (atomic) TSOutgoingMessageState messageState;
@property (atomic) BOOL hasSyncedTranscript;
@property (atomic) NSString *customMessage;
@property (atomic) NSString *mostRecentFailureText;
@property (atomic) BOOL wasDelivered;
// For outgoing, non-legacy group messages sent from this client, this
// contains the list of recipients to whom the message has been sent.
//
// This collection can also be tested to avoid repeat delivery to the
// same recipient.
@property (atomic) NSArray<NSString *> *sentRecipients;
@property (atomic) TSGroupMetaMessage groupMetaMessage;
@end
#pragma mark -
@implementation TSOutgoingMessage
@synthesize sentRecipients = _sentRecipients;
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
if (!_attachmentFilenameMap) {
_attachmentFilenameMap = [NSMutableDictionary new];
}
// Migrate message state.
if (_messageState == TSOutgoingMessageStateSent_OBSOLETE) {
_messageState = TSOutgoingMessageStateSentToService;
} else if (_messageState == TSOutgoingMessageStateDelivered_OBSOLETE) {
_messageState = TSOutgoingMessageStateSentToService;
_wasDelivered = YES;
}
if (!_sentRecipients) {
_sentRecipients = [NSArray new];
}
}
return self;
}
@ -38,7 +77,11 @@ NS_ASSUME_NONNULL_BEGIN
inThread:(nullable TSThread *)thread
messageBody:(nullable NSString *)body
{
return [self initWithTimestamp:timestamp inThread:thread messageBody:body attachmentIds:[NSMutableArray new]];
return [self initWithTimestamp:timestamp
inThread:thread
messageBody:body
attachmentIds:[NSMutableArray new]
expiresInSeconds:0];
}
- (instancetype)initWithTimestamp:(uint64_t)timestamp
@ -73,6 +116,38 @@ NS_ASSUME_NONNULL_BEGIN
attachmentIds:(NSMutableArray<NSString *> *)attachmentIds
expiresInSeconds:(uint32_t)expiresInSeconds
expireStartedAt:(uint64_t)expireStartedAt
{
TSGroupMetaMessage groupMetaMessage
= ([thread isKindOfClass:[TSGroupThread class]] ? TSGroupMessageDeliver : TSGroupMessageNone);
return [self initWithTimestamp:timestamp
inThread:thread
messageBody:body
attachmentIds:attachmentIds
expiresInSeconds:expiresInSeconds
expireStartedAt:expireStartedAt
groupMetaMessage:groupMetaMessage];
}
- (instancetype)initWithTimestamp:(uint64_t)timestamp
inThread:(nullable TSThread *)thread
groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage
{
return [self initWithTimestamp:timestamp
inThread:thread
messageBody:@""
attachmentIds:[NSMutableArray new]
expiresInSeconds:0
expireStartedAt:0
groupMetaMessage:groupMetaMessage];
}
- (instancetype)initWithTimestamp:(uint64_t)timestamp
inThread:(nullable TSThread *)thread
messageBody:(nullable NSString *)body
attachmentIds:(NSMutableArray<NSString *> *)attachmentIds
expiresInSeconds:(uint32_t)expiresInSeconds
expireStartedAt:(uint64_t)expireStartedAt
groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage
{
self = [super initWithTimestamp:timestamp
inThread:thread
@ -85,13 +160,10 @@ NS_ASSUME_NONNULL_BEGIN
}
_messageState = TSOutgoingMessageStateAttemptingOut;
_sentRecipients = [NSArray new];
_hasSyncedTranscript = NO;
_groupMetaMessage = groupMetaMessage;
if ([thread isKindOfClass:[TSGroupThread class]]) {
self.groupMetaMessage = TSGroupMessageDeliver;
} else {
self.groupMetaMessage = TSGroupMessageNone;
}
_attachmentFilenameMap = [NSMutableDictionary new];
OWSAssert(self.receivedAtDate);
@ -117,20 +189,172 @@ NS_ASSUME_NONNULL_BEGIN
- (BOOL)shouldStartExpireTimer
{
switch (self.messageState) {
case TSOutgoingMessageStateSent:
case TSOutgoingMessageStateDelivered:
case TSOutgoingMessageStateSentToService:
return self.isExpiringMessage;
case TSOutgoingMessageStateAttemptingOut:
case TSOutgoingMessageStateUnsent:
return NO;
case TSOutgoingMessageStateSent_OBSOLETE:
case TSOutgoingMessageStateDelivered_OBSOLETE:
OWSAssert(0);
return self.isExpiringMessage;
}
}
#pragma mark - Update Methods
// This method does the work for the "updateWith..." methods. Please see
// the header for a discussion of those methods.
- (void)applyChangeToSelfAndLatestOutgoingMessage:(YapDatabaseReadWriteTransaction *)transaction
changeBlock:(void (^)(TSOutgoingMessage *))changeBlock
{
OWSAssert(transaction);
changeBlock(self);
NSString *collection = [[self class] collection];
TSOutgoingMessage *latestMessage = [transaction objectForKey:self.uniqueId inCollection:collection];
if (latestMessage) {
changeBlock(latestMessage);
[latestMessage saveWithTransaction:transaction];
} else {
// This message has not yet been saved.
[self saveWithTransaction:transaction];
}
}
- (void)setSendingError:(NSError *)error
- (void)updateWithSendingError:(NSError *)error
{
OWSAssert(error);
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[self applyChangeToSelfAndLatestOutgoingMessage:transaction
changeBlock:^(TSOutgoingMessage *message) {
[message setMessageState:TSOutgoingMessageStateUnsent];
[message setMostRecentFailureText:error.localizedDescription];
}];
}];
}
- (void)updateWithMessageState:(TSOutgoingMessageState)messageState
{
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[self applyChangeToSelfAndLatestOutgoingMessage:transaction
changeBlock:^(TSOutgoingMessage *message) {
[message setMessageState:messageState];
}];
}];
}
- (void)updateWithHasSyncedTranscript:(BOOL)hasSyncedTranscript
{
_mostRecentFailureText = error.localizedDescription;
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[self applyChangeToSelfAndLatestOutgoingMessage:transaction
changeBlock:^(TSOutgoingMessage *message) {
[message setHasSyncedTranscript:hasSyncedTranscript];
}];
}];
}
- (void)updateWithCustomMessage:(NSString *)customMessage transaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSAssert(customMessage);
OWSAssert(transaction);
[self applyChangeToSelfAndLatestOutgoingMessage:transaction
changeBlock:^(TSOutgoingMessage *message) {
[message setCustomMessage:customMessage];
}];
}
- (void)updateWithCustomMessage:(NSString *)customMessage
{
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[self updateWithCustomMessage:customMessage transaction:transaction];
}];
}
- (void)updateWithWasDeliveredWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSAssert(transaction);
[self applyChangeToSelfAndLatestOutgoingMessage:transaction
changeBlock:^(TSOutgoingMessage *message) {
[message setWasDelivered:YES];
}];
}
- (void)updateWithWasDelivered
{
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[self updateWithWasDeliveredWithTransaction:transaction];
}];
}
#pragma mark - Sent Recipients
- (NSArray<NSString *> *)sentRecipients
{
@synchronized(self)
{
return _sentRecipients;
}
}
- (void)setSentRecipients:(NSArray<NSString *> *)sentRecipients
{
@synchronized(self)
{
_sentRecipients = [sentRecipients copy];
}
}
- (void)addSentRecipient:(NSString *)contactId
{
@synchronized(self)
{
OWSAssert(_sentRecipients);
OWSAssert(contactId.length > 0);
NSMutableArray *sentRecipients = [_sentRecipients mutableCopy];
[sentRecipients addObject:contactId];
_sentRecipients = [sentRecipients copy];
}
}
- (BOOL)wasSentToRecipient:(NSString *)contactId
{
OWSAssert(self.sentRecipients);
OWSAssert(contactId.length > 0);
return [self.sentRecipients containsObject:contactId];
}
- (NSUInteger)sentRecipientsCount
{
OWSAssert(self.sentRecipients);
return self.sentRecipients.count;
}
- (void)updateWithSentRecipient:(NSString *)contactId transaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSAssert(transaction);
[self applyChangeToSelfAndLatestOutgoingMessage:transaction
changeBlock:^(TSOutgoingMessage *message) {
[message addSentRecipient:contactId];
}];
}
- (void)updateWithSentRecipient:(NSString *)contactId
{
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[self updateWithSentRecipient:contactId transaction:transaction];
}];
}
#pragma mark -
- (OWSSignalServiceProtosDataMessageBuilder *)dataMessageBuilder
{
TSThread *thread = self.thread;

@ -84,8 +84,7 @@ static NSString *const OWSFailedMessagesJobMessageStateIndex = @"index_outoing_m
}
DDLogDebug(@"%@ marking message as unsent", self.tag);
message.messageState = TSOutgoingMessageStateUnsent;
[message save];
[message updateWithMessageState:TSOutgoingMessageStateUnsent];
count++;
}];

@ -20,7 +20,23 @@ NS_ASSUME_NONNULL_BEGIN
* but *sometimes* we don't want to retry when we know it's a terminal failure, so we allow the
* caller to indicate this with isRetryable=NO.
*/
typedef void (^RetryableFailureHandler)(NSError *_Nonnull error, BOOL isRetryable);
typedef void (^RetryableFailureHandler)(NSError *_Nonnull error);
// Message send error handling is slightly different for contact and group messages.
//
// 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 -
NS_SWIFT_NAME(MessageSender)
@interface OWSMessageSender : NSObject {

@ -39,6 +39,7 @@
#import <AxolotlKit/SessionBuilder.h>
#import <AxolotlKit/SessionCipher.h>
#import <TwistedOakCollapsingFutures/CollapsingFutures.h>
#import <objc/runtime.h>
NS_ASSUME_NONNULL_BEGIN
@ -51,6 +52,42 @@ void AssertIsOnSendingQueue()
#endif
}
static void *kNSError_MessageSender_IsRetryable = &kNSError_MessageSender_IsRetryable;
static void *kNSError_MessageSender_ShouldBeIgnoredForGroups = &kNSError_MessageSender_ShouldBeIgnoredForGroups;
@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);
}
@end
#pragma mark -
/**
* OWSSendMessageOperation encapsulates all the work associated with sending a message, e.g. uploading attachments,
* getting proper keys, and retrying upon failure.
@ -87,8 +124,6 @@ typedef NS_ENUM(NSInteger, OWSSendMessageOperationState) {
success:(void (^)())successHandler
failure:(RetryableFailureHandler)failureHandler;
- (void)saveMessage:(TSOutgoingMessage *)message withError:(NSError *)error;
@end
#pragma mark -
@ -136,6 +171,9 @@ NSUInteger const OWSSendMessageOperationMaxRetries = 4;
OWSCAssert(NO);
return;
}
[message updateWithMessageState:TSOutgoingMessageStateSentToService];
DDLogDebug(@"%@ succeeded.", strongSelf.tag);
aSuccessHandler();
[strongSelf markAsComplete];
@ -148,7 +186,7 @@ NSUInteger const OWSSendMessageOperationMaxRetries = 4;
return;
}
[strongSelf.messageSender saveMessage:strongSelf.message withError:error];
[strongSelf.message updateWithSendingError:error];
DDLogDebug(@"%@ failed with error: %@", strongSelf.tag, error);
aFailureHandler(error);
@ -225,10 +263,10 @@ NSUInteger const OWSSendMessageOperationMaxRetries = 4;
{
DDLogDebug(@"%@ remainingRetries: %lu", self.tag, (unsigned long)remainingRetries);
RetryableFailureHandler retryableFailureHandler = ^(NSError *_Nonnull error, BOOL isRetryable) {
RetryableFailureHandler retryableFailureHandler = ^(NSError *_Nonnull error) {
DDLogInfo(@"%@ Sending failed.", self.tag);
if (!isRetryable) {
if (![error isRetryable]) {
DDLogInfo(@"%@ Skipping retry due to terminal error: %@", self.tag, error);
self.failureHandler(error);
return;
@ -355,7 +393,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
{
AssertIsOnMainThread();
[self saveMessage:message withState:TSOutgoingMessageStateAttemptingOut];
[message updateWithMessageState:TSOutgoingMessageStateAttemptingOut];
OWSSendMessageOperation *sendMessageOperation = [[OWSSendMessageOperation alloc] initWithMessage:message
messageSender:self
success:successHandler
@ -379,14 +417,14 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
success:^() {
[self deliverMessage:message
success:successHandler
failure:^(NSError *error, BOOL isRetryable) {
failure:^(NSError *error) {
DDLogDebug(@"%@ Message send attempt failed: %@", self.tag, message.debugDescription);
failureHandler(error, isRetryable);
failureHandler(error);
}];
}
failure:^(NSError *error, BOOL isRetryable) {
failure:^(NSError *error) {
DDLogDebug(@"%@ Attachment upload attempt failed: %@", self.tag, message.debugDescription);
failureHandler(error, isRetryable);
failureHandler(error);
}];
}
@ -406,7 +444,8 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
DDLogError(@"%@ Unable to find local saved attachment to upload.", self.tag);
NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError();
// Not finding local attachment is a terminal failure.
return failureHandler(error, NO);
[error setIsRetryable:NO];
return failureHandler(error);
}
[self.uploadingService uploadAttachmentStream:attachmentStream
@ -491,36 +530,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
// 2.) fail and create a new identical error message in the thread.
[errorMessage remove];
if ([errorMessage.thread isKindOfClass:[TSContactThread class]]) {
return [self sendMessage:message success:successHandler failure:failureHandler];
}
// else it's a GroupThread
dispatch_async([OWSDispatch sendingQueue], ^{
// Avoid spamming entire group when resending failed message.
SignalRecipient *failedRecipient = [SignalRecipient fetchObjectWithUniqueID:errorMessage.recipientId];
// Normally marking as unsent is handled in sendMessage happy path, but beacuse we're skipping the common entry
// point to message sending in order to send to a single recipient, we have to handle it ourselves.
RetryableFailureHandler markAndFailureHandler = ^(NSError *error, BOOL isRetryable) {
[self saveMessage:message withError:error];
if (isRetryable) {
// FIXME: Fixing this will require a larger refactor. In the meanwhile, we don't
// retry message sending from accepting key-changes in a group. (Except for the inner retry logic w/ the
// messages API)
DDLogWarn(@"%@ Skipping retry for group-message failure during %s.", self.tag, __PRETTY_FUNCTION__);
}
failureHandler(error);
};
[self groupSend:@[ failedRecipient ]
message:message
thread:message.thread
success:successHandler
failure:markAndFailureHandler];
});
return [self sendMessage:message success:successHandler failure:failureHandler];
}
- (NSArray<SignalRecipient *> *)getRecipients:(NSArray<NSString *> *)identifiers error:(NSError **)error
@ -564,16 +574,14 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
[self getRecipients:gThread.groupModel.groupMemberIds error:&error];
if (recipients.count == 0) {
if (error) {
// If not recipients were found, there's no reason to retry. It will just fail again.
failureHandler(error, NO);
return;
} else {
if (!error) {
DDLogError(@"%@ Unknown error finding contacts", self.tag);
// If not recipients were found, there's no reason to retry. It will just fail again.
failureHandler(OWSErrorMakeFailedToSendOutgoingMessageError(), NO);
return;
error = OWSErrorMakeFailedToSendOutgoingMessageError();
}
// If no recipients were found, there's no reason to retry. It will just fail again.
[error setIsRetryable:NO];
failureHandler(error);
return;
}
[self groupSend:recipients message:message thread:gThread success:successHandler failure:failureHandler];
@ -594,13 +602,18 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
? self.storageManager.localNumber
: contactThread.contactIdentifier;
// If we block a user, don't send 1:1 messages to them. The UI
// should prevent this from occurring, but in some edge cases
// you might, for example, have a pending outgoing message when
// you block them.
OWSAssert(recipientContactId.length > 0);
NSArray<NSString *> *blockedPhoneNumbers = _blockingManager.blockedPhoneNumbers;
if ([blockedPhoneNumbers containsObject:recipientContactId]) {
DDLogInfo(@"%@ skipping 1:1 send to blocked contact: %@", self.tag, recipientContactId);
NSError *error = OWSErrorMakeMessageSendFailedToBlockListError();
// No need to retry - the user will continue to be blocked.
failureHandler(error, NO);
[error setIsRetryable:NO];
failureHandler(error);
return;
}
@ -619,7 +632,8 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
DDLogError(@"%@ contact lookup failed with error: %@", self.tag, error);
// No need to repeat trying to find a failure. Apart from repeatedly failing, it would also cause us
// to print redundant error messages.
failureHandler(error, NO);
[error setIsRetryable:NO];
failureHandler(error);
return;
}
}
@ -629,7 +643,8 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
DDLogWarn(@"recipient contact still not found after attempting lookup.");
// No need to repeat trying to find a failure. Apart from repeatedly failing, it would also cause us to
// print redundant error messages.
failureHandler(error, NO);
[error setIsRetryable:NO];
failureHandler(error);
return;
}
@ -646,13 +661,13 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
OWSAssert(NO);
NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError();
failureHandler(error, NO);
[error setIsRetryable:NO];
failureHandler(error);
}
});
}
/// For group sends, we're using chained futures to make the code more readable.
// For group sends, we're using chained futures to make the code more readable.
- (TOCFuture *)sendMessageFuture:(TSOutgoingMessage *)message
recipient:(SignalRecipient *)recipient
thread:(TSThread *)thread
@ -664,10 +679,11 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
thread:thread
attempts:OWSMessageSenderRetryAttempts
success:^{
DDLogInfo(@"Marking group message as sent to recipient: %@", recipient.uniqueId);
[message updateWithSentRecipient:recipient.uniqueId];
[futureSource trySetResult:@1];
}
failure:^(NSError *error, BOOL isRetryable) {
// FIXME what to do WRT retryable here?
failure:^(NSError *error) {
[futureSource trySetFailure:error];
}];
@ -683,14 +699,20 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
[self saveGroupMessage:message inThread:thread];
NSMutableArray<TOCFuture *> *futures = [NSMutableArray array];
for (SignalRecipient *rec in recipients) {
for (SignalRecipient *recipient in recipients) {
// We don't need to send the message to ourselves...
if ([rec.uniqueId isEqualToString:[TSStorageManager localNumber]]) {
if ([recipient.uniqueId isEqualToString:[TSStorageManager localNumber]]) {
continue;
}
if ([message wasSentToRecipient:recipient.uniqueId]) {
// Skip recipients we have already sent this message to (on an
// earlier retry, perhaps).
DDLogInfo(@"Skipping group message recipient; already sent: %@", recipient.uniqueId);
continue;
}
// ...otherwise we send.
[futures addObject:[self sendMessageFuture:message recipient:rec thread:thread]];
[futures addObject:[self sendMessageFuture:message recipient:recipient thread:thread]];
}
TOCFuture *completionFuture = futures.toc_thenAll;
@ -700,36 +722,54 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
}];
[completionFuture catchDo:^(id failure) {
// failure from toc_thenAll yeilds an array of failed Futures, rather than the future's failure.
if ([failure isKindOfClass:[NSArray class]]) {
NSArray *errors = (NSArray *)failure;
for (TOCFuture *failedFuture in errors) {
if (!failedFuture.hasFailed) {
// If at least one send succeeded, don't show message as failed.
// Else user will tap-to-resend to all recipients, including those that already received the
// message.
return successHandler();
}
}
// failure from toc_thenAll yields an array of failed Futures, rather than the future's failure.
NSError *firstRetryableError = nil;
NSError *firstNonRetryableError = nil;
// At this point, all recipients must have failed.
// But we have all this verbose type checking because TOCFuture doesn't expose type information.
id lastError = errors.lastObject;
if ([lastError isKindOfClass:[TOCFuture class]]) {
TOCFuture *failedFuture = (TOCFuture *)lastError;
if (failedFuture.hasFailed) {
id failureResult = failedFuture.forceGetFailure;
if ([failure isKindOfClass:[NSArray class]]) {
NSArray *groupSendFutures = (NSArray *)failure;
for (TOCFuture *groupSendFuture in groupSendFutures) {
if (groupSendFuture.hasFailed) {
id failureResult = groupSendFuture.forceGetFailure;
if ([failureResult isKindOfClass:[NSError class]]) {
// Generally we assume that failures are retryable
return failureHandler((NSError *)failureResult, YES);
NSError *error = failureResult;
// Some errors should be ignored when sending messages
// to groups. See discussion on
// NSError (OWSMessageSender) category.
if ([error shouldBeIgnoredForGroups]) {
continue;
}
if ([error isRetryable] && !firstRetryableError) {
firstRetryableError = error;
} else if (![error isRetryable] && !firstNonRetryableError) {
firstNonRetryableError = error;
}
}
}
}
}
DDLogWarn(@"%@ Unexpected generic failure: %@", self.tag, failure);
OWSAssert(NO);
return failureHandler(OWSErrorMakeFailedToSendOutgoingMessageError(), YES);
// If any of the group send errors are retryable, we want to retry.
// Therefore, prefer to propagate a retryable error.
if (firstRetryableError) {
return failureHandler(firstRetryableError);
} else if (firstNonRetryableError) {
return failureHandler(firstNonRetryableError);
} else {
// If we only received errors that we should ignore,
// consider this send a success, unless the message could
// not be sent to any recipient.
if (message.sentRecipientsCount == 0) {
NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeMessageSendNoValidRecipients,
NSLocalizedString(@"ERROR_DESCRIPTION_NO_VALID_RECIPIENTS",
@"Error indicating that an outgoing message had no valid recipients."));
[error setIsRetryable:NO];
failureHandler(error);
} else {
successHandler();
}
}
}];
}
@ -771,7 +811,9 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
}];
DDLogError(@"%@ Message send failed due to repeated inability to update prekeys.", self.tag);
return failureHandler(OWSErrorMakeMessageSendDisabledDueToPreKeyUpdateFailuresError(), YES);
NSError *error = OWSErrorMakeMessageSendDisabledDueToPreKeyUpdateFailuresError();
[error setIsRetryable:YES];
return failureHandler(error);
}
if (remainingAttempts <= 0) {
@ -779,7 +821,9 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
DDLogError(@"%@ Unexpected generic failure.", self.tag);
OWSAssert(NO);
return failureHandler(OWSErrorMakeFailedToSendOutgoingMessageError(), YES);
NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError();
[error setIsRetryable:YES];
return failureHandler(error);
}
remainingAttempts -= 1;
@ -799,7 +843,8 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
@"action sheet header when re-sending message which failed because of untrusted identity keys"));
// Key will continue to be unaccepted, so no need to retry. It'll only cause us to hit the Pre-Key request
// rate limit
return failureHandler(error, NO);
[error setIsRetryable:NO];
return failureHandler(error);
}
if ([exception.name isEqualToString:OWSMessageSenderRateLimitedException]) {
@ -808,17 +853,18 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
@"action sheet header when re-sending message which failed because of too many attempts"));
// We're already rate-limited. No need to exacerbate the problem.
return failureHandler(error, NO);
[error setIsRetryable:NO];
return failureHandler(error);
}
if (remainingAttempts == 0) {
DDLogWarn(
@"%@ Terminal failure to build any device messages. Giving up with exception:%@", self.tag, exception);
[self processException:exception outgoingMessage:message inThread:thread];
NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError();
// Since we've already repeatedly failed to build messages, it's unlikely that repeating the whole process
// will succeed.
return failureHandler(error, NO);
[error setIsRetryable:NO];
return failureHandler(error);
}
}
@ -847,7 +893,8 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
if (remainingAttempts <= 0) {
// Since we've already repeatedly failed to send to the messaging API,
// it's unlikely that repeating the whole process will succeed.
return failureHandler(error, NO);
[error setIsRetryable:NO];
return failureHandler(error);
}
dispatch_async([OWSDispatch sendingQueue], ^{
@ -866,13 +913,19 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
DDLogWarn(@"%@ Unable to send due to invalid credentials. Did the user's client get de-authed by registering elsewhere?", self.tag);
NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeSignalServiceFailure, NSLocalizedString(@"ERROR_DESCRIPTION_SENDING_UNAUTHORIZED", @"Error message when attempting to send message"));
// No need to retry if we've been de-authed.
return failureHandler(error, NO);
[error setIsRetryable:NO];
return failureHandler(error);
}
case 404: {
[self unregisteredRecipient:recipient message:message thread:thread];
NSError *error = OWSErrorMakeNoSuchSignalRecipientError();
// No need to retry if the recipient is not registered.
return failureHandler(error, NO);
[error setIsRetryable:NO];
// If one member of a group deletes their account,
// the group should ignore errors when trying to send
// messages to this ex-member.
[error setShouldBeIgnoredForGroups:YES];
return failureHandler(error);
}
case 409: {
// Mismatched devices
@ -883,20 +936,22 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
[NSJSONSerialization JSONObjectWithData:responseData options:0 error:&error];
if (error) {
DDLogError(@"%@ Failed to serialize response of mismatched devices: %@", self.tag, error);
return failureHandler(error, YES);
[error setIsRetryable:YES];
return failureHandler(error);
}
[self handleMismatchedDevices:serializedResponse recipient:recipient completion:retrySend];
break;
}
case 410: {
// staledevices
// Stale devices
DDLogWarn(@"Stale devices");
if (!responseData) {
DDLogWarn(@"Stale devices but server didn't specify devices in response.");
NSError *error = OWSErrorMakeUnableToProcessServerResponseError();
return failureHandler(error, YES);
[error setIsRetryable:YES];
return failureHandler(error);
}
[self handleStaleDevicesWithResponse:responseData
@ -938,12 +993,10 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
- (void)handleMessageSentLocally:(TSOutgoingMessage *)message
{
[self saveMessage:message withState:TSOutgoingMessageStateSent];
if (message.shouldSyncTranscript) {
// TODO: I suspect we shouldn't optimistically set hasSyncedTranscript.
// We could set this in a success handler for [sendSyncTranscriptForMessage:].
message.hasSyncedTranscript = YES;
[message updateWithHasSyncedTranscript:YES];
[self sendSyncTranscriptForMessage:message];
}
@ -952,7 +1005,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
- (void)handleMessageSentRemotely:(TSOutgoingMessage *)message sentAt:(uint64_t)sentAt
{
[self saveMessage:message withState:TSOutgoingMessageStateDelivered];
[message updateWithWasDelivered];
[self becomeConsistentWithDisappearingConfigurationForMessage:message];
[self.disappearingMessagesJob setExpirationForMessage:message expirationStartedAt:sentAt];
}
@ -1004,13 +1057,13 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
success:^{
DDLogInfo(@"Succesfully sent sync transcript.");
}
failure:^(NSError *error, BOOL isRetryable) {
failure:^(NSError *error) {
// FIXME: We don't yet honor the isRetryable flag here, since sendSyncTranscriptForMessage
// isn't yet wrapped in our retryable SendMessageOperation. Addressing this would require
// a refactor to the MessageSender. Note that we *do* however continue to respect the
// OWSMessageSenderRetryAttempts, which is an "inner" retry loop, encompassing only the
// messaging API.
DDLogInfo(@"Failed to send sync transcript:%@", error);
DDLogInfo(@"Failed to send sync transcript: %@ (isRetryable: %d)", error, [error isRetryable]);
}];
}
@ -1177,23 +1230,11 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
return TSUnknownMessageType;
}
- (void)saveMessage:(TSOutgoingMessage *)message withState:(TSOutgoingMessageState)state
{
message.messageState = state;
[message save];
}
- (void)saveMessage:(TSOutgoingMessage *)message withError:(NSError *)error
{
message.messageState = TSOutgoingMessageStateUnsent;
[message setSendingError:error];
[message save];
}
- (void)saveGroupMessage:(TSOutgoingMessage *)message inThread:(TSThread *)thread
{
if (message.groupMetaMessage == TSGroupMessageDeliver) {
[self saveMessage:message withState:message.messageState];
// TODO: Why is this necessary?
[message save];
} else if (message.groupMetaMessage == TSGroupMessageQuit) {
[[[TSInfoMessage alloc] initWithTimestamp:message.timestamp
inThread:thread
@ -1229,28 +1270,6 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
});
}
- (void)processException:(NSException *)exception
outgoingMessage:(TSOutgoingMessage *)message
inThread:(TSThread *)thread
{
DDLogWarn(@"%@ Got exception: %@", self.tag, exception);
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
// TODO: This error message is never created?
TSErrorMessage *errorMessage;
if (message.groupMetaMessage == TSGroupMessageNone) {
// Only update this with exception if it is not a group message as group
// messages may except for one group
// send but not another and the UI doesn't know how to handle that
[message setMessageState:TSOutgoingMessageStateUnsent];
[message saveWithTransaction:transaction];
}
[errorMessage saveWithTransaction:transaction];
}];
}
#pragma mark - Logging
+ (NSString *)tag

@ -237,9 +237,7 @@ NS_ASSUME_NONNULL_BEGIN
[TSInteraction interactionForTimestamp:envelope.timestamp withTransaction:transaction];
if ([interaction isKindOfClass:[TSOutgoingMessage class]]) {
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)interaction;
outgoingMessage.messageState = TSOutgoingMessageStateDelivered;
[outgoingMessage saveWithTransaction:transaction];
[outgoingMessage updateWithWasDeliveredWithTransaction:transaction];
}
}];
}

@ -52,10 +52,10 @@ static const CGFloat kAttachmentUploadProgressTheta = 0.001f;
successHandler();
};
RetryableFailureHandler failureHandlerWrapper = ^(NSError *_Nonnull error, BOOL isRetryable) {
RetryableFailureHandler failureHandlerWrapper = ^(NSError *_Nonnull error) {
[self fireProgressNotification:0 attachmentId:attachmentStream.uniqueId];
failureHandler(error, isRetryable);
failureHandler(error);
};
if (attachmentStream.serverId) {
@ -73,7 +73,8 @@ static const CGFloat kAttachmentUploadProgressTheta = 0.001f;
if (![responseObject isKindOfClass:[NSDictionary class]]) {
DDLogError(@"%@ unexpected response from server: %@", self.tag, responseObject);
NSError *error = OWSErrorMakeUnableToProcessServerResponseError();
return failureHandlerWrapper(error, YES);
[error setIsRetryable:YES];
return failureHandlerWrapper(error);
}
NSDictionary *responseDict = (NSDictionary *)responseObject;
@ -84,7 +85,8 @@ static const CGFloat kAttachmentUploadProgressTheta = 0.001f;
NSData *attachmentData = [attachmentStream readDataFromFileWithError:&error];
if (error) {
DDLogError(@"%@ Failed to read attachment data with error:%@", self.tag, error);
return failureHandlerWrapper(error, YES);
[error setIsRetryable:YES];
return failureHandlerWrapper(error);
}
NSData *encryptionKey;
@ -114,7 +116,8 @@ static const CGFloat kAttachmentUploadProgressTheta = 0.001f;
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
DDLogError(@"%@ Failed to allocate attachment with error: %@", self.tag, error);
failureHandlerWrapper(error, YES);
[error setIsRetryable:YES];
failureHandlerWrapper(error);
}];
}
@ -143,7 +146,8 @@ static const CGFloat kAttachmentUploadProgressTheta = 0.001f;
completionHandler:^(NSURLResponse *_Nonnull response, id _Nullable responseObject, NSError *_Nullable error) {
OWSAssert([NSThread isMainThread]);
if (error) {
return failureHandler(error, YES);
[error setIsRetryable:YES];
return failureHandler(error);
}
NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode;
@ -151,7 +155,8 @@ static const CGFloat kAttachmentUploadProgressTheta = 0.001f;
if (!isValidResponse) {
DDLogError(@"%@ Unexpected server response: %d", self.tag, (int)statusCode);
NSError *invalidResponseError = OWSErrorMakeUnableToProcessServerResponseError();
return failureHandler(invalidResponseError, YES);
[invalidResponseError setIsRetryable:YES];
return failureHandler(invalidResponseError);
}
successHandler();

@ -23,6 +23,7 @@ typedef NS_ENUM(NSInteger, OWSErrorCode) {
OWSErrorCodeNoSuchSignalRecipient = 777404,
OWSErrorCodeMessageSendDisabledDueToPreKeyUpdateFailures = 777405,
OWSErrorCodeMessageSendFailedToBlockList = 777406,
OWSErrorCodeMessageSendNoValidRecipients = 777407,
OWSErrorCodeContactsUpdaterRateLimit = 777407,
};

Loading…
Cancel
Save