1548 lines
69 KiB
Objective-C
1548 lines
69 KiB
Objective-C
//
|
|
// Copyright 2017 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
#import "MessageSender.h"
|
|
#import "AppContext.h"
|
|
#import "AxolotlExceptions.h"
|
|
#import "FunctionalUtil.h"
|
|
#import "HTTPUtils.h"
|
|
#import "NSData+keyVersionByte.h"
|
|
#import "OWSBackgroundTask.h"
|
|
#import "OWSContact.h"
|
|
#import "OWSDevice.h"
|
|
#import "OWSDisappearingMessagesJob.h"
|
|
#import "OWSError.h"
|
|
#import "OWSIdentityManager.h"
|
|
#import "OWSOperation.h"
|
|
#import "OWSOutgoingCallMessage.h"
|
|
#import "OWSOutgoingGroupCallMessage.h"
|
|
#import "OWSOutgoingReactionMessage.h"
|
|
#import "OWSOutgoingSenderKeyDistributionMessage.h"
|
|
#import "OWSOutgoingSentMessageTranscript.h"
|
|
#import "OWSOutgoingSyncMessage.h"
|
|
#import "OWSUploadOperation.h"
|
|
#import "PreKeyBundle+jsonDict.h"
|
|
#import "ProfileManagerProtocol.h"
|
|
#import "SSKEnvironment.h"
|
|
#import "SSKPreKeyStore.h"
|
|
#import "SSKSignedPreKeyStore.h"
|
|
#import "SignalRecipient.h"
|
|
#import "TSAccountManager.h"
|
|
#import "TSAttachmentStream.h"
|
|
#import "TSContactThread.h"
|
|
#import "TSGroupThread.h"
|
|
#import "TSIncomingMessage.h"
|
|
#import "TSInfoMessage.h"
|
|
#import "TSOutgoingMessage.h"
|
|
#import "TSPreKeyManager.h"
|
|
#import "TSQuotedMessage.h"
|
|
#import "TSRequest.h"
|
|
#import "TSThread.h"
|
|
#import <SignalCoreKit/NSData+OWS.h>
|
|
#import <SignalCoreKit/NSDate+OWS.h>
|
|
#import <SignalCoreKit/Threading.h>
|
|
#import <SignalServiceKit/SignalServiceKit-Swift.h>
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
NSString *NoSessionForTransientMessageException = @"NoSessionForTransientMessageException";
|
|
|
|
const NSUInteger kOversizeTextMessageSizeThreshold = 2 * 1024;
|
|
|
|
#pragma mark -
|
|
|
|
@implementation OWSOutgoingAttachmentInfo
|
|
|
|
- (instancetype)initWithDataSource:(id<DataSource>)dataSource
|
|
contentType:(NSString *)contentType
|
|
sourceFilename:(nullable NSString *)sourceFilename
|
|
caption:(nullable NSString *)caption
|
|
albumMessageId:(nullable NSString *)albumMessageId
|
|
isBorderless:(BOOL)isBorderless
|
|
isLoopingVideo:(BOOL)isLoopingVideo
|
|
{
|
|
self = [super init];
|
|
if (!self) {
|
|
return self;
|
|
}
|
|
|
|
_dataSource = dataSource;
|
|
_contentType = contentType;
|
|
_sourceFilename = sourceFilename;
|
|
_caption = caption;
|
|
_albumMessageId = albumMessageId;
|
|
_isBorderless = isBorderless;
|
|
_isLoopingVideo = isLoopingVideo;
|
|
|
|
return self;
|
|
}
|
|
|
|
- (nullable TSAttachmentStream *)asStreamConsumingDataSourceWithIsVoiceMessage:(BOOL)isVoiceMessage
|
|
error:(NSError **)error
|
|
{
|
|
TSAttachmentStream *attachmentStream =
|
|
[[TSAttachmentStream alloc] initWithContentType:self.contentType
|
|
byteCount:(UInt32)self.dataSource.dataLength
|
|
sourceFilename:self.sourceFilename
|
|
caption:self.caption
|
|
albumMessageId:self.albumMessageId];
|
|
|
|
if (isVoiceMessage) {
|
|
attachmentStream.attachmentType = TSAttachmentTypeVoiceMessage;
|
|
} else if (self.isBorderless) {
|
|
attachmentStream.attachmentType = TSAttachmentTypeBorderless;
|
|
} else if (self.isLoopingVideo || attachmentStream.isAnimated) {
|
|
attachmentStream.attachmentType = TSAttachmentTypeGIF;
|
|
}
|
|
|
|
BOOL success = [attachmentStream writeConsumingDataSource:self.dataSource error:error];
|
|
if (*error != nil) {
|
|
OWSFailDebug(@"Error: %@", *error);
|
|
return nil;
|
|
}
|
|
if (!success) {
|
|
OWSFailDebug(@"Unknown error.");
|
|
*error = OWSErrorMakeAssertionError(@"Could not consume data source.");
|
|
return nil;
|
|
}
|
|
|
|
return attachmentStream;
|
|
}
|
|
@end
|
|
|
|
#pragma mark -
|
|
|
|
/**
|
|
* OWSSendMessageOperation encapsulates all the work associated with sending a message, e.g. uploading attachments,
|
|
* getting proper keys, and retrying upon failure.
|
|
*
|
|
* Used by `MessageSender` to serialize message sending, ensuring that messages are emitted in the order they
|
|
* were sent.
|
|
*/
|
|
@interface OWSSendMessageOperation : OWSOperation
|
|
|
|
+ (instancetype)new NS_UNAVAILABLE;
|
|
- (instancetype)init NS_UNAVAILABLE;
|
|
- (instancetype)initWithMessage:(TSOutgoingMessage *)message
|
|
messageSender:(MessageSender *)messageSender
|
|
pendingTask:(PendingTask *)pendingTask
|
|
success:(void (^)(void))aSuccessHandler
|
|
failure:(void (^)(NSError *error))aFailureHandler NS_DESIGNATED_INITIALIZER;
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
|
|
@interface MessageSender (OWSSendMessageOperation)
|
|
|
|
- (void)sendMessageToService:(TSOutgoingMessage *)message
|
|
success:(void (^)(void))successHandler
|
|
failure:(RetryableFailureHandler)failureHandler;
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
|
|
@interface OWSSendMessageOperation ()
|
|
|
|
@property (nonatomic, readonly) TSOutgoingMessage *message;
|
|
@property (nonatomic, readonly) MessageSender *messageSender;
|
|
@property (nonatomic, readonly) PendingTask *pendingTask;
|
|
@property (nonatomic, readonly) void (^successHandler)(void);
|
|
@property (nonatomic, readonly) void (^failureHandler)(NSError *error);
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
|
|
@implementation OWSSendMessageOperation
|
|
|
|
- (instancetype)initWithMessage:(TSOutgoingMessage *)message
|
|
messageSender:(MessageSender *)messageSender
|
|
pendingTask:(PendingTask *)pendingTask
|
|
success:(void (^)(void))successHandler
|
|
failure:(void (^)(NSError *error))failureHandler
|
|
{
|
|
self = [super init];
|
|
if (!self) {
|
|
return self;
|
|
}
|
|
|
|
_message = message;
|
|
_messageSender = messageSender;
|
|
_pendingTask = pendingTask;
|
|
_successHandler = successHandler;
|
|
_failureHandler = failureHandler;
|
|
|
|
self.queuePriority = [MessageSender queuePriorityForMessage:message];
|
|
|
|
return self;
|
|
}
|
|
|
|
#pragma mark - OWSOperation overrides
|
|
|
|
- (nullable NSError *)checkForPreconditionError
|
|
{
|
|
__block NSError *_Nullable error = [super checkForPreconditionError];
|
|
if (error) {
|
|
if (error.isNetworkConnectivityFailure) {
|
|
OWSLogWarn(@"Precondition failure: %@.", error);
|
|
} else {
|
|
OWSFailDebug(@"Precondition failure: %@.", error);
|
|
}
|
|
return error;
|
|
}
|
|
|
|
// Sanity check preconditions
|
|
if (self.message.hasAttachments) {
|
|
[self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) {
|
|
for (TSAttachment *attachment in [self.message allAttachmentsWithTransaction:transaction.unwrapGrdbRead]) {
|
|
if (![attachment isKindOfClass:[TSAttachmentStream class]]) {
|
|
error = [OWSUnretryableMessageSenderError asNSError];
|
|
break;
|
|
}
|
|
|
|
TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment;
|
|
if (!attachmentStream.serverId && attachmentStream.cdnKey.length < 1) {
|
|
OWSFailDebug(@"Attachment missing upload state.");
|
|
error = [OWSUnretryableMessageSenderError asNSError];
|
|
break;
|
|
}
|
|
if (!attachmentStream.isUploaded) {
|
|
OWSFailDebug(@"Attachment not uploaded.");
|
|
error = [OWSUnretryableMessageSenderError asNSError];
|
|
break;
|
|
}
|
|
}
|
|
}];
|
|
}
|
|
|
|
return error;
|
|
}
|
|
|
|
- (void)run
|
|
{
|
|
if (AppExpiry.shared.isExpired) {
|
|
OWSLogWarn(@"Unable to send because the application has expired.");
|
|
NSError *error = [AppExpiredError asNSError];
|
|
[self reportError:error];
|
|
return;
|
|
}
|
|
|
|
if (TSAccountManager.shared.isDeregistered) {
|
|
OWSLogWarn(@"Unable to send because the application is deregistered.");
|
|
NSError *error = [AppDeregisteredError asNSError];
|
|
[self reportError:error];
|
|
return;
|
|
}
|
|
|
|
// If the message has been deleted, abort send.
|
|
__block TSInteraction *_Nullable latestCopy;
|
|
[self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) {
|
|
latestCopy = [TSInteraction anyFetchWithUniqueId:self.message.uniqueId transaction:transaction];
|
|
}];
|
|
BOOL messageWasRemotelyDeleted = NO;
|
|
if ([latestCopy isKindOfClass:[TSOutgoingMessage class]]) {
|
|
messageWasRemotelyDeleted = ((TSOutgoingMessage *)latestCopy).wasRemotelyDeleted;
|
|
}
|
|
|
|
[BenchManager
|
|
completeEventWithEventId:[NSString stringWithFormat:@"sendMessagePreNetwork-%llu", self.message.timestamp]];
|
|
|
|
if ((self.message.shouldBeSaved && latestCopy == nil) || messageWasRemotelyDeleted) {
|
|
OWSLogInfo(@"aborting message send; message deleted.");
|
|
NSError *error = [MessageDeletedBeforeSentError asNSError];
|
|
[self reportError:error];
|
|
return;
|
|
}
|
|
|
|
[BenchManager startEventWithTitle:[NSString stringWithFormat:@"Send Message Milestone: Network (%llu)",
|
|
self.message.timestamp]
|
|
eventId:[NSString stringWithFormat:@"sendMessageNetwork-%llu", self.message.timestamp]];
|
|
|
|
[self.messageSender sendMessageToService:self.message
|
|
success:^{ [self reportSuccess]; }
|
|
failure:^(NSError *error) { [self reportError:error]; }];
|
|
}
|
|
|
|
- (void)didSucceed
|
|
{
|
|
if (self.message.messageState != TSOutgoingMessageStateSent) {
|
|
OWSFailDebug(@"Unexpected message status: %@", self.message.statusDescription);
|
|
}
|
|
|
|
self.successHandler();
|
|
|
|
[self.pendingTask complete];
|
|
}
|
|
|
|
- (void)didFailWithError:(NSError *)error
|
|
{
|
|
OWSLogError(@"Failed with error: %@ (isRetryable: %d)", error, error.isRetryable);
|
|
self.failureHandler(error);
|
|
|
|
[self.pendingTask complete];
|
|
}
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
|
|
NSString *const MessageSenderInvalidDeviceException = @"InvalidDeviceException";
|
|
NSString *const MessageSenderRateLimitedException = @"RateLimitedException";
|
|
NSString *const MessageSenderSpamChallengeRequiredException = @"SpamChallengeRequiredException";
|
|
NSString *const MessageSenderSpamChallengeResolvedException = @"SpamChallengeResolvedException";
|
|
|
|
@interface MessageSender ()
|
|
|
|
@property (atomic, readonly) NSMutableDictionary<NSString *, NSOperationQueue *> *sendingQueueMap;
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
|
|
@implementation MessageSender
|
|
|
|
- (instancetype)init
|
|
{
|
|
self = [super init];
|
|
if (!self) {
|
|
return self;
|
|
}
|
|
|
|
_sendingQueueMap = [NSMutableDictionary new];
|
|
_pendingTasks = [[PendingTasks alloc] initWithLabel:@"Message Sends"];
|
|
|
|
OWSSingletonAssert();
|
|
|
|
return self;
|
|
}
|
|
|
|
- (NSOperationQueue *)sendingQueueForMessage:(TSOutgoingMessage *)message
|
|
{
|
|
OWSAssertDebug(message);
|
|
OWSAssertDebug(message.uniqueThreadId);
|
|
|
|
NSString *kDefaultQueueKey = @"kDefaultQueueKey";
|
|
NSString *queueKey = message.uniqueThreadId ?: kDefaultQueueKey;
|
|
OWSAssertDebug(queueKey.length > 0);
|
|
|
|
if ([kDefaultQueueKey isEqualToString:queueKey]) {
|
|
// when do we get here?
|
|
OWSLogDebug(@"using default message queue");
|
|
}
|
|
|
|
@synchronized(self) {
|
|
NSOperationQueue *sendingQueue = self.sendingQueueMap[queueKey];
|
|
|
|
if (!sendingQueue) {
|
|
sendingQueue = [NSOperationQueue new];
|
|
sendingQueue.qualityOfService = NSOperationQualityOfServiceUserInitiated;
|
|
sendingQueue.maxConcurrentOperationCount = 1;
|
|
sendingQueue.name = @"MessageSender-Chat";
|
|
self.sendingQueueMap[queueKey] = sendingQueue;
|
|
}
|
|
|
|
return sendingQueue;
|
|
}
|
|
}
|
|
|
|
+ (NSOperationQueue *)globalSendingQueue
|
|
{
|
|
static dispatch_once_t onceToken;
|
|
static NSOperationQueue *operationQueue;
|
|
|
|
dispatch_once(&onceToken, ^{
|
|
operationQueue = [NSOperationQueue new];
|
|
operationQueue.qualityOfService = NSOperationQualityOfServiceUserInitiated;
|
|
operationQueue.name = @"MessageSender-Global";
|
|
operationQueue.maxConcurrentOperationCount = 5;
|
|
});
|
|
return operationQueue;
|
|
}
|
|
|
|
- (void)sendMessage:(OutgoingMessagePreparer *)outgoingMessagePreparer
|
|
success:(void (^)(void))successHandler
|
|
failure:(void (^)(NSError *error))failureHandler
|
|
{
|
|
OWSAssertDebug([outgoingMessagePreparer isKindOfClass:[OutgoingMessagePreparer class]]);
|
|
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
__block TSOutgoingMessage *message;
|
|
if (outgoingMessagePreparer.canBePreparedWithoutTransaction) {
|
|
message = [outgoingMessagePreparer prepareMessageWithoutTransaction];
|
|
} else {
|
|
__block NSError *error;
|
|
DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
|
|
message = [outgoingMessagePreparer prepareMessageWithTransaction:transaction error:&error];
|
|
if (error != nil) {
|
|
return;
|
|
}
|
|
});
|
|
if (error != nil) {
|
|
dispatch_async(
|
|
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ failureHandler(error); });
|
|
return;
|
|
}
|
|
}
|
|
|
|
OWSAssertDebug(message);
|
|
if (message.body.length > 0) {
|
|
OWSAssertDebug(
|
|
[message.body lengthOfBytesUsingEncoding:NSUTF8StringEncoding] <= kOversizeTextMessageSizeThreshold);
|
|
}
|
|
|
|
OWSLogInfo(@"Sending message: %@, timestamp: %llu.", message.class, message.timestamp);
|
|
|
|
BOOL canUseV3 = (message.groupMetaMessage == TSGroupMetaMessageUnspecified
|
|
|| message.groupMetaMessage == TSGroupMetaMessageDeliver);
|
|
|
|
// We create a PendingTask so we can block on flushing all current message sends.
|
|
PendingTask *pendingTask = [self.pendingTasks buildPendingTaskWithLabel:@"Message Send"];
|
|
|
|
OWSSendMessageOperation *sendMessageOperation =
|
|
[[OWSSendMessageOperation alloc] initWithMessage:message
|
|
messageSender:self
|
|
pendingTask:pendingTask
|
|
success:successHandler
|
|
failure:failureHandler];
|
|
|
|
OWSAssertDebug(outgoingMessagePreparer.savedAttachmentIds != nil);
|
|
for (NSString *attachmentId in outgoingMessagePreparer.savedAttachmentIds) {
|
|
OWSUploadOperation *uploadAttachmentOperation =
|
|
[[OWSUploadOperation alloc] initWithAttachmentId:attachmentId
|
|
messageIds:@[ message.uniqueId ]
|
|
canUseV3:canUseV3];
|
|
[sendMessageOperation addDependency:uploadAttachmentOperation];
|
|
[OWSUploadOperation.uploadQueue addOperation:uploadAttachmentOperation];
|
|
}
|
|
|
|
NSOperationQueue *sendingQueue = [self sendingQueueForMessage:message];
|
|
NSOperationQueue *globalSendingQueue = MessageSender.globalSendingQueue;
|
|
|
|
// We use two "global" operations and the globalSendingQueue
|
|
// to limit how many message sends are in flight at once globally.
|
|
//
|
|
// One global operation runs _before_ sendMessageOperation and
|
|
// ensures that it will not begin while more than N messages
|
|
// sends are in flight at a time.
|
|
//
|
|
// One global operation runs _after_ sendMessageOperation and
|
|
// ensures that subsequent message sends will block behind the
|
|
// message send which are currently enqueuing.
|
|
NSOperation *globalBeforeOperation = [NSOperation new];
|
|
[sendMessageOperation addDependency:globalBeforeOperation];
|
|
NSOperation *globalAfterOperation = [NSOperation new];
|
|
[globalAfterOperation addDependency:sendMessageOperation];
|
|
|
|
@synchronized(self) {
|
|
[globalSendingQueue addOperation:globalBeforeOperation];
|
|
[sendingQueue addOperation:sendMessageOperation];
|
|
[globalSendingQueue addOperation:globalAfterOperation];
|
|
}
|
|
});
|
|
}
|
|
|
|
- (void)sendTemporaryAttachment:(id<DataSource>)dataSource
|
|
contentType:(NSString *)contentType
|
|
inMessage:(TSOutgoingMessage *)message
|
|
success:(void (^)(void))successHandler
|
|
failure:(void (^)(NSError *error))failureHandler
|
|
{
|
|
OWSAssertDebug(dataSource);
|
|
|
|
void (^successWithDeleteHandler)(void) = ^() {
|
|
successHandler();
|
|
|
|
OWSLogDebug(@"Removing successful temporary attachment message with attachment ids: %@", message.attachmentIds);
|
|
|
|
DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
|
|
[message anyRemoveWithTransaction:transaction];
|
|
[message removeTemporaryAttachmentsWithTransaction:transaction];
|
|
});
|
|
};
|
|
|
|
void (^failureWithDeleteHandler)(NSError *error) = ^(NSError *error) {
|
|
failureHandler(error);
|
|
|
|
OWSLogDebug(@"Removing failed temporary attachment message with attachment ids: %@", message.attachmentIds);
|
|
|
|
DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
|
|
[message anyRemoveWithTransaction:transaction];
|
|
[message removeTemporaryAttachmentsWithTransaction:transaction];
|
|
});
|
|
};
|
|
|
|
[self sendAttachment:dataSource
|
|
contentType:contentType
|
|
sourceFilename:nil
|
|
albumMessageId:nil
|
|
inMessage:message
|
|
success:successWithDeleteHandler
|
|
failure:failureWithDeleteHandler];
|
|
}
|
|
|
|
- (void)sendAttachment:(id<DataSource>)dataSource
|
|
contentType:(NSString *)contentType
|
|
sourceFilename:(nullable NSString *)sourceFilename
|
|
albumMessageId:(nullable NSString *)albumMessageId
|
|
inMessage:(TSOutgoingMessage *)message
|
|
success:(void (^)(void))success
|
|
failure:(void (^)(NSError *error))failure
|
|
{
|
|
OWSAssertDebug(dataSource);
|
|
|
|
OWSOutgoingAttachmentInfo *attachmentInfo = [[OWSOutgoingAttachmentInfo alloc] initWithDataSource:dataSource
|
|
contentType:contentType
|
|
sourceFilename:sourceFilename
|
|
caption:nil
|
|
albumMessageId:albumMessageId
|
|
isBorderless:NO
|
|
isLoopingVideo:NO];
|
|
[self sendUnpreparedAttachments:@[
|
|
attachmentInfo,
|
|
]
|
|
inMessage:message
|
|
success:success
|
|
failure:failure];
|
|
}
|
|
|
|
- (void)sendUnpreparedAttachments:(NSArray<OWSOutgoingAttachmentInfo *> *)attachmentInfos
|
|
inMessage:(TSOutgoingMessage *)message
|
|
success:(void (^)(void))success
|
|
failure:(void (^)(NSError *error))failure
|
|
{
|
|
OWSAssertDebug(attachmentInfos.count > 0);
|
|
OutgoingMessagePreparer *outgoingMessagePreparer = [[OutgoingMessagePreparer alloc] init:message
|
|
unsavedAttachmentInfos:attachmentInfos];
|
|
[self sendMessage:outgoingMessagePreparer success:success failure:failure];
|
|
}
|
|
|
|
- (void)sendMessageToService:(TSOutgoingMessage *)message
|
|
success:(void (^)(void))success
|
|
failure:(RetryableFailureHandler)failure
|
|
{
|
|
OWSAssertDebug(!NSThread.isMainThread);
|
|
|
|
if (SSKDebugFlags.messageSendsFail.get) {
|
|
NSError *error = [OWSUnretryableMessageSenderError asNSError];
|
|
failure(error);
|
|
return;
|
|
}
|
|
|
|
[MessageSender prepareForSendOf:message
|
|
success:^(MessageSendInfo *sendInfo) {
|
|
[self sendMessageToService:message sendInfo:sendInfo success:success failure:failure];
|
|
}
|
|
failure:failure];
|
|
}
|
|
|
|
- (AnyPromise *)unlockPreKeyUpdateFailuresPromise
|
|
{
|
|
OWSAssertDebug(!NSThread.isMainThread);
|
|
|
|
if (![TSPreKeyManager isAppLockedDueToPreKeyUpdateFailures]) {
|
|
return [AnyPromise promiseWithValue:@(1)];
|
|
}
|
|
|
|
OWSLogInfo(@"Trying to unlock prekey update.");
|
|
|
|
return AnyPromise.withFutureOn(
|
|
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(AnyFuture *future) {
|
|
OWSProdError([OWSAnalyticsEvents messageSendErrorFailedDueToPrekeyUpdateFailures]);
|
|
|
|
// Retry prekey update every time user tries to send a message while app
|
|
// is disabled due to prekey update failures.
|
|
//
|
|
// Only try to update the signed prekey; updating it is sufficient to
|
|
// re-enable message sending.
|
|
[TSPreKeyManager
|
|
rotateSignedPreKeysWithSuccess:^{
|
|
OWSLogInfo(@"New prekeys registered with server.");
|
|
[future resolveWithValue:@(1)];
|
|
}
|
|
failure:^(NSError *error) {
|
|
OWSLogWarn(@"Failed to update prekeys with the server: %@", error);
|
|
[future rejectWithError:error];
|
|
}];
|
|
});
|
|
}
|
|
|
|
- (AnyPromise *)sendPromiseForServiceIds:(NSArray<ServiceIdObjC *> *)serviceIds
|
|
message:(TSOutgoingMessage *)message
|
|
thread:(TSThread *)thread
|
|
senderCertificates:(nullable SenderCertificates *)senderCertificates
|
|
sendErrorBlock:(void (^_Nonnull)(ServiceIdObjC *address, NSError *))sendErrorBlock
|
|
{
|
|
OWSAssertDebug(!NSThread.isMainThread);
|
|
OWSAssertDebug(serviceIds.count > 0);
|
|
OWSAssertDebug(message);
|
|
OWSAssertDebug(thread);
|
|
|
|
// 1. Build the plaintext message content.
|
|
__block NSData *_Nullable plaintext = nil;
|
|
__block NSNumber *_Nullable plaintextPayloadId = nil;
|
|
DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *writeTx) {
|
|
// Record plaintext
|
|
plaintext = [message buildPlainTextData:thread transaction:writeTx];
|
|
plaintextPayloadId = [MessageSendLog recordPayload:plaintext forMessageBeingSent:message transaction:writeTx];
|
|
});
|
|
OWSLogDebug(@"built message: %@ plainTextData.length: %lu", [message class], (unsigned long)plaintext.length);
|
|
|
|
// 2. Gather "ud sending access".
|
|
NSMutableDictionary<ServiceIdObjC *, OWSUDSendingAccess *> *sendingAccessMap = [NSMutableDictionary new];
|
|
if (senderCertificates != nil) {
|
|
for (ServiceIdObjC *serviceId in serviceIds) {
|
|
if ([self.tsAccountManager.localUuid isEqual:serviceId.uuidValue]) {
|
|
continue;
|
|
}
|
|
if (message.isStorySend) {
|
|
sendingAccessMap[serviceId] = [self.udManager storySendingAccessFor:serviceId
|
|
senderCertificates:senderCertificates];
|
|
} else {
|
|
sendingAccessMap[serviceId] = [self.udManager udSendingAccessFor:serviceId
|
|
requireSyncAccess:YES
|
|
senderCertificates:senderCertificates];
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. If we have any participants that support sender key, build a promise for their send.
|
|
SenderKeyStatus *senderKeyStatus = [self senderKeyStatusFor:thread
|
|
intendedRecipients:serviceIds
|
|
udAccessMap:sendingAccessMap];
|
|
|
|
AnyPromise *_Nullable senderKeyMessagePromise = nil;
|
|
NSArray<ServiceIdObjC *> *senderKeyServiceIds = senderKeyStatus.allSenderKeyParticipants;
|
|
NSArray<ServiceIdObjC *> *fanoutServiceIds = senderKeyStatus.fanoutParticipants;
|
|
if (thread.usesSenderKey && senderKeyServiceIds.count >= 2 && message.canSendWithSenderKey) {
|
|
senderKeyMessagePromise = [self senderKeyMessageSendPromiseWithMessage:message
|
|
plaintextContent:plaintext
|
|
payloadId:plaintextPayloadId
|
|
thread:thread
|
|
status:senderKeyStatus
|
|
udAccessMap:sendingAccessMap
|
|
senderCertificates:senderCertificates
|
|
sendErrorBlock:sendErrorBlock];
|
|
|
|
OWSLogDebug(@"%lu / %lu recipients for message: %llu support sender key.",
|
|
senderKeyServiceIds.count,
|
|
serviceIds.count,
|
|
message.timestamp);
|
|
} else {
|
|
senderKeyServiceIds = @[];
|
|
fanoutServiceIds = serviceIds;
|
|
if (!message.canSendWithSenderKey) {
|
|
OWSLogInfo(
|
|
@"Last sender key send attempt failed for message %llu. Falling back to fanout.", message.timestamp);
|
|
} else {
|
|
OWSLogDebug(@"Sender key not supported for message %llu", message.timestamp);
|
|
}
|
|
}
|
|
OWSAssertDebug((fanoutServiceIds.count + senderKeyServiceIds.count) == serviceIds.count);
|
|
|
|
// 4. Build a "OWSMessageSend" for each non-senderKey recipient.
|
|
NSMutableArray<OWSMessageSend *> *messageSends = [NSMutableArray new];
|
|
for (ServiceIdObjC *serviceId in fanoutServiceIds) {
|
|
OWSUDSendingAccess *_Nullable udSendingAccess = sendingAccessMap[serviceId];
|
|
OWSMessageSend *messageSend =
|
|
[[OWSMessageSend alloc] initWithMessage:message
|
|
plaintextContent:plaintext
|
|
plaintextPayloadId:plaintextPayloadId
|
|
thread:thread
|
|
serviceId:serviceId
|
|
udSendingAccess:udSendingAccess
|
|
localAddress:self.tsAccountManager.localAddress
|
|
sendErrorBlock:^(NSError *error) { sendErrorBlock(serviceId, error); }];
|
|
[messageSends addObject:messageSend];
|
|
}
|
|
|
|
// 5. Before kicking of the per-recipient message sends, try
|
|
// to ensure sessions for all recipient devices in parallel.
|
|
return
|
|
[MessageSender ensureSessionsforMessageSendsObjc:messageSends ignoreErrors:YES].thenInBackground(^(id value) {
|
|
// 6. Perform the per-recipient message sends.
|
|
NSMutableArray<AnyPromise *> *sendPromises = [NSMutableArray array];
|
|
for (OWSMessageSend *messageSend in messageSends) {
|
|
[self sendMessageToRecipient:messageSend];
|
|
[sendPromises addObject:messageSend.asAnyPromise];
|
|
}
|
|
// 7. Add the sender-key promise
|
|
if (senderKeyMessagePromise != nil) {
|
|
[sendPromises addObject:senderKeyMessagePromise];
|
|
}
|
|
|
|
// We use resolved, not fulfilled, because we don't want the
|
|
// completion promise to execute until _all_ send promises
|
|
// have either succeeded or failed. Fulfilled executes as
|
|
// soon as any of its input promises fail.
|
|
return [AnyPromise whenResolved:sendPromises];
|
|
});
|
|
}
|
|
|
|
- (void)sendMessageToService:(TSOutgoingMessage *)message
|
|
sendInfo:(MessageSendInfo *)sendInfo
|
|
success:(void (^)(void))successHandlerParam
|
|
failure:(RetryableFailureHandler)failureHandlerParam
|
|
{
|
|
OWSAssertDebug(!NSThread.isMainThread);
|
|
|
|
void (^successHandler)(void) = ^() {
|
|
[self handleMessageSentLocally:message
|
|
success:^{ successHandlerParam(); }
|
|
failure:^(NSError *error) {
|
|
OWSLogError(
|
|
@"Error sending sync message for message: %@ timestamp: %llu", message.class, message.timestamp);
|
|
|
|
failureHandlerParam(error);
|
|
}];
|
|
};
|
|
void (^failureHandler)(NSError *) = ^(NSError *error) {
|
|
if (message.wasSentToAnyRecipient) {
|
|
[self handleMessageSentLocally:message
|
|
success:^{ failureHandlerParam(error); }
|
|
failure:^(NSError *syncError) {
|
|
OWSLogError(@"Error sending sync message for message: %@ timestamp: %llu, %@",
|
|
message.class,
|
|
message.timestamp,
|
|
syncError);
|
|
|
|
// Discard the "sync message" error in favor of the
|
|
// original error.
|
|
failureHandlerParam(error);
|
|
}];
|
|
return;
|
|
}
|
|
failureHandlerParam(error);
|
|
};
|
|
|
|
TSThread *thread = sendInfo.thread;
|
|
NSArray<ServiceIdObjC *> *recipientServiceIds = sendInfo.serviceIds;
|
|
SenderCertificates *senderCertificates = sendInfo.senderCertificates;
|
|
|
|
BOOL canSendToThread = NO;
|
|
if ([message isKindOfClass:OWSOutgoingReactionMessage.class]) {
|
|
canSendToThread = thread.canSendReactionToThread;
|
|
} else if (message.hasRenderableContent ||
|
|
[message isKindOfClass:OWSOutgoingGroupCallMessage.class] ||
|
|
[message isKindOfClass:OWSOutgoingCallMessage.class]) {
|
|
canSendToThread = [thread canSendChatMessagesToThread];
|
|
} else {
|
|
canSendToThread = thread.canSendNonChatMessagesToThread;
|
|
}
|
|
|
|
if (!canSendToThread) {
|
|
if (message.shouldBeSaved) {
|
|
return failureHandler(OWSErrorMakeAssertionError(@"Sending to thread blocked."));
|
|
} else {
|
|
// Pretend to succeed for non-visible messages like read receipts, etc.
|
|
successHandler();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if ([thread isKindOfClass:[TSContactThread class]]) {
|
|
TSContactThread *contactThread = (TSContactThread *)thread;
|
|
// In the "self-send" aka "Note to Self" special case, we only need to send certain kinds of messages.
|
|
// (In particular, regular data messages are sent via their implicit sync message only.)
|
|
if (contactThread.contactAddress.isLocalAddress && !message.canSendToLocalAddress) {
|
|
// Send to self.
|
|
OWSAssertDebug(recipientServiceIds.count == 1);
|
|
OWSLogInfo(@"dropping %@ sent to local address (expected to be sent by sync message)", [message class]);
|
|
// Don't mark self-sent messages as read (or sent) until the sync transcript is sent.
|
|
successHandler();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (recipientServiceIds.count < 1) {
|
|
// All recipients are already sent or can be skipped.
|
|
// NOTE: We might still need to send a sync transcript.
|
|
successHandler();
|
|
return;
|
|
}
|
|
|
|
BOOL isNonContactThread = thread.isNonContactThread;
|
|
NSMutableArray<NSError *> *sendErrors = [NSMutableArray array];
|
|
NSMutableDictionary<ServiceIdObjC *, NSError *> *sendErrorPerRecipient = [NSMutableDictionary dictionary];
|
|
|
|
[self unlockPreKeyUpdateFailuresPromise]
|
|
.thenInBackground(^(id value) {
|
|
return [self sendPromiseForServiceIds:recipientServiceIds
|
|
message:message
|
|
thread:thread
|
|
senderCertificates:senderCertificates
|
|
sendErrorBlock:^(ServiceIdObjC *serviceId, NSError *error) {
|
|
@synchronized(sendErrors) {
|
|
[sendErrors addObject:error];
|
|
sendErrorPerRecipient[serviceId] = error;
|
|
}
|
|
}];
|
|
})
|
|
.doneInBackground(^(id value) { successHandler(); })
|
|
.catchInBackground(^(id failure) {
|
|
// Ignore the failure value; consult sendErrors and sendErrorPerRecipientCopy.
|
|
NSError *firstRetryableError = nil;
|
|
NSError *firstNonRetryableError = nil;
|
|
|
|
NSArray<NSError *> *sendErrorsCopy;
|
|
NSDictionary<ServiceIdObjC *, NSError *> *sendErrorPerRecipientCopy;
|
|
@synchronized(sendErrors) {
|
|
sendErrorsCopy = [sendErrors copy];
|
|
sendErrorPerRecipientCopy = [sendErrorPerRecipient copy];
|
|
}
|
|
|
|
// Record the individual error for each "failed" recipient.
|
|
DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
|
|
for (ServiceIdObjC *serviceId in sendErrorPerRecipientCopy) {
|
|
NSError *error = sendErrorPerRecipientCopy[serviceId];
|
|
|
|
// Some errors should be ignored when sending messages
|
|
// to threads other than TSContactThread. See discussion on
|
|
// NSError (MessageSender) category.
|
|
if (isNonContactThread && [error shouldBeIgnoredForNonContactThreads]) {
|
|
continue;
|
|
}
|
|
|
|
SignalServiceAddress *address = [[SignalServiceAddress alloc] initWithServiceIdObjC:serviceId];
|
|
[message updateWithFailedRecipient:address error:error transaction:transaction];
|
|
}
|
|
});
|
|
|
|
for (NSError *error in sendErrorsCopy) {
|
|
// Some errors should be ignored when sending messages
|
|
// to threads other than TSContactThread. See discussion on
|
|
// NSError (MessageSender) category.
|
|
if (isNonContactThread && [error shouldBeIgnoredForNonContactThreads]) {
|
|
continue;
|
|
}
|
|
|
|
// Some errors should never be retried, in order to avoid
|
|
// hitting rate limits, for example. Unfortunately, since
|
|
// group send retry is all-or-nothing, we need to fail
|
|
// immediately even if some of the other recipients had
|
|
// retryable errors.
|
|
if ([error isFatalError]) {
|
|
failureHandler(error);
|
|
return;
|
|
}
|
|
|
|
if ([error isRetryable] && !firstRetryableError) {
|
|
firstRetryableError = error;
|
|
} else if (![error isRetryable] && !firstNonRetryableError) {
|
|
firstNonRetryableError = error;
|
|
}
|
|
}
|
|
|
|
// If any of the 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 = [MessageSenderErrorNoValidRecipients asNSError];
|
|
failureHandler(error);
|
|
} else {
|
|
successHandler();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
- (nullable TSThread *)threadForMessageWithSneakyTransaction:(TSMessage *)message
|
|
{
|
|
OWSAssertDebug(!NSThread.isMainThread);
|
|
|
|
// Try to avoid opening a write transaction.
|
|
__block TSThread *_Nullable thread = nil;
|
|
[self.databaseStorage
|
|
readWithBlock:^(SDSAnyReadTransaction *transaction) { thread = [message threadWithTransaction:transaction]; }];
|
|
if (thread != nil) {
|
|
return thread;
|
|
}
|
|
|
|
DatabaseStorageWrite(self.databaseStorage,
|
|
^(SDSAnyWriteTransaction *transaction) { thread = [self threadForMessage:message transaction:transaction]; });
|
|
return thread;
|
|
}
|
|
|
|
- (nullable TSThread *)threadForMessage:(TSMessage *)message transaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
OWSAssertDebug(!NSThread.isMainThread);
|
|
|
|
TSThread *_Nullable thread = [message threadWithTransaction:transaction];
|
|
// OWSAssertDebug(thread != nil);
|
|
|
|
// For some legacy sync messages, thread may be nil.
|
|
// In this case, we should try to use the "local" thread.
|
|
if (thread == nil && message.isSyncMessage) {
|
|
thread = [TSAccountManager getOrCreateLocalThreadWithTransaction:transaction];
|
|
if (thread == nil) {
|
|
OWSFailDebug(@"Could not restore thread for sync message.");
|
|
} else {
|
|
OWSLogInfo(@"Thread restored for sync message.");
|
|
}
|
|
}
|
|
return thread;
|
|
}
|
|
|
|
- (nullable NSArray<DeviceMessage *> *)buildDeviceMessagesForMessageSend:(OWSMessageSend *)messageSend
|
|
error:(NSError **)errorHandle
|
|
{
|
|
OWSAssertDebug(messageSend);
|
|
|
|
__block SignalRecipient *recipient;
|
|
[self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) {
|
|
recipient = [SignalRecipient getRecipientForAddress:messageSend.address
|
|
mustHaveDevices:NO
|
|
transaction:transaction];
|
|
}];
|
|
|
|
if (!recipient) {
|
|
*errorHandle = [InvalidMessageError asNSError];
|
|
return nil;
|
|
}
|
|
|
|
NSMutableArray<NSNumber *> *recipientDeviceIds = [recipient.devices.array mutableCopy];
|
|
|
|
if (messageSend.isLocalAddress) {
|
|
[recipientDeviceIds removeObject:@(self.tsAccountManager.storedDeviceId)];
|
|
}
|
|
|
|
NSMutableArray<DeviceMessage *> *deviceMessages = [NSMutableArray array];
|
|
|
|
for (NSNumber *deviceId in recipientDeviceIds) {
|
|
DeviceMessage *deviceMessage =
|
|
[self buildDeviceMessageForMessagePlaintextContent:messageSend.plaintextContent
|
|
messageEncryptionStyle:messageSend.message.encryptionStyle
|
|
recipientId:recipient.accountId
|
|
serviceId:messageSend.serviceId
|
|
deviceId:deviceId
|
|
isOnlineMessage:messageSend.message.isOnline
|
|
isTransientSenderKeyDistributionMessage:messageSend.message.isTransientSKDM
|
|
isStoryMessage:messageSend.message.isStorySend
|
|
isResendRequestMessage:messageSend.message.isResendRequest
|
|
udSendingParamsProvider:messageSend
|
|
error:errorHandle];
|
|
|
|
if (*errorHandle) {
|
|
return nil;
|
|
}
|
|
|
|
if (deviceMessage) {
|
|
[deviceMessages addObject:deviceMessage];
|
|
}
|
|
}
|
|
|
|
return deviceMessages;
|
|
}
|
|
|
|
- (nullable DeviceMessage *)buildDeviceMessageForMessagePlaintextContent:(nullable NSData *)messagePlaintextContent
|
|
messageEncryptionStyle:(EncryptionStyle)messageEncryptionStyle
|
|
recipientId:(NSString *)recipientId
|
|
serviceId:(ServiceIdObjC *)serviceId
|
|
deviceId:(NSNumber *)deviceId
|
|
isOnlineMessage:(BOOL)isOnlineMessage
|
|
isTransientSenderKeyDistributionMessage:(BOOL)isTransientSenderKeyDistributionMessage
|
|
isStoryMessage:(BOOL)isStoryMessage
|
|
isResendRequestMessage:(BOOL)isResendRequestMessage
|
|
udSendingParamsProvider:
|
|
(nullable id<UDSendingParamsProvider>)udSendingParamsProvider
|
|
error:(NSError **)errorHandle
|
|
{
|
|
OWSAssertDebug(!NSThread.isMainThread);
|
|
OWSAssertDebug(errorHandle);
|
|
|
|
DeviceMessage *deviceMessage;
|
|
@try {
|
|
deviceMessage = [self throws_deviceMessageForMessagePlaintextContent:messagePlaintextContent
|
|
messageEncryptionStyle:messageEncryptionStyle
|
|
recipientId:recipientId
|
|
serviceId:serviceId
|
|
deviceId:deviceId
|
|
isOnlineMessage:isOnlineMessage
|
|
isTransientSenderKeyDistributionMessage:isTransientSenderKeyDistributionMessage
|
|
isStoryMessage:isStoryMessage
|
|
isResendRequestMessage:isResendRequestMessage
|
|
udSendingParamsProvider:udSendingParamsProvider];
|
|
} @catch (NSException *exception) {
|
|
OWSLogWarn(@"Could not build device messages: %@", exception);
|
|
NSError *error = nil;
|
|
if ([exception.name isEqualToString:MessageSenderInvalidDeviceException]) {
|
|
// If we have an invalid device exception, remove this device from
|
|
// the recipient and suppress the error.
|
|
DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
|
|
[MessageSender updateDevicesWithServiceId:serviceId
|
|
devicesToAdd:@[]
|
|
devicesToRemove:@[ deviceId ]
|
|
transaction:transaction];
|
|
});
|
|
} else if ([exception.name isEqualToString:NoSessionForTransientMessageException]) {
|
|
// When users re-register, we don't want transient messages (like typing
|
|
// indicators) to cause users to hit the prekey fetch rate limit. So
|
|
// we silently discard these message if there is no pre-existing session
|
|
// for the recipient.
|
|
error = MessageSenderNoSessionForTransientMessageError.asNSError;
|
|
} else if ([UntrustedIdentityError isUntrustedIdentityError:exception.userInfo[NSUnderlyingErrorKey]]) {
|
|
// This *can* happen under normal usage, but it should happen relatively rarely.
|
|
// We expect it to happen whenever Bob reinstalls, and Alice messages Bob before
|
|
// she can pull down his latest identity.
|
|
// If it's happening a lot, we should rethink our profile fetching strategy.
|
|
OWSProdInfo([OWSAnalyticsEvents messageSendErrorFailedDueToUntrustedKey]);
|
|
SignalServiceAddress *recipientAddress = [[SignalServiceAddress alloc] initWithServiceIdObjC:serviceId];
|
|
error = [UntrustedIdentityError asNSErrorWithAddress:recipientAddress];
|
|
} else if ([exception.name isEqualToString:MessageSenderRateLimitedException]) {
|
|
error = [SignalServiceRateLimitedError asNSError];
|
|
} else if ([exception.name isEqualToString:MessageSenderSpamChallengeResolvedException]) {
|
|
error = [SpamChallengeResolvedError asNSError];
|
|
} else if ([exception.name isEqualToString:MessageSenderSpamChallengeRequiredException]) {
|
|
error = [SpamChallengeRequiredError asNSError];
|
|
} else if ([exception.name isEqualToString:InvalidMessageException]) {
|
|
error = [InvalidMessageError asNSError];
|
|
} else {
|
|
error = [OWSRetryableMessageSenderError asNSError];
|
|
}
|
|
if (errorHandle) {
|
|
*errorHandle = error;
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
return deviceMessage;
|
|
}
|
|
|
|
- (void)sendMessageToRecipient:(OWSMessageSend *)messageSend
|
|
{
|
|
// [sendMessageToRecipient:] prepares a per-recipient
|
|
// message send and makes the request. It is expensive
|
|
// because encryption is expensive. Therefore we want
|
|
// to globally limit the number of invocations of this
|
|
// method that are in flight at a time. We use an
|
|
// operation queue to do that.
|
|
|
|
static dispatch_once_t onceToken;
|
|
static NSOperationQueue *operationQueue;
|
|
dispatch_once(&onceToken, ^{
|
|
operationQueue = [NSOperationQueue new];
|
|
operationQueue.qualityOfService = NSOperationQualityOfServiceUserInitiated;
|
|
operationQueue.name = @"MessageSender-Recipient";
|
|
operationQueue.maxConcurrentOperationCount = 6;
|
|
});
|
|
[operationQueue addOperationWithBlock:^{ [self _sendMessageToRecipient:messageSend]; }];
|
|
}
|
|
|
|
- (void)_sendMessageToRecipient:(OWSMessageSend *)messageSend
|
|
{
|
|
OWSAssertDebug(!NSThread.isMainThread);
|
|
OWSAssertDebug(messageSend);
|
|
OWSAssertDebug(messageSend.thread);
|
|
|
|
TSOutgoingMessage *message = messageSend.message;
|
|
ServiceIdObjC *serviceId = messageSend.serviceId;
|
|
|
|
OWSLogInfo(
|
|
@"attempting to send message: %@, timestamp: %llu, recipient: %@", message.class, message.timestamp, serviceId);
|
|
|
|
if (messageSend.remainingAttempts <= 0) {
|
|
// We should always fail with a specific error.
|
|
OWSProdFail([OWSAnalyticsEvents messageSenderErrorGenericSendFailure]);
|
|
|
|
NSError *error = [OWSRetryableMessageSenderError asNSError];
|
|
return messageSend.failure(error);
|
|
}
|
|
|
|
// Consume an attempt.
|
|
messageSend.remainingAttempts = messageSend.remainingAttempts - 1;
|
|
|
|
// We need to disable UD for sync messages before we build the device messages,
|
|
// since we don't want to build a device message for the local device in the
|
|
// non-UD auth case.
|
|
if (message.isSyncMessage && ![message isKindOfClass:[OWSOutgoingSentMessageTranscript class]]) {
|
|
[messageSend disableUDAuth];
|
|
} else if (SSKDebugFlags.disableUD.value) {
|
|
OWSLogDebug(@"Disabling UD because of testable flag");
|
|
[messageSend disableUDAuth];
|
|
}
|
|
|
|
NSError *deviceMessagesError;
|
|
NSArray<DeviceMessage *> *_Nullable deviceMessages = [self buildDeviceMessagesForMessageSend:messageSend
|
|
error:&deviceMessagesError];
|
|
if (deviceMessagesError || !deviceMessages) {
|
|
OWSAssertDebug(deviceMessagesError);
|
|
return messageSend.failure(deviceMessagesError);
|
|
}
|
|
|
|
if (messageSend.isLocalAddress) {
|
|
OWSAssertDebug(message.canSendToLocalAddress);
|
|
// We can skip sending sync messages if we know that we have no linked
|
|
// devices. However, we need to be sure to handle the case where the
|
|
// linked device list has just changed.
|
|
//
|
|
// The linked device list is reflected in two separate pieces of state:
|
|
//
|
|
// * OWSDevice's state is updated when you link or unlink a device.
|
|
// * SignalRecipient's state is updated by 409 "Mismatched devices"
|
|
// responses from the service.
|
|
//
|
|
// If _both_ of these pieces of state agree that there are no linked
|
|
// devices, then can safely skip sending sync message.
|
|
//
|
|
// NOTE: Sync messages sent via UD include the local device.
|
|
|
|
__block BOOL mayHaveLinkedDevices;
|
|
[self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) {
|
|
mayHaveLinkedDevices = [OWSDeviceManager.shared mayHaveLinkedDevicesWithTransaction:transaction];
|
|
}];
|
|
|
|
BOOL hasDeviceMessages = NO;
|
|
for (DeviceMessage *deviceMessage in deviceMessages) {
|
|
if (deviceMessage.destinationDeviceId != self.tsAccountManager.storedDeviceId) {
|
|
hasDeviceMessages = YES;
|
|
break;
|
|
}
|
|
}
|
|
|
|
OWSLogInfo(@"mayHaveLinkedDevices: %d, hasDeviceMessages: %d", mayHaveLinkedDevices, hasDeviceMessages);
|
|
|
|
if (!mayHaveLinkedDevices && !hasDeviceMessages) {
|
|
OWSLogInfo(@"Ignoring sync message without secondary devices: %@", [message class]);
|
|
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
// This emulates the completion logic of an actual successful send (see below).
|
|
DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
|
|
[message updateWithSkippedRecipient:messageSend.localAddress transaction:transaction];
|
|
});
|
|
messageSend.success();
|
|
});
|
|
|
|
return;
|
|
} else if (mayHaveLinkedDevices && !hasDeviceMessages) {
|
|
// We may have just linked a new secondary device which is not yet reflected in
|
|
// the SignalRecipient that corresponds to ourself. Proceed. Client should learn
|
|
// of new secondary devices via 409 "Mismatched devices" response.
|
|
OWSLogWarn(@"account has secondary devices, but sync message has no device messages");
|
|
} else if (!mayHaveLinkedDevices && hasDeviceMessages) {
|
|
OWSFailDebug(@"sync message has device messages for unknown secondary devices.");
|
|
}
|
|
} else {
|
|
// This can happen for users who have unregistered.
|
|
// We still want to try sending to them in case they have re-registered.
|
|
if (deviceMessages.count < 1) {
|
|
OWSLogWarn(@"Message send attempt with no device messages.");
|
|
}
|
|
}
|
|
|
|
for (DeviceMessage *deviceMessage in deviceMessages) {
|
|
SSKProtoEnvelopeType messageType = deviceMessage.type;
|
|
BOOL hasValidMessageType = NO;
|
|
if (messageSend.isUDSend) {
|
|
hasValidMessageType |= (messageType == SSKProtoEnvelopeTypeUnidentifiedSender);
|
|
} else {
|
|
hasValidMessageType |= (messageType == SSKProtoEnvelopeTypeCiphertext);
|
|
hasValidMessageType |= (messageType == SSKProtoEnvelopeTypePrekeyBundle);
|
|
hasValidMessageType |= (messageType == SSKProtoEnvelopeTypePlaintextContent);
|
|
}
|
|
|
|
if (!hasValidMessageType) {
|
|
OWSFailDebug(@"Invalid message type: %ld", (long)messageType);
|
|
NSError *error = [OWSUnretryableMessageSenderError asNSError];
|
|
return messageSend.failure(error);
|
|
}
|
|
}
|
|
|
|
[self performMessageSendRequest:messageSend deviceMessages:deviceMessages];
|
|
}
|
|
|
|
- (void)handleMessageSentLocally:(TSOutgoingMessage *)message
|
|
success:(void (^)(void))successParam
|
|
failure:(RetryableFailureHandler)failure
|
|
{
|
|
OWSAssertDebug(!NSThread.isMainThread);
|
|
|
|
dispatch_block_t success = ^{
|
|
// This should not be nil, even for legacy queued messages.
|
|
TSThread *_Nullable thread = [self threadForMessageWithSneakyTransaction:message];
|
|
OWSAssertDebug(thread != nil);
|
|
|
|
TSContactThread *_Nullable contactThread;
|
|
if ([thread isKindOfClass:[TSContactThread class]]) {
|
|
contactThread = (TSContactThread *)thread;
|
|
}
|
|
|
|
if (contactThread && contactThread.contactAddress.isLocalAddress && !message.isSyncMessage) {
|
|
OWSAssertDebug(message.recipientAddresses.count == 1);
|
|
// Don't mark self-sent messages as read (or sent) until the sync transcript is sent.
|
|
//
|
|
// NOTE: This only applies to the 'note to self' conversation.
|
|
DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
|
|
for (SignalServiceAddress *sendingAddress in message.sendingRecipientAddresses) {
|
|
[message updateWithReadRecipient:sendingAddress
|
|
recipientDeviceId:self.tsAccountManager.storedDeviceId
|
|
readTimestamp:message.timestamp
|
|
transaction:transaction];
|
|
if (message.isVoiceMessage || message.isViewOnceMessage) {
|
|
[message updateWithViewedRecipient:sendingAddress
|
|
recipientDeviceId:self.tsAccountManager.storedDeviceId
|
|
viewedTimestamp:message.timestamp
|
|
transaction:transaction];
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
successParam();
|
|
};
|
|
|
|
if (message.shouldBeSaved) {
|
|
// We don't need to do this work for transient messages.
|
|
DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
|
|
TSInteraction *_Nullable latestCopy = [TSInteraction anyFetchWithUniqueId:message.uniqueId
|
|
transaction:transaction];
|
|
if (![latestCopy isKindOfClass:[TSOutgoingMessage class]]) {
|
|
OWSLogWarn(@"Could not update expiration for deleted message.");
|
|
return;
|
|
}
|
|
TSOutgoingMessage *latestMessage = (TSOutgoingMessage *)latestCopy;
|
|
[ViewOnceMessages completeIfNecessaryWithMessage:latestMessage transaction:transaction];
|
|
});
|
|
}
|
|
|
|
if (!message.shouldSyncTranscript) {
|
|
return success();
|
|
}
|
|
|
|
[message sendSyncTranscript]
|
|
.doneInBackground(^(id value) {
|
|
OWSLogInfo(@"Successfully sent sync transcript.");
|
|
|
|
DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
|
|
[message updateWithHasSyncedTranscript:YES transaction:transaction];
|
|
});
|
|
|
|
success();
|
|
})
|
|
.catchInBackground(^(NSError *error) {
|
|
OWSLogInfo(@"Failed to send sync transcript: %@ (isRetryable: %d)", error, [error isRetryable]);
|
|
|
|
failure(error);
|
|
});
|
|
}
|
|
|
|
- (DeviceMessage *)throws_deviceMessageForMessagePlaintextContent:(nullable NSData *)messagePlaintextContent
|
|
messageEncryptionStyle:(EncryptionStyle)messageEncryptionStyle
|
|
recipientId:(NSString *)recipientId
|
|
serviceId:(ServiceIdObjC *)serviceId
|
|
deviceId:(NSNumber *)deviceId
|
|
isOnlineMessage:(BOOL)isOnlineMessage
|
|
isTransientSenderKeyDistributionMessage:(BOOL)isTransientSenderKeyDistributionMessage
|
|
isStoryMessage:(BOOL)isStoryMessage
|
|
isResendRequestMessage:(BOOL)isResendRequestMessage
|
|
udSendingParamsProvider:
|
|
(nullable id<UDSendingParamsProvider>)udSendingParamsProvider
|
|
{
|
|
OWSAssertDebug(!NSThread.isMainThread);
|
|
|
|
if (!messagePlaintextContent) {
|
|
OWSRaiseException(InvalidMessageException, @"No message proto");
|
|
}
|
|
|
|
// This may involve blocking network requests.
|
|
[self throws_ensureRecipientHasSessionForId:recipientId
|
|
serviceId:serviceId
|
|
deviceId:deviceId
|
|
isOnlineMessage:isOnlineMessage
|
|
isTransientSenderKeyDistributionMessage:isTransientSenderKeyDistributionMessage
|
|
isStoryMessage:isStoryMessage
|
|
udSendingParamsProvider:udSendingParamsProvider];
|
|
|
|
__block NSError *encryptionError;
|
|
__block DeviceMessage *__nullable deviceMessage = nil;
|
|
DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
|
|
switch (messageEncryptionStyle) {
|
|
case EncryptionStyleWhisper:
|
|
deviceMessage = [self encryptMessageWithPlaintextContent:messagePlaintextContent
|
|
serviceId:serviceId
|
|
deviceId:deviceId.intValue
|
|
udSendingParamsProvider:udSendingParamsProvider
|
|
transaction:transaction
|
|
error:&encryptionError];
|
|
break;
|
|
case EncryptionStylePlaintext:
|
|
deviceMessage = [self wrapPlaintextMessageWithPlaintextContent:messagePlaintextContent
|
|
serviceId:serviceId
|
|
deviceId:deviceId.intValue
|
|
isResendRequestMessage:isResendRequestMessage
|
|
udSendingParamsProvider:udSendingParamsProvider
|
|
transaction:transaction
|
|
error:&encryptionError];
|
|
break;
|
|
default:
|
|
encryptionError = OWSErrorMakeAssertionError(@"Unrecognized encryption style");
|
|
break;
|
|
}
|
|
if (!deviceMessage || encryptionError) {
|
|
return;
|
|
}
|
|
});
|
|
|
|
if (encryptionError) {
|
|
OWSRaiseException(InvalidMessageException, @"Failed to encrypt message: %@", encryptionError);
|
|
}
|
|
|
|
return deviceMessage;
|
|
}
|
|
|
|
- (void)throws_ensureRecipientHasSessionForId:(NSString *)recipientId
|
|
serviceId:(ServiceIdObjC *)serviceId
|
|
deviceId:(NSNumber *)deviceId
|
|
isOnlineMessage:(BOOL)isOnlineMessage
|
|
isTransientSenderKeyDistributionMessage:(BOOL)isTransientSenderKeyDistributionMessage
|
|
isStoryMessage:(BOOL)isStoryMessage
|
|
udSendingParamsProvider:(nullable id<UDSendingParamsProvider>)udSendingParamsProvider
|
|
{
|
|
OWSAssertDebug(!NSThread.isMainThread);
|
|
OWSAssertDebug(recipientId);
|
|
OWSAssertDebug(serviceId);
|
|
OWSAssertDebug(deviceId);
|
|
|
|
__block BOOL hasSession;
|
|
[self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) {
|
|
SSKSessionStore *sessionStore = [self signalProtocolStoreForIdentity:OWSIdentityACI].sessionStore;
|
|
hasSession = [sessionStore containsActiveSessionForAccountId:recipientId
|
|
deviceId:[deviceId intValue]
|
|
transaction:transaction];
|
|
}];
|
|
if (hasSession) {
|
|
return;
|
|
}
|
|
|
|
__block dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
|
__block PreKeyBundle *_Nullable bundle;
|
|
__block NSException *_Nullable exception;
|
|
[MessageSender makePrekeyRequestWithRecipientId:recipientId
|
|
serviceId:serviceId
|
|
deviceId:deviceId
|
|
isOnlineMessage:isOnlineMessage
|
|
isTransientSenderKeyDistributionMessage:isTransientSenderKeyDistributionMessage
|
|
isStoryMessage:isStoryMessage
|
|
udSendingParamsProvider:udSendingParamsProvider
|
|
success:^(PreKeyBundle *_Nullable responseBundle) {
|
|
bundle = responseBundle;
|
|
dispatch_semaphore_signal(sema);
|
|
}
|
|
failure:^(NSError *error) {
|
|
NSNumber *_Nullable statusCode = error.httpStatusCode;
|
|
OWSLogVerbose(@"statusCode: %@", statusCode);
|
|
if ([MessageSender isMissingDeviceError:error]) {
|
|
// Can't throw exception from within callback as it's probabably a different thread.
|
|
exception = [NSException exceptionWithName:MessageSenderInvalidDeviceException
|
|
reason:@"Device not registered"
|
|
userInfo:@{ NSUnderlyingErrorKey : error }];
|
|
} else if ([MessageSender isPrekeyRateLimitError:error]) {
|
|
// Can't throw exception from within callback as it's probabably a different thread.
|
|
exception = [NSException exceptionWithName:MessageSenderRateLimitedException
|
|
reason:@"Too many prekey requests"
|
|
userInfo:@{ NSUnderlyingErrorKey : error }];
|
|
} else if ([SpamChallengeRequiredError isSpamChallengeRequiredError:error]) {
|
|
// Can't throw exception from within callback as it's probabably a different thread.
|
|
exception = [NSException exceptionWithName:MessageSenderSpamChallengeRequiredException
|
|
reason:@"Spam challenge required"
|
|
userInfo:@ { NSUnderlyingErrorKey : error }];
|
|
} else if ([SpamChallengeResolvedError isSpamChallengeResolvedError:error]) {
|
|
// Can't throw exception from within callback as it's probabably a different thread.
|
|
exception = [NSException exceptionWithName:MessageSenderSpamChallengeResolvedException
|
|
reason:@"Spam challenge resolved"
|
|
userInfo:@ { NSUnderlyingErrorKey : error }];
|
|
} else if ([UntrustedIdentityError isUntrustedIdentityError:error]) {
|
|
// Can't throw exception from within callback as it's probabably a different thread.
|
|
exception = [NSException exceptionWithName:UntrustedIdentityKeyException
|
|
reason:@"Identity key is not valid"
|
|
userInfo:@{ NSUnderlyingErrorKey : error }];
|
|
} else if ([MessageSenderNoSessionForTransientMessageError isTransientMessageError:error]) {
|
|
// Can't throw exception from within callback as it's probabably a different thread.
|
|
exception = [NSException exceptionWithName:NoSessionForTransientMessageException
|
|
reason:@"No session for transient message"
|
|
userInfo:@ { NSUnderlyingErrorKey : error }];
|
|
} else if (error.isNetworkConnectivityFailure) {
|
|
OWSLogWarn(@"Network failure in prekey request.");
|
|
} else {
|
|
OWSFailDebug(@"Error: %@", error);
|
|
}
|
|
dispatch_semaphore_signal(sema);
|
|
}];
|
|
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
|
|
if (exception) {
|
|
OWSLogVerbose(@"exception: %@", exception);
|
|
@throw exception;
|
|
}
|
|
|
|
if (!bundle) {
|
|
NSString *missingPrekeyBundleException = @"missingPrekeyBundleException";
|
|
OWSRaiseException(
|
|
missingPrekeyBundleException, @"Can't get a prekey bundle from the server with required information");
|
|
}
|
|
|
|
__block BOOL success;
|
|
__block NSError *error;
|
|
DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
|
|
success = [[self class] createSessionFor:bundle
|
|
recipientId:recipientId
|
|
serviceId:serviceId
|
|
deviceId:deviceId
|
|
transaction:transaction
|
|
error:&error];
|
|
});
|
|
if (!success) {
|
|
// NSUnderlyingErrorKey isn't a normal NSException user info key, but it's appropriate here.
|
|
OWSRaiseExceptionWithUserInfo(
|
|
@"sessionCreationException", @{ NSUnderlyingErrorKey : error }, @"Failed to create session: %@", error);
|
|
}
|
|
}
|
|
|
|
+ (NSOperationQueuePriority)queuePriorityForMessage:(TSOutgoingMessage *)message
|
|
{
|
|
return message.hasRenderableContent ? NSOperationQueuePriorityNormal : NSOperationQueuePriorityLow;
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation OutgoingMessagePreparerHelper
|
|
|
|
#pragma mark -
|
|
|
|
+ (BOOL)doesMessageNeedsToBePrepared:(TSOutgoingMessage *)message
|
|
{
|
|
if (message.allAttachmentIds.count > 0 || message.messageSticker != nil || message.quotedMessage != nil) {
|
|
return YES;
|
|
}
|
|
if (message.hasFailedRecipients) {
|
|
return YES;
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
// NOTE: Any changes to this method should be reflected in doesMessageNeedsToBePrepared.
|
|
+ (NSArray<NSString *> *)prepareMessageForSending:(TSOutgoingMessage *)message
|
|
transaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
OWSAssertDebug(message);
|
|
OWSAssertDebug(transaction);
|
|
|
|
NSMutableArray<NSString *> *attachmentIds = [NSMutableArray new];
|
|
|
|
if (message.attachmentIds) {
|
|
[attachmentIds addObjectsFromArray:message.attachmentIds];
|
|
}
|
|
|
|
if (message.quotedMessage.thumbnailAttachmentId) {
|
|
// We need to update the message record here to reflect the new attachments we may create.
|
|
[message anyUpdateOutgoingMessageWithTransaction:transaction
|
|
block:^(TSOutgoingMessage *blockParamMessage) {
|
|
TSAttachmentStream *thumbnail = [blockParamMessage.quotedMessage
|
|
createThumbnailIfNecessaryWithTransaction:transaction];
|
|
if (thumbnail.uniqueId) {
|
|
[attachmentIds addObject:thumbnail.uniqueId];
|
|
}
|
|
}];
|
|
}
|
|
|
|
if (message.contactShare.avatarAttachmentId != nil) {
|
|
TSAttachment *attachment = [message.contactShare avatarAttachmentWithTransaction:transaction];
|
|
if ([attachment isKindOfClass:[TSAttachmentStream class]]) {
|
|
[attachmentIds addObject:attachment.uniqueId];
|
|
} else {
|
|
OWSFailDebug(@"unexpected avatarAttachment: %@", attachment);
|
|
}
|
|
}
|
|
|
|
if (message.linkPreview.imageAttachmentId != nil) {
|
|
TSAttachmentStream *_Nullable attachment =
|
|
[TSAttachmentStream anyFetchAttachmentStreamWithUniqueId:message.linkPreview.imageAttachmentId
|
|
transaction:transaction];
|
|
if (attachment == nil) {
|
|
OWSFailDebug(@"Missing attachment: %@", attachment);
|
|
} else {
|
|
[attachmentIds addObject:attachment.uniqueId];
|
|
}
|
|
}
|
|
|
|
if (message.messageSticker.attachmentId != nil) {
|
|
TSAttachmentStream *_Nullable attachment =
|
|
[TSAttachmentStream anyFetchAttachmentStreamWithUniqueId:message.messageSticker.attachmentId
|
|
transaction:transaction];
|
|
if (attachment == nil) {
|
|
OWSFailDebug(@"Missing attachment: %@", attachment);
|
|
} else {
|
|
[attachmentIds addObject:attachment.uniqueId];
|
|
}
|
|
}
|
|
|
|
// When we start a message send, all "failed" recipients should be marked as "sending".
|
|
[message updateAllUnsentRecipientsAsSendingWithTransaction:transaction];
|
|
|
|
if (message.messageSticker != nil) {
|
|
// Update "Recent Stickers" list to reflect sends.
|
|
[StickerManager stickerWasSent:message.messageSticker.info transaction:transaction];
|
|
}
|
|
|
|
return attachmentIds;
|
|
}
|
|
|
|
+ (BOOL)insertAttachments:(NSArray<OWSOutgoingAttachmentInfo *> *)attachmentInfos
|
|
forMessage:(TSOutgoingMessage *)outgoingMessage
|
|
transaction:(SDSAnyWriteTransaction *)transaction
|
|
error:(NSError **)error
|
|
{
|
|
OWSAssertDebug(attachmentInfos.count > 0);
|
|
OWSAssertDebug(outgoingMessage);
|
|
|
|
// Eventually we'll pad all outgoing attachments, but currently just stickers.
|
|
// Currently this method is only used to process "body" attachments, which
|
|
// cannot be sent along with stickers.
|
|
OWSAssertDebug(outgoingMessage.messageSticker == nil);
|
|
|
|
NSMutableArray<TSAttachmentStream *> *attachmentStreams = [NSMutableArray new];
|
|
for (OWSOutgoingAttachmentInfo *attachmentInfo in attachmentInfos) {
|
|
TSAttachmentStream *_Nullable attachmentStream =
|
|
[attachmentInfo asStreamConsumingDataSourceWithIsVoiceMessage:outgoingMessage.isVoiceMessage error:error];
|
|
if (*error != nil) {
|
|
return NO;
|
|
}
|
|
if (attachmentStream == nil) {
|
|
OWSFailDebug(@"Unknown error.");
|
|
*error = OWSErrorMakeAssertionError(@"Could not insert attachments.");
|
|
return NO;
|
|
}
|
|
OWSAssert(attachmentStream != nil);
|
|
[attachmentStreams addObject:attachmentStream];
|
|
}
|
|
|
|
[outgoingMessage
|
|
anyUpdateOutgoingMessageWithTransaction:transaction
|
|
block:^(TSOutgoingMessage *blockParamOutgoingMessage) {
|
|
NSMutableArray<NSString *> *attachmentIds =
|
|
[blockParamOutgoingMessage.attachmentIds mutableCopy];
|
|
for (TSAttachmentStream *attachmentStream in attachmentStreams) {
|
|
[attachmentIds addObject:attachmentStream.uniqueId];
|
|
}
|
|
blockParamOutgoingMessage.attachmentIds = [attachmentIds copy];
|
|
}];
|
|
|
|
for (TSAttachmentStream *attachmentStream in attachmentStreams) {
|
|
[attachmentStream anyInsertWithTransaction:transaction];
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
@end
|
|
|
|
NS_ASSUME_NONNULL_END
|